Статьи

Темная сторона Hibernate AUTO flush

Вступление

Теперь, когда я описал основы стратегий сброса JPA и Hibernate , я могу продолжить раскрывать удивительное поведение режима сброса AUTO Hibernate .

Не все запросы вызывают сброс сеанса

Многие полагают, что Hibernate всегда сбрасывает сессию перед выполнением любого запроса. Хотя этот подход мог бы быть более интуитивным и, вероятно, ближе к AUTO FlushModeType JPA , Hibernate пытается оптимизировать его. Если текущий выполненный запрос не попадет в ожидающие операторы SQL INSERT / UPDATE / DELETE, очистка не обязательна.

Как указано в справочной документации , стратегия сброса AUTO может иногда синхронизировать текущий контекст постоянства перед выполнением запроса. Было бы более интуитивно понятно, если бы авторы фреймворка решили назвать его FlushMode.SOMETIMES.

JPQL / HQL и SQL

Как и многие другие решения ORM, Hibernate предлагает ограниченный язык запросов сущностей ( JPQL / HQL ), который в значительной степени основан на синтаксисе SQL-92 .

Язык запросов сущностей переводится в SQL текущим диалектом базы данных, поэтому он должен предлагать одинаковые функциональные возможности для разных продуктов баз данных. Поскольку большинство систем баз данных относятся к жалобам SQL-92, Entity Query Language является абстракцией наиболее распространенного синтаксиса запросов к базам данных.

Хотя вы можете использовать Entity Query Language во многих случаях (выбирая Entities и даже проекции), бывают случаи, когда его ограниченные возможности не соответствуют расширенному запросу. Всякий раз, когда мы хотим использовать некоторые конкретные методы запросов, такие как:

у нас нет другого выбора, кроме как запускать собственные запросы SQL.

Hibernate — это постоянная структура. Hibernate никогда не предназначался для замены SQL. Если какой-то запрос лучше выражен в собственном запросе, то не стоит жертвовать производительностью приложения на алтаре переносимости базы данных.

AUTO flush и HQL / JPQL

Сначала мы собираемся проверить, как ведет себя режим сброса AUTO, когда HQL-запрос должен быть выполнен. Для этого мы определяем следующие несвязанные объекты:

flushautoentities

Тест выполнит следующие действия:

  • Человек будет настойчивым.
  • Выбор пользователя (ей) не должен вызывать сброс.
  • Запрашивая Person, сброс AUTO должен инициировать синхронизацию перехода состояния объекта (INSERT person должен быть выполнен до выполнения запроса select).
1
2
3
4
Product product = new Product();
session.persist(product);
assertEquals(0L,  session.createQuery("select count(id) from User").uniqueResult());
assertEquals(product.getId(), session.createQuery("select p.id from Product p").uniqueResult());

Предоставление следующего вывода SQL:

1
2
3
4
[main]: o.h.e.i.AbstractSaveEventListener - Generated identifier: f76f61e2-f3e3-4ea4-8f44-82e9804ceed0, using strategy: org.hibernate.id.UUIDGenerator
Query:{[select count(user0_.id) as col_0_0_ from user user0_][]}
Query:{[insert into product (color, id) values (?, ?)][12,f76f61e2-f3e3-4ea4-8f44-82e9804ceed0]}
Query:{[select product0_.id as col_0_0_ from product product0_][]}

Как видите, выбор пользователя не вызвал сброс сеанса. Это связано с тем, что Hibernate проверяет текущее пространство запросов относительно ожидающих операторов таблицы. Если текущий выполняющийся запрос не перекрывается с незапущенными операторами таблицы, сброс может быть безопасно проигнорирован.

HQL может обнаружить сброс продукта даже для:

  • Подразделы выбирает
    1
    2
    3
    4
    5
    session.persist(product);
    assertEquals(0L,  session.createQuery(
        "select count(*) " +
        "from User u " +
        "where u.favoriteColor in (select distinct(p.color) from Product p)").uniqueResult());

    В результате получается правильный флеш-колл:

    1
    2
    Query:{[insert into product (color, id) values (?, ?)][Blue,2d9d1b4f-eaee-45f1-a480-120eb66da9e8]}
    Query:{[select count(*) as col_0_0_ from user user0_ where user0_.favoriteColor in (select distinct product1_.color from product product1_)][]}
  • Или тета-стиль присоединяется
    1
    2
    3
    4
    5
    session.persist(product);
    assertEquals(0L,  session.createQuery(
        "select count(*) " +
        "from User u, Product p " +
        "where u.favoriteColor = p.color").uniqueResult());

    Запуск ожидаемого сброса:

    1
    2
    Query:{[insert into product (color, id) values (?, ?)][Blue,4af0b843-da3f-4b38-aa42-1e590db186a9]}
    Query:{[select count(*) as col_0_0_ from user user0_ cross join product product1_ where user0_.favoriteColor=product1_.color][]}

Причина, по которой это работает, заключается в том, что Entity Queries анализируются и транслируются в SQL-запросы. Hibernate не может ссылаться на несуществующую таблицу, поэтому он всегда знает таблицы базы данных, по которым будет выполнен запрос HQL / JPQL.

Таким образом, Hibernate знает только те таблицы, на которые мы явно ссылаемся в нашем HQL-запросе. Если текущие ожидающие операторы DML подразумевают триггеры базы данных или каскадирование на уровне базы данных, Hibernate не будет знать об этом. Таким образом, даже для HQL режим очистки AUTO может вызвать проблемы согласованности.

АВТОМАТИЧЕСКИЙ сброс и собственный SQL-запрос

Когда дело касается нативных запросов SQL, все становится намного сложнее. Hibernate не может анализировать запросы SQL, поскольку он поддерживает только ограниченный синтаксис запросов к базе данных. Многие системы баз данных предлагают собственные функции, которые выходят за рамки возможностей Hibernate Entity Query.

Запросы к таблице Person с собственным SQL-запросом не будут вызывать сброс, вызывая проблему несоответствия:

1
2
3
Product product = new Product();
session.persist(product);
assertNull(session.createSQLQuery("select id from product").uniqueResult());
1
2
3
DEBUG [main]: o.h.e.i.AbstractSaveEventListener - Generated identifier: 718b84d8-9270-48f3-86ff-0b8da7f9af7c, using strategy: org.hibernate.id.UUIDGenerator
Query:{[select id from product][]}
Query:{[insert into product (color, id) values (?, ?)][12,718b84d8-9270-48f3-86ff-0b8da7f9af7c]}

Недавно сохраненный Продукт был вставлен только во время фиксации транзакции, поскольку собственный запрос SQL не вызвал сброс. Это основная проблема согласованности, которую трудно отладить или даже предвидеть многие разработчики. Это еще одна причина для постоянной проверки автоматически сгенерированных операторов SQL .

Такое же поведение наблюдается даже для именованных собственных запросов:

1
2
3
4
@NamedNativeQueries(
    @NamedNativeQuery(name = "product_ids", query = "select id from product")
)
assertNull(session.getNamedQuery("product_ids").uniqueResult());

Таким образом, даже если запрос SQL предварительно загружен, Hibernate не извлечет связанное пространство запроса для сопоставления его с ожидающими инструкциями DML.

Отмена текущей стратегии сброса

Даже если текущий сеанс определяет стратегию сброса по умолчанию, вы всегда можете переопределить ее на основе запроса.

Режим сброса запросов

Режим ВСЕГДА сбрасывает контекст постоянства перед выполнением любого запроса (HQL или SQL). На этот раз Hibernate не применяет оптимизацию, и все ожидающие переходы состояния объекта будут синхронизированы с текущей транзакцией базы данных.

1
assertEquals(product.getId(), session.createSQLQuery("select id from product").setFlushMode(FlushMode.ALWAYS).uniqueResult());

Указание Hibernate, какие таблицы должны быть синхронизированы

Вы также можете добавить правило синхронизации в текущий выполняемый SQL-запрос. После этого Hibernate узнает, какие таблицы базы данных необходимо синхронизировать перед выполнением запроса. Это также полезно для кэширования второго уровня .

1
assertEquals(product.getId(), session.createSQLQuery("select id from product").addSynchronizedEntityClass(Product.class).uniqueResult());

Вывод

Режим очистки AUTO сложен, и устранение проблем согласованности на основе запросов — кошмар сопровождающего. Если вы решите добавить триггер базы данных, вам придется проверить все запросы Hibernate, чтобы убедиться, что они не будут работать с устаревшими данными.

Я предлагаю использовать ВСЕГДА режим сброса, даже если авторы Hibernate предупредили нас, что:

эта стратегия почти всегда не нужна и неэффективна.

Несоответствие является гораздо более серьезной проблемой, чем некоторые преждевременные приливы. Хотя смешивание операций и запросов DML может привести к ненужной очистке, эту ситуацию не так сложно исправить. Во время сеансовой транзакции лучше всего выполнять запросы в начале (когда не должно быть синхронизировано ни одного ожидающего перехода состояния объекта) и ближе к концу транзакции (когда текущий контекст персистентности все равно будет сброшен).

Операции перехода между состояниями сущностей должны быть перенесены в конец транзакции, чтобы избежать их чередования с операциями запроса (поэтому предотвращается преждевременный запуск сброса).