使用 Hibernate/JPA 进行递归更新插入
Java 11、Spring、Hibernate 和 Java 11 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 个表都将完全为空(在本例中) - 或者,可能有数百个
考试
记录,每个记录都有自己的多个部分
,每个部分都有多个自己的问题 记录(例如表中充满了数据)
- 显然,如果其中有 0 个
- 我的服务器将从另一个源(不是这个 MySQL DB),让我们将这些称为“导入的考试”
- 这确实不重要,但基本上文件将通过 FTP 传输到文件夹中,并且异步作业将这些文件反序列化为
Exam
实例
- 这确实不重要,但基本上文件将通过 FTP 传输到文件夹中,并且异步作业将这些文件反序列化为
- 如果这些导入的
Exam
之一具有name
值与 MySQL DB 中的任何Exam
实体匹配,我想要导入的Exam
及其整个子树对象图(其所有部分) ,以及每个部分的问题)以完全覆盖匹配的数据库Exam
及其子树/对象图- 例如,如果数据库有一个名为“sally”的考试,它有 1 个部分,该部分有 4 个问题,然后导入的
考试
也有一个名称“sally”,我希望它完全递归地完全覆盖“DB sally”考试以及该考试的所有部分和问题 - 发生此覆盖时,属于“旧”(现有)
考试
的所有部分和问题都将被删除,并被新导入考试的部分和问题覆盖/替换
- 例如,如果数据库有一个名为“sally”的考试,它有 1 个部分,该部分有 4 个问题,然后导入的
- 但如果导入考试的名称不与数据库中的任何考试名称匹配,我希望将其作为全新的
考试< /code> 实例,其整个子树/对象图持久化到各自的表中
我有一个 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< /code>
考试
参考 (exam_id
)。因此,Exam
被视为“附加”,但其子树仍被视为(用 JPA 术语)“分离”。- 当 Hibernate 保留导入考试的
Section
时,它们会被分离,但由于父Exam
已附加,因此exam_id
预计为非-null
即使这个理论不完全准确,我想我已经足够接近了。无论如何,这里的修复是什么?我如何告诉 Hibernate/JPA“嘿朋友,这个导入的考试与现有的匹配,所以它需要完全(递归地)覆盖数据库中的匹配< /em>”?
更新
如果我尝试将服务代码更改为:
@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' 不能为 null
异常。
Java 11, Spring, Hibernate & MySQL here. I have some tables:
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),
);
And the JPA entity classes modeling them:
@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
And the repositories used for persisting them:
@Repository
public interface ExamRepository extends JpaRepository<Exam, Long> {
Optional<Exam> findByName(String name);
}
Here is my situation:
- at any given point in time, the database will have 0+
exam
records in it- obviously if it has 0
exam
entities in it, it will also not have anysection
orquestion
entities in it either, and thus, all 3 tables would be completely empty (in this case) - or, there could be hundreds of
exam
records, each with their own multiplesection
s, and each section with a multitude of its ownquestion
records (e.g. the tables are full of data)
- obviously if it has 0
- my server will be receiving a new
Exam
instance (with its own "subtree" ofSection
andQuestion
instances) from another source (not this MySQL DB), let us refer to these a "Imported Exams"- it really shouldn't matter but basically files will be FTP'd into a folder and an asynchronous job deserializes these files into
Exam
instances
- it really shouldn't matter but basically files will be FTP'd into a folder and an asynchronous job deserializes these files into
- if one of these Imported
Exam
s has aname
value that matches any of theExam
entities in the MySQL DB, I want the importedExam
and its entire subtree object graph (all its sections, and each section's questions) to completely overwrite the matching DBExam
and its subtree/object graph- so for example, if the DB has an Exam named "sally" and it has 1 section and that section has 4 questions, and then an imported
Exam
also has a name of "sally", I want it to completely overwrite the "DB sally" exam, and all of that exam's sections and questions, completely and recursively - when this overwrite happens, all the sections and questions belonging to the "old" (existing)
Exam
are deleted and overwritten/replaced by the sections and questions of the new imported exam
- so for example, if the DB has an Exam named "sally" and it has 1 section and that section has 4 questions, and then an imported
- but if the import Exam's name doesn't match any exam names in the DB, I want it inserted as a brand new
Exam
instance, with its whole entire subtree/object graph persisted to their respective tables
I have an ExamService
for doing this:
@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);
}
}
Currently my database does have an exam named "sally". So there will be a match.
When this code runs I get the following exception:
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
So what I think is happening here is:
- The code sees that there is an existing exam that matches the imported exam's name (hence
maybeExistingExam
is present and non-null); then importedExam.setId(existingExam.getId())
executes and now the imported exam has the existing exam's ID, however its nestedSection
instances still have anull
Exam
reference (exam_id
). Hence theExam
is considered "attached" yet its subtree is still considered (in JPA parlance) to be "detached".- When Hibernate goes to persist the imported exam's
Section
s, they are detached but since the parentExam
is attached, theexam_id
is expected to be non-null
Even if that theory isn't completely accurate, I think I'm close enough. Regardless, what's the fix here? How do I tell Hibernate/JPA "hey pal, this imported exam matches an existing, so it needs to completely (recursively) overwrite the match in the DB"?
Update
If I try changing the service code to:
@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);
}
}
}
I get a ConstraintViolationException: Column 'exam_id' cannot be null
exception when it executes examRepository.save(importedExam)
.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我无法复制您的确切例外情况,但经过一番修改后,我让它工作起来,至少在本地...
这就是我更改服务的方式 - 我删除了现有的检查,然后然后保存新的。老实说,考虑到您的唯一键是复合的并且会有新的 id,这应该没有什么区别,但这是正确的逻辑顺序,所以最好坚持下去。
您已经在级联持久和合并操作,因此保存应该没问题。要使删除工作,您需要为部分和问题添加级联删除操作。
在考试中级联部分删除。
并分段删除级联问题。
I wasn't able to replicate your exact exceptions, but after tinkering a bit, i made it work, locally at least...
That's how i changed the service - i delete the existing exam, and then save the new one. To be honest that should not make difference, considering that your unique keys are composite and there will be new ids, but this the correct logical order, so it's better to stick to it.
You are already cascading persist and merge operations, so save should be ok. For delete to work you would need to add cascading for removal operations both for sections and questions.
In exams cascade sections removal.
And cascade questions removal in sections.
我最终不得不手动将考试、其各个部分以及每个部分的问题双向链接在一起。在保存之前,一切都解决了。
例子:
I ended up having to manually link the exam, its sections, and each section's questions together, bi-directionally. Prior to the save, and that fixed everything.
Example: