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);