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=?
Комментариев нет:
Отправить комментарий