1

My environment

Java 7/JPA 2/Hibernate 5.1.

My Scenario

I'm building a Repository pattern implementation. All code is written and it all works fine when no error condition happens.

However, let's say that three entity instances are added to the repository. First and third are ok, but second lacks value for a mandatory (not null) column. A DB error will be returned when the repository is saved.

When facing this condition a batch process should just write a log message somewhere, skip the invalid object and continue to the others. In order to do that, this invalid entity should only be removed from the repository, what means to detach it from the underlying EntityManager this repository uses.

My problem

The repository.exclude(entity) method call (that internally detaches the entity from the EntityManager) seems to not be working and a second attempt to save the repository fails again.

My (partial) AbstractRepository class

public abstract class AbstractRepository<T> {
    private static Map<String,EntityManagerFactory> entityManagerFactories = new HashMap<String,EntityManagerFactory>();
    private static Map<EntityManagerFactory,EntityManager> entityManagers = new HashMap<EntityManagerFactory,EntityManager>();
    private EntityManager entityManager;
    private List<T> addedEntities = new ArrayList<T>();
    private List<T> deletedEntities = new ArrayList<T>();
    private List<T> updatedEntities = new ArrayList<T>();

    protected Class<T> entityClass = getEntityClass();

    // Many other declarations

    protected EntityManager createEntityManager() throws Exception {
        String persistenceUnitName = getPersistenceUnitName(); // using Reflection
        EntityManagerFactory entityManagerFactory = getEntityManagerFactory(persistenceUnitName);
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        return entityManager;
    }

    public T add(T entity) {
        addedEntities.add(entity);
        return entity;
    }

    public void save() throws Exception {
        EntityManager entityManager = getEntityManager();
        EntityTransaction transaction = entityManager.getTransaction(); 
        transaction.begin();
        try {
            for(T entity : addedEntities)
                entityManager.persist(entity);
            for(T entity : updatedEntities)
                entityManager.merge(entity);
            for(T entity : deletedEntities)
                entityManager.remove(entity);
            transaction.commit();
        } catch(Exception e) {
            if(transaction.isActive())
                transaction.rollback();
            throw e;
        }
        addedEntities.clear();
        updatedEntities.clear();
        deletedEntities.clear();
    }

    public T exclude(T entity) throws Exception {
        if(entity == null)
            return null;
        addedEntities.remove(entity);
        deletedEntities.remove(entity);
        updatedEntities.remove(entity);
        getEntityManager().detach(entity);
        return entity;
    }

    public EntityManager getEntityManager() throws Exception {
        if(entityManager == null)
            entityManager = createEntityManager();
        return entityManager;
    }
}

My Repository declaration

@PersistenceUnit(unitName = "my-ds")
public class MestreRepository extends AbstractRepository<Mestre, Long> {
    public List<Mestre> all() throws Exception {
        List<Mestre> result = getEntityManager().createQuery("from Mestre", Mestre.class).getResultList();
        return result;
    }
}

My test code

public class Main {
    public static void main(String[] args) {
        MestreRepository allMestres = new MestreRepository();

        Mestre mestre1 = new Mestre();
        mestre1.setNome("Mestre 1");
        Mestre mestre2 = new Mestre(); // This one lacks Nome and will fail to be saved
        Mestre mestre3 = new Mestre();
        mestre3.setNome("Mestre 3");

        allMestres.add(mestre1);
        allMestres.add(mestre2);
        allMestres.add(mestre3);

        System.out.println("Saving 3 mestres");
        try {
            allMestres.save();
            System.out.println("All 3 mestres saved"); // never happens!
        } catch(Exception e) {
            System.out.println("Error when salving 3 mestres");
            try {
                System.out.println("Excluding mestre 2");
                allMestres.exclude(mestre2);
                System.out.println("Salving other 2 mestres");
                allMestres.save();
                System.out.println("All 2 mestres salved"); // never happens!
            } catch(Exception e2) {
                System.out.println("Still having errors");
            }
        }

        allMestres.close();
    }
}

My persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
    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">
    <persistence-unit name="my-ds" transaction-type="RESOURCE_LOCAL">
        <class>domain.Mestre</class>
        <class>domain.Detalhe</class>

        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        <properties>
            <!-- Hibernate properties -->
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver"/>
            <property name="hibernate.connection.url" value="xxx"/>
            <property name="hibernate.connection.username" value="yyy"/>
            <property name="hibernate.connection.password" value="***"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle10gDialect"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
        </properties>
    </persistence-unit>
 </persistence>

The save() method updated

Here is a new version of the save() method that makes things work. It was needed to call flush() before commit() and no more persist() for those entities that did not create any problem, but merge() them, since they already have an Id.

public void save() throws Exception {
    List<T> processedEntities = new ArrayList<T>();
    EntityManager entityManager = getEntityManager();
    EntityTransaction transaction = entityManager.getTransaction(); 
    transaction.begin();
    try {
        for(T entity : addedEntities) {
            entityManager.persist(entity);
            processedEntities.add(entity);
        }
        for(T entity : updatedEntities)
            entityManager.merge(entity);
        for(T entity : deletedEntities)
            entityManager.merge(entity);
        entityManager.flush();
        transaction.commit();
    } catch(Exception e) {
        updatedEntities.addAll(processedEntities);
        addedEntities.removeAll(processedEntities);

        if(transaction.isActive())
            transaction.rollback();
        throw e;
    }
    addedEntities.clear();
    updatedEntities.clear();
    deletedEntities.clear();
}
AlexSC
  • 1,823
  • 3
  • 28
  • 54
  • What about [`Session.evict()`](http://docs.jboss.org/hibernate/orm/4.1/javadocs/org/hibernate/Session.html#evict%28java.lang.Object%29)? – XtremeBaumer Aug 14 '18 at 13:09
  • where is your transaction boundary? – Simon Martinelli Aug 14 '18 at 13:09
  • @XtremeBaumer: I prefer to stick to JPA instead calling a Hibernate specific method, unless this is absolutely necessary – AlexSC Aug 14 '18 at 13:14
  • @SimonMartinelli: I just added my `save()` method that shows that – AlexSC Aug 14 '18 at 13:15
  • @XtremeBaumer: I decided to give it a try on `Session.evict()`, but the behavior was precisely the same – AlexSC Aug 14 '18 at 13:24
  • Are you sure that it is exactly the same entity (before and after removing) that is causing you trouble? – XtremeBaumer Aug 14 '18 at 13:31
  • I created a test with detach() and detach removes the object from the Persistence Context. I think @XtremeBaumer is right. You have not the same entity instance or the same entity manager. – Simon Martinelli Aug 14 '18 at 13:55
  • @XtremeBaumer: I just added my test code so you can see it. The `mestre2` variable holds the invalid entity. This is the same variable I pass to `exclude()`, so in my understanding I am trying to detach the correct entity – AlexSC Aug 14 '18 at 14:14
  • @SimonMartinelli: thanks for your dedication. I just posted my test code so you can evaluate it! The interesting thing is, after calling `save()` all 3 instances have received their ids. – AlexSC Aug 14 '18 at 14:17
  • em.detach(entity) – Gab Aug 14 '18 at 14:18
  • What is `allMestres.add(mestre1);` doing exactly? you havent posted the code for `add` method – XtremeBaumer Aug 14 '18 at 14:25
  • @XtremeBaumer: I just posted the code for `add()` method. You will notice that it is really simple. When an entity is added to the repository, it goes to a list dedicated to hold the added (not yet persisted) instances. The `save()` method will run over that list and add it to the EntityManager using the `persist()` method. After doing that, `save()` will try to commit the current transaction. All this works in no-error situations. – AlexSC Aug 14 '18 at 14:36
  • Have you already tried debugging your code properly to see the actual flow? – XtremeBaumer Aug 14 '18 at 14:40
  • @XtremeBaumer: yes, many times, that's why I have all the try-catch blocks in the test code. Let me give you another piece of information: I configured log4j to show the parameter values hibernate is using when sending SQL sentences to the data server. This log shows me that the parameter values bound to the sentences are the same in the first and the second calls for `save()`, what suggests me that the instance in `mestre2`is not being detached from the EntityManager. I asure that the call for `exclude()` is happening and that the call for `detach()` is as well. – AlexSC Aug 14 '18 at 14:49
  • According to [this guide](https://www.logicbig.com/tutorials/java-ee-tutorial/jpa/detaching.html), you need to `flush()` before `detaching` the entity. Should be worth a try. Also try to close the transaction before calling `exclude` – XtremeBaumer Aug 15 '18 at 06:21
  • @XtremeBaumer: it's not really the same context because in all examples in that link the EntityManager was begin closed after the operations, but it gave me some hint about `flush()`. In fact, using `flush()` combined with some rewriting of my `save()` method fixed the process. Please, post your suggestion as an answer and I will accept it. Thank you so much! – AlexSC Aug 15 '18 at 10:46
  • You should then update your question to show the changes you made to the `save()` method (as well as showing how you use `flush()` now) – XtremeBaumer Aug 15 '18 at 10:49
  • @XtremeBaumer: Did it! – AlexSC Aug 15 '18 at 10:52

3 Answers3

4

Converting an comment to an answer:

According to this guide, you need to flush() before detaching the entity

Update code from OP:

public void save() throws Exception {
    List<T> processedEntities = new ArrayList<T>();
    EntityManager entityManager = getEntityManager();
    EntityTransaction transaction = entityManager.getTransaction(); 
    transaction.begin();
    try {
        for(T entity : addedEntities) {
            entityManager.persist(entity);
            processedEntities.add(entity);
        }
        for(T entity : updatedEntities)
            entityManager.merge(entity);
        for(T entity : deletedEntities)
            entityManager.merge(entity);
        entityManager.flush();
        transaction.commit();
    } catch(Exception e) {
        updatedEntities.addAll(processedEntities);
        addedEntities.removeAll(processedEntities);

        if(transaction.isActive())
            transaction.rollback();
        throw e;
    }
    addedEntities.clear();
    updatedEntities.clear();
    deletedEntities.clear();
}
XtremeBaumer
  • 6,275
  • 3
  • 19
  • 65
0
  • CAUTION

Definitely the usage of merge as stand alone for updating is not suffice i.e.

for(T entity : updatedEntities)
        entityManager.merge(entity);

The merge is going to copy the detached entity state (source) to a managed entity instance (destination) thus for updating entities from datasource you need to first bring them to managed state first then detach and mute the de attache entity as you wish then call the merge to bring it again to managed state to synchronization with flush or commit

enter image description here

Thus the proper code for updating will be like this

    public <T> BehindCacheDirector<R, V> update(Class<?> type, T t, Long v) {
            entityManager.detach(entityManager.find(type, v));
            entityManager.merge(t);
    ...        
    }

All most Same story holds for delete, you need to bring the entity that you wish to delete to managed state then change it state to remove state then calling syntonization commands, i.e.

    public BehindCacheBuilder<R, V> remove(Class<?> type, Object object) {
        entityManager.remove(entityManager.find(type, object));
    ...
    }

Find more in here.

Lunatic
  • 1,519
  • 8
  • 24
-3

Unfortunately, there's no way to disconnect one object from the entity manager in the current JPA implementation, AFAIR.

EntityManager.clear() will disconnect all the JPA objects, so that might not be an appropriate solution in all the cases, if you have other objects you do plan to keep connected.

So your best bet would be to clone the objects and pass the clones to the code that changes the objects. Since primitive and immutable object fields are taken care of by the default cloning mechanism in a proper way, you won't have to write a lot of plumbing code (apart from deep cloning any aggregated structures you might have).

Angad Bansode
  • 825
  • 6
  • 15
  • 1
    That's wrong! detach is exactly doing what AlexSC needs. See the JavaDoc https://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html#detach-java.lang.Object- – Simon Martinelli Aug 14 '18 at 13:56