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