【问题标题】:Spring Data JPA: duplicate records after sorting by a property in a joined tableSpring Data JPA:按联接表中的属性排序后的重复记录
【发布时间】:2020-02-22 13:28:37
【问题描述】:

我在我的项目中遇到了一个问题,因此我将提供一个使用演示项目的示例。假设有两个实体:教师和学生。

@Entity
@Table(name = "STUDENTS")
public class Student {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String surname;

    @ManyToOne
    private Teacher teacher;

    // getters and setters
}
@Entity
@Table(name = "TEACHERS")
public class Teacher  {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String surname;

    @OneToMany(mappedBy = "teacher")
    List<Student> students = new ArrayList<>();

    // getters and setters

}

有一个存储库和一个服务类:

public interface TeacherRepository extends PagingAndSortingRepository<Teacher, Long>, JpaSpecificationExecutor<Teacher> {
}
@Service
public class TeacherService {

    @Autowired
    private TeacherRepository teacherRepository;

    @Autowired
    private TeacherToDTOMapper teacherToDTOMapper;

    public Page<TeacherDTO> findAll(Pageable pageable) {
        Specification<Teacher> specification = (root, criteriaQuery, criteriaBuilder) -> {

            return criteriaBuilder.lessThanOrEqualTo(root.get("id"), 2);
        };
        return teacherRepository.findAll(specification, pageable).map(teacherToDTOMapper::mapEntityToDTO);
    }
}

最后是一个控制器类:

@RestController
public class TeacherController {

    @Autowired
    private TeacherService teacherService;

    @GetMapping("/teachers")
    @ResponseStatus(HttpStatus.OK)
    public PagedModel<EntityModel<TeacherDTO>> findAll(PagedResourcesAssembler<TeacherDTO> resourcesAssembler, Pageable pageable) {
        Page<TeacherDTO> page = teacherService.findAll(pageable);
        return resourcesAssembler.toModel(page);
    }
}

在 UI 上有一个table of teachers

用户过滤掉 id > 2 的教师,并按学生姓名对他们进行排序。预期的结果应该完全按照该顺序包含 Aliyah Wiggins 和 Macy Reyes(因为学生 Amina Parks 应该是排序后的第一个)。 http://localhost:8080/teachers?page=0&size=3&sort=students.name,asc

但是,实际结果是:

{
  "_embedded": {
    "teacherDTOList": [
      {
        "id": 1,
        "name": "Aliyah",
        "surname": "Wiggins",
        "students": [
          {
            "id": 1,
            "name": "Amina",
            "surname": "Parks"
          },
          {
            "id": 2,
            "name": "Caleb",
            "surname": "Baker"
          },
          {
            "id": 3,
            "name": "Valentino",
            "surname": "Kent"
          },
          {
            "id": 4,
            "name": "Jaron",
            "surname": "Luna"
          },
          {
            "id": 5,
            "name": "Cayden",
            "surname": "Hoover"
          }
        ]
      },
      {
        "id": 1,
        "name": "Aliyah",
        "surname": "Wiggins",
        "students": [
          {
            "id": 1,
            "name": "Amina",
            "surname": "Parks"
          },
          {
            "id": 2,
            "name": "Caleb",
            "surname": "Baker"
          },
          {
            "id": 3,
            "name": "Valentino",
            "surname": "Kent"
          },
          {
            "id": 4,
            "name": "Jaron",
            "surname": "Luna"
          },
          {
            "id": 5,
            "name": "Cayden",
            "surname": "Hoover"
          }
        ]
      },
      {
        "id": 2,
        "name": "Macy",
        "surname": "Reyes",
        "students": [
          {
            "id": 6,
            "name": "Jasmin",
            "surname": "Friedman"
          },
          {
            "id": 7,
            "name": "Randall",
            "surname": "Archer"
          },
          {
            "id": 8,
            "name": "Caroline",
            "surname": "Davila"
          },
          {
            "id": 9,
            "name": "Sarah",
            "surname": "Pace"
          }
        ]
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/teachers?page=0&size=3&sort=students.name,asc"
    }
  },
  "page": {
    "size": 3,
    "totalElements": 3,
    "totalPages": 1,
    "number": 0
  }
}

Aliyah Wiggins 有重复记录。我还没有找到一种适当的方法来获得预期的结果,所以现在我认为搜索与一个实体相关的对象并在另一个实体中按属性排序是不正确的。 演示项目可用here

【问题讨论】:

    标签: java sql spring spring-boot spring-data-jpa


    【解决方案1】:

    我开始认为尝试按学生(即“许多”)的名字对教师(即一对多关系中的“一个”)进行排序是错误的。尝试按学生姓名排序后,我们将得到以下结果:

    ROW    TEACHER_NAME     TEACHER_SURNAME     STUDENT_FULL_NAME
    1      Evan             Fuentes             Allyson Noble
    2      Aliyah           Wiggins             Amina Parks
    3      Evan             Fuentes             Ashlyn Berg
    4      Aliyah           Wiggins             Caleb Baker
    ...
    

    一位老师可以同时是第一名、第三名和最后一名。所以,那个排序之王是没有意义的。 此外,Many-To-Many 更适合该示例,但仍不能改变事实。

    【讨论】:

      【解决方案2】:

      您可以将查询设置为返回规范中的不同值:

      criteriaQuery.distinct(true);
      

      这通常由 Spring Data 在 findAll 方法上设置为 true,但在您使用自己的规范时并不暗示。您现在可以看到数据库连接查询的结果(教师将为每个学生重复一次)。

      我建议在模型中添加一个标志属性,而不是按 id 跳过。引入hidden / deleted / private 之类的东西会简化代码并使其更具可读性。然后您可以使用一个空的存储库接口方法:findByActiveTrue();

      【讨论】:

      • criteriaQuery.distinct(true); return criteriaBuilder.lessThanOrEqualTo(root.get("id"), 2);不幸的是,它没有帮助: org.h2.jdbc.JdbcSQLSyntaxErrorException: Order by expression "STUDENTS1_.NAME" 在这种情况下必须在结果列表中; SQL 语句:select distinct teacher0_.id as id1_1_,teacher0_.name as name2_1_,teacher0_.surname as surname3_1_ from teacherteacher0_ left outer join students students1_ on teacher0_.id=students1_.teacher_id where teacher0_.id
      • 错误是由您按不在选择本身中的列排序的事实引起的。我认为您将不得不在代码中进行排序,因为从查询的角度来看它没有多大意义。您可以将 @OrderBy("name") 注释添加到学生列表并根据teacher.getStudents().get(0) 对教师列表进行排序(如果我理解正确的要求)。另外,不应该是 >=2 吗?
      【解决方案3】:

      您可以使用命名实体图来避免重复记录。

      @NamedEntityGraph(name = "Teacher.students" ,attributeNodes = @NamedAttributeNode("students"))
      @Entity
      @Table(name = "TEACHERS")
      public class Teacher  {
          @Id
          @GeneratedValue
          private Long id;
          private String name;
          private String surname;
      
          @OneToMany(mappedBy = "teacher")
          List<Student> students = new ArrayList<>();
      
          // getters and setters
      
      }
      

      并将 findAll 方法覆盖为

      @EntityGraph(value = "Teacher.students" , type = EntityGraphType.LOAD)
      Page<Teacher> findAll(Specification<Teacher> spec, Pageable pageable);
      

      【讨论】:

      • 谢谢,实体图是一个有趣的功能,但是,它对我的​​实际项目没有帮助。在演示中,我在教师类中只有一个一对多关系,但在真实的类中有 3 个这样的关系。这些表有 > 100k 记录。实体图方法返回笛卡尔积并且不考虑分页,因此有> 400k条记录。显示一个页面的内容需要一分钟以上。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-09-28
      • 1970-01-01
      • 2021-11-13
      • 1970-01-01
      • 2015-02-18
      • 2019-05-31
      • 2017-06-08
      相关资源
      最近更新 更多