
当 JPA 实体使用 @EmbeddedId 定义复合主键时,若新创建的实体其主键值已存在于一级缓存或数据库中(或 Hibernate 误判为“已托管”),save() 将跳过 INSERT,导致无日志、无数据写入。
当 jpa 实体使用 `@embeddedid` 定义复合主键时,若新创建的实体其主键值已存在于一级缓存或数据库中(或 hibernate 误判为“已托管”),`save()` 将跳过 insert,导致无日志、无数据写入。
在使用 JPA + Hibernate 管理带复合主键(@EmbeddedId)的实体时,一个典型却易被忽视的行为是:repository.save(entity) 并不总是执行 INSERT —— 它本质上是 persist() 或 merge() 的语义组合,具体行为取决于实体的“托管状态”(managed state)。
你的代码中存在两个关键隐患:
-
主键未正确初始化或类型不匹配
在示例调用中:entity.setId(new MyEntityId("id_1", "e56962a6-4747-49cd-b67b-bf8b01975c4c"));MyEntityId的字段类型为Long,但传入的是String字面量(如"id_1")。这将导致字段值为null(因类型不匹配无法自动转换),最终id1 == null && id2 == null。Hibernate 将该实体视为“临时(transient)但主键为空”,而根据 JPA 规范,空复合主键无法参与脏检查或标识比较,可能导致实体被静默忽略,或在后续 flush 时抛出IdentifierGenerationException(取决于配置)。务必确保构造时类型严格一致:MyEntityId id = new MyEntityId(); id.setId1(1001L); // ✅ Long 类型 id.setId2(2002L); entity.setId(id);
Hibernate 缓存/状态判断逻辑触发了“无操作”
如答案所指出:若你创建的MyEntity实例,其MyEntityId的值(如id1=1, id2=2)恰好与当前 Session 中已加载的某个托管实体相同(例如之前findAll()返回的某条记录),Hibernate 会认为这是同一个实体,save()将退化为merge(),且因其他字段(如otherData)未变更,最终连 UPDATE 都不触发 —— 日志中仅见 SELECT,无 INSERT/UPDATE。
✅ 正确做法如下:
-
显式使用
persist()强制插入(推荐用于新实体)
修改 Repository 接口以暴露persist()(需自定义实现或切换为JpaRepository#saveAndFlush()配合清空缓存):@Repository public class MyEntityRepositoryImpl implements MyEntityRepositoryCustom { @PersistenceContext private EntityManager em; @Override public MyEntity persist(MyEntity entity) { em.persist(entity); return entity; } } -
或确保主键唯一性 + 显式设置非空业务字段
MyEntity entity = new MyEntity(); MyEntityId id = new MyEntityId(); id.setId1(System.currentTimeMillis()); // 确保 id1 唯一 id.setId2(1L); entity.setId(id); entity.setOtherData("initial_value"); // ✅ 必须设值,避免全 null 导致状态识别异常 repository.saveAndFlush(entity); // 强制立即 flush,观察 SQL 日志
⚠️ 注意事项:
-
@Embeddable类必须实现equals()和hashCode(),否则 Hibernate 无法正确比较复合主键相等性(极易引发重复插入或丢失更新); - 启用 Hibernate SQL 日志(
spring.jpa.show-sql=true+logging.level.org.hibernate.SQL=DEBUG)是定位此类问题的首要手段; - 不要依赖
save()的“插入语义”——它本质是“使实体受管”,而非“强制插入”。
总结:复合主键实体未保存,90% 源于主键字段 null、类型错配、或主键值意外重复。务必验证 id 对象非空、类型匹配、equals/hashCode 正确,并优先用 saveAndFlush() + 日志确认实际执行的 SQL。


















