java - 如何使用唯一约束实现@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
我正在使用email
API 响应中的字段检查维护者是否已经在数据库中。如果维护者已经在场,我会使用它,如果没有,我会创建一个新的。我将每个维护者都与包相关联。
当我尝试保存包时,出现以下错误
引起:org.postgresql.util.PSQLException:错误:重复键值违反唯一约束“maintainer_email_key”
我想我知道为什么会出现此错误。Java 保存新的 npm 包版本,同时尝试保存所有维护者。如果已经存在的维护者是maintainers
Set
Postgres 的一部分,则会引发错误。
所以我的问题是:我怎样才能防止这个错误?我如何告诉 hiberante 只保存新的维护者,而现有的维护者只将关联添加到npm_package_version_maintainers
表中?我不应该尝试将现有维护者再次保存到数据库中。
编辑 2021/06/14
我创建了一个演示 repo 来重现我的问题。
https://github.com/zemirco/hibernate-issue
尽管我添加hashCode
了equals
所有实体,但我仍然遇到同样的错误。我想我们已经很接近了,但我仍然缺少一些东西。关系是
包 -> 版本 -> 维护者
所以对于每个包都有多个版本。每个版本都有多个维护者。同一个维护者可以有多个版本。所以最后当我尝试保存 Package Hibernate 尝试保存所有版本和所有维护者。然后我得到一个维护者可能已经存在于数据库中的错误。
解决方案
你的问题是
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);
}
推荐阅读
- reactjs - 如何更改我也作为道具收到的组件的道具?
- java - Java Selenium 错误:NoClassDefFoundError:org/openqa/selenium/HasAuthentication
- java - 设备主题模式确实会阻止应用主题模式。安卓工作室 Java
- google-apps-script - 在 Google 表格的不同列上检查最大值和最小值
- sql-server - 在权限链的情况下拒绝不优先?
- r - X轴为年月的ggplot
- java - 如何防止测试中违反参照完整性约束?
- r - Tidyverse ('jsonlite') 的命名空间加载失败
- sql - 如何在sqlite中组合一列?
- java - 具有嵌套查询和 UUID 数组字段的 HQL/JPQL 查询