保存子实体的重复键值

Posted

技术标签:

【中文标题】保存子实体的重复键值【英文标题】:Duplicate key value on saving child entities 【发布时间】:2022-01-24 07:21:38 【问题描述】:

当我尝试保存多对多关系的实体(角色)时,子实体的 id 生成不正确,并且我收到 DataIntegrityViolationException。

用子实体保存父实体的一部分:

public Organization create(Organization organization) 
        Organization created = save(organization);
        Role role = new Role();
        role.setCode("test");
        created.getRoles().add(role);
        return save(created);
    

休眠调试和异常:

Hibernate: insert into organization (company_code, full_legal_name, id) values (?, ?, ?)
Hibernate: insert into role (code, description, read_only, reserved) values (?, ?, ?, ?)
2021-12-23 10:34:50.459  WARN 13464 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: 23505
2021-12-23 10:34:50.459 ERROR 13464 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : ERROR: duplicate key value violates unique constraint "role_pkey"
  Detail: Key (id)=(15) already exists.

父实体:

@Entity        
public class Organization 

    @Id
    Long id;

    @ManyToMany(cascade = CascadeType.ALL, fetch= FetchType.EAGER)
    @JoinTable(name = "organization_role",
            joinColumns = @JoinColumn(name = "organization_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    Set<Role> roles = new HashSet<>();
    

角色实体:

@Entity
public class Role implements Serializable 

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

Liquibase 表: 角色:

CREATE TABLE role
(
    id          SERIAL                                    NOT NULL,
    code        VARCHAR(32)                               NOT NULL,
    CONSTRAINT role_pkey PRIMARY KEY (id)
);

组织:

CREATE TABLE organization (
    id bigint NOT NULL,
    CONSTRAINT organization_pkey PRIMARY KEY (id)
);

组织-角色:

CREATE TABLE organization_role (
    role_id INTEGER  NOT NULL,
    organization_id BIGINT NOT NULL,
    CONSTRAINT fk_organization_role_role FOREIGN KEY (role_id) REFERENCES role(id),
    CONSTRAINT fk_organization_role_organization FOREIGN KEY (organization_id) REFERENCES organization(id),
    CONSTRAINT user_organization_pkey PRIMARY KEY (role_id, organization_id)
);

【问题讨论】:

【参考方案1】:

这与 MTM 无关。只有当您的 PK 序列(串行)落后于某种方式时,这种情况才有可能发生,现在它会生成表中已经存在的密钥。为什么会发生这种情况的几个想法:

您重新创建了与序列关联的序列,因此它再次从 1 开始。但在 15 之前没有 ID 的记录,因此前 14 次插入成功。 您插入的记录具有指定的显式 ID,而不是使用 serial 功能。以下是重现问题的方法(第二次插入会导致问题出现):
drop table if exists org;
create table org (
    id serial,
    name text,
    constraint org_pk primary key (id)
);

insert into org(name) values('name1'); -- works fine
insert into org(id, name) values(2, 'name2'); -- we don't use the sequence
insert into org(name) values('name3');-- sequence generates 2 again and insert fails

您需要找出谁搞砸了序列/谁插入了指定 ID 的行。并更新那个地方:

    要么根本不指定 ID - 串行会为你做这件事 或在插入语句中显式访问序列:
insert into org(id, name) values(nextval('org_id_seq'), 'name2');
    或 update the sequence 在插入行后使用 setval()。

【讨论】:

是的,最初的 16 次插入是通过 liquibase 指定的 id 完成的。它会破坏顺序吗?如果是这种情况,有没有办法用另一个 liquibase changeLog 来解决这个问题?只是一个旁注 - 插入了 16 个角色,而不是 14 个,这也很奇怪为什么它专门尝试添加 id=15 用 3 种修复迁移的方法更新了答案。第一个是最简单的。 我已经通过运行添加它 SELECT setval('role_id_seq', (SELECT MAX(id) FROM role) + 1);在我最后一次手动插入之后。我不能“根本不指定 ID”的原因是因为它已经在以前的 liquibase 更改日志中完成了。但这就是答案,谢谢!【参考方案2】:

尝试先保存角色,然后将其添加到 Org,然后保存 Org:

Organization created = save(organization);
    Role role = new Role();
    role.setCode("test");
    save(role); // this assign a persistant primary key to the role
    created.getRoles().add(role);
    return save(created);

【讨论】:

感谢您的评论,但是在我应用答案中的修复后一切正常,因此无需预先创建角色

以上是关于保存子实体的重复键值的主要内容,如果未能解决你的问题,请参考以下文章

如何阻止实体框架尝试保存/插入子对象?

使用 EF Core 保存附加实体时如何删除子实体

Cascade保存子实体失败了JPA(Spring数据+ Hibernate)?

实体框架代码第一个值对象持久保存到数据库[重复]

JPA 存储库:将实体保存在大表中的问题 - 超时错误 [重复]

核心数据 - 将实体保存为 pList / 字典