Java EE: JPA

Jakarta Persistence API

介绍

  • JPA是一套API规范,从EJB中分离出来,是持久层的流行API

  • 单纯引入JPA是没用的,其著名实现有Hibernate等,

  • Spring Data JPA不是JPA的实现,而是基于JPA的一套Repository接口

  • JPA定义了一套ORM框架,使POJO得以和关系联系起来

  • 接口依赖:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.2.0</version>
    </dependency>
  • hibernate实现依赖:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.1.4.Final</version>
    </dependency>

核心组件

Persistence

  • Persistence用于获取EntityManagerFactory
  • 配置:

EntityManagerEntityManagerFactory

  • EntityManagerFactory全局单例且线程安全,用于获取EntityManager
  • EntityManager接口管理对象的持久化操作,定义了四种实体状态:
    • Transient:新建状态
    • Managed:托管状态
    • Detached:分离状态
    • Removed:移除状态
  • EntityManager实例包含以下方法:
    • persist(T):准备保存实体,会生成实体的主键,但可能直到flush才保存
    • find(Class<T>, Object pk):立即加载获取指定主键(第二个参数)的对象
    • getReference(Class<T>, Object pk):获取一个实体代理,只有在访问时才查询
    • merge(T):合并分离出去的实体,返回托管实体(传进去的实体仍是分离状态)
    • remove(T):删除实体

EntityTransaction

  • 用于事务管理

基于注解的ORM配置

单表注解

  • @Entity注解一个POJO类是实体类

  • @Table注解一个实体类所对应的关系,若不使用,则@Entity注解类的默认表名为类名的全大写

    在该注解内定义约束等类似SQL中在属性外的表内定义约束

    • name:指定表名
    • schema:指定模式
    • catalog:指定表所属的目录
    • uniqueConstraints:指定表的唯一性约束
    • indexes:指定表的索引
    • check:指定表的检查约束
  • @UniqueConstraint注解表示一个唯一性约束,在@TableuniqueConstraints中起作用

    • name:约束名
    • columns:受约束的属性组
  • @Index注解表示一个索引,在@Tableindexes中起作用

    • name:索引名
    • columnList:属性组,是一个字符串,不同属性由,连接
  • @CheckConstraint注解表示一个检查约束,在@Tablecheck中起作用

    • name:约束名
    • constraint:一个本地SQL约束
  • @Id注解作用于属性,标明一个主键

  • @GeneratedValue:作用于被@Id注解的属性,标明如何初始化主键值,strategy属性有以下选择

    • GenerationType.AUTO(默认)策略:根据具体的数据库,自适应策略
    • GenerationType.IDENTITY(常用)策略:依赖数据库的AUTO_INCREMENT(或其他)自增
    • GenerationType.SEQUENCE(可能高性能)策略:依赖数据库的序列,需要配置@GeneratedValuegenerator属性
    • GenerationType.UUID(特殊场景)策略:依赖UUID生成器

    定义@GeneratedValue后,允许使用不带主键参数的构造方法

  • @Transient注解作用于属性,标明这个属性永远不会被持久化

  • @Column注解作用于属性,是数据库的列映射

    • name:指定属性映射的列名,默认为属性名
    • unique:布尔值,标识是否唯一
    • nullable:布尔值,标识是否允许为null
    • columnDefinition:指定本地SQL为该列的定义,而不是使用JPA生成
    • length:指定长度
    • check:同@Tablecheck
  • @Version:表示该对象应使用乐观锁来查询/更新,若在事务中发现冲突则会抛出OptimisticLockException

属性组主键注解

  • 大部分约束,例如唯一性约束、检查约束、非空约束,由之前介绍的注解可以覆盖大部分场景(包括属性组的约束)

    但依靠上述注解无法使一个属性组作为主键

  • @Embeddable注解标明一个类是可嵌入的

    该类的所有属性不需要@Column注解,且该类不需要@Table以及@Entity注解,应该作为一个普通的POJO,只是多了一个@Embeddable

    为了方便作为主键,需要覆写equals()hashCode()方法,且实现Serializable空接口

  • @EmbeddedId注解标明一个属性是主键,且类型是一个@Embeddable类,可用于属性组为主键的场景

    默认不支持AUTOIDENTITY的策略生成嵌入主键,可以使用SEQUENCE策略

  • @Embedded注解标明一个属性的类型是一个@Embeddable

    JPA转化为DDL语句时,会检测所有的Embedded属性,将该类的每个属性就像一般属性那样写成SQL

  • 默认情况下,@Embeddable类中的所有属性对应的列名就是属性名,就像没有添加@Column注解的一般属性那样

    在作为实体类的属性时,建议用@AttributeOverride注解清晰地决定映射:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @Entity
    @Table(name = "embed_demo", schema = "demo")
    public class EmbedEntity {
    @Embeddable
    public static class EmbedId implements Serializable {
    private Long uid;
    private String username;
    // 重写 equals() hashCode() ...
    }

    @EmbeddedId
    @AttributeOverrides({
    @AttributeOverride(name = "uid", column = @Column(name = "uid" /** 其它约束 */
    )),
    @AttributeOverride(name = "username", column = @Column(name = "username"))
    })
    private EmbedId id;

    @Embedded
    @AttributeOverrides({
    @AttributeOverride(name = "uid", column = @Column(name = "parent_uid" /** 其它约束 */
    )),
    @AttributeOverride(name = "username", column = @Column(name = "parent_username"))
    })
    private EmbedId pid;
    }

多表关联注解

  • @JoinColumns注解包含多个@JoinColumn,表示一个属性有多个外键依赖

  • @JoinColumn注解表示一个属性是外键,这样的属性其类型应是一个@Entity类(或是一个列表包含@Entity类)而不应该是引用的属性的类型,JPA会将这个实体类视为外表,在其中查找外键

    • name:该属性在本表的列名
    • referencedColumnName:该属性所引用的其它表的列名
    • foreignKey:具体的外键细节
    • unique:一般来说,外键是和其它表的主键相关联的,因此不需要设置unique属性,只需要知道遇到其它情况时记得设置unique=true即可
  • @ForeignKey注解在@JoinColumnforeignKey中起作用

    • name:外键约束名
    • foreignKeyDefinition:使用本地DDL定义外键
  • @JoinTable是用于生成中间表的注解,通常配合@ManyToMany一起使用,它包括@Table的所有属性,同时多出若干属性用于拆解多对多关系

    • joinColumns:指向己方实体的外键列,由若干@JoinColumn组成@JoinColumn里的name定义中间表的列名,referencedColumnName指向己方表的列名
    • inverseJoinColumns:指向对方实体的外键列,类似joinColumns,只不过referencedColumnName指向对方表的列名
    • foreignKeyinverseForeignKey:类似@JoinColumnforeignKey
  • @MapsId@PrimaryKeyJoinColumns@PrimaryKeyJoinColumn

    有时,一个实体的外键不仅是被引用表的主键,而且是引用表的主键(例如为了满足范式要求而拆表),即它们共享主键

    @MapsId支持使用共享主键,value属性默认是被引用表的主键列名,使用@MapsId后,本实体类的@Id属性自动关联到被引用表的主键上,因此不需要@GeneratedValue注解也能使用不含主键参数的构造方法

    我们知道@JoinColumn等注解是会体现在JPA生成的DDL中的,但在共享主键的场景下,@Id属性就是外键,此时如果希望通过对象引用导航到主实体的实例上,就可以使用@PrimaryKeyJoinColumn@PrimaryKeyJoinColumns注解,它们和一般的@JoinColumn注解的唯一区别是:DDL不会带有多余的外键列

  • 数据库的映射基数有四种:一对一、一对多、多对一、多对多,其中只有多对多需要使用中间表,其它在由ER图转化为关系模式时都可变作“多”那方的外键引用“一”那方的主键

    JPA中,有点反直觉的是,外键是整个被引用实体的引用而不是这个实体的主键类型

    特别是在双向关联的场景下,如果仅靠@JoinColumn@JoinTable注解,JPA将只知道有关联,但不知道返回值的数量,无法使JPA知道对方是多方还是一方,己方是多方还是一方,那么对象导航(绑定)就无法进展下去(无法知道返回的是List<T>还是单纯作为一个实体类来解析)

    综上,联系类型的注解是很有必要的,且多对多@ManyToMany需要配合@JoinTable,而@ManyToOne@OneToOne需要配合@JoinColumn@OneToMany通常不和任何列映射注解配合使用

  • @OneToOne注解:表示一对一注解

    两个@OneToOne可配对实现双向关联,其中一方为@JoinColumn+@OneToOne(optional=...)、另一方为@OneToOne(mappedBy=...)

    • cascade:设置级联的操作

    • fetch:设置级联操作的时机

    • optional:设置参与度约束,默认为true,即允许为null

      在数据库中,设置参与度约束是通过将外键设置为NOT NULL限制的

    • orphanRemoval:设置关系断开的级联操作,即在主控方断开关联后后是否级联地删除关系另一方的相应记录

    • mappedBy:通常用于双向关联的场景,主控方若想要维护一个反向的对象导航,不需要(也不应该)像外键持有方那样使用@JoinColumn,而是使用mappedBy属性

      在这种场景下,反向导航不会被数据库存储,即这个属性不是一个@Column@JoinColumn属性,而是一个仅由JPA根据mappedBy获取的反向引用

      mappedBy属性的值是对方实体的属性名,而不是对方表的列名

  • @OneToOne注解包含所有映射基数注解的所有属性,因为外键方可以是任意一方,所以有optional属性,又因为被引用的那方可以是任意一方,所以有mappedBy属性

    在一对多或多对一的映射基数上,多方一般是持有外键的那方,所以@OneToMany只有mappedBy(因为一方不持有外键)、@ManyToOne只有optional(因为多方持有外键,外键可选)

    此外,@OneToMany注解属性的类型应该是一个集合,例如List

    @OneToMany(mappedBy=...)@JoinColumn+@ManyToOne(optional=...)配对实现双向关联

  • @ManyToMany是一个特殊的映射基数,它只有mappedBy属性,因为在数据库中,多对多的映射基数需要借助中间表,这个中间表持有两个外键,从而将@ManyToMany拆成两个@OneToMany

    • 单向关联:如果不使用@JoinTable也会加上一个默认的@JoinTable,所以建议显式定义@JoinTable便于管理

    • 双向关联:像@OneToOne那样,需要一方使用mappedBy属性,避免生成两个中间表

      @JoinTable+@ManyToMany@ManyToManay(mappedBy=...)配对实现多对多的双向关联

  • CascadeType:作用于映射基数注解里的cascade属性,JPA在实体中的一大优点是实体含有整个的引用而不只是其它实体的主键,因此也可以实现对实体进行持久化操作的同时对其含有的外键进行相同的操作

    • PERSIST:主体保存时,同时保存其关联的实体

    • MERGE:主体更新时,同时更新其关联的实体

    • REMOVE:主体删除时,同时删除其关联的实体

      orphanRemoval在关系断开时就会级联删除(包括将关联属性替换、设null),而REMOVE策略只是在主体删除时才进行级联删除

    • REFRESH:主体刷新时,同时获取其关联的实体

    • DETACH:实例被分离出持久层管理时,同时将其关联的实例也分离出去

    • ALL:包括以上所有策略

  • FetchType:作用于映射基数注解里的fetch属性,表示加载的策略

    • LAZY:懒加载,只在真正访问时加载

      LAZY是一对多、多对多注解的默认值,因为是List通常需要很长的时间加载,许多时候是没必要的

    • EAGER:急加载,查询到主实体后立刻加载该引用

      EAGER是一对一、多对一注解的默认值,因为仅加载一个关联实体的实例并不花多久

继承注解

例子

  • 自关联的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Entity
    @Table("employee")
    public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "eid")
    private Integer eid;

    @JoinColumn(name = "manager", referencedColumnName = "eid", foreignKey = @ForeignKey(name = "fk_EmpManager_EmpEid"))
    @ManyToOne(optional = true)
    private Employee manager;

    @OneToMany(mappedBy = "manager")
    private List<Employee> emps;
    }
  • 共享主键例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @Entity
    @Table("user")
    public class User {
    @Id
    @Column(name = "uid")
    private Long uid;

    @Column(name = "username", nullable = false, length = 50)
    private String username;

    @OneToOne(mappedBy = "user")
    private UserDetail userDetail;
    }

    @Entity
    @Table("user_detail")
    public class UserDetail {
    @Id
    @Column(name = "uid")
    private Long uid;

    @PrimaryKeyJoinColumn(referencedColumnName = "uid")
    @MapsId
    @OneToOne(optional = false)
    private User user;

    // 其它信息
    @Column(name = "password", check = @CheckConstraint("password ~ ^[0-9a-zA-Z]{8,20}$"))
    private String password;
    }

JPQL

参数化查询

  • 为了防止SQL注入,参数化查询提供了类型检查,避免了SQL语句的拼接
  • 命名参数:使用方法中形参的名字引用值,如:paramName表示取paramName这个形参的值
  • 位置参数:使用1-idx的位置来索引形参,如?1表示取第一个参数的值

查询细节

  • JPQL(Java Persistence Query Language),是JPA规范提供的查询语言,它和SQL的语法类似,但是操作对象是实体类,而不是关系

    也因此,JPQL是编译时检查的,类型检查能防止很多查询错误

  • 由于有映射基数的注解,在FROM子句使用JOIN时,可以不用ON关键字,除非需要复杂的查询条件

  • 推荐使用面向对象的思想编写JPQL,即要多表查询时,使用A JOIN A.B而不是A JOIN B,用.运算符使用对象导航

  • 对于列表类型,允许使用[0-idx]访问其中的实体实例

  • 一对多的关系(列表类型)的懒加载会导致N+1查询问题,如果大部分场景需要懒加载,但在特定场景需要获取多方的所有实体时,可以使用JOIN FETCH,能优化为两次查询

  • 多态查询:

  • 在不使用Spring Data JPA的场景下,查询结果无法自动转化成返回类型的对象,需要使用NEW ObjClass(...)

面向对象的便捷函数

  • TYPE(obj):获取其类型
  • TREAT(obj AS Type):向下转型
  • SIZE(collection):获取集合的大小,是COUNT()加子查询的便捷版
  • obj MEMBER OF collection:类似IN
  • collection IS EMPTY:等价于SIZE(collection) = 0

Criteria API

JPA生命周期