英文:
How to avoid duplicate insertions of "many" entity in one-to-many relationship when using a unique constraint in eclipselink/JPA
问题
我正在处理一个使用Spring MVC和EclipseLink 2.5.2与mysql数据库的大型代码库。数据库及其结构是直接创建的,而不是通过任何代码优先的方法。我的问题涉及一对多关系中的两个表。
在Java方面,以下是我配置的这两个实体类:
@Entity
public class Role {
// ...其他属性...
@Id
@TableGenerator(name = "ROLE.ID", allocationSize = 1, initialValue = 1)
@GeneratedValue(strategy = GenerationType.TABLE, generator = "ROLE.ID")
public Long getID() {
return this.id;
}
// ...其他方法...
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "ROLE_ID", referencedColumnName = "ID", insertable = false, updatable = false)
public Set<RoleDomain> getRoleDomains() {
return roleDomains;
}
// ...其他方法...
}
@Entity
@Table(name = "ROLE_DOMAIN")
public class RoleDomain {
// ...其他属性...
@Id
@TableGenerator(name = "ROLE_DOMAIN.ID", allocationSize = 1, initialValue = 1)
@GeneratedValue(strategy = GenerationType.TABLE, generator = "ROLE_DOMAIN.ID")
public Long getId() {
return id;
}
// ...其他方法...
@Column(name = "ROLE_ID", nullable = false)
public Long getRoleId() {
return roleId;
}
// ...其他方法...
@Column(name = "DOMAIN", length = 255)
public String getDomain() {
return domain;
}
// ...其他方法...
}
假设在这个表结构中,已经有一个在ROLE表中的记录,并且在ROLE_DOMAIN表中有一个引用它的记录,这将转换为一个名为myRole
的Role
对象,其中包含roleDomains
中的RoleDomain
。
现在,当我添加一个新的RoleDomain
并使用Spring Data Repository保存,像这样:
myRole.add(new RoleDomain("some string"));
roleRepository.save(myRole);
在数据库中,我会得到一个违反唯一约束的异常,即ROLE_ID和DOMAIN重复插入的情况。
最奇怪的问题是,如果我从数据库中移除唯一约束(注意:将Java注解配置保持完全相同。在数据库中字面上只是"DROP CONSTRAINT..."),然后save
调用就能正常工作。不会在ROLE_DOMAIN中创建重复项。它会按预期进行,只是将新记录添加到ROLE_DOMAIN中。
我不理解为什么数据库中的唯一约束会导致EclipseLink表现得如此不一致。我是否配置有误?谢谢。
编辑:
我刚刚尝试将RoleDomain
类上的@Table
注解替换为以下内容:
@Table(name = "ROLE_DOMAIN", uniqueConstraints =
@UniqueConstraint(columnNames = {"ROLE_ID", "DOMAIN"}))
但没有改变任何事情。
英文:
I am working on a large codebase using Spring MVC with EclipseLink 2.5.2 on a mysql database. The database and its structure are created directly, not through any code-first approach. My problem concerns 2 tables in a one-to-many relationship.
CREATE TABLE ROLE (
ID BIGINT(20) PRIMARY KEY,
-- OTHER FIELDS --
);
CREATE TABLE ROLE_DOMAIN (
ID BIGINT(20) PRIMARY KEY,
ROLE_ID BIGINT(20) NOT NULL,
DOMAIN VARCHAR(255) NOT NULL
-- OTHER FIELDS --
);
ALTER TABLE ROLE_DOMAIN ADD CONSTRAINT FK_ROLE_DOMAIN_ROLE_ID FOREIGN KEY (ROLE_ID) REFERENCES ROLE_BASE (ID) ON DELETE CASCADE;
ALTER TABLE ROLE_DOMAIN ADD CONSTRAINT UQ_ROLE_DOMAIN_ROLE_ID_DOMAIN UNIQUE (ROLE_ID, DOMAIN);
And in java, this is how I've got the two entities configured.
@Entity
public class Role {
private Long id;
private Set<RoleDomain> roleDomains = new HashSet<>();
@Id
@TableGenerator(name = "ROLE.ID", allocationSize = 1, initialValue = 1)
@GeneratedValue(strategy = GenerationType.TABLE, generator = "ROLE.ID")
public Long getID() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "ROLE_ID", referencedColumnName = "ID", insertable = false, updatable = false)
public Set<RoleDomain> getRoleDomains() {
return roleDomains;
}
public void setRoleDomains(Set<RoleDomain> roleDomains) {
this.roleDomains = roleDomains;
}
}
@Entity
@Table(name = "ROLE_DOMAIN")
public class RoleDomain {
private Long id;
private Long roleId;
private String domain;
@Id
@TableGenerator(name = "ROLE_DOMAIN.ID", allocationSize = 1, initialValue = 1)
@GeneratedValue(strategy = GenerationType.TABLE, generator = "ROLE_DOMAIN.ID")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column(name = "ROLE_ID", nullable = false)
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
@Column(name = "DOMAIN", length = 255)
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
}
Say that in this table structure, I already have a record in ROLE and a record in ROLE_DOMAIN that references it, translating to a Role
object named myRole
containing the RoleDomain
in roleDomains
.
Now, when I add a new RoleDomain
and save using a spring data repository like this:
myRole.add(new RoleDomain("some string"));
roleRepository.save(myRole);
I get an exception for a duplicate insert violating my unique constraint on ROLE_ID and DOMAIN in the database.
[EL Warning]: 2020-10-22 14:53:22.405--UnitOfWork(994047815)--Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.6.8.v20190620-d6443d8be7): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '198732-some string' for key 'UQ_ROLE_DOMAIN_ROLE_ID_DOMAIN'
Error Code: 1062
Call: INSERT INTO ROLE_DOMAIN (ID, DOMAIN, ROLE_ID) VALUES (?, ?, ?)
bind => [27, some other string, 198732]
The weirdest thing about this problem is that if I remove the unique constraint from the database (Note: keeping the java annotation configuration EXACTLY the same. Literally just "DROP CONSTRAINT..." in the db) then the save
call works just fine. It doesn't create duplicates in ROLE_DOMAIN. It does exactly what it's supposed to, just adds the new record to ROLE_DOMAIN.
I don't understand how a unique constraint in the db would cause eclipselink to act this inconsistently. Do I have something configured wrongly? Thanks.
EDIT:
I have just now tried replacing the @Table
annotation on the RoleDomain
class with this:
@Table(name = "ROLE_DOMAIN", uniqueConstraints =
@UniqueConstraint(columnNames = {"ROLE_ID", "DOMAIN"}))
It didn't change anything.
答案1
得分: 1
您的约束存在问题,EclipseLink在批处理时对语句进行排序,将删除操作放在最后 - 这是为了让您有机会清理其他约束,在删除行之前修改现有行。您可以通过在UnitOfWork上使用setShouldPerformDeletesFirst方法来更改此行为。由于这是本机API,您需要取消包装EntityManager以访问它,可以使用以下代码:
em.unwrap(org.eclipse.persistence.sessions.UnitOfWork.class)
如果您在事务中,这将仅为此EntityManager中的UnitOfWork设置。因此,如果您需要在任何地方始终使用它,您将希望使用自己的会话适配器类创建会话监听器,以便监听postAcquireUnitOfWork事件并在其上调用setShouldPerformDeletesFirst。
英文:
The issue with your constraint is that EclipseLink orders statements for batching, putting deletes last - this is to give you a chance to clean up other constraints, to modify existing rows before rows get deleted. This can be changed so that deletes are issued first using the setShouldPerformDeletesFirst method on the UnitOfWork. As this is native api, you will have to unwrap the EntityManager to get at it, using
em.unwrap(org.eclipse.persistence.sessions.UnitOfWork.class)
if you are in a transaction. This will only be set for the UnitOfWork within this EntityManager, so if you need it everywhere always, you will want to have a session listener with your own session adaptor class to listen for postAcquireUnitOfWork and call setShouldPerformDeletesFirst on it.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论