Transkacje JPA


Transkacje JPA

Ten wpis jest kontynuacją tematu transakcji także gorąco zachęcam do zapoznania się z artykułem w którym omówiono mechanizmy izolacji transakcji z użyciem JDBC.

https://javaleader.pl/2019/10/03/transakcje-jdbc-z-uzyciem-bazy-h2-i-mysql/

W przypadku JDBC poziom izolacji transakcji ustanawiany jest dla danego połączenia z bazą danych. Dalej odpowiedzialność za równoległe wykonywanie transakcji przekazane jest do danego systemu RDBMS. JPA czyli standard mapowania relacyjno-obiektowego w Javie zapewnia warstwę abstrakcji między aplikacją a bazą danych. W JPA wyróżniamy dwa podejścia do tematu równoległego przetwarzania transakcji:

Podejście optymistyczne

Podejście optymistyczne zakłada, że aktualnie wykonywana transakcja jest jedyną transakcją która dokonuje zmian na bazie danych. Model ten optymistycznie zakłada, że modyfikowanie i odczytywanie tych samych danych przez różnych użytkowników jest mało prawdopodobne. W związku z tym można przeprowadzić transakcję bez blokowania zasobów (do czasu zatwierdzenia transakcji – polecenie commit() weryfikacja konfliktu jest opóźniona). Jeśli w trakcie wykonywania transakcji stan bazy danych ulegnie zmianie na skutek wykonywania innej transakcji która modyfikuje stan encji to otrzymamy wyjątek OptimisticLockException. Transakcja zostanie wtedy wycofana – (z ang. rollback). Informacja o zmianie stanu encji przechowywana jest w specjalnym polu oznaczonym adnotacją @Version. Istnienie tego pola nie jest wymagane, ale przydatne jeśli chcielibyśmy mieć dostęp do aktualnej wersji encji. Ponadto w momencie zadeklarowania pola które trzyma wersję encji domyślnie zastosowanym mechanizmem blokowania będzie mechanizm blokowania optymistycznego. JPA zwiększa numer wersji w bazie danych przy każdym zapisie. Blokowanie optymistyczne realizowane jest na poziomie aplikacji.

Optimistic locking for writing operations is used implicitly by the JPA provider.

[źródło] https://www.byteslounge.com/tutorials/locking-in-jpa-lockmodetype

  • manager.lock(entity, LockMode.OPTIMISTIC);
  • manager.lock(entity, LockMode.OPTIMISTIC_FORCE_INCREMENT);

Brak ochrony przed anomalią phantom read!

Optimistic locking

[źródło] https://enterprisecraftsmanship.com/posts/optimistic-locking-automatic-retry/

Aby zobaczyć blokowanie optymistyczne w praktyce zaczniemy od konfiguracji projektu dla JPA – tworzymy nowy projekt z użyciem mavena – plik pom.xml – niezbędne zależności:

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<java.version>1.8</java.version>
	<hibernate.version>4.3.6.Final</hibernate.version>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>${hibernate.version}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-c3p0</artifactId>
        <version>${hibernate.version}</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.31</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>3.8.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Plik .\src\main\resources\META-INF\persistence.xml:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
	http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">

    <persistence-unit name="jpa-example" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>

            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3307/javaleader?useLegacyDatetimeCode=false&amp;serverTimezone=UTC" />
            <property name="javax.persistence.jdbc.user" value="root" />
            <property name="javax.persistence.jdbc.password" value="root" />
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <property name="hibernate.hbm2ddl.auto" value="update"/>

            <!-- Configuring Connection Pool -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="500" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="2000" />
        </properties>
    </persistence-unit>
</persistence>

Klasa która pozwala pobrać instancję entityManagera:

public class EntityManagerUtil {

	private static EntityManager entityManager;

	private EntityManagerUtil() {
	}
	
	public static EntityManager getEntityManager() {
		if(entityManager==null){
			EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("jpa-example");
			return emFactory.createEntityManager();
		}
		return entityManager;
	}
}

Model – klasa Employee:

@NamedQueries({
	@NamedQuery(name = Employee.FIND_ALL, query = "SELECT e FROM Employee e order by e.name"),
})
@Entity
@Table(name = "JL_EMP")
public class Employee implements Serializable {

private static final long serialVersionUID = 1607726899931733607L;
	
	public static final String FIND_ALL = "demo.model.Employee.find_all";

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private int id;

	@Column(name = "NAME")
	private String name;

	@Column(name = "version_num")
	@Version
	private int version;

	// setters & getters & toString
}

Interfejs do przykładowych operacji CRUD:

public interface EmployeeCrud {
	List<Employee> findAllEmployee();
	Employee saveEmployee(Employee employeeBE);
	Employee updateEmployee(Employee employeeBE);
}

Implementacja powyższego interfejsu:

public class EmployeeCrudImpl implements EmployeeCrud {

	public Employee saveEmployee(Employee employeeBE) {
		EntityManager em = EntityManagerUtil.getEntityManager();
		em.getTransaction().begin();
		em.persist(employeeBE);
		em.getTransaction().commit();
		return employeeBE;
	}

	public Employee updateEmployee(Employee employeeBE) {
		EntityManager em = EntityManagerUtil.getEntityManager();
		em.getTransaction().begin();
		em.merge(employeeBE);
		em.getTransaction().commit();
		return employeeBE;
	}

	@SuppressWarnings("unchecked")
	public List<Employee> findAllEmployee() {
		EntityManager em = EntityManagerUtil.getEntityManager();
		Query query = em.createNamedQuery(Employee.FIND_ALL);
		return query.getResultList();
	}
}

Klasa testująca:

public class Application {

	public static void main(String args[]) {

		EmployeeCrud employeeCrud = new EmployeeCrudImpl();

		List<Employee> employees = employeeCrud.findAllEmployee();
		if (!employees.isEmpty()) {
			Employee employee = employees.get(0);
			employee.setName("James");
			employeeCrud.updateEmployee(employee);
			// Here Optimistic lock exception
			employeeCrud.updateEmployee(employee);
		} else {
			Employee employee = new Employee();
			employee.setName("emp");
			employeeCrud.saveEmployee(employee);
		}
	}
}

Po pierwszym uruchomieniu aplikacji w bazie danych zauważyć można jeden nowo dodany rekord:

Przy drugim uruchomieniu aplikacji wynik jest dokładnie taki sam, zmieniamy teraz klasę Application w następujący oto sposób (imię pracownika jest zmienione):

public class Application {

	public static void main(String args[]) {

		EmployeeCrud employeeCrud = new EmployeeCrudImpl();

		List<Employee> employees = employeeCrud.findAllEmployee();
		if (!employees.isEmpty()) {
			Employee employee = employees.get(0);
			employee.setName("James - update");
			employeeCrud.updateEmployee(employee);
			// Here Optimistic lock exception
			employeeCrud.updateEmployee(employee);
		} else {
			Employee employee = new Employee();
			employee.setName("emp");
			employeeCrud.saveEmployee(employee);
		}
	}
}

i ponownie uruchamiamy aplikację w trybie debug ustawiając pułapkę na linii:

employeeCrud.updateEmployee(employee);

w następujący sposób:

Po uruchomieniu:

Wniosek:

W bazie danych zapisana wersja encji wynosi 2 natomiast wersja encji w linii gdzie ustawiona jest pułapka wynosi 1 co oznacza, że wersje te są różne i otrzymujemy  wspomniany już błąd – OptimisticLockException:

Exception in thread "main" javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [model.Employee#1]
	at org.hibernate.jpa.spi.AbstractEntityManagerImpl.wrapStaleStateException(AbstractEntityManagerImpl.java:1785)

Podejście pesymistyczne

Podejście pesymistyczne (dostępne tylko od wersji JPA 2.0) zakłada, że w momencie pobierania encji z bazy danych pobierany obiekt (wiersz na poziomie bazodanowym) jest zablokowany. Model ten pesymistycznie zakłada wystąpienie konfliktów, dlatego dane są blokowane za każdym razem, gdy jakaś transakcja próbuje je odczytać lub modyfikować. Blokowanie pesymistyczne realizowane jest na poziomie bazodanowym.

Pessimistic locking is done at the database level. The JPA provider will delegate any pessimistic locking application requests to the database. The database itself will in fact lock the records and will only release the lock(s) after transaction completion (commit or rollback).

[źródło] https://www.byteslounge.com/tutorials/locking-in-jpa-lockmodetype

  • manager.lock(entity, LockMode.WRITE);
  • manager.lock(entity, LockMode.READ);
  • manager.lock(entity.FORCE_INCREMENT);

Wszystkie powyższe opcje zapewniają ochronę przed anomaliami: dirty reads i unrepeatable reads.

shared lock – LockMode.READ

  • Wyobraźmy sobie sytuację w której na sali szkoleniowej znajduje się masa studentów. Każdy ze studentów czyta to co nauczyciel zapisał na tablicy a nauczyciel czeka dopóki wszyscy zanotują potrzebne rzeczy. Dopiero później ściera tablicę i zapisuje kolejne rzeczy. Jest to sytuacja shared lock gdzie każdy ze studentów zakłada opcję LockMode.READ. W momencie założenia opcji LockMode.READ exclusive lock nie może być uzyskany.

exclusive lock – LockMode.WRITE

  • Wyobraźmy sobie sytuację w której na sali szkoleniowej znajduje się masa studentów. Nauczyciel pisze coś na tablicy zasłaniając sobą to co właśnie zapisuje. Żaden ze studentów nie jest w stanie przeczytać tego co aktualnie zapisywane jest na tablicy. Żaden inny nauczyciel nie może wejść do sali i zacząć pisać swoje rzeczy bo studenci byliby mocno zmieszani. Jest tu sytuacja exclusive lock gdzie nauczyciel zakłada opcję LockMode.WRITE. W momencie założenia opcji LockMode.WRITE shared lock nie może być uzyskany.

LockMode – FORCE_INCREMENT

  • Opcja taka jak LockMode.WRITE wprowadzona aby zachować kompatybilność z encjami które są wersjonowane. Jeśli encja nie jest wersjonowana – brak adnotacji @Version to zależy od dostawcy implementacji JPA w jaki sposób ten przypadek zostanie obsłużony. Może się zdarzyć, że zostanie wyrzucony błąd – PersistanceException.

Aby zobaczyć blokowanie pesymistyczne w praktyce zaczniemy od konfiguracji projektu dla JPA – tworzymy nowy projekt z użyciem mavena – plik pom.xml – niezbędne zależności:

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<java.version>1.8</java.version>
	<hibernate.version>4.3.6.Final</hibernate.version>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>${hibernate.version}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-c3p0</artifactId>
        <version>${hibernate.version}</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.31</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>3.8.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Plik .\src\main\resources\META-INF\persistence.xml:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
	http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">

    <persistence-unit name="jpa-example" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>

            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3307/javaleader?useLegacyDatetimeCode=false&amp;serverTimezone=UTC" />
            <property name="javax.persistence.jdbc.user" value="root" />
            <property name="javax.persistence.jdbc.password" value="root" />
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.show_sql" value="false" />
            <property name="hibernate.format_sql" value="false" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <property name="hibernate.hbm2ddl.auto" value="update"/>

            <!-- Configuring Connection Pool -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="500" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="2000" />
        </properties>
    </persistence-unit>
</persistence>

Klasa która pozwala pobrać instancję entityManagera:

public class EntityManagerUtil {

	private static EntityManager entityManager;

	private EntityManagerUtil() {
	}
	
	public static EntityManager getEntityManager() {
		if(entityManager==null){
			EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("jpa-example");
			return emFactory.createEntityManager();
		}
		return entityManager;
	}
}

Model – klasa Employee:

@NamedQueries({
	@NamedQuery(name = Employee.FIND_ALL, query = "SELECT e FROM Employee e order by e.name"),
})
@Entity
@Table(name = "JL_EMP")
public class Employee implements Serializable {

private static final long serialVersionUID = 1607726899931733607L;
	
	public static final String FIND_ALL = "demo.model.Employee.find_all";

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private int id;

	@Column(name = "NAME")
	private String name;

	@Column(name = "version_num")
	@Version
	private int version;

	// setters & getters & toString
}

Interfejs do przykładowych operacji CRUD:

public interface EmployeeCrud {
	Employee saveEmployee(Employee employeeBE);
	void updateEmployee();
	void readEmployee();
}

Implementacja powyższego interfejsu:

public class EmployeeCrudImpl implements EmployeeCrud {

	public Employee saveEmployee(Employee employeeBE) {
		EntityManager em = EntityManagerUtil.getEntityManager();
		em.getTransaction().begin();
		em.persist(employeeBE);
		em.getTransaction().commit();
		return employeeBE;
	}

	public void updateEmployee() {
		System.out.println("before update employee");
		EntityManager em = EntityManagerUtil.getEntityManager();
		em.getTransaction().begin();
		Employee employee = em.find(Employee.class, 1, LockModeType.PESSIMISTIC_WRITE);
		System.out.println(employee);
		System.out.println("after lock PESSIMISTIC_WRITE");
		try {
			TimeUnit.SECONDS.sleep(1);
			System.out.println("waiting 1 second");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		employee.setName("new emp name");
		em.getTransaction().commit();
		System.out.println("after updated");
	}

	public void readEmployee() {

		System.out.println("read employee");

		try {
			TimeUnit.MILLISECONDS.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		EntityManager em = EntityManagerUtil.getEntityManager();
		em.getTransaction().begin();
		Employee employee = em.find(Employee.class, 1, LockModeType.PESSIMISTIC_READ);
		System.out.println(employee);
		System.out.println("after lock PESSIMISTIC_READ");
		em.getTransaction().commit();
		System.out.println("after read");
	}
}

Klasa testująca:

public class Application {

	public static void main(String args[]) {

		EmployeeCrud employeeCrud = new EmployeeCrudImpl();

		try {
			ExecutorService es = Executors.newFixedThreadPool(3);

			Employee employee = new Employee();
			employee.setName("emp");
			employeeCrud.saveEmployee(employee);
			es.execute(() -> {
				employeeCrud.updateEmployee();
			});
			es.execute(() -> {
				employeeCrud.readEmployee();
			});
			es.shutdown();
			es.awaitTermination(1, TimeUnit.MINUTES);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

Po uruchomieniu:

before update employee
read employee
Employee{id=1, name='new emp name', version=3}
after lock PESSIMISTIC_WRITE
waiting 1 second
after updated
Employee{id=1, name='new emp name', version=3}
after lock PESSIMISTIC_READ
after read

Wniosek:

  • Za każdym razem kiedy aplikacja jest uruchamiana wynik na konsoli jest dokładnie taki sam. Oznacza to, że w momencie założenia blokady PESSIMISTIC_WRITE nie można założyć żadnej innej blokady dopóki transakcja nie zostanie zatwierdzona. Zmieńmy teraz instrukcję:
Employee employee = em.find(Employee.class, 1, LockModeType.PESSIMISTIC_WRITE);

na:

Employee employee = em.find(Employee.class, 1, LockModeType.PESSIMISTIC_READ);

Zmiana rodzaju blokady powoduje wynik:

before update employee
read employee
Employee{id=1, name='new emp name', version=3}
after lock PESSIMISTIC_WRITE
Employee{id=1, name='new emp name', version=3}
after lock PESSIMISTIC_READ
after read
waiting 1 second
after updated

Wniosek:

  • Założenie blokady PESSIMISTIC_READ powoduje, że kolejne blokady typu PESSIMISTIC_READ mogą być założone przez inne transakcje nawet wtedy kiedy pierwsza transakcja nie zostanie zatwierdzona.

Obrazowo blokowanie optymistyczne i pesymistyczne przedstawić można w następujący sposób:

12 Optimistic vs Pessimistic Locking Pessimistic (SQL) Optimistic (CQL) SELECT FOR UPDATE SELECT FOR UPDATE COMMIT COMMIT ...

[źródło] https://www.slideshare.net/IanChen17/investigation-of-transactions-in-cassandra

Kod źródłowy do wglądu na GitHub!

Jeśli chcesz uzyskać dostęp do GitHuba na 30 dni i pobrać kod źródłowy wyślij smsa o treśći DOSTEP.EDUSESSION na numer 7943. Tyle wiedzy a koszt to tylko 9 PLN (11.07 PLN z VAT).





Leave a comment

Your email address will not be published.


*