4.22.2013

Hibernate интересные моменты часть 3.

Версионность - это оптимистическая блокировка, она реализована специальным, внутренним механизмом в Hibernate, который позволяет нам  контролировать транзакции. Зачем она (версионность) нам нужна - представте что одну и ту же запись, практически в одно и то же время редактируют 2 пользователя (две разные транзакции) и перезаписывают данные, чьи изменения применятся, а чьи потеряются? Вот на этот вопрос я и постараюсь ответить в этой статье. Оптимистическая блокировка обычно (практически всегда)  применяется в месте с базой данных у которой выставлен read committed transaction isolation level, который позволяет наилучше и наибыстрее обрабатывать данные поступающие от различных пользователей (транзакций). И так ответ на вопрос - возможны 3 случая:
  1. Last commit wins - обе транзакции применятся успешно, и 2-я транзакция перезапишит изменения 1-й транзакции, изменения внесенные 1-й транзакцией потеряются, ни каких ошибок не будет!
  2. First commit wins - 1-я транзакция примениться успешно и при коммите (commit) 2-й транзакции будет выдана ошибка, даные 2-й транзакции потеряются!
  3. 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=?

4.18.2013

Hibernate интересные моменты часть 2

Эту статью я бы хотел посвятить транзакциям и как они реализованы в Hibernate. Сначала давайте расмотрим какие интересные моменты могут случаться в "черном ящике" - Баз Данных (oracle,mysql,interbase,mssql ...):

  1. lost update -  может случиться когда две транзакции одновременно обновляют, выполняют операцию update над некоторой записью и 2 транзакция (самая последния транзакция по времени) заканчиваеться не удачно, она откатывает все изменения внесенные 1 и 2 транзакцией. Такая ситуация может случиться когда БД не использует блокировок (locking) и конкурирующие транзакции (concurrent transactions) в ней  не изолированны друг от друга.
  2. dirty read - может случиться когда 1-я транзакция изменяет строку, 2-я транзакция читает эту строку, 1-я откатывает изменения зделанные в транзакции. 2-я транзакция имеет не достоверные данные.
  3. unrepeatable read - может случиться когда 1-я транзакция дважды читает одну и ту же строку, и получает разные результаты, в промежутке между двумя чтениями 2-я транзакция изменяет ту же строку, и 1-я транзакция при повторном чтении получает новые данные. Так же частным случаем unrepeatable read является  проблема second lost updates problem. Представте себе что две конкурирующих транзакций одновременно читают одну и туже строчку, потом 1-я транзакция изменяет её и завершается удачно(commit), потом 2-я транзакция изменяет ту же строчку и перезаписывает данные, при этом теряются все изменения внесенные 1-й транзакцией.
  4. phantom read - может случиться когда одна и та же транзакция читает строки дважды через какой-то промежуток времени, и 2-я транзакция вставляет новую строчку или удаляет, до второго чтения.
И для предотвращения этих казусов в ANSI SQL стандарте предусмотрены standard isolation levels:
  1. read uncommitted transaction isolation - сдесь может случиться  dirty read,  но никогда не случиться lost update. Ни какая транзакция не может перезаписать не подтвержденные данные, достигается за счет блокировки (exclusive write locks), однако они могут быть прочтены любой транзакцией.
  2.  read committed transaction isolation - сдесь может случиться  unrepeatable read, но никогда не случиться dirty read. Транзакции чтения ни когда не блокируют читаемые строки, но при изменении данных, блокируются изменяемые строки.
  3. repeatable read transaction isolation - сдесь может случиться  phantom read, но никогда не случиться dirty read и unrepeatable read. Транзакции чтения блокируют данные строки. И в них не могут писать, ни какие данные транзакции, читать могут все. Транзакции записи блокирют все другие транзакции.
  4. serializable transaction isolation - представте очередь из транзакций и они выполняются одна, за другой по "очереди".
И так отсюда следует что, чем выше уровень транзакции тем не поворотнее становиться работа с базой данных, по этому нужно правильно выбрать устраивающий вас уровень, чем и займемся далее.Каждая база данных имеет по умолчанию transaction isolation level - обычно это read committed (чаще встречается) или repeatable read. Вы конечно можете изминить этот уровень в объекте java.sql.Connection драйвера JDBC:

1 - Read uncommitted isolation
2 - Read committed isolation
4 - Repeatable read isolation
8 - Serializable isolation

Или в Hibernate в параметре hibernate.connection.isolation проставить нужный вам уровень изоляции транзакции:

    <?xml version='1.0' encoding='utf-8'?>
    <!DOCTYPE hibernate-configuration PUBLIC
            "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
            "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
    <hibernate-configuration>
        <session-factory>
            <!-- Database connection settings -->
            <property name="connection.driver_class">org.h2.Driver</property>
            <property name="connection.url">jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE</property>
            <property name="connection.username">sa</property>
            <property name="connection.password"></property>
    ...        
            <property name="connection.isolation">1</property>
            <!-- 1-Read uncommitted isolation
                 2-Read committed isolation
                 4-Repeatable read isolation
                 8-Serializable isolation   -->
    ...
        </session-factory>
    </hibernate-configuration> 
    
    при запуске приложения в логе hibernate  появятся такие строчки:
    17.04.2013 21:45:31 org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
    INFO: HHH000149: JDBC isolation level: READ_UNCOMMITTED
    
    PS.
    если вы используете пул подключения баз данных ч/з application server - то меняйте настройки у него (у сервера), параметр hibernate.connection.isolation не влияет на настройки сервера. Так же хочу сказать что у Hibernate реализованы 2 механизма контроля транзакций - это version checking (версионность - оптимистическая блокировка,  реализован специальный внутренний механизм в Hibernate) и pessimistic locking (песимистическая блокировка - реализовано блокировками  уровня баз данных). Надеюсь вскоре я напишу пару статей посвященных блокировкам в Hibernate.