Я использую Hibernate 5 и Oracle 12. С помощью приведенного ниже запроса я хочу произвольно выбрать сущность из набора сущностей:
Query query = getSession().createQuery("SELECT e FROM Entity e ... <CONDITIONS> ... AND ROWNUM = 1");
Optional<Entity> entity = query.list().stream().findAny();
// Change the entity in some way. The changes will also make sure that the entity won't appear in the next query run based on <CONDITIONS>
...
Это работает, но только если все транзакции, выполняющие код, выполняютсяпоследовательно.Поэтому я также хочу убедиться, что уже прочитанная сущность не будет прочитана в другой транзакции.Я попробовал это с блокировкой:
Query query = getSession().createQuery("SELECT e FROM Entity e ... <CONDITIONS> ... AND ROWNUM = 1")
.setLockMode("this", LockMode.PESSIMISTIC_READ);
Но похоже, что Hibernate преобразует эту конструкцию в SELECT ... FOR UPDATE
, которая не мешает другой транзакции прочитать сущность, ожидая, пока другие транзакции, использующие ее, не завершат, а затемприменяя свои собственные изменения к сущности.
Можно ли установить какую-либо блокировку для сущности, чтобы она гарантированно исчезла из результата запроса в другой транзакции?
Я написал несколькоэкспериментальный код, чтобы понять, как работает блокировка в Hibernate.Он моделирует две транзакции, ключевые шаги которых (выбор и фиксация) могут быть выполнены в различном порядке путем настройки параметров метода transaction()
.На этот раз Field
используется вместо Entity
, но это не имеет значения.Каждая транзакция читает тот же Field
, обновляет свой атрибут description
и фиксирует.
private static final LockMode lockMode = LockMode.PESSIMISTIC_WRITE;
enum Order {T1_READS_EARLIER_COMMITS_LATER, T2_READS_EARLIER_COMMITS_LATER};
@Test
public void firstReadsTheOtherRejected() {
ExecutorService es = Executors.newFixedThreadPool(3);
// It looks like the transaction that commits first is the only transaction that can make changes.
// The changes of the other one will be ignored.
final Order order = Order.T1_READS_EARLIER_COMMITS_LATER;
// final Order order = Order.T2_READS_EARLIER_COMMITS_LATER;
es.execute(() -> {
switch (order) {
case T1_READS_EARLIER_COMMITS_LATER:
transaction("T1", 1, 8);
break;
case T2_READS_EARLIER_COMMITS_LATER:
transaction("T1", 4, 1);
break;
}
});
es.execute(() -> {
switch (order) {
case T1_READS_EARLIER_COMMITS_LATER:
transaction("T2", 4, 1);
break;
case T2_READS_EARLIER_COMMITS_LATER:
transaction("T2", 1, 8);
break;
}
});
es.shutdown();
try {
es.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void transaction(String name, int delayBeforeRead, int delayBeforeCommit) {
Transaction tx = null;
Session session = null;
try {
session = factory.openSession();
tx = session.beginTransaction();
try {
TimeUnit.SECONDS.sleep(delayBeforeRead);
} catch (InterruptedException e) {
e.printStackTrace();
}
Query query = session.createQuery("SELECT f FROM Field f WHERE f.description=?1").setLockMode("this", lockMode);
query.setString("1", DESC);
Field field = (Field) query.uniqueResult();
String description1 = field.getDescription();
System.out.println(name + " : FIELD READ " + description1);
try {
TimeUnit.SECONDS.sleep(delayBeforeCommit);
} catch (InterruptedException e) {
e.printStackTrace();
}
field.setDescription(name);
session.update(field);
System.out.println(name + " : FIELD UPDATED");
tx.commit();
} catch (Exception e) {
fail();
if (tx != null) {
tx.rollback();
}
} finally {
session.close();
}
System.out.println(name + " : COMMITTED");
}
и вывод:
T1 : FIELD READ This is a field for testing
апр 19, 2019 5:28:01 PM org.hibernate.loader.Loader determineFollowOnLockMode
WARN: HHH000445: Alias-specific lock modes requested, which is not currently supported with follow-on locking; all acquired locks will be [PESSIMISTIC_WRITE]
апр 19, 2019 5:28:01 PM org.hibernate.loader.Loader shouldUseFollowOnLocking
WARN: HHH000444: Encountered request for locking however dialect reports that database prefers locking be done in a separate select (follow-on locking); results will be locked after initial query executes
Hibernate: select field0_.ID as ID1_9_, field0_.DESCRIPTION as DESCRIPTION2_9_, field0_.NAME as NAME3_9_, field0_.TYPE as TYPE4_9_ from FIELD field0_ where field0_.DESCRIPTION=?
Hibernate: select ID from FIELD where ID =? for update
T1 : FIELD UPDATED
Hibernate: update FIELD set DESCRIPTION=?, NAME=?, TYPE=? where ID=?
T2 : FIELD READ This is a field for testing
T1 : COMMITTED
апр 19, 2019 5:28:07 PM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
T2 : FIELD UPDATED
Hibernate: update FIELD set DESCRIPTION=?, NAME=?, TYPE=? where ID=?
INFO: HHH000030: Cleaning up connection pool [jdbc:oracle:thin:@localhost:1521:oracle]
T2 : COMMITTED
Process finished with exit code 0
После выполнения столбец description
содержит T2
.Похоже, режим pessimistic_write
работает.Сделка, которая написана первой - выиграна.И это было T2
.Но что случилось с T1?T1 : COMMITTED
также видно на выходе.Пока T1
ничего не меняет, это приемлемо для меня, но мне нужен индикатор того, что T1
не удалось, чтобы я мог повторить чтение / выбор.
Я был не прав.Я запускал код несколько раз и с разными результатами.Иногда описание столбца содержит T1, иногда T2.