使用 JPA 在 EJB 中的同一事务中连续删除和保留同一实体

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

我有一个非常特殊的情况,我需要更新 JPA (EclipseLink 2.6.0) 不允许的主键。因此,首先删除实体,然后插入新值。

涉及的表具有 GlassFish Server 用于 JAAS 身份验证所需的预定义结构。

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_idgroup_id一起构成复合主键。 group_table 中的 group_id 是引用 user_role_table 中的 user_id 的外键。 GroupTable 持有来自 @Embeddable class、GroupTablePK.

@EmbeddedId

很少需要此信息。因此,我不会发布涉及的实体 class。


尝试通过首先删除提供的实体 GroupTable 然后使用新值 group_id 持久化同一实体来模拟更新,如下所示(在使用 CMT 的 EJB 中)。

同样,这是一个非常特殊的情况,甚至更新用户的权限也是相当罕见的。只是值得事先提供功能。

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);
}

这些方法在同一事务中执行,并且它们的工作非常好,但前提是 update() 方法中唯一的注释行未被注释。否则,它会抱怨 group_table 中主键的重复条目 - 在持久化导致重复插入产生的实体之前,首先要删除的实体不会被删除。

为什么在持久化实体之前需要 entityManager.flush();?这是到数据库的额外往返,应该避免。

Hibernate 文档说,

Flushing is the process of synchronizing the underlying persistent store with persistable state held in memory.

因此 flush() 会将您的持久状态(在您的情况下通过调用 delete(groupTable) 删除 groupTable)与底层数据库同步。总之 flush 之后,hibernate 会将这些更改写入 DB。

因此,当您评论 entityManager.flush(); 时,hibernate 不会将更改与数据库同步(写入),导致它抱怨 group_table 中主键的重复条目。所以在这种情况下有必要调用 flush

注意:flush() 可能有助于在正在进行的事务和最终 commit 更改之间持久保存数据。因此,如果之后出现问题,您也可以回滚之前的更改,例如批处理 insert/update.

为了避免通过 EntityManager#flush(); 进行额外的数据库往返,我使用 EclipseLink 特定的 CopyGroup 克隆指定的实体,然后按照 Chris 在下面评论部分中的建议将其持久化到数据库中这个问题。比如,

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;
}

问题中显示的delete()方法保持不变。

CopyGroup.CASCADE_PRIVATE_PARTS是默认的深度级别,如果CopyGroup没有attribute/s指定显式,只表示relationship/s私有属性与实体中的所有其他属性一起被克隆。

如果 CopyGroup 但是,指定至少一个属性 显式 (使用 CopyGroup#addAttribute()), then the default depth level is CopyGroup.CASCADE_TREE 仅复制 attribute/s 指定的 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.


补充:

如果CopyGroup#setShouldResetVersion(true)(带true)与CASCADE_PRIVATE_PARTSCASCADE_ALL_PARTSNO_CASCADE中的任何一个一起使用,则主键attribute/s 将不会设置克隆对象。

如果 CopyGroup#setShouldResetVersion(false)(与 false)与 CASCADE_TREE 一起使用,则主键 attribute/s 将是 copied/set。否则,如果给定 true(使用 CASCADE_TREE),则不会设置未使用 CopyGroup#addAttribute() 指定的主键 attribute/s(除非显式指定,即需要要明确)。

有关 CopyGroup 的更多详细信息,请参阅以下 link。