手动设置实体主键后调用EntityManager#persist()的结果及实体状态详解
嘿,这两个都是JPA里基础但关键的问题,我来给你掰扯清楚:
问题1:手动设置主键后调用EntityManager#persist()的结果
这个得看你实体的主键生成策略,不同情况结果完全不一样:
- 如果主键用的是
GenerationType.ASSIGNED(明确要手动分配主键):那完全没问题,persist()会正常把这个实体插入数据库,JPA会直接用你设置的主键值,不会多管闲事。 - 如果主键是自动生成类型(比如
IDENTITY自增、SEQUENCE序列、TABLE表生成):这里分两种情况:- 像Hibernate这类比较“灵活”的JPA实现,可能会直接忽略自动生成策略,用你设置的主键值插入,但这其实不符合JPA规范;
- 严格遵循JPA规范的实现(比如EclipseLink)会直接抛出
EntityExistsException——因为persist()的设计初衷是处理全新的、没有主键的实体,你手动设了主键,JPA会默认认为这个实体已经在数据库里存在了,所以报错。
- 补充个小技巧:如果你确实想在自动生成策略下手动设主键,别用
persist(),改用merge()。merge()会先查数据库里有没有这个主键的记录,有就更新,没有就插入,完美适配这种场景。
问题2:JPA实体的状态详解
JPA实体从创建到销毁,会经历4种核心状态,每个状态的行为和EntityManager的交互逻辑完全不同,我给你逐个拆解:
1. 新建状态(Transient)
说白了就是刚用new关键字创建出来的“野生”实体,既没和EntityManager扯上关系,数据库里也找不到对应的记录。比如你写User user = new User();,这时候user就是新建状态,哪怕你手动给它设了主键,只要没被EntityManager处理过,还是新建状态。
- 进入方式:直接
new实例即可; - 状态转换:调用
persist(user)会把它变成托管状态;如果没人引用它,被垃圾回收就直接销毁。
2. 托管状态(Managed)
这是JPA里最“听话”的状态,实体和EntityManager绑定在一起,EntityManager会全程跟踪它的一举一动——你改个属性值,不用手动调用update,事务提交的时候自动同步到数据库。数据库里已经有对应的记录(或者即将在事务提交时插入)。
- 进入方式:
- 用
persist()处理新建状态的实体; - 用
find()、getReference()从数据库加载实体; - 用
merge()处理游离状态的实体,返回的新实例是托管的; - 用
refresh()刷新游离实体,也会变回托管。
- 用
- 状态转换:
- 事务提交/调用
flush(),只是把变化同步到数据库,状态还是托管; - 调用
detach(user)会把它踢成游离状态; - 调用
remove(user)会标记它为删除状态; - EntityManager一关闭,所有托管实体直接变游离。
- 事务提交/调用
3. 游离状态(Detached)
相当于实体和EntityManager“分手”了,EntityManager不再跟踪它的任何变化——你哪怕把实体的属性改得面目全非,数据库里的记录也不会有动静。但数据库里还是有这条记录的,只是实体和EntityManager没关系了。
- 进入方式:
- EntityManager关闭;
- 手动调用
detach(); - 事务提交后,有些JPA实现会把实体从缓存里清掉,自动变游离;
- 把实体序列化再反序列化,出来的实例也是游离的(毕竟原来的EntityManager早就不存在了)。
- 状态转换:
- 调用
merge(user)会生成一个新的托管实例,原来的游离实例还是游离; - 调用
refresh(user)会重新从数据库拉数据,把实体变回托管; - 别对游离实体调用
persist(),会报错,因为persist()只认新建实体。
- 调用
4. 删除状态(Removed)
这个状态很好理解:实体被EntityManager标记为“要删除”,只要事务一提交,数据库里对应的记录就会被删掉。这时候实体还是和EntityManager关联的,但你不能再修改它的属性了。
- 进入方式:对托管状态的实体调用
remove(user); - 状态转换:事务提交后,实体就相当于被销毁了;如果在提交前反悔,调用
persist(user)还能撤销删除,变回托管状态。
内容的提问来源于stack exchange,提问作者Lakshay Khurana
相关产品推荐
相关产品推荐

