使用 Spring Data Jpa 查询出的实体含有List属性的字段时向List添加属性引发的异常

项目场景:

还是之前那个项目,使用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());

代码删除,好像也不会发生这种只提交最后一个值的情况了(有待确认),但是这种方法很有可能会影响业务,不推荐使用这种方式,也暂时没有找到恢复正常的原因

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Data JPA 中,我们可以通过定义 Repository 接口中的方法,使用方法名来自动生成查询语句。但是有时候,我们需要自定义查询结果,比如说只需要查询结果中的部分字段,或者对查询结果进行聚合操作等。这时,我们可以使用 Spring Data JPA 提供的投影(Projection)功能来自定义查询结果。 投影是指将实体类中的一部分属性或关联属性映射成一个接口或类,从而返回一个自定义的结果集。Spring Data JPA 支持三种投影方式: 1. 接口投影:定义一个接口,接口中声明需要的属性Spring Data JPA 将根据接口定义的属性生成查询结果。 2. 类投影:定义一个类,类中声明需要的属性Spring Data JPA 将根据类定义的属性生成查询结果。 3. 动态投影:可以根据查询条件动态地返回不同的投影结果,比如说根据用户的角色返回不同的查询结果。 下面是一个简单的例子,演示如何使用接口投影来自定义查询结果: 定义一个接口投影: ```java public interface UserProjection { String getUsername(); String getEmail(); } ``` 在 UserRepository 中使用接口投影: ```java public interface UserRepository extends JpaRepository<User, Long> { List<UserProjection> findByUsername(String username); } ``` 通过上面的代码,我们可以在 UserRepository 中定义一个方法 findByUsername,该方法会返回一个 List<UserProjection> 类型的结果集,该结果集中只包含 username 和 email 两个字段。 当我们调用 findByUsername 方法时,Spring Data JPA 将根据方法名自动生成查询语句,并根据 UserProjection 接口中定义的属性生成查询结果。 当然,除了接口投影,还有其他的投影方式,你可以根据自己的需求选择适合的方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值