Недавно мы опубликовали статью о том, как правильно связать тип 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 indexPreparedStatement 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
|
@Entitypublic 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;@Converterpublic 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;@Entitypublic 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 . |