项目场景:
还是之前那个项目,使用SpringBoot+SpringDataJpa框架,又发现了一个Spring Data 容易出错的问题
问题描述:
有一个实体类,里边有一个属性是一对多的,我们使用了 List 来保存字段,然后有一个接口涉及到更新这个字段值,比如说原来这个字段有两个值,现在调用这个接口给这个字段再增加两个值,理论上现在这个字段上会有四个值(关联的表上会有四条对应的数据),但是最后我们发现这个字段上只有三个值,只有原来的两个值和最后添加上的那个值,但是Debug跟代码的时候又是正常的,四个值全都可以正常提交
代码:
生产代码不拿出来了,使用一个学生、课程、成绩的关系来举个例子
学生信息实体类:
package com.example.test.entity;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* @author quhan
*/
@Entity
public class Student {
@Id
@Column(name = "STUDENT_NAME")
private String studentName;
@Column(name = "AGE")
private Integer age;
/**
* 分数List
*/
@OneToMany(mappedBy = "student", fetch = FetchType.LAZY)
private List<Score> scoreList;
public Student() {
}
/**
* 可以方便得向已经存在的数据中添加成绩信息
*/
public void addScoreList(Score score) {
if (this.scoreList == null) {
this.scoreList = new ArrayList<>();
}
this.scoreList.add(score);
}
}
课程信息实体类:
package com.example.test.entity;
import javax.persistence.*;
import java.util.List;
import java.util.Objects;
/**
* @author quhan
*/
@Entity
public class Course {
/**
* 课程名称
*/
@Id
@Column(name = "COURSE_NAME")
private String courseName;
/**
* 教师名字
*/
@Column(name = "TEACHER_NAME")
private String teacherName;
/**
* 分数List
*/
@OneToMany(mappedBy = "course", fetch = FetchType.LAZY)
private List<Score> sourceList;
public Course() {
}
public Course(String courseName, String teacherName) {
this.courseName = courseName;
this.teacherName = teacherName;
}
}
成绩信息实体类:
package com.example.test.entity;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
/**
* @author quhan
*/
@Entity
public class Score {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid")
@Column(name = "id")
private String id;
/**
* 分数
*/
private Integer score;
/**
* 学生
*/
@ManyToOne
@JoinColumn(name = "STUDENT_NAME")
private Student student;
/**
* 课程
*/
@ManyToOne
@JoinColumn(name = "COURSE_NAME")
private Course course;
public Score() {
}
public Score(Integer score, Student student, Course course) {
this.score = score;
this.student = student;
this.course = course;
}
}
最开始发生问题的Service:
package com.example.test.service;
import com.example.test.dao.CourseDao;
import com.example.test.dao.ScoreDao;
import com.example.test.dao.StudentDao;
import com.example.test.entity.Course;
import com.example.test.entity.Score;
import com.example.test.entity.Student;
import com.example.test.util.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
/**
* @author quhan
*/
@Service
public class StudentService {
@Autowired
StudentDao studentDao;
@Autowired
CourseDao courseDao;
@Autowired
ScoreDao scoreDao;
@Transactional(rollbackFor = Exception.class)
public String update(String studentName, List<Pair<String, Integer>> courseAndScoreList) {
Student student = studentDao.findById(studentName).orElse(null);
if (student == null) {
return "未找到对应学生信息";
}
ArrayList<String> errorCourseList = new ArrayList<>();
for (Pair<String, Integer> courseAndScore : courseAndScoreList) {
Score score = scoreDao.findByStudentStudentNameAndCourseCourseName(studentName, courseAndScore.getFirst());
if (score == null) {
Course course = courseDao.findById(courseAndScore.getFirst()).orElse(null);
if (course == null) {
errorCourseList.add(courseAndScore.getFirst());
} else {
student.addScoreList(new Score(100, student, course));
}
} else {
score.setScore(courseAndScore.getSecond());
student.addScoreList(score);
}
}
if (student.getScoreList() != null && student.getScoreList().size() > 0) {
scoreDao.saveAll(student.getScoreList());
}
if (errorCourseList.size() > 0) {
return errorCourseList.toString() + "课程信息异常";
} else {
return "成功";
}
}
解决过程:
1、发现问题的时候首先想到的是开发机本地试一下,测试确实有问题,添加两条数据,只有最后一个可以成功添加进去。
2、确认了问题以后,开始使用Debug跟一下代码,看是不是代码逻辑写的有问题,是不是某一个地方导致了只有最后一个值才能成功add进去List,断点打到add的循环体里边(上边 StudentService 代码的for循环里边)然后奇怪的事情就来了,Debug看到每一条数据都成功添加进List了,然后去看数据库,发现每一条添加进去的数据都在数据库里,添加是成功的。
3、然后换了断点位置,换到for循环外( StudentService 代码的 scoreDao.saveAll() 的位置),这时候发现问题又出来了,又是只有最后一个值才能成功添加进数据库。
4、之后开始怀疑是因为从数据库搜索出来的对象被JPA代理过的才导致的问题,所以修改了一下代码,不再使用 Student 的 addScoreList() 方法,换成将 Student 的ScoreList使用 get 方法获取出来,然后在Service中执行add添加进List试一下:
修改后的代码:
public String update(String studentName, List<Pair<String, Integer>> courseAndScoreList) {
Student student = studentDao.findById(studentName).orElse(null);
if (student == null) {
return "未找到对应学生信息";
}
List<Score> scoreList = student.getScoreList();
ArrayList<String> errorCourseList = new ArrayList<>();
for (Pair<String, Integer> courseAndScore : courseAndScoreList) {
Score score = scoreDao.findByStudentStudentNameAndCourseCourseName(studentName, courseAndScore.getFirst());
if (score == null) {
Course course = courseDao.findById(courseAndScore.getFirst()).orElse(null);
if (course == null) {
errorCourseList.add(courseAndScore.getFirst());
} else {
scoreList.add(new Score(100, student, course));
}
} else {
score.setScore(courseAndScore.getSecond());
scoreList.add(score);
}
}
if (scoreList != null && scoreList.size() > 0) {
scoreDao.saveAll(scoreList);
}
if (errorCourseList.size() > 0) {
return errorCourseList.toString() + "课程信息异常";
} else {
return "成功";
}
}
然后重新测试,发现问题依然存在,还是只有最后一条能成功添加进去。
5、重新测试发现问题依然存在,但还是怀疑是JPA代理过对象导致的问题,所以把注意力转向那个 List 属性的字段,因为 List 本身是个接口,当时还不清楚JPA查询出来的具体实现类是什么,然后继续Debug跟代码,看了一下是一个叫 PersistentBag(org.hibernate.collection.internal.PersistentBag) 的实体类,怀疑这个实体类有问题,然后修改了一下 Student 的 addScoreList() 方法,代码如下:
public void addScoreList(Score score) {
if (this.scoreList == null || this.scoreList instanceof PersistentBag) {
ArrayList<Score> scoreList;
if (this.scoreList != null) {
scoreList = new ArrayList<>(this.scoreList);
} else {
scoreList = new ArrayList<>();
}
scoreList.add(score);
this.scoreList = scoreList;
} else {
this.getScoreList().add(score);
}
}
查询之后将 scoreList 字段的 PersistentBag 类型转为一个 ArrayList 然后再执行add方法,改完以后,重新测试,证实数据可以完整添加到数据库中。
解决方案:
临时的解决方案就是自己创建一个 List ,把原来的 PersistentBag 替换一下,这个问题就能解决了,但是具体的原因以及有没有更好的解决办法暂时没有仔细找。
后来又试了一下,把 for 循环中的
Score score = scoreDao.findByStudentStudentNameAndCourseName(studentName, courseAndScore.getFirst());
代码删除,好像也不会发生这种只提交最后一个值的情况了(有待确认),但是这种方法很有可能会影响业务,不推荐使用这种方式,也暂时没有找到恢复正常的原因