Недавно мы опубликовали статью о том, как правильно связать тип Oracle DATE
в SQL / JDBC и jOOQ . Эта статья получила небольшую поддержку в Reddit с интересным замечанием Влада Михалча , который часто пишет в своем блоге о Hibernate, JPA, управлении транзакциями и пулах соединений . Влад отметил, что эту проблему также можно решить с помощью Hibernate, и мы вскоре рассмотрим это.
В чем проблема с Oracle DATE?
Проблема, которая была представлена в предыдущей статье, связана с тем, что, если запрос использует фильтры для столбцов Oracle DATE
:
1
2
3
4
5
|
// execute_at is of type DATE and there's an index PreparedStatement stmt = connection .prepareStatement( "SELECT * " + "FROM rentals " + "WHERE rental_date > ? AND rental_date < ?" ); |
… И мы используем java.sql.Timestamp
для наших значений привязки:
1
2
|
stmt.setTimestamp( 1 , start); stmt.setTimestamp( 2 , end); |
… Тогда план выполнения станет очень плохим при ПОЛНОМ СКАНИРОВАНИИ ТАБЛИЦЫ или, возможно, при ПОЛНОМ СКАНИРОВАНИИ ИНДЕКСА, даже если мы должны были получить регулярное СКАНИРОВАНИЕ НА ИНДЕКСНОМ ДИАПАЗОНЕ.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
------------------------------------- | Id | Operation | Name | ------------------------------------- | 0 | SELECT STATEMENT | | |* 1 | FILTER | | |* 2 | TABLE ACCESS FULL | RENTAL | ------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(:1<=:2) 2 - filter((INTERNAL_FUNCTION( "RENTAL_DATE" )>=:1 AND INTERNAL_FUNCTION( "RENTAL_DATE" )<=:2)) |
Это связано с тем, что столбец базы данных расширяется от Oracle DATE
до Oracle TIMESTAMP
помощью этого INTERNAL_FUNCTION()
, а не java.sql.Timestamp
значение java.sql.Timestamp
до Oracle DATE
.
Более подробную информацию о самой проблеме можно увидеть в предыдущей статье
Предотвращение этого INTERNAL_FUNCTION () с Hibernate
Вы можете исправить это с помощью проприетарного API Hibernate, используя org.hibernate.usertype.UserType
.
Предполагая, что у нас есть следующая сущность:
01
02
03
04
05
06
07
08
09
10
|
@Entity public class Rental { @Id @Column (name = "rental_id" ) public Long rentalId; @Column (name = "rental_date" ) public Timestamp rentalDate; } |
А теперь давайте запустим этот запрос здесь (для примера я использую Hibernate API, а не JPA):
1
2
3
4
5
|
List<Rental> rentals = session.createQuery( "from Rental r where r.rentalDate between :from and :to" ) .setParameter( "from" , Timestamp.valueOf( "2000-01-01 00:00:00.0" )) .setParameter( "to" , Timestamp.valueOf( "2000-10-01 00:00:00.0" )) .list(); |
План выполнения, который мы сейчас получаем, снова неэффективен:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
------------------------------------- | Id | Operation | Name | ------------------------------------- | 0 | SELECT STATEMENT | | |* 1 | FILTER | | |* 2 | TABLE ACCESS FULL | RENTAL | ------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(:1<=:2) 2 - filter((INTERNAL_FUNCTION( "RENTAL0_" . "RENTAL_DATE" )>=:1 AND INTERNAL_FUNCTION( "RENTAL0_" . "RENTAL_DATE" )<=:2)) |
Решение состоит в том, чтобы добавить эту аннотацию @Type
ко всем соответствующим столбцам …
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
@Entity @TypeDefs ( value = @TypeDef ( name = "oracle_date" , typeClass = OracleDate. class ) ) public class Rental { @Id @Column (name = "rental_id" ) public Long rentalId; @Column (name = "rental_date" ) @Type (type = "oracle_date" ) public Timestamp rentalDate; } |
и зарегистрируйте следующий упрощенный UserType
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
import java.io.Serializable; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.sql.Types; import java.util.Objects; import oracle.sql.DATE; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.usertype.UserType; public class OracleDate implements UserType { @Override public int [] sqlTypes() { return new int [] { Types.TIMESTAMP }; } @Override public Class<?> returnedClass() { return Timestamp. class ; } @Override public Object nullSafeGet( ResultSet rs, String[] names, SessionImplementor session, Object owner ) throws SQLException { return rs.getTimestamp(names[ 0 ]); } @Override public void nullSafeSet( PreparedStatement st, Object value, int index, SessionImplementor session ) throws SQLException { // The magic is here: oracle.sql.DATE! st.setObject(index, new DATE(value)); } // The other method implementations are omitted } |
Это будет работать, потому что использование специфичного для oracle.sql.DATE
типа oracle.sql.DATE
будет иметь тот же эффект на ваш план выполнения, что и явное приведение переменной bind в вашем операторе SQL, как показано в предыдущей статье : CAST(? AS DATE)
. План выполнения теперь желаемый:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
------------------------------------------------------ | Id | Operation | Name | ------------------------------------------------------ | 0 | SELECT STATEMENT | | |* 1 | FILTER | | | 2 | TABLE ACCESS BY INDEX ROWID| RENTAL | |* 3 | INDEX RANGE SCAN | IDX_RENTAL_UQ | ------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(:1<=:2) 3 - access( "RENTAL0_" . "RENTAL_DATE" >=:1 AND "RENTAL0_" . "RENTAL_DATE" <=:2) |
Если вы хотите воспроизвести эту проблему, просто запросите в любом столбце Oracle DATE
значение привязки java.sql.Timestamp
через JPA / Hibernate и получите план выполнения, как указано здесь .
Не забудьте очистить общие пулы и буферные кэши, чтобы обеспечить вычисление новых планов между выполнениями, потому что сгенерированный SQL каждый раз один и тот же.
Могу ли я сделать это с JPA 2.1?
На первый взгляд, похоже, что новая функция конвертера в JPA 2.1 ( которая работает так же, как и функция конвертации в jOOQ ) должна справиться с задачей . Мы должны быть в состоянии написать:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
import java.sql.Timestamp; import javax.persistence.AttributeConverter; import javax.persistence.Converter; import oracle.sql.DATE; @Converter public class OracleDateConverter implements AttributeConverter<Timestamp, DATE>{ @Override public DATE convertToDatabaseColumn(Timestamp attribute) { return attribute == null ? null : new DATE(attribute); } @Override public Timestamp convertToEntityAttribute(DATE dbData) { return dbData == null ? null : dbData.timestampValue(); } } |
Этот конвертер может затем использоваться с нашей организацией:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
import java.sql.Timestamp; import javax.persistence.Column; import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.Id; @Entity public class Rental { @Id @Column (name = "rental_id" ) public Long rentalId; @Column (name = "rental_date" ) @Convert (converter = OracleDateConverter. class ) public Timestamp rentalDate; } |
Но, к сожалению, это не работает из коробки, поскольку Hibernate 4.3.7 будет думать, что вы собираетесь связать переменную типа VARBINARY
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// From org.hibernate.type.descriptor.sql.SqlTypeDescriptorRegistry public <X> ValueBinder<X> getBinder(JavaTypeDescriptor<X> javaTypeDescriptor) { if ( Serializable. class .isAssignableFrom( javaTypeDescriptor.getJavaTypeClass() ) ) { return VarbinaryTypeDescriptor.INSTANCE.getBinder( javaTypeDescriptor ); } return new BasicBinder<X>( javaTypeDescriptor, this ) { @Override protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { st.setObject( index, value, jdbcTypeCode ); } }; } |
Конечно, мы, возможно, можем каким-то образом настроить SqlTypeDescriptorRegistry
для создания нашего собственного «связующего», но затем мы вернемся к API, специфичному для Hibernate. Эта конкретная реализация, вероятно, является «ошибкой» на стороне Hibernate, которая была зарегистрирована здесь, для записи:
https://hibernate.atlassian.net/browse/HHH-9553
Вывод
Абстракции протекают на всех уровнях, даже если JCP считает их «стандартом». Стандарты часто являются средством оправдания отраслевого стандарта де-факто в ретроспективе (с некоторой политикой, конечно). Давайте не будем забывать, что Hibernate не начинался как стандарт и в корне изменил способ, которым обычные люди из J2EE думали о постоянстве 14 лет назад.
В этом случае мы имеем:
- Oracle SQL, фактическая реализация
- Стандарт SQL, который определяет
DATE
совсем не так, как Oracle - ojdbc, расширяющий JDBC для обеспечения доступа к функциям Oracle
- JDBC, который следует стандарту SQL относительно временных типов
- Hibernate, который предлагает собственный API для доступа к функциям Oracle SQL и ojdbc при привязке переменных
- JPA, который снова следует стандарту SQL и JDBC относительно временных типов
- Модель вашей сущности
Как вы можете видеть, фактическая реализация (Oracle SQL) просочилась прямо в вашу собственную сущностную модель, либо через UserType
Hibernate, либо через JPA Converter
. С тех пор он, как мы надеемся, будет защищен от вашего приложения (до тех пор, пока это не произойдет), что позволит вам забыть об этой неприятной маленькой детали Oracle SQL.
В любом случае, если вы хотите решить реальные проблемы клиентов (то есть существенную проблему с производительностью), вам придется прибегнуть к API-интерфейсу конкретного поставщика из Oracle SQL, ojdbc и Hibernate — вместо того, чтобы притворяться, что SQL Стандарты JDBC и JPA — это практический результат.
Но это, наверное, хорошо. Для большинства проектов итоговая блокировка реализации полностью приемлема.
Ссылка: | Утечка абстракций, или Как правильно связать Oracle DATE с Hibernate от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и JOOQ . |