В документации hibernate можно встретить термин version
checking. Версионность - это оптимистическая блокировка, она реализована
специальным, внутренним механизмом
Hibernate, который позволяет нам контролировать транзакции. Представьте что
одну и ту же запись, в одно и то же
время редактируют два пользователя, то есть существуют две разные транзакции, и
они перезаписывают данные, чьи изменения применятся, а чьи потеряются? Вот на
этот вопрос и постараемся найти ответ. Оптимистическая блокировка практически всегда,
применяется у базы данных, у которой выставлен read committed transaction
isolation level, этот уровень позволяет быстрее обрабатывать данные поступающие
от различных пользователей, разных транзакций.
И так, ответ на наш вопрос, возможны три различных случая:
1) Last
commit wins - обе транзакции применятся успешно, и вторая транзакция
перезапишет изменения первой транзакции, изменения, внесенные первой
транзакцией, потеряются, ни каких ошибок не будет!
2) First commit wins - первая транзакция
примениться успешно и при коммите (commit) второй транзакции будет выдана
ошибка, данные по второй транзакции потеряются!
3) Merge conflicting updates - первая
транзакция примениться успешно, при коммите второй транзакции будет предложено
объединить результаты двух транзакций, т.е. слить их воедино.
По умолчанию в Hibernate применяется Last
commit wins - для чистоты эксперимента будем запускать 1-ю транзакцию в
отдельном потоке класс Transaction1, имитируем
при этом многопользовательский режим работы, смотрим на листинг:
public class Transaction1 implements Runnable {
private SessionFactory
factory;
private Long id;
public
Transaction1(SessionFactory factory, Long id) {
this.factory = factory;
this.id = new Long(id);
}
@Override
public void run() {
Session session =
factory.openSession();
session.getTransaction().begin();
Person person2 =
(Person)session.get(Person.class,id);
System.out.format("Transaction1 before
change Person: %s\n",person2);
person2.setName("Person Transaction1");
try {
//задержка программы на 3 секунды
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.format("Transaction1 after change Person:
%s\n",person2);
session.getTransaction().commit();
session.close();
}
}
Код второй транзакции, имитация второго
пользователя, будет выполнять основная программа, класс Transaction2 смотрим
листинг:
public class Transaction2 {
private SessionFactory
sessionFactory;
public Transaction2() {
try {
sessionFactory = new
Configuration().configure().buildSessionFactory();
Session session =
null;
try {
session =
sessionFactory.openSession();
session.getTransaction().begin();
Person person =
new Person();
person.setName("Vitaly");
session.save(person);
System.out.printf("Insert Person: %s\n", person);
Long id =
person.getId();
session.getTransaction().commit();
session.close();
Transaction1
transaction2 = new Transaction1(sessionFactory, id);
Thread thread =
new Thread(transaction2);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch
(InterruptedException e) {
e.printStackTrace();
}
session =
sessionFactory.openSession();
session.getTransaction().begin();
person = (Person)
session.get(Person.class, id);
person.setName("Person Transaction2");
System.out.format("Transaction2 after change Person: %s\n",
person);
try {
/*ждем пока 1-я транзакция
завершится и коммитим наши изменения во 2-й транзакции.*/
thread.join();
} catch
(InterruptedException e) {
e.printStackTrace();
}
session.flush();
session.clear();
Query query =
session.createQuery("Select p FROM Person p");
List<Person>
persons = query.list();
for (Person p11 :
persons) {
System.out.printf("Person = %s\n", p11);
}
session.getTransaction().commit();
} finally {
if (session !=
null) session.close();
}
} finally {
if (sessionFactory !=
null) sessionFactory.close();
}
}
public static void main(String
args[]) {
new Transaction2();
}
}
в выводе программы прослеживается потеря
данных внесенных 1-й транзакцией:
Transaction2 after change Person: Person{id=1, name='Person
Transaction2'}
Transaction1 after change Person: Person{id=1, name='Person
Transaction1'}
Hibernate: update Prerson set MyName=? where id=?
Hibernate: update Prerson set MyName=? where id=?
Hibernate: select person0_.id as id0_, person0_.MyName as MyName0_ from
Prerson person0_
Person = Person{id=1, name='Person Transaction2'}
Когда мы включаем версионность, то начинает
работать First
commit wins, а так как Merge conflicting
updates, это частный случай First commit wins, то и он
реализуем для пользователей. Для того что бы включить версионность в Hibernate нужно лишь добавить числовое поле в сущность
(Person) и применить аннотацию @Version к этому полю. По поводу типа поля обычно выбирают тип Long, но
можно использовать типы дат Date или Calendar,
но их не рекомендуют использовать в продуктивной системе, ибо при интенсивной
работе может быть совпадение значений времени в нескольких транзакциях, а при
использовании числового значения hibernate всегда
увеличивает его на одну единицу. Смотрим на листинг, добавляем поле version1:
@Entity
@Table(name = "Prerson")
public class Person implements Serializable {
static final long
serialVersionUID = -7593775012501239455L;
@Id @GeneratedValue(strategy =
GenerationType.IDENTITY)
private Long id;
@Column(name =
"MyName")
private String name;
@Version //просто, добавляем поле и аннотацию, версионность включена
private Long version1;
…
}
В этом примере, Hibernate автоматически все сделает за нас, увеличит на единицу поле version1, когда измениться Person, автоматически проверит поле version1 и если был изменен Person
,
то вызовет исключительную ситуацию org.hibernate.StaleObjectStateException. Запустим нашу программу и посмотрим, как работает hibernate:
Transaction1 before change Person: Person{id=1, name='Vitaly',
version1=0}
Transaction2 before change Person: Person{id=1, name='Vitaly',
version1=0}
// запросили один
и тот же объект и меняем его в двух транзакциях
Hibernate: update Prerson set MyName=?, version1=? where id=? and
version1=?
Transaction1 after change Person: Person{id=1, name='Person
Transaction1', version1=1}
//первая
транзакция изменила объект в Базе Данных, при этом сменилось поле version1 на //единицу, version1=1
Transaction2 after change Person: Person{id=1, name='Person
Transaction2', version1=0}
Hibernate: update Prerson set MyName=?, version1=? where id=? and
version1=?
//пытаемся найти
объект в базе и изменить его, поле id осталось
неизменным, поле version1 уже //не равно
нулю, но мы ищем по ключу where id=1 and version1=0, в базе данных нет совпадения, //hibernate не может обновить данные, вызывает Exception
Exception in thread "main"
org.hibernate.StaleObjectStateException: Row was updated or deleted by another
transaction (or unsaved-value mapping was incorrect): [org.vit.ch1.Person#1]
После того как в сессии произошла любая
ошибка, связанная прямо или косвенно с hibernate, вы должны откатить все
данные транзакции, сделать .rollback(), hibernate не гарантирует что любые объекты с которыми
работала сессия в транзакции будут персистентными (persistent), т.е. правильно
сохранены в таблицах базы данных, если произошла ошибка. Есть и другой способ у hibernate включить оптимистическую блокировку. Для этого нужно добавить аннотацию @OptimisticLocking()
к сущности, а поле @Version private
Long version1 закомментировать. Эта
аннотация принимает параметр type, типа OptimisticLockType:
Тип поля
|
Описание
|
OptimisticLockType.ALL
|
в условии where будут, присутствовать все поля сущности, для
нашего класса Person
sql код будет выглядеть так:
update
Prerson set MyName=? where id=? and MyName=? and MyText=?
|
OptimisticLockType.DIRTY
|
в условии where будут, присутствовать только измененные поля сущности
и идентификатор, поле помеченное
аннотацией @id. Для нашего класса Person получим такой sql код:
update Prerson set MyName=? where id=? and
MyName=?
|
OptimisticLockType.VERSION
|
Применяется с аннотацией @Version. В условии where будет, присутствовать поле помеченное
аннотацией @Version
и
идентификатор . Формируется такой sql код:
update
Prerson set MyName=?, version1=? where id=? and version1=?
|
OptimisticLockType. NONE
|
Действует по умолчанию, в условии where будет, присутствовать поле
помеченное аннотацией @id, идентификатор.
Будет формироваться sql
код,
такого вида:
update
Prerson set MyName=? where
id=?
|
Когда вы будете, применять аннотацию @OptimisticLocking() к
сущности, вы должны добавить так же аннотацию
@DynamicUpdate, которая
разрешает hibernate
использовать дополнительные поля при генерации sql кода после ключевого слова where, если вы конечно будете, только использовать типы полей
OptimisticLockType.ALL или OptimisticLockType.DIRTY, смотрим на листинг, как все вместе будет выглядеть:
@Entity()
@OptimisticLocking(type = OptimisticLockType.ALL) @DynamicUpdate
public class Person implements Serializable {
@Id private Long id;
@Column(name =
"MyName")
private String name;
…
}
Комментариев нет:
Отправить комментарий