Как Hibernate обрабатывает LockMode.PESSIMISTIC_WRITE
? Он обрабатывает это так же, как SELECT FOR UPDATE
в нативных запросах?
Я запускаю 2 экспериментальные транзакции (T1 и T2), которые выполняют тот же собственный SQL, который выбирает объект, используя SELECT FOR UPDATE
, а затем обновляет его. Я запускаю их в 2 потока, чтобы убедиться, что время указано ниже:
1) T1 SELECT ... FOR UPDATE (based on a condition that becomes false after update)
2) T2 SELECT ... FOR UPDATE (based on a condition that becomes false after update)
3) T2 updates the entity
4) T1 updates the entity
Это всегда приводит к тому же потоку и результату, который я понимаю и ожидаю:
1) T1 reads the single entity based on a condition
2) T2 doesn't select anything
3) T1 updates the entity so that it cannot be selected based on the condition anymore
4) T2 doesn't update anything
Сущность имеет тип FIELD
, который имеет 4 атрибута: ID, NAME, TYPE, DESCRIPTION
. Только DESCRIPTION
обновляется.
Вот код для запуска такой транзакции:
private void selectForUpdateNativeQueryTransaction(String name, int delayBeforeRead, int delayBeforeCommit) {
Transaction tx = null;
Session session = null;
try {
session = factory.openSession();
tx = session.beginTransaction();
System.out.println(name + " BEGIN");
try {
TimeUnit.SECONDS.sleep(delayBeforeRead);
} catch (InterruptedException e) {
e.printStackTrace();
}
SQLQuery sqlQuery = session.createSQLQuery("SELECT * FROM FIELD WHERE DESCRIPTION=?1 FOR UPDATE");
sqlQuery.setParameter("1", DESC);
List results = sqlQuery.list();
System.out.println(name + " SELECT EXECUTED");
results.forEach(r -> {
Object[] row = (Object[]) r;
for (int i = 0; i < row.length; i++) {
System.out.println(name + " : FIELD READ [" + row[i] + "]");
}
});
try {
TimeUnit.SECONDS.sleep(delayBeforeCommit);
} catch (InterruptedException e) {
e.printStackTrace();
}
Query query = session.createSQLQuery("UPDATE FIELD SET DESCRIPTION=?1 WHERE DESCRIPTION=?2");
query.setParameter("1", name);
query.setParameter("2", DESC);
int result = query.executeUpdate();
System.out.println(name + " UPDATED ROWS: " + result);
tx.commit();
} catch (Exception e) {
fail();
if (tx != null) {
tx.rollback();
}
} finally {
session.close();
}
System.out.println(name + " COMMITTED");
}
Ниже приведен код для запуска обеих транзакций в разных потоках:
@Test
public void firstReadsTheOtherRejected() {
ExecutorService es = Executors.newFixedThreadPool(3);
Runnable selectForUpdateNativeQueryTransaction1 = () -> {
selectForUpdateNativeQueryTransaction("T1", 1, 8);
};
Runnable selectForUpdateNativeQueryTransaction2 = () -> {
selectForUpdateNativeQueryTransaction("T2", 3, 2);
};
Runnable selectForUpdateHqlQueryTransaction1 = () -> {
selectForUpdateHqlQueryTransaction("T1", 1, 8);
};
Runnable selectForUpdateHqlQueryTransaction2 = () -> {
selectForUpdateHqlQueryTransaction("T2", 3, 2);
};
// es.execute(selectForUpdateHqlQueryTransaction1);
// es.execute(selectForUpdateHqlQueryTransaction2);
es.execute(selectForUpdateNativeQueryTransaction1);
es.execute(selectForUpdateNativeQueryTransaction2);
es.shutdown();
try {
es.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
А вот и вывод:
T2 BEGIN
T1 BEGIN
Hibernate: SELECT * FROM FIELD WHERE DESCRIPTION=? FOR UPDATE
T1 SELECT EXECUTED
T1 : FIELD READ [16]
T1 : FIELD READ [Test field]
T1 : FIELD READ [Test type]
T1 : FIELD READ [This is a field for testing]
Hibernate: SELECT * FROM FIELD WHERE DESCRIPTION=? FOR UPDATE
Hibernate: UPDATE FIELD SET DESCRIPTION=? WHERE DESCRIPTION=?
T1 UPDATED ROWS: 1
T1 COMMITTED
T2 SELECT EXECUTED
Hibernate: UPDATE FIELD SET DESCRIPTION=? WHERE DESCRIPTION=?
T2 UPDATED ROWS: 0
T2 COMMITTED
Примечание: T2 UPDATED ROWS: 0
Однако, если я пытаюсь добиться того же, используя Hql, это дает мне странный вывод.
Вот код для транзакции:
private void selectForUpdateHqlQueryTransaction(String name, int delayBeforeRead, int delayBeforeCommit) {
Transaction tx = null;
Session session = null;
try {
session = factory.withOptions().interceptor(new HibernateInterceptor()).openSession();
tx = session.beginTransaction();
System.out.println(name + " BEGIN");
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);
List fields = query.list();
System.out.println(name + " SELECT EXECUTED");
fields.forEach(obj -> {
Field field = (Field) obj;
System.out.println(name + " : FIELD READ [" + field.getId() + "]");
System.out.println(name + " : FIELD READ [" + field.getName() + "]");
System.out.println(name + " : FIELD READ [" + field.getType() + "]");
System.out.println(name + " : FIELD READ [" + field.getDescription() + "]");
});
try {
TimeUnit.SECONDS.sleep(delayBeforeCommit);
} catch (InterruptedException e) {
e.printStackTrace();
}
Session finalSession = session;
fields.forEach(obj -> {
Field field = (Field) obj;
field.setDescription(name);
finalSession.update(field);
});
System.out.println(name + " UPDATED ROWS: " + fields.size());
tx.commit();
} catch (Exception e) {
fail();
if (tx != null) {
tx.rollback();
}
} finally {
session.close();
}
System.out.println(name + " COMMITTED");
}
Выход:
T2 BEGIN
T1 BEGIN
апр 22, 2019 4:29:37 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]
апр 22, 2019 4:29:37 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 SELECT EXECUTED
T1 : FIELD READ [16]
T1 : FIELD READ [Test field]
T1 : FIELD READ [Test type]
T1 : FIELD READ [This is a field for testing]
апр 22, 2019 4:29:39 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]
апр 22, 2019 4:29:39 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 UPDATED ROWS: 1
previousState in onFlushDirty(): []
previousState in onFlushDirty(): This is a field for testing
previousState in onFlushDirty(): Test field
previousState in onFlushDirty(): []
previousState in onFlushDirty(): Test type
currentState in onFlushDirty(): []
currentState in onFlushDirty(): T1
currentState in onFlushDirty(): Test field
currentState in onFlushDirty(): []
currentState in onFlushDirty(): Test type
Hibernate: update FIELD set DESCRIPTION=?, NAME=?, TYPE=? where ID=?
T2 SELECT EXECUTED
T1 COMMITTED
T2 : FIELD READ [16]
T2 : FIELD READ [Test field]
T2 : FIELD READ [Test type]
T2 : FIELD READ [This is a field for testing]
T2 UPDATED ROWS: 1
previousState in onFlushDirty(): []
previousState in onFlushDirty(): This is a field for testing
previousState in onFlushDirty(): Test field
previousState in onFlushDirty(): []
previousState in onFlushDirty(): Test type
currentState in onFlushDirty(): []
currentState in onFlushDirty(): T2
currentState in onFlushDirty(): Test field
currentState in onFlushDirty(): []
currentState in onFlushDirty(): Test type
Hibernate: update FIELD set DESCRIPTION=?, NAME=?, TYPE=? where ID=?
T2 COMMITTED
Как вы, возможно, заметили, я использую Hibernate Interceptor для перехвата операций SQL. Interceptor.onFlushDirty()
вызывается до SQL UPDATE
. Поэтому мне не понятно, почему я вижу в выходных данных обе транзакции, обновляющие значение:
...
currentState in onFlushDirty(): T1
...
currentState in onFlushDirty(): T2
...
Первый вопрос был:
Действительно ли только 1 обновление или обе транзакции обновляют значение? Должно быть только 1 обновление, так как я изменил код, отвечающий за UPDATE, чтобы он не перезаписывал старое значение, а добавил к нему имя транзакции. После того, как обе транзакции были завершены, столбец в конце не содержал что-то вроде "... T1: T2". Это хорошо.
Но следующий вопрос был:
Почему версия HQL выбирает старую сущность, а собственная версия SQL - нет? Это из-за кеширования? Я хочу точно знать, какая транзакция "удалась", а какая нет.