Особливості переходу на Hibernate 6. Покращення та зворотна сумісність зі старими версіями
Всім привіт. Я Сергій Моренець, розробник, викладач, спікер та технічний письменник. Хочу поділитися з вами своїм досвідом переходу на нову версію Hibernate 6, яка вийшла два місяці тому. Ця версія розроблялася цілих 6 (!) років і мені як розробнику, який використовує цю технологію з 2008 року, було цікаво на практиці розібрати та застосувати її. Відповідно, досвід міграції кількох наших проєктів і став фундаментом для цієї статті. Ми також розглядаємо Hibernate на нашому тренінгу з роботи з реляційними базами даних. Найголовніше, на що б я хотів звернути увагу — це питання покращень та зворотної сумісності зі старими версіями.
Основні зміни в Hibernate 6
Якщо уважно вивчити документацію, то головне, чим розробники Hibernate змогли похвалитися, це:
- Перехід на JDK 11.
- Підтримка Jakarta Persistence 3.0 (і Jakarta EE 9).
- Поліпшення під час роботи з JDBC (доступ до стовпців у вибірці ResultSet за їх індексом, а не по імені).
- Міграція на ANTLR 4.0 (теж покращення продуктивності).
- Нова Semantic Query Model (SQM), яка використовується як при роботі з JPQL/HQL, так і Criteria API для того, щоб перетворити їх на SQL.
Звичайно, найбільш фундаментальна зміна — це підтримка Jakarta Persistence 3.0, яку ми знали раніше як JPA. Вперше за майже 20 років існування нова специфікація несумісна з попередніми версіями через сумно вiдому заборону Oracle на використання пакетів javax.*. Ця заборона, по-перше, загальмувала розробку самої Jakarta EE, по-друге, порушила зворотну сумісність.
В результаті склалася цікава картина. Jakarta EE 9 все-таки вийшла в грудні
У новій версії розробникам довелося перенести пакети всіх типів з javax.* в jakarta.*, а також усі властивості javax.persistence.* на jakarta.persistence.* та вміст XSD файлів. Така ж процедура загрожує і всім розробникам, але якщо вам не хочеться витрачати на цей час, є автоматичні засоби міграції, як Eclipse Transformer, створений Open Liberty. Ця зміна нічого не дає розробникам, але дозволяє припинити топтання на місці і просунутися в розвитку Enterprise Java. Як результат підтримки Jakarta EE 9 Hibernate теж довелося перейти на JDK 11.
Якщо ви все ж таки пройшли цей етап, давайте поговоримо про те, які ще зміни на вас чекають
Зміни, які можуть вас не порадувати
Проєкт Hibernate стартував у 2001 році, і хоча він був призначений для сегменту корпоративної Java (Java EE), але довгий час розвивався самостійно. І тільки в 2006 році Java-суспільство схаменулося і створило специфікацію JPA 1.0, фактично на основі того API, яке вже було в Hibernate. З цього моменту у вас було два вибори:
- Використовувати Hibernate через прошарок JPA.
- Використовувати без JPA, одержуючи всі ті можливості, які відсутні в JPA (наприклад, формули або кешування
2-го рівня).
З тих пір Hibernate постійно оновлював свій код, прагнучи позбавитися свого внутрішнього API на користь JPA, і це знайшло своє відображення у версії 6.0. По-перше, дуже багато методів з інтерфейсу Session оголошено як deprecated з підозрою, що їх можуть швидко видалити, наприклад, saveOrUpdate (його немає в аналогічному EntityManager з JPA):
* @deprecated use {@link #merge(String, Object)} or {@link #persist(String, Object)}*/
@Deprecated(since = "6.0")
void saveOrUpdate(String entityName, Object object);
Документація пропонує замість нього:
session.saveOrUpdate(station));
використовувати merge або persist (які є в EntityManager):
if (station.getId() == 0) {session.persist(station);
} else {session.merge(station);
}
З одного боку, це є логічним рішенням. З іншого боку, ми якраз і використовували saveOrUpdate, щоб не замислюватися про те, а як визначити, чи це нова сутність або persistent. У тому ж Spring Data в репозиторіях є зручний метод save(), який виконує ту саму функцію. Але якщо для saveOrUpdate є альтернатива, то деякі методи оголошені deprecated без будь-якої заміни:
* @deprecated With no real replacement
*/
@Deprecated( since = "6.0" )
void replicate(String entityName, Object object, ReplicationMode replicationMode) ;
Ще одна біда торкнулася такого компонента як Hibernate Criteria. Коли в JPA 2.0 створювалася аналогічна специфікація (Criteria API), то вона в результаті була реалізована абсолютно несхожим чином, і розробники Hibernate замислилися, що робити зі своїми Criteria. Адже підтримувати відразу два API не так просто. При цьому пікантність ситуації була в тому, що Criteria API був складніший у використанні, і як зауважив один із Hibernate експертів Влад Міхальча, навіть він сам не зможе написати найпростіший Criteria API код без доступу до Інтернету. У результаті у Hibernate 5.2 вони були оголошені deprecated (це
Тепер, якщо у вас у проєкті все ще використовуються Hibernate Criteria:
public List<Station> findAllByCriteria(StationCriteria stationCriteria) { return query(session -> {Criteria criteria = session.createCriteria(Station.class);
if (stationCriteria.transportType() != null) {criteria.add(Restrictions.eq(Station.FIELD_TRANSPORT_TYPE, stationCriteria.transportType()));
}
if (!StringUtils.isEmpty(stationCriteria.name())) {criteria = criteria.createCriteria(Station.FIELD_CITY);
criteria.add(Restrictions.eq(City.FIELD_NAME, stationCriteria.name()));
}
return criteria.list();
});
}
То вам потрібно або не переходити на Hibernate 6, або переписувати цей фрагмент коду на динамічний JPQL, або на JPA Criteria API:
public List<Station> findAllByCriteria(StationCriteria stationCriteria) { return query(session -> {CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<Station> criteria = builder.createQuery(Station.class);
Root<Station> root = criteria.from(Station.class);
List<Predicate> predicates = new ArrayList<>();
if (stationCriteria.transportType() != null) {predicates.add(builder.equal(root.get(Station.FIELD_TRANSPORT_TYPE), stationCriteria.transportType()));
}
if (!StringUtils.isEmpty(stationCriteria.name())) {Join<Station, City> city = root.join(Station.FIELD_CITY);
predicates.add(builder.equal(city.get(City.FIELD_NAME), stationCriteria.name()));
}
Predicate predicate = builder.and(predicates.toArray(new Predicate[] {}));criteria.select(root).where(predicate);
TypedQuery<Station> query = session.createQuery(criteria);
return query.getResultList();
});
}
Ще одна проблема — з Hibernate 6 зник клас SchemaExport та весь його пакет org.hibernate.tool.hbm2ddl. Цей клас зручний тим, що з його допомогою можна згенерувати DDL для створення схеми бази даних:
SchemaExport schema = new SchemaExport();
schema.setDelimiter(";");schema.setOutputFile(folder + "schema_" + dialect.getSimpleName() + ".sql");
schema.create(EnumSet.of(TargetType.SCRIPT), metadata.buildMetadata());
Спершу могло здатися, що клас просто був вилучений, тому що у керівництві міграції про це немає жодного слова. Однак детальне дослідження показує, що ці класи перенесені в нову залежність hibernate-ant. В принципі, таке перенесення логічне, тому що ці класи не використовуються безпосередньо в Hibernate Core, а по суті, потрібні для утилітних завдань. Але згадати про це було б не зайвим. Я створив на це ticket.
Я думаю, всі добре знають таку властивість як Hibernate Dialect. Вона містить специфічні для певної бази даних налаштування та mapping даних. На жаль, Hibernate не завжди може з драйвера дістати тип бази даних і найголовніше — версію (бо вони не завжди повертаються драйвером). Тому, наприклад, для Postgres існує цілих 10 класів діалектів. І так для багатьох інших баз даних. Швидше за все, розробникам Hibernate набридло плодити нові класи діалектів, тому в 6.0 всі вони оголошені deprecated. А як заміна їм доданий новий інтерфейс DatabaseVersion, в реалізаціях якого можна визначити три атрибути версії СУБД — major, minor і micro:
/**
*
* @deprecated use {@code MySQLDialect(800)}*/
@Deprecated
public class MySQL8Dialect extends MySQLDialect { public MySQL8Dialect() {super( DatabaseVersion.make( 8 ) );
Правда, не дуже зрозуміло, як можна буде вказувати важливу властивість hibernate.dialect, якщо ці класи будуть видалені в майбутньому (адже ми вказуємо клас, а не об’єкт).
Під час тестування одного проєкту виявилася справжня проблема, яка ускладнила перехід. Якщо по суті використовується формула:
@Formula("(SELECT COUNT(*) FROM Users)")private int total;
При спробі виконати нативний (SQL) запит:
List<Product> products = session.createNativeQuery("SELECT * FROM PRODUCTS", Product.class).getResultList();викидається виключення:
Caused by: org.hibernate.exception.SQLGrammarException: Unable to find column position by name: total
at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:64)
at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:56)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:109)
Оскільки це працювало в попередніх версіях Hibernate, то це регресія, причини якої не зрозумілі. Я відкрив тикет на цю помилку, але поки що він на стадії аналізу.
Приводи для оптимізму
Є зміни, що тішать око. Наприклад, за аналогією з preview features в JDK в Hibernate додали анотацію @Incubating, яка означає, що цей API/SPI може змінитися незабаром:
@Incubating
public interface Query<R> extends SelectionQuery<R>, MutationQuery, TypedQuery<R> {Як ви бачите, в Hibernate 6 вирішили розділити всі запити на вибірки (selection query) та зміни (mutation query), для чого створили два нові інтерфейси. Тепер ви можете при роботі з Session вказати, який з типів запитів вам потрібен:
MutationQuery mutationQuery = session.createMutationQuery("UPDATE Product SET enabled = 0 WHERE price is null");mutationQuery.executeUpdate();
SelectionQuery<Product> selectionQuery = session.createSelectionQuery("FROM Product", Product.class);Тепер у вас менше можливостей помилитися у запиті, тому що якщо ви вкажете SELECT запит у mutation query, це відразу викличе помилку без відправки його до БД:
MutationQuery mutationQuery = session.createMutationQuery("FROM Product);Яка доля цього покращення? Якщо воно не потрапить до Jakarta Persistence API, то доведеться вдаватися до подібних хитрощів:
Session session = em.unwrap(Session.class);
Ще одне поліпшення — читання з ResultSet за індексом позиції, а не по імені стовпця має підвищити ефективність, а також зменшити розмір генерованого SQL, тому що більше не потрібно вигадувати додаткові aliases для кожного поля у вибірці.
Інтеграція з Spring Data
Оскільки дуже багато розробників використовується Hibernate у зв’язку з Spring Data, цікаво розглянути, як тут просуваються відносини з міграцією. Тут також не все гладко. Розмови про підтримку Hibernate 6 почалися ще в 2018 році, коли озвучувалися плани одночасної інтеграції і Hibernate 5, і Hibernate 6. Але зворотно-несумісний перехід на Jakarta EE 9 це унеможливив, і в результаті інженери Spring вирішили в майбутніх версіях підтримувати тільки
Висновки
Hibernate 6 — нова версія популярного продукту, яка дозволяє йому відповідати специфікації Jakarta EE 9. Хоча вона пропонує поліпшення в плані продуктивності, але в той же час деякі (публічні) API видалені, а деякі — без будь-якої тривіальної заміни, що ускладнює міграцію. Особливість Hibernate в тому, що ви можете використовувати його трьома способами:
- У чистому вигляді.
- Через такий прошарок як JPA.
- Через ще один рівень абстракції — Spring Data.
У цій статті я постарався розібрати всі три випадки, і як виявилося, перехід на Hibernate 6 дуже відрізняється для кожного з них. Також потрібно відзначити, що Hibernate 6 перейшов на JDK 11 як мінімально підтримувану версію, і якщо у вас проєкті все ще JDK 8 (або більш ранні версії), то ви не зможете його використовувати.
2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівДякую за статтю, міграція буде невеселою.
Так, на моїй пам’яті це перший випадок в Enterprise Java після переходу з EJB 1.x на EJB 2.x, коли міграція на нову версію приносить стільки незручностей через несумісність API