Hibernate/JPA 的递归更新
Recursive upserts with Hibernate/JPA
Java 11,Spring,休眠 & MySQL 这里。我有一些表:
create table if not exists exam
(
id int(11) not null auto_increment,
name varchar(100) not null,
display_name varchar(250),
constraint exam_pkey primary key (id),
);
create table if not exists section
(
id int(11) not null auto_increment,
exam_id int(11) not null,
name varchar(100) not null,
display_name varchar(250),
`order` int(11) not null,
constraint section_pkey primary key (id),
constraint section_exam_fkey foreign key (exam_id) references exam (id),
constraint section_name_key unique (exam_id, name),
);
create table if not exists question
(
id int(11) not null auto_increment,
section_id int(11) not null,
name varchar(100) not null,
type varchar(25) not null,
`order` int(11) not null,
constraint question_pkey primary key (id),
constraint question_exam_fkey foreign key (section_id) references section (id),
constraint question_name_key unique (section_id, name),
);
JPA 实体 类 为它们建模:
@Getter
@Setter
@Entity
@Table(name = "exam")
public class Exam {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Pattern(regexp = "\w+")
private String name;
private String displayName;
@OneToMany(mappedBy = "exam", fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@OrderBy("order asc")
private SortedSet<Section> sections = new TreeSet<>();
}
@Getter
@Setter
@Entity
@Table(name = "section")
public class Section implements Comparable<Section> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "exam_id")
@JsonIgnore
private Exam exam;
@Pattern(regexp = "\w+")
private String name;
private String displayName;
@Column(name="`order`")
private Long order;
@OneToMany(mappedBy = "section", fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@OrderBy("order asc")
private SortedSet<Question> questions = new TreeSet<>();
@Override
public int compareTo(Section other) {
return ObjectUtils.compare(order, other.getOrder());
}
}
// Question is a huge, complicated entity and I don't think I need to show it for
// someone to answer this question so I am omitting it for now, but I can add it
// in here if anyone thinks it makes a difference in providing the answer
以及用于持久化它们的存储库:
@Repository
public interface ExamRepository extends JpaRepository<Exam, Long> {
Optional<Exam> findByName(String name);
}
这是我的情况:
- 在任何给定的时间点,数据库中都会有 0+
exam
条记录
- 显然,如果其中有 0 个
exam
个实体,它也不会有任何 section
或 question
个实体,因此,所有 3 个表都将是完整的空(在本例中)
- 或者,可能有数百条
exam
条记录,每条记录都有自己的多个 section
,每个部分都有自己的大量 question
条记录(例如表格充满了数据)
- 我的服务器将从另一个来源(不是这个 MySQL DB),让我们参考这些“进口考试”
- 这真的不重要,但基本上文件会被 FTP 放入一个文件夹中,异步作业将这些文件反序列化为
Exam
个实例
- 如果其中一个导入的
Exam
具有与 MySQL 数据库中的任何 Exam
实体匹配的 name
值,我想要导入的 Exam
及其整个子树对象图(所有部分,以及每个部分的问题)以完全覆盖匹配的 DB Exam
及其 subtree/object 图
- 例如,如果数据库有一个名为“sally”的考试,它有 1 个部分,该部分有 4 个问题,然后导入的
Exam
也有一个名称“sally”,我希望它完全递归地完全覆盖“DB sally”考试,以及该考试的所有部分和问题
- 发生这种覆盖时,所有属于“旧”(现有)
Exam
的部分和问题都将被删除,并且 overwritten/replaced 属于新导入考试的部分和问题
- 但是如果导入考试的名称不匹配数据库中的任何考试名称,我希望它作为一个全新的
Exam
实例插入,连同它的整个整个 subtree/object 图保存在各自的表中
我有一个 ExamService
可以做到这一点:
@Service
public class ExamService {
@Autowired
private ExamRepository examRepository;
public void upsertExamFromImport(Exam importedExam) {
// for this demonstration, pretend importedExam.getName() is "sally" at runtime
Optional<Exam> maybeExistingExam = examRepository.findByName(importedExam.getName());
if (maybeExistingExam.isPresent()) {
Exam existingExam = maybeExistingExam.get();
// tell JPA/DB that the import IS the new matching exam
importedExam.setId(existingExam.getId());
}
examRepository.save(importedExam);
}
}
目前我的数据库确实有一个名为“sally”的考试。所以会有一场比赛。
运行此代码时出现以下异常:
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
java.sql.SQLIntegrityConstraintViolationException: Column 'exam_id' cannot be null
所以我认为这里发生的是:
- 代码发现存在与导入的考试名称匹配的现有考试(因此
maybeExistingExam
存在且非空);然后
importedExam.setId(existingExam.getId())
执行,现在导入的考试具有现有考试的 ID,但是其嵌套的 Section
实例仍然具有 null
Exam
引用(exam_id
).因此 Exam
被认为是“附加的”,但它的子树仍然被认为(用 JPA 的说法)是“分离的”。
- 当 Hibernate 继续保留导入的检查的
Section
时,它们是分离的,但是由于附加了父 Exam
,因此 exam_id
应该是非空的
即使该理论不完全准确,我认为我已经足够接近了。无论如何,这里有什么解决办法? 我怎么告诉 Hibernate/JPA “嘿伙计,这个导入的考试匹配现有的,所以它需要完全(递归)覆盖数据库中的匹配项"?
更新
如果我尝试将服务代码更改为:
@Service
public class ExamService {
@Autowired
private ExamRepository examRepository;
public void upsertExamFromImport(Exam importedExam) {
// for this demonstration, pretend importedExam.getName() is "sally" at runtime
Optional<Exam> maybeExistingExam = examRepository.findByName(importedExam.getName());
examRepository.save(importedExam);
if (maybeExistingExam.isPresent()) {
Exam existingExam = maybeExistingExam.get();
examRepository.delete(existingExam);
}
}
}
我在执行 examRepository.save(importedExam)
时遇到 ConstraintViolationException: Column 'exam_id' cannot be null
异常。
我无法复制您的确切异常,但经过一些修补后,我让它工作,至少在本地......
@Service
public class ExamService {
@Autowired
private ExamRepository examRepository;
public void upsertExamFromImport(Exam importedExam) {
Optional<Exam> maybeExistingExam = examRepository.findByName(importedExam.getName());
if (maybeExistingExam.isPresent()) {
Exam existingExam = maybeExistingExam.get();
this.examRepository.delete(existingExam);
}
this.examRepository.save(importedExam);
}
}
这就是我更改服务的方式 - 我删除了现有的考试,然后然后保存了新考试。老实说,考虑到你的唯一键是复合的,并且会有新的 id,这应该没有什么区别,但这是正确的逻辑顺序,所以最好坚持下去。
您已经在级联持久化和合并操作,所以保存应该没问题。要使删除生效,您需要为部分和问题的删除操作添加级联。
@OneToMany(mappedBy = "exam", fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
@OrderBy("order asc")
private SortedSet<Section> sections = new TreeSet<>();
在考试级联部分删除。
@OneToMany(mappedBy = "section", fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
@OrderBy("order asc")
private SortedSet<Question> questions = new TreeSet<>();
并在部分中删除级联问题。
我最终不得不手动 link 考试,它的部分,以及每个部分的问题,bi-directionally。在保存之前,这修复了一切。
示例:
exam.getSections().forEach(section -> {
section.setExam(exam);
section.getQuestions().forEach(question -> {
question.setSection(section);
});
});
examRepository.save(exam);
Java 11,Spring,休眠 & MySQL 这里。我有一些表:
create table if not exists exam
(
id int(11) not null auto_increment,
name varchar(100) not null,
display_name varchar(250),
constraint exam_pkey primary key (id),
);
create table if not exists section
(
id int(11) not null auto_increment,
exam_id int(11) not null,
name varchar(100) not null,
display_name varchar(250),
`order` int(11) not null,
constraint section_pkey primary key (id),
constraint section_exam_fkey foreign key (exam_id) references exam (id),
constraint section_name_key unique (exam_id, name),
);
create table if not exists question
(
id int(11) not null auto_increment,
section_id int(11) not null,
name varchar(100) not null,
type varchar(25) not null,
`order` int(11) not null,
constraint question_pkey primary key (id),
constraint question_exam_fkey foreign key (section_id) references section (id),
constraint question_name_key unique (section_id, name),
);
JPA 实体 类 为它们建模:
@Getter
@Setter
@Entity
@Table(name = "exam")
public class Exam {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Pattern(regexp = "\w+")
private String name;
private String displayName;
@OneToMany(mappedBy = "exam", fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@OrderBy("order asc")
private SortedSet<Section> sections = new TreeSet<>();
}
@Getter
@Setter
@Entity
@Table(name = "section")
public class Section implements Comparable<Section> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "exam_id")
@JsonIgnore
private Exam exam;
@Pattern(regexp = "\w+")
private String name;
private String displayName;
@Column(name="`order`")
private Long order;
@OneToMany(mappedBy = "section", fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@OrderBy("order asc")
private SortedSet<Question> questions = new TreeSet<>();
@Override
public int compareTo(Section other) {
return ObjectUtils.compare(order, other.getOrder());
}
}
// Question is a huge, complicated entity and I don't think I need to show it for
// someone to answer this question so I am omitting it for now, but I can add it
// in here if anyone thinks it makes a difference in providing the answer
以及用于持久化它们的存储库:
@Repository
public interface ExamRepository extends JpaRepository<Exam, Long> {
Optional<Exam> findByName(String name);
}
这是我的情况:
- 在任何给定的时间点,数据库中都会有 0+
exam
条记录- 显然,如果其中有 0 个
exam
个实体,它也不会有任何section
或question
个实体,因此,所有 3 个表都将是完整的空(在本例中) - 或者,可能有数百条
exam
条记录,每条记录都有自己的多个section
,每个部分都有自己的大量question
条记录(例如表格充满了数据)
- 显然,如果其中有 0 个
- 我的服务器将从另一个来源(不是这个 MySQL DB),让我们参考这些“进口考试”
- 这真的不重要,但基本上文件会被 FTP 放入一个文件夹中,异步作业将这些文件反序列化为
Exam
个实例
- 这真的不重要,但基本上文件会被 FTP 放入一个文件夹中,异步作业将这些文件反序列化为
- 如果其中一个导入的
Exam
具有与 MySQL 数据库中的任何Exam
实体匹配的name
值,我想要导入的Exam
及其整个子树对象图(所有部分,以及每个部分的问题)以完全覆盖匹配的 DBExam
及其 subtree/object 图- 例如,如果数据库有一个名为“sally”的考试,它有 1 个部分,该部分有 4 个问题,然后导入的
Exam
也有一个名称“sally”,我希望它完全递归地完全覆盖“DB sally”考试,以及该考试的所有部分和问题 - 发生这种覆盖时,所有属于“旧”(现有)
Exam
的部分和问题都将被删除,并且 overwritten/replaced 属于新导入考试的部分和问题
- 例如,如果数据库有一个名为“sally”的考试,它有 1 个部分,该部分有 4 个问题,然后导入的
- 但是如果导入考试的名称不匹配数据库中的任何考试名称,我希望它作为一个全新的
Exam
实例插入,连同它的整个整个 subtree/object 图保存在各自的表中
我有一个 ExamService
可以做到这一点:
@Service
public class ExamService {
@Autowired
private ExamRepository examRepository;
public void upsertExamFromImport(Exam importedExam) {
// for this demonstration, pretend importedExam.getName() is "sally" at runtime
Optional<Exam> maybeExistingExam = examRepository.findByName(importedExam.getName());
if (maybeExistingExam.isPresent()) {
Exam existingExam = maybeExistingExam.get();
// tell JPA/DB that the import IS the new matching exam
importedExam.setId(existingExam.getId());
}
examRepository.save(importedExam);
}
}
目前我的数据库确实有一个名为“sally”的考试。所以会有一场比赛。
运行此代码时出现以下异常:
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
java.sql.SQLIntegrityConstraintViolationException: Column 'exam_id' cannot be null
所以我认为这里发生的是:
- 代码发现存在与导入的考试名称匹配的现有考试(因此
maybeExistingExam
存在且非空);然后 importedExam.setId(existingExam.getId())
执行,现在导入的考试具有现有考试的 ID,但是其嵌套的Section
实例仍然具有null
Exam
引用(exam_id
).因此Exam
被认为是“附加的”,但它的子树仍然被认为(用 JPA 的说法)是“分离的”。- 当 Hibernate 继续保留导入的检查的
Section
时,它们是分离的,但是由于附加了父Exam
,因此exam_id
应该是非空的
即使该理论不完全准确,我认为我已经足够接近了。无论如何,这里有什么解决办法? 我怎么告诉 Hibernate/JPA “嘿伙计,这个导入的考试匹配现有的,所以它需要完全(递归)覆盖数据库中的匹配项"?
更新
如果我尝试将服务代码更改为:
@Service
public class ExamService {
@Autowired
private ExamRepository examRepository;
public void upsertExamFromImport(Exam importedExam) {
// for this demonstration, pretend importedExam.getName() is "sally" at runtime
Optional<Exam> maybeExistingExam = examRepository.findByName(importedExam.getName());
examRepository.save(importedExam);
if (maybeExistingExam.isPresent()) {
Exam existingExam = maybeExistingExam.get();
examRepository.delete(existingExam);
}
}
}
我在执行 examRepository.save(importedExam)
时遇到 ConstraintViolationException: Column 'exam_id' cannot be null
异常。
我无法复制您的确切异常,但经过一些修补后,我让它工作,至少在本地......
@Service
public class ExamService {
@Autowired
private ExamRepository examRepository;
public void upsertExamFromImport(Exam importedExam) {
Optional<Exam> maybeExistingExam = examRepository.findByName(importedExam.getName());
if (maybeExistingExam.isPresent()) {
Exam existingExam = maybeExistingExam.get();
this.examRepository.delete(existingExam);
}
this.examRepository.save(importedExam);
}
}
这就是我更改服务的方式 - 我删除了现有的考试,然后然后保存了新考试。老实说,考虑到你的唯一键是复合的,并且会有新的 id,这应该没有什么区别,但这是正确的逻辑顺序,所以最好坚持下去。
您已经在级联持久化和合并操作,所以保存应该没问题。要使删除生效,您需要为部分和问题的删除操作添加级联。
@OneToMany(mappedBy = "exam", fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
@OrderBy("order asc")
private SortedSet<Section> sections = new TreeSet<>();
在考试级联部分删除。
@OneToMany(mappedBy = "section", fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
@OrderBy("order asc")
private SortedSet<Question> questions = new TreeSet<>();
并在部分中删除级联问题。
我最终不得不手动 link 考试,它的部分,以及每个部分的问题,bi-directionally。在保存之前,这修复了一切。
示例:
exam.getSections().forEach(section -> {
section.setExam(exam);
section.getQuestions().forEach(question -> {
question.setSection(section);
});
});
examRepository.save(exam);