Search code examples
hibernatejpaejbeclipselink

Removing and persisting the same entity in a row in the same transaction in EJB using JPA


I have a very special case where I need to update a primary key which is disallowed by JPA (EclipseLink 2.6.0). Therefore, the entity is first deleted and then inserted with new values.

The tables involved have a predefined structure being required by GlassFish Server for JAAS authentication.

mysql> describe user_role_table;
+-------------+---------------------+------+-----+---------+-------+
| Field       | Type                | Null | Key | Default | Extra |
+-------------+---------------------+------+-----+---------+-------+
| user_id     | varchar(176)        | NO   | PRI | NULL    |       |
| password    | varchar(255)        | NO   |     | NULL    |       |
| row_version | bigint(20) unsigned | NO   |     | 0       |       |
+-------------+---------------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

mysql> describe group_table;
+---------------+---------------------+------+-----+---------+-------+
| Field         | Type                | Null | Key | Default | Extra |
+---------------+---------------------+------+-----+---------+-------+
| user_group_id | varchar(176)        | NO   | PRI | NULL    |       |
| group_id      | varchar(15)         | NO   | PRI | NULL    |       |
| row_version   | bigint(20) unsigned | NO   |     | 0       |       |
+---------------+---------------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

user_group_id and group_id together form a composite primary key. group_id in group_table is a foreign key referencing user_id in user_role_table. GroupTable holds an @EmbeddedId from an @Embeddable class, GroupTablePK.

This information will seldom be needed. Therefore, I am not posting the entity classes involved.


An update is attempted to be simulated by first removing the supplied entity, GroupTable and then persisting the same entity using a new value of group_id as follows (in an EJB using CMT).

Again, this is a very special case and even updating a user's authority is fairly rare. Just that it is worth providing the functionality beforehand.

public GroupTable update(GroupTable groupTable, String userId, String oldGroupId) {
    String newGropuId = groupTable.getGroupTablePK().getGroupId();
    groupTable.getGroupTablePK().setGroupId(oldGropuId);

    if (delete(groupTable)) {
        // entityManager.flush();
        groupTable.setUserRoleTable(entityManager.getReference(UserRoleTable.class, userId));
        groupTable.getGroupTablePK().setGroupId(newGropuId);
        entityManager.persist(groupTable);
    }

    return groupTable;
}
public boolean delete(GroupTable groupTable) {
    groupTable.setUserRoleTable(entityManager.getReference(UserRoleTable.class, groupTable.getUserRoleTable().getUserId()));
    GroupTable managedGroupTable = entityManager.merge(groupTable);
    managedGroupTable.getUserRoleTable().getGroupTableList().remove(groupTable);
    entityManager.remove(managedGroupTable);
    return !entityManager.contains(managedGroupTable);
}

These methods are executed in the same transaction and they do their job pretty well but only if the only commented line inside the update() method is uncommented. Otherwise, it complains about a duplicate entry for a primary key in the group_table - the entity which is to be removed first is not removed prior to persisting that entity causing a duplicate insert to spawn.

Why is entityManager.flush(); required prior to persisting the entity? It is an additional round trip to the database and should be avoided.


Solution

  • In order to avoid an additional database round trip through EntityManager#flush();, I used EclipseLink specific CopyGroup to clone the specified entity before persisting it to the database as suggested by Chris in the comment section below the question. Such as,

    import org.eclipse.persistence.jpa.JpaEntityManager;
    import org.eclipse.persistence.sessions.CopyGroup;
    
    public GroupTable update(GroupTable groupTable, UserTable userTable, String oldGroupId) {
        String newGroupId = groupTable.getGroupTablePK().getGroupId();
        groupTable.getGroupTablePK().setGroupId(oldGroupId);
        GroupTable copy = null;
    
        if (delete(groupTable)) {
    
            CopyGroup copyGroup = new CopyGroup();
            copyGroup.setShouldResetPrimaryKey(true);
            copyGroup.setShouldResetVersion(true);
            copyGroup.setDepth(CopyGroup.CASCADE_PRIVATE_PARTS); // Implicit in this case.
    
            copy = (GroupTable) entityManager.unwrap(JpaEntityManager.class).copy(groupTable, copyGroup);
    
            GroupTablePK groupTablePK = new GroupTablePK();
            groupTablePK.setGroupId(newGroupId);
            groupTablePK.setUserGroupId(groupTable.getGroupTablePK().getUserGroupId());
    
            copy.setGroupTablePK(groupTablePK);
            copy.getUserRoleTable().getGroupTableList().clear();
            UserRoleTable managedUserRoleTable = entityManager.find(UserRoleTable.class, userTable.getEmailId());
            copy.setUserRoleTable(managedUserRoleTable);
            managedUserRoleTable.getGroupTableList().add(copy); // Use a defensive link management method instead.
            entityManager.persist(copy);
        }
    
        return copy;
    }
    

    The delete() method as shown in the question is left untouched.

    CopyGroup.CASCADE_PRIVATE_PARTS is the default depth level, If CopyGroup has no attribute/s specified explicitly, indicating only the relationship/s being privately owned are cloned along with all other attributes in the entity.

    If CopyGroup however, specifies at least one attribute explicitly (using CopyGroup#addAttribute()), then the default depth level is CopyGroup.CASCADE_TREE which only copies the attribute/s as specified by addAttribute().

    CopyGroup#setShouldResetPrimaryKey(true) - Set if the primary key should be reset to null. CopyGroup#setShouldResetVersion(true) - Set if the version should be reset to null.


    Additional :

    If CopyGroup#setShouldResetVersion(true) (with true) is used along with anyone of CASCADE_PRIVATE_PARTS, CASCADE_ALL_PARTS or NO_CASCADE, then the primary key attribute/s of the cloned object will not be set.

    If CopyGroup#setShouldResetVersion(false) (with false) is used along with CASCADE_TREE, then the primary key attribute/s will be copied/set. Otherwise, if it is given true (using CASCADE_TREE), then the primary key attribute/s which are not specified with CopyGroup#addAttribute() will not be set (unless explicitly specified i.e. one needs to be explicit).

    More details about CopyGroup can be found in the following link.