Hibernate интересные моменты часть 3.
Версионность - это оптимистическая блокировка, она реализована специальным, внутренним механизмом в Hibernate, который позволяет нам контролировать транзакции. Зачем она (версионность) нам нужна - представте что одну и ту же запись, практически в одно и то же время редактируют 2 пользователя (две разные транзакции) и перезаписывают данные, чьи изменения применятся, а чьи потеряются? Вот на этот вопрос я и постараюсь ответить в этой статье. Оптимистическая блокировка обычно (практически всегда) применяется в месте с базой данных у которой выставлен read committed transaction isolation level, который позволяет наилучше и наибыстрее обрабатывать данные поступающие от различных пользователей (транзакций). И так ответ на вопрос - возможны 3 случая:
- Last commit wins - обе транзакции применятся успешно, и 2-я транзакция перезапишит изменения 1-й транзакции, изменения внесенные 1-й транзакцией потеряются, ни каких ошибок не будет!
- First commit wins - 1-я транзакция примениться успешно и при коммите (commit) 2-й транзакции будет выдана ошибка, даные 2-й транзакции потеряются!
- Merge conflicting updates - 1-я транзакция примениться успешно, при коммите 2-й транзакции будет предложено объединить результаты двух транзакций.
По умолчанию в Hibernate применяется Last commit wins - для чистоты эксперимента будем запускать 2-ю транзакцию в отдельном потоке, имитирую при этом многопользовательский режим работы:
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import java.util.concurrent.TimeUnit;
public class OneSession implements Runnable {
private SessionFactory factory;
private Long id;
public OneSession(SessionFactory factory, Long id) {
this.factory = factory;
this.id = new Long(id);
}
@Override
public void run() {
Session session = factory.openSession();
session.beginTransaction();
Person person2 = (Person) session.get(Person.class, id);
person2.setSureName("Insert Thread %s\n" + Thread.currentThread().getName());
try {
//выжидаем 3 секунды - даем main запросить не измененный Person и применяем изменения
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
session.getTransaction().commit();
session.close();
}
}
код основной программы:
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class Main {
private SessionFactory sessionFactory;
public Main() {
try {
sessionFactory = new Configuration().configure().buildSessionFactory();
Long id = insertPersons(sessionFactory);
OneSession oneSession = new OneSession(sessionFactory, id);
Thread thread = new Thread(oneSession);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Session session = sessionFactory.openSession();
session.getTransaction().begin();
Person person2 = (Person) session.get(Person.class, id);
person2.setSureName("Main!");
try {
//ждем пока 1-я транзакция завершится и коммитем наши изменения во 2-й транзакции.
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
session.getTransaction().commit();
session.close();
session = sessionFactory.openSession();
session.getTransaction().begin();
person2 = (Person) session.get(Person.class, id);
System.out.printf("Person after change: %s\n", person2);
session.getTransaction().commit();
session.close();
} finally {
if (sessionFactory != null) sessionFactory.close();
}
}
private Long insertPersons(SessionFactory sessionFactory) {
Session session = sessionFactory.openSession();
session.beginTransaction();
Person person = new Person();
person.setFirstName("Vitaly");
person.setSureName("Lopanov");
Set<Phone> phones = new HashSet<Phone>();
Phone phone1 = new Phone();
phone1.setNumber("515555");
Phone phone2 = new Phone();
phone2.setNumber("525555");
phones.add(phone1);
phones.add(phone2);
person.setPhones(phones);
session.save(person);
System.out.printf("Person before change: %s\n", person);
session.getTransaction().commit();
session.close();
return person.getId();
}
public static void main(String args[]) {
new Main();
}
}
в выводе программы прослеживается потеря данных внесенных 1-й транзакцией:
.... Person before change: Person(id=1, firstName=Vitaly, sureName=Lopanov, phones=[Phone(id=1, number=525555), Phone(id=2, number=515555)]) .... Person after change: Person(id=1, firstName=Vitaly, sureName=Main!, phones=[Phone(id=1, number=525555), Phone(id=2, number=515555)])
Когда мы включаем версионность - то начинает работать First commit wins, а так как Merge conflicting updates - это часный случай First commit wins, то и он реализуем для пользователей. Для того что бы включить версионность в Hibernate нужно лишь добавить числовое поле в сушность(Entity):
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
@Entity @Table(name = "Persons") @ToString @EqualsAndHashCode
public class Person implements Serializable {
static final long serialVersionUID = -7593775012501239455L;
@Id @GeneratedValue() @Column(name = "personId")
private Long id;
@Version //включаем версионность
private Long version1;
@Column(name = "fName")
private String firstName;
@Column(name = "sName")
private String sureName;
@OneToMany(cascade = CascadeType.ALL,orphanRemoval = false)
@JoinColumn(name = "phoneId1",referencedColumnName = "personId")
private Set<Phone> phones;
... getters and setters
}
и Hibernate автоматически все сделает за нас. Hibernate увеличит на единицу поле version1, когда измениться Person, автоматически проверит поле version1 и если был изменен Person - то вызовет исключительную ситуацию org.hibernate.StaleObjectStateException. Опять запустим нашу программу и посмотрим как работает hibernate:
До изменения Person: Person(id=1, version1=0, firstName=Vitaly, sureName=Insert Thread, phones=[Phone(id=1, number=525555), Phone(id=2, number=515555)]) Hibernate: update Persons set fName=Vitaly, sName=Insert Thread, version1=1 where personId=1 and version1=0 1-я транзакция закончилась успешно поле version1=1
Пытаемся применить вторую транзакцию Hibernate: update Persons set fName=Vitaly, sName=Main!, version1=1 where personId=1 and version1=0 т.к. такого поля version1=0 уже не существует то Hibernete проверит количество измененных строк (row count) который вернул нам JDBC driver и вызовет исключительную ситуацию ... Exception in thread "main" org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [Person#1]
В принципе есть другой способ включить оптимистическую блокировку добавить optimisticLock, а поле закоментировать //@Version private Long version1; :
...
@Entity @Table(name = "Persons") @ToString @EqualsAndHashCode
@org.hibernate.annotations.Entity(
optimisticLock = org.hibernate.annotations.OptimisticLockType.ALL
,dynamicUpdate = true
)
public class Person implements Serializable {
....
OptimisticLockType.ALL в условии where присутствуют все поля: update Persons set sName=? where personId=? and fName=? and sName=? OptimisticLockType.DIRTY в условии where присутствуют только измененные поля: Hibernate: update Persons set sName=? where personId=? and sName=?