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- 配置:
EntityManager与EntityManagerFactory
EntityManagerFactory全局单例且线程安全,用于获取EntityManagerEntityManager接口管理对象的持久化操作,定义了四种实体状态: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注解表示一个唯一性约束,在@Table的uniqueConstraints中起作用name:约束名columns:受约束的属性组
@Index注解表示一个索引,在@Table的indexes中起作用name:索引名columnList:属性组,是一个字符串,不同属性由,连接
@CheckConstraint注解表示一个检查约束,在@Table的check中起作用name:约束名constraint:一个本地SQL约束
@Id注解作用于属性,标明一个主键@GeneratedValue:作用于被@Id注解的属性,标明如何初始化主键值,strategy属性有以下选择GenerationType.AUTO(默认)策略:根据具体的数据库,自适应策略GenerationType.IDENTITY(常用)策略:依赖数据库的AUTO_INCREMENT(或其他)自增GenerationType.SEQUENCE(可能高性能)策略:依赖数据库的序列,需要配置@GeneratedValue的generator属性GenerationType.UUID(特殊场景)策略:依赖UUID生成器
定义
@GeneratedValue后,允许使用不带主键参数的构造方法@Transient注解作用于属性,标明这个属性永远不会被持久化@Column注解作用于属性,是数据库的列映射name:指定属性映射的列名,默认为属性名unique:布尔值,标识是否唯一nullable:布尔值,标识是否允许为nullcolumnDefinition:指定本地SQL为该列的定义,而不是使用JPA生成length:指定长度check:同@Table的check
@Version:表示该对象应使用乐观锁来查询/更新,若在事务中发现冲突则会抛出OptimisticLockException
属性组主键注解
大部分约束,例如唯一性约束、检查约束、非空约束,由之前介绍的注解可以覆盖大部分场景(包括属性组的约束)
但依靠上述注解无法使一个属性组作为主键
@Embeddable注解标明一个类是可嵌入的该类的所有属性不需要
@Column注解,且该类不需要@Table以及@Entity注解,应该作为一个普通的POJO,只是多了一个@Embeddable为了方便作为主键,需要覆写
equals()与hashCode()方法,且实现Serializable空接口@EmbeddedId注解标明一个属性是主键,且类型是一个@Embeddable类,可用于属性组为主键的场景默认不支持
AUTO、IDENTITY的策略生成嵌入主键,可以使用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
public class EmbedEntity {
public static class EmbedId implements Serializable {
private Long uid;
private String username;
// 重写 equals() hashCode() ...
}
private EmbedId id;
private EmbedId pid;
}
多表关联注解
@JoinColumns注解包含多个@JoinColumn,表示一个属性有多个外键依赖@JoinColumn注解表示一个属性是外键,这样的属性其类型应是一个@Entity类(或是一个列表包含@Entity类)而不应该是引用的属性的类型,JPA会将这个实体类视为外表,在其中查找外键name:该属性在本表的列名referencedColumnName:该属性所引用的其它表的列名foreignKey:具体的外键细节unique:一般来说,外键是和其它表的主键相关联的,因此不需要设置unique属性,只需要知道遇到其它情况时记得设置unique=true即可
@ForeignKey注解在@JoinColumn的foreignKey中起作用name:外键约束名foreignKeyDefinition:使用本地DDL定义外键
@JoinTable是用于生成中间表的注解,通常配合@ManyToMany一起使用,它包括@Table的所有属性,同时多出若干属性用于拆解多对多关系joinColumns:指向己方实体的外键列,由若干@JoinColumn组成@JoinColumn里的name定义中间表的列名,referencedColumnName指向己方表的列名inverseJoinColumns:指向对方实体的外键列,类似joinColumns,只不过referencedColumnName指向对方表的列名foreignKey与inverseForeignKey:类似@JoinColumn的foreignKey
@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
public class Employee {
private Integer eid;
private Employee 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
public class User {
private Long uid;
private String username;
private UserDetail userDetail;
}
public class UserDetail {
private Long uid;
private User user;
// 其它信息
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:类似INcollection IS EMPTY:等价于SIZE(collection) = 0