
本文详解如何在使用两个不同数据库(如 Derby 和 PostgreSQL)的 JPA 项目中,安全地复用同一实体对象进行持久化和更新操作,避免因共享主键状态导致的 Updates are not allowed 异常。
本文详解如何在使用两个不同数据库(如 derby 和 postgresql)的 jpa 项目中,安全地复用同一实体对象进行持久化和更新操作,避免因共享主键状态导致的 `updates are not allowed` 异常。
在基于 JPA(尤其是 EclipseLink)的多数据源架构中,开发者常尝试将同一实体实例(如 LocalCertificate)先后提交至两个独立的 EntityManager(对应不同 PersistenceUnit),以实现数据双写或迁移。但这种做法极易触发如下异常:
The attribute [id] of class 'LocalCertificate' is mapped to a primary key column in the database. Updates are not allowed
该错误的本质并非 SQL 层面的约束冲突,而是 JPA 实体生命周期管理机制的副作用:当一个实体在第一个 EntityManager 中被 persist() 或 merge() 后,其状态(包括 @Id 字段、托管状态、变更跟踪标记等)已被该上下文绑定;若直接将该已托管/已修改的实体传入第二个 EntityManager,EclipseLink 会将其识别为“已存在主键的托管实体”,进而拒绝后续的 merge() 操作——因为主键字段被标记为不可更新。
✅ 正确实践:实体隔离 + 显式 ID 管理
要彻底规避此问题,必须遵循两个核心原则:
- 每个 EntityManager 使用独立的实体实例(避免状态污染);
- 主键生成策略需脱离数据库自增(IDENTITY),改由应用层统一控制(确保双库写入时 ID 一致)。
✅ 方案一:使用 CopyGroup 安全复制实体(推荐用于更新场景)
EclipseLink 提供了高效的实体深拷贝工具 CopyGroup,可精准复制属性值并剥离原 EntityManager 的元数据绑定:
public void update(LocalCertificate cert) {
// 第一持久化单元:Derby
EntityManager locEm = emf.createEntityManager();
try {
EntityTransaction ta = locEm.getTransaction();
ta.begin();
locEm.merge(cert); // 此处 cert 可能是 detached 状态,merge 合法
ta.commit();
} finally {
locEm.close();
}
// 第二持久化单元:PostgreSQL —— 必须使用副本!
CopyGroup copyGroup = new CopyGroup();
copyGroup.setShouldResetPrimaryKey(false); // 保留原 id 值(关键!)
LocalCertificate certCopy = (LocalCertificate)
locEm.unwrap(JpaEntityManager.class).copy(cert, copyGroup);
EntityManager posEm = eemf.createEntityManager();
try {
EntityTransaction tta = posEm.getTransaction();
tta.begin();
posEm.merge(certCopy); // 使用干净副本,无状态冲突
tta.commit();
} finally {
posEm.close();
}
}⚠️ 注意:copyGroup.setShouldResetPrimaryKey(false) 是关键配置,否则 @Id 字段会被重置为 null,导致插入失败。
✅ 方案二:改用应用层 UUID 主键(推荐用于新增场景)
由于 GenerationType.IDENTITY 依赖数据库自增序列,无法保证 Derby 与 PostgreSQL 生成相同 ID,因此必须弃用:
@Table(name = "certs")
public class LocalCertificate implements Serializable {
private static final long serialVersionUID = -5003848691574858779L;
@Id
@Column(name = "id", length = 36) // PostgreSQL 支持 UUID 类型,也可用 VARCHAR(36)
private String id; // ✅ 改为 String 类型,支持 UUID
@Column(name = "d_id")
private String d_id;
// ... 其他字段保持不变
// 应用层 ID 分配逻辑
public void assignId() {
if (this.id == null || this.id.trim().isEmpty()) {
this.id = UUID.randomUUID().toString();
}
}
// 在 persist 前显式调用
public void store() {
assignId(); // ✅ 关键:先分配 ID!
// Derby 写入
EntityManager locEm = emf.createEntityManager();
try {
locEm.getTransaction().begin();
locEm.persist(this);
locEm.getTransaction().commit();
} finally {
locEm.close();
}
// PostgreSQL 写入 —— 复用同一 id 值,但需新实例
LocalCertificate pgCert = new LocalCertificate();
pgCert.setId(this.getId());
pgCert.setD_id(this.getD_id());
pgCert.setCertificate(this.getCertificate());
pgCert.setRevoked(this.isRevoked());
EntityManager posEm = eemf.createEntityManager();
try {
posEm.getTransaction().begin();
posEm.persist(pgCert);
posEm.getTransaction().commit();
} finally {
posEm.close();
}
}
}? 关键注意事项总结
- ❌ 禁止将同一个实体实例传入多个 EntityManager 执行 persist() 或 merge();
- ✅ 新增场景优先采用 UUID 或 Snowflake ID 等应用层可控主键,彻底规避数据库自增不一致问题;
- ✅ 更新场景务必使用 CopyGroup 或手动构造副本,确保每个 EntityManager 操作的是独立对象;
- ✅ 所有 EntityManager 实例必须显式 close()(建议配合 try-with-resources 或 finally 块);
- ✅ 若使用 @Convert 或 @Lob 字段(如 X509CertificateHolder),需确认 PostgreSQL JDBC 驱动及方言正确支持 bytea 或 BYTEA 类型映射;
- ✅ 生产环境应避免在业务方法内硬编码 Persistence.createEntityManagerFactory(),建议通过 CDI 或 Spring 管理 EntityManagerFactory 生命周期。
通过以上改造,您即可在 Derby 与 PostgreSQL 之间安全、一致地同步证书数据,同时消除主键更新限制带来的运行时异常。










