hibernate 查询id为空懒加载_hibernate 查询时指定查询字段、级联表的一种方式

本文转载于 SegmentFault 社区 社区专栏:河北工业大学梦云智软件开发团队 作者:myskies     
     最近在进行数据统计查询时屡次遇到慢查询事件,最终发现问题发生在 hibernate 的查询操作上。 hibernate 中 @ManyToOne 注解上的 FetchType 默认值为 FetchType.EAGER ,在进行查询操作时, hibernate 会自动的发起关联表的 join 查询。一旦关联的表太多则会大幅地影响查询效率。 在简单的数据查询中,上述查询机制并无可厚非:此机制能够在查询某个实体时,自动关联查询相关实体,这使得程序开发变得异常简单。但正是由于此方法会关联查询出过多的信息,使得在进行大量的数据操作时给数据库带来了过多的压力,数据库不堪重负,随之带来慢查询。 解决由于关联查询造成的慢查询问题的方法有几个:比如牺牲部分便利性为 @ManyToOne 注解添加 fetch = FetchType.LAZY 属性;再比如可以用综合查询专门的创建一个视图,并在综合查询中调用视图中的数据;再比如还可以为综合查询专门建立一个返回值类型。 本文给出一种通过代码来定义返回的字段、自动去除无用的关联查询的方法。
 

情景设置

  假设有以下 4 张表,分别为学生、班级、教师、学校。每个表中均有两个字段,分别为 id 及 name。er 图如下: 18e1126644281f6d8aeda03327c8253c.png   数据表间的关系均为 n:1 ,示例实体如下:  
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY ➊)
private Long id;

private String name;

@ManyToOne(cascade = CascadeType.PERSIST ➋)
private Clazz clazz;

// 省略空构造函数★及setter/getter
}
  • 设置为自增
  • 设置为级联保存
  • 空构造函数很重要,必须有
[success] 班级、教师、学校三个实体的代码均参考上述代码完成。
public interface StudentRepository extends CrudRepository<Student, Long>, JpaSpecificationExecutor {
}

 

测试

查询测试:  
@SpringBootTest
class StudentRepositoryTest {
@Autowired
StudentRepository studentRepository;

@Autowired
private EntityManager entityManager; ➊

Student student;

@BeforeEach ➋
public void beforeEach() {
School school = new School();
school.setName("测试学校");
Teacher teacher = new Teacher();
teacher.setName("测试教师");
Clazz clazz = new Clazz();
clazz.setName("测试班级");
this.student = new Student();
student.setName("测试学生");
teacher.setSchool(school);
clazz.setTeacher(teacher);
student.setClazz(clazz);
this.studentRepository.save(student);
}

@Test
public void find() {
this.studentRepository.findById(student.getId()).get();
}
}
  • 备用
  • 老的版本中使用的是 @Before,具体请参数本文给出的 github 链接
生成的 sql 语句如下:  
select 
student0_.id as id1_2_0_, student0_.clazz_id as clazz_id3_2_0_, student0_.name as name2_2_0_,
clazz1_.id as id1_0_1_, clazz1_.name as name2_0_1_, clazz1_.teacher_id as teacher_3_0_1_,
teacher2_.id as id1_3_2_, teacher2_.name as name2_3_2_, teacher2_.school_id as school_i3_3_2_,
school3_.id as id1_1_3_, school3_.name as name2_1_3_
from student student0_
left outer join clazz clazz1_ on student0_.clazz_id=clazz1_.id
left outer join teacher teacher2_ on clazz1_.teacher_id=teacher2_.id
left outer join school school3_ on teacher2_.school_id=school3_.id
where student0_.id=1
如上所示 hibernate 在查询学生时,会关联查询学生实体中通过 @ManyToOne 注解的字段,并且它还会聪明的依次累推关联查询班级实体中的教师字段以及教师实体对应的学校字段。
 

Selection

Hiberante 在综合中提供了 Selection 来解决查询时冗余字段与冗余关联的问题,在使用 Selection 来进行查询时需要先在实体类中建立对应的构造函数,假设当前仅需要查询出学生的 id,name 信息。则首先需要建立以下构造函数:  
    public Student(Long id, String name) {
this.id = id;
this.name = name;
System.out.println("student construct");
}
示例代码如下:   
    @Test
public void findByColumn() {
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder(); ➊
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Student.class); ➊
Root root = criteriaQuery.from(Student.class); ➊
criteriaQuery
.multiselect(root.get("id"), root.get("name")) ➋
.where(criteriaBuilder.equal(root.get("id").as(Long.class), student.getId().toString())); ➌
TypedQuery query = this.entityManager.createQuery(criteriaQuery); ➍
List students = query.getResultList(); ➎
}
}
  • 创建用于综合查询的 criteriaBuilder、criteriaQuery、root。
  • 创建本次查询的输出字段为 student 实体的 id、name 字段。
  • 设置查询条件
  • 生成预查询
  • 执行查询
执行测试控制台相关信息如下  
select student0_.id as col_0_0_, student0_.name as col_1_0_
from student student0_
where student0_.id=1

student construct
如上所示,在综合查询中使用了 multiselect 指定输出字段后, hibernate 进行查询时在进行 select 时只选择了规定字段 student.id、student.name ,并且在查询中并没有关联其它表。在查询出数据后,调用了 Student 实体中的构造函数。
 

关联查询

在需要进行关联查询时仍可按上述的步骤:先建立对应的构造函数,再设置相应的选择条件。比如需要查询出班级 id 及教师 id 的信息,代码如下:  
    public Student(Long id, String name, Long clazzId, Long teacherId) {
this.id = id;
this.name = name;
this.clazz = new Clazz();
this.clazz.setId(clazzId);
this.clazz.setTeacher(new Teacher());
this.clazz.getTeacher().setId(teacherId);
System.out.println("student construct invoked");
}
查询代码如下:  
    @Test
public void findByColumnWithJoin() {
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Student.class);
Root root = criteriaQuery.from(Student.class);
criteriaQuery
.multiselect(root.get("id"),
root.get("name"),
root.get("clazz").get("id"),
root.get("clazz").get("teacher").get("id"))
.where(criteriaBuilder.equal(root.get("id").as(Long.class), student.getId().toString()));
TypedQuery query = this.entityManager.createQuery(criteriaQuery);
List students = query.getResultList();
}
执行日志如下:   
select student0_.id as col_0_0_, student0_.name as col_1_0_, student0_.clazz_id as col_2_0_,
clazz1_.teacher_id as col_3_0_
from student student0_
cross join clazz clazz1_
where student0_.clazz_id=clazz1_.id and student0_.id=1

student construct invoked
如上所示 hibrenate 自动构建了有需要级联 sql 语句。
 

Selection< Tuple >

如果不想使用添加构造函数的方法来进行查询,还可以使用 Selection 。仍与上述查询为例:使用 Sel ection 进行查询的代码如下:  
    @Test
public void findByColumnWithJoinAndTuple() {
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Tuple.class); ➊
Root root = criteriaQuery.from(Student.class);
criteriaQuery
.multiselect(root.get("id"),
root.get("name"),
root.get("clazz").get("id"),
root.get("clazz").get("teacher").get("id"))
.where(criteriaBuilder.equal(root.get("id").as(Long.class), student.getId().toString()));
TypedQuery query = this.entityManager.createQuery(criteriaQuery); ➋
List tuples = query.getResultList();
List students = new ArrayList<>();
tuples.forEach(tuple -> {
Student student = new Student();
student.setId((Long) tuple.get(0)); ➌
student.setName((String) tuple.get(1)); ➌
student.setClazz(new Clazz());
student.getClazz().setId((Long) tuple.get(2)); ➌
student.getClazz().setTeacher(new Teacher());
student.getClazz().getTeacher().setId((Long) tuple.get(3)); ➌
students.add(student);
});
}
  • CriteriaQuery 泛型使用 Tuple
  • 预查询时泛型同样使用 Tuple
  • 使用 tuple.get(index) 方法以及类型强制转换将返回的 Tuple 类型的数据转换为 Student
  控制台主要信息如下:
select student0_.id as col_0_0_, student0_.name as col_1_0_, student0_.clazz_id as col_2_0_,
clazz1_.teacher_id as col_3_0_
from student student0_
cross join clazz clazz1_
where student0_.clazz_id=clazz1_.id and student0_.id=1
生成的 sql 代码仍然言简意赅。


  

注意事项

由于此查询方法在查询过程中使用了 hardCode 格式的字符串 (比如 root.get("id")) ,此字符串依赖于实体结构。实体结构发生变化后 Spring JPA 并不会在系统启动时有任何的错误产生,而一旦调用了相关的查询方法便会由于该字符串与实体类不对应造成系统 500 错误 。所以在使用此查询方法时,必须结合单元测试来使用!
 

总结

  hibernate 是款优秀的 ORM 框架,是 spring jpa 的默认选型。团队一直遵从站在巨人的肩膀上,相信巨人的选择都是对的原则,在生产项目的选型上全部毫不犹豫的选择了 hibernate 。但近期生产项目中的一些统计查询工作它的表现却不如人意,与手写 sql 相比有着较大的差距。因此,开始对 hibernate 产生怀疑的同时近一步的加深了对其深入的学习。在此期间还学习了很少的 mybatis 的相关知识。
本文结论:正确合理的使用 hibernate ,不论是在新增、更新、删除数据,还要是批量删除、综合查询数据上, hibernate 都具有在牺牲少量可控性能的前提下达到快速、便捷、面向对象的开发特点,应当成为中小型项目的首选。
 

参考文档

https://www.objectdb.com/java/jpa/query/jpql/select 文本示例代码: https://github.com/mengyunzhi/hibernate-select-specific-columns 作者:河北工业大学梦云智开发团队 潘杰

  - END - dd9b2dc8d40ee0716ba24e511f2faf80.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值