首页 > 解决方案 > 如何使用唯一约束实现@ManyToMany?

问题描述

我想为 npm 包版本及其维护者建模。查看以下 API 响应。

https://registry.npmjs.org/react

与版本反应的包17.0.2有多个维护者。

    "maintainers": [
        {
            "name": "sebmarkbage",
            "email": "sebastian@calyptus.eu"
        },
        {
            "name": "gaearon",
            "email": "dan.abramov@gmail.com"
        },
        {
            "name": "acdlite",
            "email": "npm@andrewclark.io"
        },
        {
            "name": "brianvaughn",
            "email": "briandavidvaughn@gmail.com"
        },
        {
            "name": "fb",
            "email": "opensource+npm@fb.com"
        },
        {
            "name": "trueadm",
            "email": "dg@domgan.com"
        },
        {
            "name": "sophiebits",
            "email": "npm@sophiebits.com"
        },
        {
            "name": "lunaruan",
            "email": "lunaris.ruan@gmail.com"
        }
    ],

一个维护者可以有多个包,例如gaearon也可以是另一个包的维护者。

这是我目前的做法。这是我的NpmPackageVersion.java

@Entity
public class NpmPackageVersion {

  public NpmPackageVersion() {}

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

  @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
  @JsonManagedReference
  private Set<Maintainer> maintainers = new HashSet<>();

  public void addMaintainer(Maintainer maintainer) {
    this.maintainers.add(maintainer);
    maintainer.getNpmPackageVersions().add(this);
  }

  public void removeMaintainer(Maintainer maintainer) {
    this.maintainers.remove(maintainer);
    maintainer.getNpmPackageVersions().remove(this);
  }
}

这是我的Maintainer.java

@Entity
public class Maintainer {

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

  @ManyToMany(fetch = FetchType.LAZY, mappedBy = "maintainers")
  @JsonBackReference
  private Set<NpmPackageVersion> npmPackageVersions = new HashSet<>();

  private String name;

  private String email;

  private String url;
}

这是我的数据库表。

CREATE TABLE IF NOT EXISTS npm_package_version (
    id BIGSERIAL PRIMARY KEY,
    version TEXT NOT NULL
)

CREATE TABLE IF NOT EXISTS maintainer (
    id SERIAL PRIMARY KEY,
    name TEXT,
    email TEXT UNIQUE,
    url TEXT
)

CREATE TABLE IF NOT EXISTS npm_package_version_maintainers (
    npm_package_versions_id BIGINT NOT NULL REFERENCES npm_package_version,
    maintainers_id INT NOT NULL REFERENCES maintainer
)

如您所见,我对维护者电子邮件有一个独特的约束。

我现在正在查询 npm 注册表并尝试将数据存储在我的数据库中。

// do the http request and use a DTO, then create a new entity
var npmPackageVersion = new NpmPackageVersion();
npmPackageVersion.setVersion(version);
// add maintainers
if (value.maintainers() != null) {
  value
  .maintainers()
  .forEach(m -> {
    var maintainer = maintainerRepository.findByEmailIgnoreCase(m.email()).orElseGet(() -> {
      var inner = new Maintainer();
      inner.setEmail(m.email());
      inner.setName(m.name());
      inner.setUrl(m.url());
      return inner;
    });
    npmPackageVersion.addMaintainer(maintainer);
  });
}

// then save the new version

我正在使用emailAPI 响应中的字段检查维护者是否已经在数据库中。如果维护者已经在场,我会使用它,如果没有,我会创建一个新的。我将每个维护者都与包相关联。

当我尝试保存包时,出现以下错误

引起:org.postgresql.util.PSQLException:错误:重复键值违反唯一约束“maintainer_email_key”

我想我知道为什么会出现此错误。Java 保存新的 npm 包版本,同时尝试保存所有维护者。如果已经存在的维护者是maintainers SetPostgres 的一部分,则会引发错误。

所以我的问题是:我怎样才能防止这个错误?我如何告诉 hiberante 只保存新的维护者,而现有的维护者只将关联添加到npm_package_version_maintainers表中?我不应该尝试将现有维护者再次保存到数据库中。


编辑 2021/06/14

我创建了一个演示 repo 来重现我的问题。

https://github.com/zemirco/hibernate-issue

尽管我添加hashCodeequals所有实体,但我仍然遇到同样的错误。我想我们已经很接近了,但我仍然缺少一些东西。关系是

包 -> 版本 -> 维护者

所以对于每个包都有多个版本。每个版本都有多个维护者。同一个维护者可以有多个版本。所以最后当我尝试保存 Package Hibernate 尝试保存所有版本和所有维护者。然后我得到一个维护者可能已经存在于数据库中的错误。

标签: javaspring-boothibernatejpa

解决方案


你的问题是

maintainerRepository.findByEmail(dto.email())

由于您的交易尚未完成,因此同一电子邮件将多次返回 null。

之后,您将添加许多项目

var inner = new Maintainer();
inner.setEmail(dto.email());
inner.setName(dto.name());
inner.setUrl(dto.url());
maintainerRepository.save(inner);
return inner;

使用相同的电子邮件。但是在事务完成之前不会调用它。并且在事务结束时,您有许多调用maintainerRepository.save(inner);会抛出 Unique Constraint Ex。

您在 Github 的示例中面临同样的问题。您多次添加相同的维护者,这将导致底层 Hibernate 机制多次调用保存。

一种可行的解决方案是使用单独的方法,您将首先保存通过电子邮件过滤的维护者。您应该使用注释该方法@Transactional(propagation = Propagation.REQUIRES_NEW)

第二种方法也用注释@Transactional(propagation = Propagation.REQUIRES_NEW)将始终调用 justfindByEmail并将对象放入其中npmPackageVersion或任何其他对象。在这里,所有维护者都已经保存在数据库中。

(propagation = Propagation.REQUIRES_NEW)有一些缺点和可能的问题,但是如果没有单独的事务,您在这里想要作为批处理作业就无法轻松实现。或者我现在看不到它:)

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveMainteners(Dto dto){
    if(maintenerRepository.findByEmail(dto.getEmail) == null){
       maintenerRepository.save(new Maintener(dto));
    }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addMaintener(Dto dto){
    var npmPackageVersion = new NpmPackageVersion();
    npmPackageVersion.setVersion(version);
    npmPackageVersion.setDescription(value.description());
    npmPackageVersion.setHomepage(value.homepage());
    Maintainer maintainer = maintenerRepository.findByEmail(dto.getEmail);
    npmPackageVersion.addMaintainer(maintainer);
}

推荐阅读