【问题标题】:How to select from subclass entity property using Spring Data / JPQL?如何使用 Spring Data / JPQL 从子类实体属性中进行选择?
【发布时间】:2021-08-31 12:41:23
【问题描述】:

我需要查询子类的属性。以下是涉及的实体:

@Entity
@Data
public class Course {

    @Id
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "personId")
    private Person person;

}

@Entity
@Data
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {

    @Id
    private Long id;

    private String name;

}

@Entity
@Data
@EqualsAndHashCode(callSuper=true)
public class Student extends Person {

    @ManyToOne
    @JoinColumn(name="studentTypeId")
    private StudentType studentType;
            
}

@Entity
@Data
public class StudentType {

    @Id
    private Long id;
    
    @Convert(converter = StudentTypeConverter.class)
    private StudentTypeEnum name;
    
    @RequiredArgsConstructor
    @Getter
    @ToString
    public static enum StudentTypeEnum {
        GRADUATE("Graduate");
        
        private final String name;
        
    }

}

我正在尝试使用 Spring Data 存储库进行查询:

public interface CourseRepository extends Repository<Course>  {
    
    List<Course> findByCoursePersonStudentTypeName(@Param("studentTypeName") String studentTypeName);
}

但这不起作用。它会生成一个错误声明No property StudentTypeName found for Person!。这是有道理的,因为该属性仅存在于 Student

如何编写此方法(最好)或使用 JPQL 按学生类型查找课程?

【问题讨论】:

  • 课程中的人应该是学生吗?
  • @pirho - 不。就我而言,它通常是学生,但我需要其他类型的人参加的灵活性。
  • 为什么要用StudentType 包裹StudentTypeEnumStudentTypeConverter的作用是什么,有什么作用?是不是可以使用枚举name作为字符串?

标签: hibernate jpa spring-data-jpa spring-data jpql


【解决方案1】:

这个怎么样:

@Query("select c from Course c join c.person as p where UPPER(p.studentType.name) = UPPER(?1)")
List<Course> findByCoursePersonStudentTypeName(String studentTypeName);

这使用了 Hibernate 的一个称为隐式转换的特性。

【讨论】:

  • 这很好用。您能否提供讨论隐式转换的 Hibernate 文档的链接?
  • 很遗憾,我找不到关于此的文档。我将创建一个问题来添加有关此的文档:hibernate.atlassian.net/browse/HHH-14699
【解决方案2】:

试试这个

@Query("select c from Course c join c.person as p, Student s where p.id = s.id and s.studentType.name = ?1")
List<Course> findByCoursePersonStudentTypeName(StudentTypeEnum studentTypeEnum);

【讨论】:

  • +1 这很接近。如果我更改,参数类型从 StringStudentTypeEnum 它可以工作。但我真的很想将参数保留为String,并让转换器转换为StudentTypeEnum
  • @James 使用 StudentTypeEnum 更新了答案
  • 谢谢。但这需要我传入GRADUATE。我希望能够传入Graduategraduate 等。我构建的转换器忽略大小写。您将如何修改查询以继续使用 String
  • @James 如何通过 StudentTypeEnum 调用上述查询并传递以下 new StudentTypeConverter().convertToEntityAttribute(studentTypeString) 作为参数?
  • 是的,这通常会很好。但这里的问题是,这是通过 Spring Data Rest 直接暴露给客户端的。
【解决方案3】:

存储库中的以下查询对我有用:

public interface CourseRepository extends Repository<Course, Long> {

    @Query("select c" +
            " from Course c" +
            " inner join Student s on c.person.id = s.id" +
            " where s.studentType is not null" +
            " and s.studentType.name = :studentTypeName")
    List<Course> findByCoursePersonStudentTypeName(@Param("studentTypeName") StudentType.StudentTypeEnum studentTypeName);

}

下面我将插入您在@Converter 注释中指定使用的枚举类型转换器代码:

public class StudentTypeConverter implements AttributeConverter<StudentType.StudentTypeEnum, String> {

    private final Map<String, StudentType.StudentTypeEnum> groupByName = Arrays
            .stream(StudentType.StudentTypeEnum.values())
            .collect(Collectors.toMap(StudentType.StudentTypeEnum::getName, type -> type));

    @Override
    public String convertToDatabaseColumn(StudentType.StudentTypeEnum attribute) {
        return attribute.getName();
    }

    @Override
    public StudentType.StudentTypeEnum convertToEntityAttribute(String dbData) {
        return groupByName.get(dbData);
    }
}

我还在枚举中添加了一种类型

        GRADUATE("Graduate"),
        OTHER("Other type");

下面给出方法调用及其结果:

List<Course> result = courseRepository.findByCoursePersonStudentTypeName(StudentType.StudentTypeEnum.GRADUATE);

我还粘贴了数据库中给出的数据:

  1. 学生类型


  2. 学生

  3. 课程

【讨论】:

    【解决方案4】:

    老实说,您的设计违背了非常基本的面向对象原则,并且对未来的维护和可扩展性存在问题。

    您正在尝试使用超类Person 来生成可以注册课程的不同子类型的人员。但另一方面,您希望找到与 Person 的特定子类型相关联的 Course,这完全破坏了 Generic 设置。这就是为什么 Java 编译器无法帮助您自动将查询链接到实体子类型,并且 Java 运行时会为您提供异常,因为它们不知道 Person 的任何子类型是否具有 StudentType 属性。该语言只是拒绝做可能导致错误或意外结果的事情。

    正如其他一些回复所指出的,您可以通过使用 Hibernate(或其他 ORM)提供的工具来实现您想要的。但这种解决方案显然是一种反模式,因为它会将业务知识泄露到 SQL 查询中。

    它将您的设计从面向对象转变为面向 SQL。

    我的建议是重构您的模型,一种选择是生成您的课程类,这在您的业务领域中似乎是多态的:

    public abstract class Course<T extends Person> {
        ...
    
        @ManyToOne
        @JoinColumn(name = "personId")
        private T person;
    }
    
    public class CourseRegisteredByStudent extends Course<Student> {
        
    }
    
    public class CourseRegisteredByStaff extends Course<Staff> {
       
    }
    
    public interface CousreRegsiteredByStudentRepositry extends Repository<CousreRegsiteredByStudent> { 
        List<CousreRegsiteredByStudent> findByType(@Param("studentTypeName") String studentTypeName);
    }
    
    ...
    
    

    这里是如何使用 Hibernate Any 映射来处理泛型类型实体的演示链接。

    https://github.com/zzantozz/testbed/tree/master/hibernate-any (借用这个帖子的链接https://stackoverflow.com/a/7001162/1109002

    虽然还有一些改进空间,但这种设计将关注点划分为清晰的边界,避免将领域逻辑泄漏到 Repository 层。在设计领域模型时,如果你必须编写自定义 SQL 而不是持久化框架提供的默认 Repository 函数,那么你应该停下来思考一下模型设计是否有问题。

    【讨论】:

      猜你喜欢
      • 2017-05-20
      • 1970-01-01
      • 1970-01-01
      • 2021-06-14
      • 2019-08-21
      • 1970-01-01
      • 2019-01-07
      • 1970-01-01
      • 2019-02-09
      相关资源
      最近更新 更多