Hibernate非常适合业务明确、稳定、规范(例如:遵循数据库范式)的项目, 我非常推荐你在这类项目中使用Hibernate
请大家根据自己项目的实际情况选择合适的框架, 不要为了使用而使用, 导致产生不必要的麻烦和误解
本项目使用Kotlin作为主要语言, 但如果与Java的版本有较大差异时, 我也会单独写一份Java版的样例供大家参考
项目 | 版本 | 备注 |
---|---|---|
Java | 21 | |
Kotlin | 1.9.21 | |
Spring Boot | 3.2.0 | |
Hibernate | 6.3.1.Final | |
PostgreSQL | 16 | (为了更贴合实际, 所以替换成了PG) |
建议引入以下依赖, 该依赖会自动生成实体类的一些可能会用到的对象和常量, 方便在后面的条件查询时使用
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
</dependency>
在引入Lombok的前提下, 可以参考以下方式
/**
* 不直接使用@Data的原因是, Lombok自动生成的方法会在调用时触发懒加载, 例如toString会打印实体类中所有属性的值
*/
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@DynamicUpdate
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey = ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
private Clazz clazz;
@CreationTimestamp
@Column(insertable = false)
private LocalDateTime createdAt;
}
创建一个实体对象, 利用Builder模式, 可以只填写非空字段
var po = Student.builder().name("xxx").build();
Kotlin需要配合NoArg、AllOpen插件才能正常使用, 可以参考本项目的配置
@Entity
@DynamicUpdate
class Student(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var id: Long = 0,
@Column(nullable = false)
var name: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey = ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
var clazz: Clazz,
@CreationTimestamp
@Column(insertable = false)
var createdAt: LocalDateTime = LocalDateTime.now()
)
创建一个实体对象, 利用Kotlin的特性, 可以只填写非空字段
val student = Student(name = "xxx")
如果想方便的联表查询, 必须维护
@OneToMany, 可有可无, 不是特别有用
建议新建一个Repository接口来继承JpaRepository、JpaSpecificationExecutor, 这样每个实体仓库都只需要继承一个接口即可, 具体参考项目中的AbstractRepository接口。
在定义好对应的实体类和Repository接口后, 只需要调用Repository的findById方法即可实现单表查询
// 用于不确定记录是否存在的场景
studentRepository.findByIdOrNull
// 用于确认记录肯定存在时, 仅仅是需要一个引用时使用
studentRepository.getReferenceById
涉及多表查询时, 请先维护好实体们的@ManyToOne
在维护好多对一关系后, 只需像下面一样即可实现多表关联查询
val student = studentRepository.getReferenceById(1L)
// 查询学生所在的班级名称
println(student.clazz.name)
// 该方法在开启懒加载时会导致产生2次查询的问题, 后面的章节会介绍
具体可以到ManyToOneTest#test1中进行试用和调试
该章节将为大家介绍如何在Spring Data Jpa中如何进行简单条件查询
如果我们需要根据id查询, 可以使用我们之前编写好的Repository接口的getReferenceById方法查询
光有根据Id查询是不能满足日常的开发工作的, 我们通常还会需要根据其他字段进行查询
此时我们可以使用Spring Data Jpa提供的Query Methods来快速编写一些简单的查询方法
interface StudentRepository: AbstractRepository<Student, Long> {
/**
* 根据名称查询
*/
fun findAllByName(name: String): List<Student>
}
调用该方法, 会为我们自动生成如下的HQL语句
select s from Student s where s.name = ?1
如果你也有这样的烦恼的, 可以尝试一下@Query注解, 它支持我们直接编写HQL或SQL
interface StudentRepository: AbstractRepository<Student, Long> {
/**
* 根据名称查询
*/
@Query("""
select s from Student s where s.name = ?1
""")
fun findAllByNameWithQuery(name: String): List<Student>
/**
* 根据名称查询, 使用原生sql语句
*/
@Query("""
select * from student where name = ?1
""", nativeQuery = true)
fun findAllByNameWithNativeQuery(name: String): List<Student>
}
JPA默认支持的函数不多, 可以参考该章节 知道支持的函数列表
对于原生函数的调用, 可以通过以下方法来实现
select s from Student s where function('to_char', s.createdAt, 'yyyy-MM-dd HH:mm:ss')
详情可见单元测试EntityManagerTest#testNativeFunction
默认情况下, JPA每次更新都会set所有的非主键字段, 但有些时候我们只需要更新部分字段, 该如何实现呢?
使用@DynamicUpdate注解, 有了该注解的实体类, 在进行更新操作时, 只会更新有数据变更的列
有些时候, 我们希望就算某些属性发送了变更, 也不要更新到数据库中, 此时只需要在@Column的参数声明updatable = false即可
默认情况下, ManyToOne会自动生成一条外键, 部分公司或开发人员可能更倾向于使用没有外键的方式
我们可以通过使用JoinColumn注解取消外键的生成
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey = ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
var clazz: Clazz
在手动开启事务的情况下(open-in-view的不算), Jpa会提交你对实体类做的任何修改(尽管你没有调用更新方法).
/**
* 在这个例子中的最后,
* 我们修改了clazz的name,
* 尽管我们没有进行任何的更新和提交操作,
* jpa还是替我们提交了对clazz的修改
*/
@Transactional
@GetMapping("/test")
open fun test(): String {
val clazz = clazzHelper.create()
clazz.name = "modify"
return String.format("%s %s", clazz.id, clazz.name)
}
据我了解目前无法对JPA的这种行为进行限制, 不过如果我们换一种思路, 实体类就是数据库中记录的引用, 更新实体类就是在更新表记录, 这样是否更加容易接受呢?
所以, 最好不要将实体类用于其他用途, 只作为数据库记录的载体而使用。
在涉及懒加载操作时, 需要主动开启事务
看过之前章节的人应该会发现, 简单条件查询很难满足实际开发需求, 我们可以通过接下来的内容来了解如何在Spring Data Jpa中进行复杂条件查询
可以尝试了解一下HQL,
它比Spring Data Jpa提供的方法更加灵活
接下来为大家介绍一些复杂的查询案例, 看看是否能解决你的需求
日常开发中经常会进行一些联表查询, 只返回一个实体对象是远远不能满足需求的
此时需要另外定义一个类来接收这多个实体
// 类定义
class StudentClassDto(
val student: Student,
val clazz: Clazz) {
}
// Repository中的方法可以这样写
@Query("""
select
new io.fantasy0v0.po.student.dto.StudentClassDto(s, c)
from Student s left join Clazz c on c = s.clazz
""")
fun findAll(): List<StudentClassDto>
详情可见单元测试DtoTest#test_1
@Query对应的代码版
val cb = entityManager.criteriaBuilder
val cq = cb.createQuery(StudentClassDto::class.java)
val root = cq.from(Student::class.java)
cq.multiselect(
root,
root[Student_.clazz]
)
cq.where(cb.equal(root[Student_.id], student.id))
val query = entityManager.createQuery(cq)
query.firstResult = 0
query.maxResults = 1
Assertions.assertEquals(student.id, query.singleResult.student.id)
Assertions.assertEquals(student.clazz.id, query.singleResult.clazz.id)
详情可见单元测试SpecificationTest#testDto
尽量直接通过HQL或@Query将实体转换成DTO或者VO, 而不是直接操作实体
@Query("""
Select s from Student s left join fetch Clazz c on c = s.clazz
Where
(?1 is null or c.id = ?1)
and
(?2 is null or s.id = ?2)
""")
fun findAll_2(clazzId: Long?, studentId: Long?): List<Student>
TODO 目前发现的问题
- @Query不能和Specification同时使用
- Specification只能返回Entity
- 如果结果依赖了懒加载字段, 在查询时需要手动标记一下需要fetch的字段
TODO
利用@Subselect注解来解决复杂多表查询, 同时还解决了动态条件的问题
如果不存在动态条件那可以直接使用@Query注解
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Immutable
@Subselect("""
SELECT
s.id, s.name, c.id as clazz_id, c.name as clazz_name
from student s left join clazz c on s.clazz_id = c.id
""")
@Synchronize({"student", "clazz"})
public class StudentView {
@Id
private long id;
@Column
private String name;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Clazz1 clazz;
@Column
private String clazzName;
}
由于我们这个类对应的并非数据库的表, 所以我们需要取消和增加一些注解来表明它无法进行更新操作
需要取消的注解:
- @Setter
- @Builder
需要增加的注解:
- @Immutable 表明该Entity只读
- @Subselect 关联的查询语句
- @Synchronize 自动flush指定表, 避免无法查询到对应的数据
findById可以帮我们快速获取一个实体类, 但是我们的实体类中如果有懒加载字段, 并且我们还需要使用这个懒加载字段时, 就会产生* 2次查询*
使用以下的hql可以帮助我们在获取实体类体的同时,获取它的懒加载字段的实体, 并且只产生1次查询
select s from Student s join fetch Clazz where s.id = 1
从Hibernate 6开始, 如果只获取id, 不会触发懒加载
详情请查看LazyTest#getClazzId方法的代码及日志
val optional = studentRepository.findById(studentId)
assertTrue(optional.isPresent)
val student = optional.get()
// 只获取id不会触发
log.debug("clazz id: {}", student.clazz.id)
// 触发懒加载, 由于没有事物, 所以导致报错
log.debug("clazz name: {}", student.clazz.name)
Session session = (Session) this.entityManager.unwrap(Session.class);
Long id = session.getIdentifier(entity);