Ostatnio znajomy podrzucił mi przykładowe pytanie z rozmowy kwalifikacyjnej dotyczące JPA/Hibernate i stwierdziłem, że jest to ciekawy przykład na przedstawienie działanie Persistence Context.
Pytanie
- W bazie mamy informacje o kilku obiektach
- pobieramy jeden obiekt (encję) o id 3
- zmieniamy pole w tym obiekcie (set)
- pobieramy WSZYSTKIE obiekty z bazy, dla danej Encji za pomocą .createQuery
- Czy obiekt o id 3, w liście danych pobranych w kroku 5, ma wartość starą czy zaktualizowaną?
A przenosząc to na grunt przykładowego kodu:
Guest ali = guestRepository.findById(3); System.out.println(ali); ali.setAge(7); System.out.println(ali); List select_g_from_guest_g = em.createQuery("SELECT g FROM Guest g").getResultList(); System.out.println("--------------------------------"); select_g_from_guest_g.forEach(quest -> System.out.println(quest));
Sednem tego pytania jest zachowanie Persistence Context przy pobraniu wszystkich elementów z bazy. Czy nadpisze on zmienioną wartość czy nie?
Persistence Context
Zanim przejdziemy do rozwiązania zróbmy szybkie przypomnienie czym jest PC.
Przy korzystaniu z Hibernate, czy innej implementacji JPA, pomiędzy naszą aplikacją a bazą danych istnieje pewna dodatkowa warstwa, która zajmuję się zarządzaniem i synchronizacją naszych obiektów (tych z klas oznaczonych jako encja) z bazą danych. Jest to właśnie Persistence Context.
Tyle w takim dużym skrócie.
Kod przykładu
Dla sprawdzenia rozwiązania utworzyłem prostą aplikację, oto jej kod:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>hotelreservationjpa</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.release>15</maven.compiler.release> </properties> <build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <release>${maven.compiler.release}</release> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.4.23.Final</version> </dependency> </dependencies> </project>
META-INF/persistence.xml:
<?xml version="1.0" encoding="UTF-8" ?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> <persistence-unit name="thePersistenceUnit" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <properties> <property name="connection.driver_class" value="org.h2.Driver"/> <property name="hibernate.connection.url" value="jdbc:h2:./db/repository"/> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.hbm2ddl.auto" value="create-drop"/> <property name="hibernate.show_sql" value="true" /> <property name="hibernate.use_sql_comments" value="true" /> </properties> </persistence-unit> </persistence>
Guest.java:
package pl.clockworkjava.hotelreservation.jpa; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class Guest { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String name; private int age; public Guest() { } public Guest(String name, int age) { this.name = name; this.age = age; } public long getId() { return this.id; } public void setAge(int newAge) { this.age = newAge; } public void getAge() { int x = this.age + 5; } public String getName() { return name; } @Override public String toString() { return "Guest{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
GuestRepository.java:
package pl.clockworkjava.hotelreservation.jpa; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction; public class GuestRepository { EntityManager em; public GuestRepository(EntityManager em) { this.em = em; } public void createNewGuest(String name, int age) { System.out.println(" ---------- CREATE ---------- "); Guest newOne = new Guest(name, age); EntityTransaction transaction = em.getTransaction(); transaction.begin(); System.out.println(" ---------- Persisting in new transaction ---------- "); em.persist(newOne); System.out.println(" NewGuest ID " + newOne.getId()); System.out.println(" ---------- Closing transaction ---------- "); transaction.commit(); } public Guest findById(long id) { System.out.println(" -------------- FIND BY ID ----------"); return em.find(Guest.class, id); } public void updateAge(Guest guest, int newAge) { EntityTransaction transaction = em.getTransaction(); System.out.println(" -------------- UPDATE ----------"); transaction.begin(); guest.setAge(newAge); transaction.commit(); } public void delete(Guest guest) { EntityTransaction transaction = em.getTransaction(); transaction.begin(); em.remove(guest); transaction.commit(); } }
App.java:
package pl.clockworkjava.hotelreservation.jpa; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import java.util.Arrays; import java.util.List; public class App { private static EntityManagerFactory factory = Persistence.createEntityManagerFactory("thePersistenceUnit"); private static EntityManager em = factory.createEntityManager(); public static void main(String[] args) { GuestRepository guestRepository = new GuestRepository(em); guestRepository.createNewGuest("Paweł", 34); guestRepository.createNewGuest("Kinga", 37); guestRepository.createNewGuest("Alicja", 6); guestRepository.createNewGuest("Gabriel", 5); em.getTransaction().begin(); em.flush(); em.clear(); em.getTransaction().commit(); // wrzucenie wszystkiego z PC do bazy danych i jego wyczyszczenie, by wrócić do "czystego" stanu. Guest ali = guestRepository.findById(3); System.out.println(ali); ali.setAge(7); System.out.println(ali); List select_g_from_guest_g = em.createQuery("SELECT g FROM Guest g").getResultList(); System.out.println("--------------------------------"); select_g_from_guest_g.forEach(quest -> System.out.println(quest)); } }
Wynik i dlaczego właśnie tak
Efekt powyższego programu jest następujący:
-------------- FIND BY ID ---------- Hibernate: select guest0_.id as id1_0_0_, guest0_.age as age2_0_0_, guest0_.name as name3_0_0_ from Guest guest0_ where guest0_.id=? Guest{id=3, name='Alicja', age=6} Guest{id=3, name='Alicja', age=7} Hibernate: /* SELECT g FROM Guest g */ select guest0_.id as id1_0_, guest0_.age as age2_0_, guest0_.name as name3_0_ from Guest guest0_ -------------------------------- Guest{id=1, name='Paweł', age=34} Guest{id=2, name='Kinga', age=37} Guest{id=3, name='Alicja', age=7} Guest{id=4, name='Gabriel', age=5}
Dane (age) zaktualizowane w obiekcie Alicja pozostały nowe, nawet po pobraniu wszystkich obiektów z bazy danych.
Dzieje się tak dlatego, że Persistence Context jest traktowany jako posiadający „świeższą” wersję danego obiektu, niż to, co jest w bazie. Tak długo jak w PC istnieje obiekt o danym ID to _nie_ zostanie on nadpisany przy ponownym pobieraniu tego obiektu z bazy danych.
Gdyby „wyrzucić” dany obiekt z Persistence Contextu BEZ wywołania .flush bądź zamknięcia transakcji (co powoduję zapisanie danych z PC do bazy danych) to przy ponownym pobraniu obiektu o ID 3, to wiek wróci do wartości 6. PC nie miał szansy zsynchronizować swojego stanu z bazą danych.
Przykładowy kod:
Guest ali = guestRepository.findById(3); System.out.println(ali); ali.setAge(7); System.out.println(ali); em.detach(ali); // remove from persistence context Guest sameAli = guestRepository.findById(3); System.out.println(sameAli);
Mam nadzieję, że ten przykład przybliżył Ci nieco działanie Persistence Context.
By być na bieżąco i mieć realny wpływ na tematykę tworzonych artykułów zapraszam do dołączenia do mojego newslettera.
Hej, przydatny artykuł.
Pytanie, kiedy dane z Persistence Context są zapisywane do bazy?
Pozdrawiam
Albo przy zamknięciu sesji manualnym em.getTransaction().commit(), a gdy pracujemy ze springiem i @Transactional to już framework o to dba. Albo przy ręcznym wywołaniu em.flush(), który zrzutuje cały Persistence Context na bazę danych.