Block Image

Per utilizzare al meglio JPA, dovremmo conoscere bene i concetti di Persistence Unit, Persistence Context ed EntityManager.

Cos'è una Persistence Unit

Una Persistence Unit definisce l'insieme di tutte le classi Java gestite dall'EntityManager di JPA. Tutte le entità di una Persistence Unit rappresentano dati all'interno di un database. Quindi se volessimo mappare due database in JPA, dovremmo definire due Persistence Unit.
Le Persistence Units sono configurate all'interno del file persistence.xml.

Le classi delle Persistence Units possono essere impacchettate come parte di un archivio JAR, WAR o EJB oppure come file JAR che può poi essere incluso in un WAR o EAR. In particolare:

  1. Se si impacchettano in un file EJB JAR, il file persistence.xml dovrebbe essere messo nella directory META-INF della EJB JAR.
  2. Se si impacchettano in un file WAR, il persistence.xml dovrebbe trovarsi nella directory WEB-INF/classes/META-INF del file WAR.
  3. Se si impacchettano in un file JAR che sarà incluso in un file WAR o EAR, il file JAR dovrebbe trovarsi:
    • nella cartella WEB-INF/lib del WAR
    • nella cartella della library dell'EAR

Cos'è un Persistence Context e un EntityManager?

Un Persistence Context è una Session che racchiude un insieme di entità gestite in un preciso momento dall'EntityManager, che controlla il loro ciclo di vita. Inoltre l'EntityManager ha delle API che servono per creare e rimuovere entities, cercarle tramite la loro primary key ed effettuare query su di esse.
Quando un Persistence Context (Session) termina, le entities precedentemente managed diventano detached.

Una istanza di EntityManager può essere ottenuta sia tramite l'Inject di un Container (come un JEE Container o Spring) oppure attraverso l'interfaccia EntityManagerFactory (solitamente in applicazioni JSE). La cosa fondamentale da sapere è che il comportamento dell'EntityManager cambia a seconda se venga iniettato dal container o creato dall'applicazione.

EntityManager gestito dal container (declaratively Transaction)

@PersistenceContext
private EntityManager entityManager;

@Transactional
@Override
public T save(T entity) {
    if(getterId(entity) == null) {
        entityManager.persist(entity);
    }
    else {
        entityManager.merge(entity);
    }
    return entity;
}

Il container, prima di effettuare un'operazione sulla Entity, verifica se c'è un Persistence Context connesso alla transazione; se non presente, crea un nuovo Persistence Context (sessione) e la collega. In pratica viene creato automaticamente un EntityManager per ogni nuova transazione. Le operazioni come i commit e i rollback vengono gestiti automaticamente dal Container.

NOTA: il comportamento di default dell'EntityManager è quello descritto poc'anzi. In particolare si dice che il Persistence Context è di tipo Transaction-scoped. Esiste però anche un altro tipo di Persistence Context chiamato Extended, per la gestione di beans statefull, ma è poco utilizzato.

EntityManager gestito dall'applicazione (programmatically Transaction)

protected static EntityManagerFactory entityManagerFactory;

static {
        entityManagerFactory = Persistence.createEntityManagerFactory("TEST_UNIT");
}

@Override
public T save(T entity) {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    try {
        entityManager.getTransaction().begin();

        if(getterId(entity) == null) {
            entityManager.persist(entity);
        }
        else {
            entityManager.merge(entity);
        }
        entityManager.getTransaction().commit();
    }
    catch (Exception e) {
        entityManager.getTransaction().rollback();
    }

    entityManager.close();

    return entity;
}

Qui lo sviluppatore crea ogni volta l'EntityManager dall'EntityManagerFactory. Ha più controllo sul flusso, ma anche piú responsabilità (ad esempio deve ricordarsi di chiudere l'EntityManager, deve esplicitamente chiamare il commit e il rollback delle operazioni).

Ora che abbiamo capito questi concetti di base, creiamo una applicazione con delle classi DAO. Useremo le entities del post JPA Reletions col database H2 in-memory.

Vedremo l'uso dell'EntityManager gestito dall'applicazione, e poi faremo un confronto con quello gestito dal Container.

Passo 1: creiamo l'interfaccia JpaDao

Questa interfaccia conterrà i metodi generali di findById, save, etc...

public interface JpaDao<T extends JpaEntity, ID> {

    T findById(ID id);

    Collection<T> findAll();

    T save(T entity);

    void delete(T entity);

    void clear();
}

Creiamo l'interfaccia UserDao che semplicemente estende JpaDao:

public interface UserDao extends JpaDao<UserEntity, Long> {
}

Passo 2: creiamo la classe astratta JpaDaoImpl

public abstract class JpaDaoImpl<T extends JpaEntity, ID> implements JpaDao<T, ID> {

    protected static EntityManagerFactory entityManagerFactory;

    private Class<T> persistentClass;

    private final String FIND_ALL;

    static {
        entityManagerFactory = Persistence.createEntityManagerFactory("TEST_UNIT");
    }


    public JpaDaoImpl() {

        this.persistentClass = (Class<T>) ((ParameterizedType) getClass()
                .getGenericSuperclass()).getActualTypeArguments()[0];

        FIND_ALL = "select e from " + persistentClass.getSimpleName() + " e";
    }

    @Override
    public T findById(ID id) {
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        T entity = entityManager.find(persistentClass, id);
        entityManager.close();
        return entity;
    }

    @Override
    public Collection<T> findAll() {
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        Collection<T> entities = entityManager.createQuery(FIND_ALL, persistentClass).getResultList();
        entityManager.close();
        return entities;
    }

    @Override
    public T save(T entity) {
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        try {
            entityManager.getTransaction().begin();

            if(getterId(entity) == null) {
                entityManager.persist(entity);
            }
            else {
                entityManager.merge(entity);
            }
            entityManager.getTransaction().commit();
        }
        catch (Exception e) {
            entityManager.getTransaction().rollback();
        }

        entityManager.close();

        return entity;
    }

    @Override
    public void delete(T entity) {
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        try {
            entityManager.getTransaction().begin();
            entity = entityManager.merge(entity);
            entityManager.remove(entity);
            entityManager.getTransaction().commit();
        }
        catch (Exception e) {
            entityManager.getTransaction().rollback();
        }

        entityManager.close();

    }

    //use a forEach loop and not a delete query because JPA not use the cascade in the query
    @Override
    public void clear() {
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        try {
            entityManager.getTransaction().begin();
            Collection<T> all = findAll();
            all.stream().map(entityManager::merge)
                    .forEach(entityManager::remove);

            entityManager.getTransaction().commit();
        }
        catch (Exception e) {
            entityManager.getTransaction().rollback();
        }
        entityManager.close();
    }

    //get the value of the field annotated with @Id
    private ID getterId(T entity) {
        try {
            Field id = Arrays.stream(persistentClass.getDeclaredFields())
                    .filter(field -> field.isAnnotationPresent(Id.class))
                    .findAny()
                    .orElse(null);

            id.setAccessible(true);

            return (ID) id.get(entity);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

Analizziamo il codice:

  1. Il campo entityManagerFactory viene inizializzato allo startup dell'app. Legge il persistence unit all'interno del file persistence.xml.
  2. Il campo persistentClass viene valorizzato con la classe concreta dell'Entity.
  3. Ogni volta che viene invocato un metodo, creiamo l'EntityManager dall'EntityManagerFactory.
  4. Per le operazioni di scrittura sul database, creiamo e gestiamo manualmente le transazioni.
  5. Il metodo delete, prima di richiamare la remove dell'EntityManager, chiama il merge che trasforma un eventuale entity detached in managed (le operazioni possono essere fatte solo su entity managed).
  6. Il metodo getterId, tramite reflection, prende il valore del campo annotato con @Id di JPA.
Il metodo clear elimina tutte le righe della tabella. Usiamo il remove dell'EntityManager per ogni entity piuttosto che una sola query JPQL poiché le query non supportano le operazioni di cascade. Ad esempio, se provassimo ad eliminare con una query DELETE tutte le righe di USERS, potremmo avere degli errori sulla violazione del vincolo FK su CONTACTS se non eliminiamo prima le righe su CONTACTS. Usando il remove dell'EntityManager invece, quando eliminiamo una riga di USERS verrà eliminata anche l'eventuale riga di CONTACTS grazie al cascade settato nella relazione OneToOne.

Abbiamo creato una classe DAO generale che può essere usata da tutte le Entity! Creiamo la classe concreta UserDaoImpl:

public class UserDaoImpl extends JpaDaoImpl<UserEntity, Long> implements UserDao {

    private static UserDaoImpl userDao;

    private UserDaoImpl() {}

    public static UserDao getInstance() {
        if(userDao == null) {
            userDao = new UserDaoImpl();
        }

        return userDao;
    }

}

Da notare che UserDaoImpl è un semplice singleton che non contiene metodi. Contiene già tutti i findAll, findById, save, etc, visto che estende JpaDaoImpl.

Passo 3: creiamo il file persistence.xml

All'interno di resources/META-INF, creiamo il persistence.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<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_2_0.xsd"
             version="2.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="TEST_UNIT" transaction-type="RESOURCE_LOCAL">
        <class>com.vincenzoracca.jpaproject.entities.UserEntity</class>
        <class>com.vincenzoracca.jpaproject.entities.CarEntity</class>
        <class>com.vincenzoracca.jpaproject.entities.ContactEntity</class>
        <class>com.vincenzoracca.jpaproject.entities.ArticleEntity</class>

        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:testdb" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="javax.persistence.jdbc.user" value="sa" />
            <property name="javax.persistence.jdbc.password" value="sa" />

            <!-- Hibernate create the database schema automatically -->
            <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
            <property name="hibernate.show_sql" value="true" />
        </properties>

    </persistence-unit>
</persistence>

Analizziamo il file:

  1. Nella sezione persistence-unit, dichiaramo il nome della unit e il tipo, che può essere RESOURCE_LOCAL o JTA.\

JTA è un tipo di transazione gestita dai soli container JEE, magari scriverò un articolo al riguardo: basti sapere che se dobbiamo deployare l'applicazione all'interno di un application server, può essere utile usare questo tipo di transazione. 2. Le sezioni class, indicano le varie classi Java mappate come entities. 3. La sezione properties indica le varie properties JPA e, in questo caso, di Hibernate, che possiamo settare.

NOTA: JTA supporta sia l'EntityManager gestito dal container che quello gestito dall'applicazione. Nel secondo caso è possibile iniettare dal container una classe javax.transaction.UserTransaction.

Passo 4: testiamo i DAOs

Creiamo dei jUnit per testare i DAO:

public class JpaRelationsTest {

    private UserDao userDao;
    private CarDao carDao;
    private ArticleDao articleDao;

    @Before
    public void init() {
        userDao = UserDaoImpl.getInstance();
        carDao = CarDaoImpl.getInstance();
        articleDao = ArticleDaoImpl.getInstance();
        System.out.println("\n*************************************************");
    }

    @After
    public void destroy(){
        System.out.println("*************************************************\n");
        System.out.println("BEGIN destroy");
        articleDao.clear();
        carDao.clear();
        userDao.clear();
        System.out.println("END destroy\n");
    }
    

    @Test
    public void oneToOneTest() {
        System.out.println("BEGIN oneToOneTest");
        assertEquals(0, userDao.findAll().size());
        UserEntity userEntity = createUser();
        ContactEntity contactEntity = createContact(userEntity);

        userDao.save(userEntity);
        UserEntity retrieved = userDao.findById(userEntity.getId());
        System.out.println(retrieved);
        assertEquals(userEntity, retrieved);
        assertEquals(contactEntity, userEntity.getContactEntity());
        System.out.println("END oneToOneTest");
    }

    private UserEntity createUser(){
        UserEntity userEntity = new UserEntity();
        userEntity.setCode("1");
        userEntity.setName("Vincenzo");
        userEntity.setSurname("Racca");
        return userEntity;
    }

    private ContactEntity createContact(UserEntity userEntity) {
        ContactEntity contactEntity = new ContactEntity();
        contactEntity.setCity("Naples");
        contactEntity.setTelephoneNumber("333333333");
        contactEntity.setUserEntity(userEntity);
        userEntity.setContactEntity(contactEntity);
        return contactEntity;
    }

}

Analizziamo il metodo oneToOneTest:

  1. Ci assicuriamo con assertEquals(0, userDao.findAll().size()) che la tabella USERS sia vuota.
  2. Creiamo un userEntity, un suo contactEntity e poi salviamo lo userEntity. Ricordiamo che UserEntity, avendo il cascade su ContactEntity,

quando verrà persistito, verrà salvato a cascata anche l'eventuale ContactEntity. 3. Recuperiamo poi lo userEntity dal db e verifichiamo che lui e il suo contactEntity siano uguali a quelli inseriti un attimo prima.

Se abilitiamo i log sulla Transazione, vediamo che effettivamente le insert su USERS e CONTACTS sono avvenute all'interno della stessa transazione. Quando infatti facciamo esplicitamente commit della transazione, JPA inserisce anche contactEntity:

22:12:10.602 [main] DEBUG o.h.e.t.internal.TransactionImpl - begin
Hibernate: insert into USERS (user_id, code, name, surname) values (null, ?, ?, ?)
22:12:17.066 [main] DEBUG o.h.e.t.internal.TransactionImpl - committing
Hibernate: insert into CONTACTS (city, telephone_number, user_id) values (?, ?, ?)

Container managed vs Application managed

Ma cosa succederebbe se un metodo della business logic avesse più metodi DAO e volessimo richiamarli all'interno della stessa transazione?
Attualmente, con i metodi che abbiamo scritto, si creerebbe comunque una transazione per ogni DAO, quindi non avremmo un'unica transazione.
Una possibile soluzione, sempre parlando di EntityManager gestito dall'applicazione, è quella di usare la CDI (Context and Dependecy Injection) di Java e iniettare l'EntityManager.

Per quanto riguarda l'EntityManager gestito dai Container, non avremo questo problema, poiché nei metodi annotati con @Transactional, controlla se c'è già in esecuzione una transazione. Se si, usa quella, altrimenti ne crea una nuova (questo è il comportamento di default, ma è modificabile).

Vediamo la classe JpaDaoImpl con EntityManager gestito dal Container:

public abstract class JpaDaoImpl<T extends JpaEntity, ID> implements JpaDao<T, ID> {


    private Class<T> persistentClass;

    private final String FIND_ALL;

    @PersistenceContext
    private EntityManager entityManager;


    public JpaDaoImpl() {

        this.persistentClass = (Class<T>) ((ParameterizedType) getClass()
                .getGenericSuperclass()).getActualTypeArguments()[0];

        FIND_ALL = "select e from " + persistentClass.getSimpleName() + " e";
    }

    @Override
    public T findById(ID id) {
        T entity = entityManager.find(persistentClass, id);
        return entity;
    }

    @Override
    public Collection<T> findAll() {
        Collection<T> entities = entityManager.createQuery(FIND_ALL, persistentClass).getResultList();
        return entities;
    }

    @Transactional
    @Override
    public T save(T entity) {
        if(getterId(entity) == null) {
            entityManager.persist(entity);
        }
        else {
            entityManager.merge(entity);
        }
        return entity;
    }

    @Transactional
    @Override
    public void delete(T entity) {
        entity = entityManager.merge(entity);
        entityManager.remove(entity);
    }

    //use a forEach loop and not a delete query because JPA not use the cascade in the query
    @Transactional
    @Override
    public void clear() {
        Collection<T> all = findAll();
        all.forEach(this::delete);
    }

    //get the value of the field annotated with @Id
    private ID getterId(T entity) {
        try {
            Field id = Arrays.stream(persistentClass.getDeclaredFields())
                    .filter(field -> field.isAnnotationPresent(Id.class))
                    .findAny()
                    .orElse(null);

            id.setAccessible(true);

            return (ID) id.get(entity);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

Come possiamo notare, le transazioni sono gestite automaticamente e l'EntityManager non è creato dallo sviluppatore ma bensì dal Container.

Notiamo anche che il metodo clear richiama il delete della stessa classe. Questo è possibile perché appunto il Container creerà una sola transazione, nel metodo clear. Quando poi passerà al metodo delete, essendoci già una transazione, userà quella, e quindi il commit verrà fatto solo alla fine del metodo clear.

Conclusioni

In questo articolo su JPA abbiamo visto che cos'è un EntityManager, come usarlo, sia con application manged che con container managed.

Potete trovare il progetto completo sul mio github a questo link: JPA Project.
Troverete un modulo dedicato all'application managed e uno a quello container, usando Spring 5.

Articoli su JPA: JPA