Persistence Context – studium przypadku z rozmowy kwalifikacyjnej

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

  1. W bazie mamy informacje o kilku obiektach
  2. pobieramy jeden obiekt (encję) o id 3
  3. zmieniamy pole w tym obiekcie (set)
  4. pobieramy WSZYSTKIE obiekty z bazy, dla danej Encji za pomocą .createQuery
  5. 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.

2 myśli w temacie “Persistence Context – studium przypadku z rozmowy kwalifikacyjnej”

    1. 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.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *