How to correctly update an entity that has a collection?

Viewed 69

I'm trying to write a Java JSF 2.3 app, with Primefaces and Hibernate as JPA provider. I'm having trouble updating an entity that has a SortedSet of another entity. I made the minimal reproducible example.

So let's call them EntityTest like a parent entity and EntityTestChild like a child, as following:

@Entity
public class EntityTest implements Serializable {

private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

private String testString;

@OneToMany(fetch = FetchType.LAZY, orphanRemoval = true)
@OrderBy("id ASC")
private SortedSet<EntityTestChild> childs = new TreeSet<>();

//getters and setters for id, testString and childs. omitted for brevity.
//equals based on id, hashcode and toString omitted for brevity.

The other entity:

@Entity
public class EntityTestChild implements Serializable, Comparable<EntityTestChild> {

private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

private String stringChild;

public EntityTestChild()
{
    stringChild = "Hi, I'm a child";
}
//getters and setters, comparator, equals, hashcode and toString.

With a JSF page, I want to create and persist instances of parent entity EntityTest. And for each of them, I want to manage their child entities EntityTestChild

So for that I made a quick JSF page: Example page for managing entities

JSF Page code:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:p="http://primefaces.org/ui">
    <h:head>
        <title>Test</title>
    </h:head>
    <h:body>
        This is a test 
        <h:form id="testForm"> 
            <p:inputText value="#{viewTestBean.testStr}"/>
            <br/>
            <p:messages id="msgs"/>
            <br/>
            <p:commandButton actionListener="#{viewTestBean.addToTable()}"
                             value="add to table"
                             update="@form" 
                             icon="pi pi-plus"/>
            <br/>
            <p:commandButton actionListener="#{viewTestBean.setSelectedEntity(null)}"
                             icon="pi pi-times"
                             update="@form"/>
            <p:dataTable id="testTable"
                         value="#{viewTestBean.table}"
                         var="ent2"> 
                <p:column headerText="string">
                    <h:outputText value="#{ent2.testString}"/>
                </p:column>
                <p:column headerText="Action">
                    <p:commandButton icon="pi pi-check"
                                     update="@form"
                                     style="margin-right:10px"
                                     actionListener="#{viewTestBean.setSelectedEntity(ent2)}"/>
                    <p:commandButton icon="pi pi-trash"
                                     actionListener="#{viewTestBean.removeTableEnt(ent2)}"
                                     update="@form"/>
                </p:column>
            </p:dataTable>
            
            <h:panelGroup rendered="#{viewTestBean.selectedEntity != null}">
                <p:inputText value="#{viewTestBean.testStr2}"/>
                <p:commandButton actionListener="#{viewTestBean.addChild()}"
                                 update="@form"
                                 value="add child"
                                 icon="pi pi-check"/>
                <p:dataTable id="tableChilds"
                             rendered="#{viewTestBean.selectedEntity != null}"
                             value="#{viewTestBean.selectedEntity.childs}"
                             var="child">
                    <p:column headerText="child">
                        <h:outputText value="#{child.stringChild}"/>
                    </p:column>
                    <p:column headerText="action">
                        <p:commandButton update="@form"
                                         actionListener="#{viewTestBean.removeChild(child)}"
                                         icon="pi pi-trash"/>
                    </p:column>
                </p:dataTable>
            </h:panelGroup>
        </h:form>
    </h:body>
</html>

It has a ViewScoped CDI bean as a backing bean:

@Named(value = "viewTestBean")
@ViewScoped
public class ViewTestBean implements Serializable {
 
    @Inject
    DataAppBean dataAppBean;
    
    private List<EntityTest> table;
    
    private String testStr;
    private String testStr2;
    private EntityTest selectedEntity;
    
    public ViewTestBean() {
    }
    
    public void removeChild(EntityTestChild child)
    {
        selectedEntity.getChilds().remove(child);
        try {
            dataAppBean.getEntityTestJpaController().edit(selectedEntity);
        }  catch (Exception ex) {}
         
        try {
            dataAppBean.getEntityTestChildController().destroy(child.getId());
        }  catch (Exception ex) {}
    }
    
    public void addChild()
    {
        EntityTestChild newChild = new EntityTestChild();
        newChild.setStringChild(testStr2);
        try {
            dataAppBean.getEntityTestChildController().create(newChild);
        } catch (Exception ex) { }
        
        selectedEntity.getChilds().add(newChild);
        
        try {
            dataAppBean.getEntityTestJpaController().edit(selectedEntity);
        }  catch (Exception ex) { }
    }

    public EntityTest getSelectedEntity() {
        return selectedEntity;
    }

    public void setSelectedEntity(EntityTest selectedEntity) {
        this.selectedEntity = selectedEntity;
    }
   
    public void addToTable()
    {
       EntityTest newEntity = new EntityTest();
       newEntity.setTestString(testStr);
       
       try{dataAppBean.getEntityTestJpaController().create(newEntity);} 
       catch (Exception ex) { return; }
       
       getTable().add(newEntity);
    }
    
    public void removeTableEnt(EntityTest ent)
    {
        if (selectedEntity == ent) selectedEntity = null;
        try {
            dataAppBean.getEntityTestJpaController().destroy(ent.getId());
        } catch (Exception ex) { }
        getTable().remove(ent);
    }
 
    public List<EntityTest> getTable()
    { 
        if (table == null)
            table = dataAppBean.getEntityTestJpaController().findEntityTestEntities();
          
        return table;
    }
    
    public String getTestStr() { return testStr; }
    public void setTestStr(String testStr) { this.testStr = testStr; }
    public String getTestStr2() { return testStr2; }
    public void setTestStr2(String testStr2) { this.testStr2 = testStr2; }
    
}

With a click in the Check button, we should see below the table for child entities.

DataAppBean is an ApplicationScoped CDI bean. That is the data service, and holds the persistence helper classes.

@Named(value = "dataAppBean")
@ApplicationScoped
public class DataAppBean {

private EntityTestJpaController entityTestController;
private EntityTestChildJpaController entityTestChildController;
private EntityManagerFactory emf;
private UserTransaction utx;

public EntityTestJpaController getEntityTestJpaController()
{
    if (this.entityTestController == null)
        this.entityTestController = new EntityTestJpaController(getUserTransaction(), getEntityManagerFactory());
        return this.entityTestController; 
}
    
public EntityTestChildJpaController getEntityTestChildController()
{
    if (this.entityTestChildController == null)
        this.entityTestChildController = new EntityTestChildJpaController(getUserTransaction(), getEntityManagerFactory());
    return this.entityTestChildController; 
}

public EntityManagerFactory getEntityManagerFactory()
{
    if (this.emf == null)
        this.emf = Persistence.createEntityManagerFactory("ar.org.ptpchaco_SICasosTierras_war_1.0PU");
    return this.emf;
}

public UserTransaction getNewUserTransaction()
{
    UserTransaction output = null;
    
    try {output = (UserTransaction) new InitialContext().lookup("java:comp/UserTransaction");} 
    catch (NamingException ex) { }
    
    return output;
}

public UserTransaction getUserTransaction()
{
    if (this.utx == null)
        this.utx = getNewUserTransaction(); 
    
    return this.utx;
}

The JPA Controllers are auto generated with NetBeans 13 IDE. And looks like following:

public class EntityTestJpaController implements Serializable {

public EntityTestJpaController(UserTransaction utx, EntityManagerFactory emf) {
    this.utx = utx;
    this.emf = emf;
}
private UserTransaction utx = null;
private EntityManagerFactory emf = null;

public EntityManager getEntityManager() {
    return emf.createEntityManager();
}

public void create(EntityTest entityTest) throws RollbackFailureException, Exception {
    EntityManager em = null;
    try {
        utx.begin();
        em = getEntityManager();
        em.persist(entityTest);
        utx.commit();
    } catch (Exception ex) {
        try {
            utx.rollback();
        } catch (Exception re) {
            throw new RollbackFailureException("An error occurred attempting to roll back the transaction.", re);
        }
        throw ex;
    } finally {
        if (em != null) {
            em.close();
        }
    }
}

public void edit(EntityTest entityTest) throws NonexistentEntityException, RollbackFailureException, Exception {
    EntityManager em = null;
    try {
        utx.begin();
        em = getEntityManager();
        entityTest = em.merge(entityTest);
        utx.commit();
    } catch (Exception ex) {
        try {
            utx.rollback();
        } catch (Exception re) {
            throw new RollbackFailureException("An error occurred attempting to roll back the transaction.", re);
        }
        String msg = ex.getLocalizedMessage();
        if (msg == null || msg.length() == 0) {
            Long id = entityTest.getId();
            if (findEntityTest(id) == null) {
                throw new NonexistentEntityException("The entityTest with id " + id + " no longer exists.");
            }
        }
        throw ex;
    } finally {
        if (em != null) {
            em.close();
        }
    }
}

public void destroy(Long id) throws NonexistentEntityException, RollbackFailureException, Exception {
    EntityManager em = null;
    try {
        utx.begin();
        em = getEntityManager();
        EntityTest entityTest;
        try {
            entityTest = em.getReference(EntityTest.class, id);
            entityTest.getId();
        } catch (EntityNotFoundException enfe) {
            throw new NonexistentEntityException("The entityTest with id " + id + " no longer exists.", enfe);
        }
        em.remove(entityTest);
        utx.commit();
    } catch (Exception ex) {
        try {
            utx.rollback();
        } catch (Exception re) {
            throw new RollbackFailureException("An error occurred attempting to roll back the transaction.", re);
        }
        throw ex;
    } finally {
        if (em != null) {
            em.close();
        }
    }
}

public List<EntityTest> findEntityTestEntities() {
    return findEntityTestEntities(true, -1, -1);
}

public List<EntityTest> findEntityTestEntities(int maxResults, int firstResult) {
    return findEntityTestEntities(false, maxResults, firstResult);
}

private List<EntityTest> findEntityTestEntities(boolean all, int maxResults, int firstResult) {
    EntityManager em = getEntityManager();
    try {
        CriteriaQuery cq = em.getCriteriaBuilder().createQuery();
        cq.select(cq.from(EntityTest.class));
        Query q = em.createQuery(cq);
        if (!all) {
            q.setMaxResults(maxResults);
            q.setFirstResult(firstResult);
        }
        return q.getResultList();
    } finally {
        em.close();
    }
}

This JPA controllers generated by NetBeans, wraps the persist operation with a create method, and merge operation with an edit method. It creates and find the entities but these are quickly detached because of the em.close() instruction. So every data the JSF app handles is always detached. Nothing is managed. Below is 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="ar.org.ptpchaco_SICasosTierras_war_1.0PU" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>java:/JNDICasosTierras</jta-data-source>
   
    <class>ar.org.ptpchaco.siconflictostierras.storage.EntityTest</class>
    <class>ar.org.ptpchaco.siconflictostierras.storage.EntityTestChild</class>
    <exclude-unlisted-classes>true</exclude-unlisted-classes>
    <properties>
      <property name="hibernate.hbm2ddl.auto" value="update"/>
      <property name="hibernate.show_sql" value="false"/>
      <property name="hibernate.generate_statistics" value="false"/>
      <property name="hibernate.enable_lazy_load_no_trans" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

So my logic is as following. I first create a parent entity, and persist it. The instance get an ID. the JPA controller executes the transaction and flushes into the database (MySQL). MySQL inserts the row in the table. Fine.

Then I want to add a child to the parent entity. So I create the child, persist it. It receives and id. I add it to the SortedSet of the parent entity. And then try to merge the parent entity, for updating the SortedSet of child entities. That is done with the addChild() method.

My problem is. I load the page. Select any parent entity. Add a child, fine. Add a second child. Have an exception:

16:54:14,140 WARN [org.hibernate.engine.jdbc.spi.SqlExceptionHelper] (default task-1) SQL Error: 1062, SQLState: 23000 16:54:14,140 ERROR [org.hibernate.engine.jdbc.spi.SqlExceptionHelper] (default task-1) Duplicate entry '8-28' for key 'PRIMARY' 16:54:14,140 INFO [org.hibernate.engine.jdbc.batch.internal.AbstractBatchImpl] (default task-1) HHH000010: On release of batch it still contained JDBC statements...

Both entities are inserted in the database, parent and child, but when I try to update the parent (for second time), the relationship join table entitytest_entitytestchild tries to insert a duplicate pair of keys.

As you can see I have no much clue..., and I suspect i'm making so many things so wrong.

Please I need any advice on whatever I am doing wrong. Why does it work on the first time?, and not on the second time? Why Hibernate tries to insert two times the same pair of keys? What is the correct way of updating a parent entity that has a List or Set of other entities? Is my ApplicationScoped approach of a data service correct? The emf and utx objects should live as long as app scoped?

I'm using: JSF 2.3 Primefaces 10 JPA 2.1 with Hibernate 5.3 as persistence provider. MySQL 5.7 as database Netbeanas 13 as IDE Wildfly 26 as server.

0 Answers
Related