JPA(二)-注解
参考文献
- 拉钩教育–Spring Data JPA原理与实战
@NonNull、@NonNullApi、@Nullable
-
从 Spring Data 2.0 开始,JPA 新增了@NonNull @NonNullApi @Nullable,是对 null 的参数和返回结果做的支持。
-
@NonNullApi:在包级别用于声明参数,以及返回值的默认行为是不接受或产生空值的。
1
2.springframework.lang.NonNullApi
package com.myrespository; -
@NonNull:用于不能为空的参数或返回值(在 @NonNullApi 适用的参数和返回值上不需要)
-
@Nullable:用于可以为空的参数或返回值。
1
2
3
4
5//当我们添加@Nullable 注解之后,参数和返回结果这个时候就都会允许为 null 了;
User findByEmailAddress(; EmailAddress emailAdress)
//返回结果允许为 null,参数不允许为 null 的情况
Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress);
@Query
-
JpaQueryLookupStrategy 关键源码剖析
- 先打开 QueryExecutorMethodInterceptor 类,默认的策略是CreateIfNotFound,找到如下代码:
@Query 的基本用法
1 | package org.springframework.data.jpa.repository; |
- @Query 用法是使用 JPQL 为实体创建声明式查询方法。我们一般只需要关心 @Query 里面的 value 和 nativeQuery、countQuery 的值即可
JPQL的语法
1 | 查询: |
-
语法参考文档: https://docs.oracle.com/html/E13946_04/ejb3_langref.html
-
示例
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案例 1: 要在 Repository 的查询方法上声明一个注解,这里就是 注解标注的地方。
public interface UserRepository extends JpaRepository<User, Long>{
User findByEmailAddress(String emailAddress);
}
案例 2: LIKE 查询,注意 firstname 不会自动加上“%”关键字。
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByFirstnameEndsWith(String firstname);
}
案例 3: 直接用原始 SQL,nativeQuery = true 即可。
public interface UserRepository extends JpaRepository<User, Long> {
User findByEmailAddress(String emailAddress);
}
注意:nativeQuery 不支持直接 Sort 的参数查询。
案例 4: 下面是nativeQuery 的排序错误的写法,会导致无法启动。
public interface UserRepository extends JpaRepository<User, Long> {
List<UserInfoEntity> findByFirstName(String firstName,Sort sort);
}
案例 5: nativeQuery 排序的正确写法。
List<UserInfoEntity> findByFirstName(String firstName,String sort);
//调用的地方写法last_name是数据里面的字段名,不是对象的字段名
repository.findByFirstName("jackzhang","last_name");
@Query 的排序
-
@Query中在用JPQL的时候,想要实现排序,方法上直接用 PageRequest 或者 Sort 参数都可以做到。
-
state_field_path_expression JPQL
的定义,并且 Sort 的对象支持一些特定的函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15案例 6: Sort and JpaSort 的使用,它可以进行排序。
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByAndSort(String lastname, Sort sort);
List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}
//调用方的写法,如下:
repo.findByAndSort("lannister", new Sort("firstname"));
repo.findByAndSort("stark", new Sort("LENGTH(firstname)"));
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)"));
repo.findByAsArrayAndSort("bolton", new Sort("fn_len"));
@Query 的分页
-
@Query 的分页分为两种情况,分别为 JPQL 的排序和 nativeQuery 的排序。
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案例 7:直接用 Page 对象接受接口,参数直接用 Pageable 的实现类即可。
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findByLastname(String lastname, Pageable pageable);
}
//调用者的写法
repository.findByFirstName("jackzhang",new PageRequest(1,10));
案例 8: 对原生 SQL 的分页支持,并不是特别友好,因为这种写法比较“骇客”,可能随着版本的不同会有所变化。我们以 MySQL 为例。
这里需要注意:这个注释 / #pageable# / 必须有。
public interface UserRepository extends JpaRepository<UserInfoEntity, Integer>, JpaSpecificationExecutor<UserInfoEntity> {
Page<UserInfoEntity> findByFirstName(String firstName, Pageable pageable);
}
//调用者的写法
return userRepository.findByFirstName("jackzhang",new PageRequest(1,10, Sort.Direction.DESC,"last_name"));
//打印出来的sql
select * from user_info where first_name=? /* #pageable# */ order by last_name desc limit ?, ?
@Query 之 Projections 应用返回指定 DTO
1 |
|
- 打开
org.hibernate.query.criteria.internal.expression.function.ParameterizedFunctionExpression
会发现 Hibernate 支持的关键字有这么多
@Query 动态查询解决方法
1 | 实现 的动态参数查询。 |
@Param 用法
- @Param 注解指定方法参数的具体名称,通过绑定的参数名字指定查询条件,这样不需要关心参数的顺序。
@Entity
-
JPA 协议中关于 Entity 的相关规定
-
实体是直接进行数据库持久化操作的领域对象(即一个简单的 POJO,可以按照业务领域划分),必须通过 @Entity 注解进行标示。
-
实体必须有一个 public 或者 protected 的无参数构造方法。
-
持久化映射的注解可以标示在 Entity 的字段 field 上
1
2
private String userName; -
也可以将持久化注解运用在 Entity 里面的 get/set 方法上,通常我们是放在 get 方法中,如下所示:
1
2
3
4
public String getUserName(){
return userName;
}
-
-
概括起来,就是 Entity 里面的注解生效只有两种方式:将注解写在字段上或者将注解写在方法上(JPA 里面称 Property)。需要注意的是,在同一个 Entity 里面只能有一种方式生效,也就是说,注解要么全部写在 field 上面,要么就全部写在 Property 上面
- 只要是在 @Entity 的实体里面被注解标注的字段,都会被映射到数据库中,除了使用 @Transient 注解的字段之外。
- 实体里面必须要有一个主键,主键标示的字段可以是单个字段,也可以是复合主键字段。
-
@Entity 用于定义对象将会成为被 JPA 管理的实体,必填,将字段映射到指定的数据库表中,使用起来很简单,直接用在实体类上面即可,通过源码表达的语法如下:
1
2
3
4
5//表示此注解只能用在class上面
public Entity {
//可选,默认是实体类的名字,整个应用里面全局唯一。
String name() default "";
}
@Table
-
@Table 用于指定数据库的表名,表示此实体对应的数据库里面的表名,非必填,默认表名和 entity 名字一样。
1
2
3
4
5
6
7
8
9
10
11@Target(TYPE) //一样只能用在类上面
public @interface Table {
//表的名字,可选。如果不填写,系统认为好实体的名字一样为表名。
String name() default "";
//此表所在schema,可选
String schema() default "";
//唯一性约束,在创建表的时候有用,表创建之后后面就不需要了。
UniqueConstraint[] uniqueConstraints() default { };
//索引,在创建表的时候使用,表创建之后后面就不需要了。
Index[] indexes() default {};
}
@Access
-
用于指定 entity 里面的注解是写在字段上面,还是 get/set 方法上面生效,非必填。在默认不填写的情况下,当实体里面的第一个注解出现在字段上或者 get/set 方法上面,就以第一次出现的方式为准;也就是说,一个实体里面的注解既有用在 field 上面,又有用在 properties 上面的时候.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Long id;
public String getUserName(){
return userName;
}
那么由于 是实体里面第一个出现的注解,并且作用在字段上面,所以所有写在 get/set 方法上面的注解就会失效。而 可以干预默认值,指定是在 fileds 上面生效还是在 properties 上面生效
//表示此注解可以运用在class上(那么这个时候就可以指定此实体的默认注解生效策略了),也可以用在方法上或者字段上(表示可以独立设置某一个字段或者方法的生效策略);
public Access {
//指定是字段上面生效还是方法上面生效
AccessType value();
}
public enum AccessType {
FIELD,
PROPERTY;
private AccessType() {
}
}
@Id
- 定义属性为数据库的主键,一个实体里面必须有一个主键,但不一定是这个注解,可以和 @GeneratedValue 配合使用或成对出现
@GeneratedValue
- 主键生成策略
1 | public @interface GeneratedValue { |
@Enumerated
- 这个注解很好用,因为它对 enum 提供了下标和 name 两种方式,用法直接映射在 enum 枚举类型的字段上。
1 | //作用在方法和字段上 |
@Basic
- 表示属性是到数据库表的字段的映射。如果实体的字段上没有任何注解,默认即为 @Basic。也就是说默认所有的字段肯定是和数据库进行映射的,并且默认为 Eager 类型。
1 | public Basic { |
@Transient
- 表示该属性并非一个到数据库表的字段的映射,表示非持久化属性。JPA 映射数据库的时候忽略它,与 @Basic 有相反的作用。也就是每个字段上面 @Transient 和 @Basic 必须二选一,而什么都不指定的话,默认是 @Basic。
@Column
- 定义该属性对应数据库中的列名。
1 | public Column { |
@Temporal
- 用来设置 Date 类型的属性映射到对应精度的字段,存在以下三种情况
- @Temporal(TemporalType.DATE)映射为日期 // date (只有日期)
- @Temporal(TemporalType.TIME)映射为日期 // time (只有时间)
- @Temporal(TemporalType.TIMESTAMP)映射为日期 // date time (日期+时间)
示例
1 | package com.example.jpa.example1; |
联合主键
-
可以通过 javax.persistence.EmbeddedId 和 javax.persistence.IdClass 两个注解实现联合主键的效果.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57第一步:新建一个 UserInfoID 类里面是联合主键。
package com.example.jpa.example1;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
public class UserInfoID implements Serializable {
private String name,telephone;
}
第二步:再新建一个 UserInfo 的实体,采用 引用联合主键类。
public class UserInfo {
private Integer ages;
private String name;
private String telephone;
}
第三步:新增一个 UserInfoReposito 类来做 CRUD 操作。
package com.example.jpa.example1;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserInfoRepository extends JpaRepository<UserInfo,UserInfoID> {
}
第四步:写一个测试用例,测试一下。
package com.example.jpa.example1;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.Optional;
public class UserInfoRepositoryTest {
private UserInfoRepository userInfoRepository;
public void testIdClass() {
userInfoRepository.save(UserInfo.builder().ages(1).name("jack").telephone("123456789").build());
Optional<UserInfo> userInfo = userInfoRepository.findById(UserInfoID.builder().name("jack").telephone("123456789").build());
System.out.println(userInfo.get());
}
}
Hibernate: create table user_info (name varchar(255) not null, telephone varchar(255) not null, ages integer, primary key (name, telephone))
Hibernate: select userinfo0_.name as name1_3_0_, userinfo0_.telephone as telephon2_3_0_, userinfo0_.ages as ages3_3_0_ from user_info userinfo0_ where userinfo0_.name=? and userinfo0_.telephone=?
UserInfo(ages=1, name=jack, telephone=123456789)
@Embeddable 与 @EmbeddedId 注解使用
1 | 第一步:在我们上面例子中的 UserInfoID 里面添加 注解。 |
@IdClass
和 @EmbeddedId
的区别是什么?
-
如上面测试用例,在使用的时候,Embedded 用的是对象,而 IdClass 用的是具体的某一个字段;
-
二者的JPQL 也会不一样:
- 用
@IdClass
JPQL 的写法:SELECT u.name FROM UserInfo u
- 用
@EmbeddedId
的 JPQL 的写法:SELECT u.userInfoId.name FROM UserInfo u
- 用
-
联合主键还有需要注意的就是,它与唯一性索引约束的区别是写法不同,如上面所讲,唯一性索引的写法如下:
1
2
private String uniqueNumber;
实体之间的继承关系如何实现?
- 在 Java 面向对象的语言环境中,@Entity 之间的关系多种多样,而根据 JPA 的规范,我们大致可以将其分为以下几种:
- 纯粹的继承,和表没关系,对象之间的字段共享。利用注解 @MappedSuperclass,协议规定父类不能是 @Entity。
- 单表多态问题,同一张 Table,表示了不同的对象,通过一个字段来进行区分。利用
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
注解完成,只有父类有 @Table。 - 多表多态,每一个子类一张表,父类的表拥有所有公用字段。通过
@Inheritance(strategy = InheritanceType.JOINED)
注解完成,父类和子类都是表,有公用的字段在父表里面。 - Object 的继承,数据库里面每一张表是分开的,相互独立不受影响。通过
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
注解完成,父类(可以是一张表,也可以不是)和子类都是表,相互之间没有关系。
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
-
父类实体对象与各个子实体对象共用一张表,通过一个字段的不同值代表不同的对象。
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69我们抽象一个 Book 对象,如下所示:
package com.example.jpa.example1.book;
import lombok.Data;
import javax.persistence.*;
public class Book {
private Long id;
private String title;
}
再新建一个 BlueBook 对象,作为 Book 的子对象。
package com.example.jpa.example1.book;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
public class BlueBook extends Book{
private String blueMark;
}
再新建一个 RedBook 对象,作为 Book 的另一子对象。
//红皮书
public class RedBook extends Book {
private String redMark;
}
这时,我们一共新建了三个 Entity 对象,其实都是指 book 这一张表,通过 book 表里面的 color 字段来区分红书还是绿书。我们继续做一下测试看看结果。
我们再新建一个 RedBookRepositor 类,操作一下 RedBook 会看到如下结果:
package com.example.jpa.example1.book;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RedBookRepository extends JpaRepository<RedBook,Long>{
}
然后再新建一个测试用例。
package com.example.jpa.example1;
import com.example.jpa.example1.book.RedBook;
import com.example.jpa.example1.book.RedBookRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
public class RedBookRepositoryTest {
private RedBookRepository redBookRepository;
public void testRedBook() {
RedBook redBook = new RedBook();
redBook.setTitle("redbook");
redBook.setRedMark("redmark");
redBook.setId(1L);
redBookRepository.saveAndFlush(redBook);
RedBook r = redBookRepository.findById(1L).get();
System.out.println(r.getId()+":"+r.getTitle()+":"+r.getRedMark());
}
}
Hibernate: create table book (color varchar(31) not null, id bigint not null, title varchar(255), blue_mark varchar(255), red_mark varchar(255), primary key (id))
Hibernate: insert into book (title, red_mark, color, id) values (?, ?, 'red', ?)
@Inheritance(strategy = InheritanceType.JOINED)
-
在这种映射策略里面,继承结构中的每一个实体(entity)类都会映射到数据库里一个单独的表中。也就是说,每个实体(entity)都会被映射到数据库中,一个实体(entity)类对应数据库中的一个表。
-
其中根实体(root entity)对应的表中定义了主键(primary key),所有的子类对应的数据库表都要共同使用 Book 里面的 @ID 这个主键。
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45首先,我们改一下上面的三个实体,测试一下InheritanceType.JOINED,改动如下:
package com.example.jpa.example1.book;
import lombok.Data;
import javax.persistence.*;
public class Book {
private Long id;
private String title;
}
其次,我们 Book 父类、改变 Inheritance 策略、删除 DiscriminatorColumn,你会看到如下结果。
package com.example.jpa.example1.book;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;
public class BlueBook extends Book{
private String blueMark;
}
package com.example.jpa.example1.book;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;
public class RedBook extends Book {
private String redMark;
}
然后,BlueBook和RedBook也删除DiscriminatorColumn,新增 ,和 book 父类共用一个主键值,而 RedBookRepository 和测试用例不变
Hibernate: create table blue_book (blue_mark varchar(255), book_id bigint not null, primary key (book_id))
Hibernate: create table book (id bigint not null, title varchar(255), primary key (id))
Hibernate: create table red_book (red_mark varchar(255), book_id bigint not null, primary key (book_id))
Hibernate: alter table blue_book add constraint FK9uuwgq7a924vtnys1rgiyrlk7 foreign key (book_id) references book
Hibernate: alter table red_book add constraint FKk8rvl61bjy9lgsr9nhxn5soq5 foreign key (book_id) references book@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
-
我们在使用 @MappedSuperClass 主键的时候,如果不指定 @Inhertance,默认就是此种TABLE_PER_CLASS模式。当然了,我们也显示指定,要求继承基类的都是一张表,而父类不是表,是 java 对象的抽象类。我们看一个例子。
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
31
32
33
34
35
36
37
38
39
40首先,还是改一下上面的三个实体。
package com.example.jpa.example1.book;
import lombok.Data;
import javax.persistence.*;
@Entity(name="book")
@Data
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Book {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
private String title;
}
其次,Book 表采用 TABLE_PER_CLASS 策略,其子实体类都代表各自的表,实体代码如下:
package com.example.jpa.example1.book;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Entity;
@Entity
@Data
@EqualsAndHashCode(callSuper=false)
public class RedBook extends Book {
private String redMark;
}
package com.example.jpa.example1.book;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Entity;
@Entity
@Data
@EqualsAndHashCode(callSuper=false)
public class BlueBook extends Book{
private String blueMark;
}
这时,从 RedBook 和 BlueBook 里面去掉 PrimaryKeyJoinColumn,而 RedBookRepository 和测试用例不变,我们执行看一下结果。
Hibernate: create table blue_book (id bigint not null, title varchar(255), blue_mark varchar(255), primary key (id))
Hibernate: create table book (id bigint not null, title varchar(255), primary key (id))
Hibernate: create table red_book (id bigint not null, title varchar(255), red_mark varchar(255), primary key (id))
-
实体与实体之间的关联关系
- 实体与实体之间的关联关系一共分为四种,分别为 OneToOne、OneToMany、ManyToOne 和 ManyToMany;而实体之间的关联关系又分为双向的和单向的。
@OneToOne 关联关系
-
@OneToOne 一般表示对象之间一对一的关联关系,它可以放在 field 上面,也可以放在 get/set 方法上面。其中 JPA 协议有规定,如果是配置双向关联,维护关联关系的是拥有外键的一方,而另一方必须配置 mappedBy;如果是单项关联,直接配置在拥有外键的一方即可。
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67举个例子:user 表是用户的主信息,user_info 是用户的扩展信息,两者之间是一对一的关系。user_info 表里面有一个 user_id 作为关联关系的外键,如果是单项关联,我们的写法如下
package com.example.jpa.example1;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
public class User {
private Long id;
private String name;
private String email;
private String sex;
private String address;
}
User 实体里面什么都没变化,不需要添加 注解。我们只需要在拥有外键的一方配置就可以,所以 UserInfo 的代码如下:
package com.example.jpa.example1;
import lombok.*;
import javax.persistence.*;
public class UserInfo {
private Long id;
private Integer ages;
private String telephone;
//维护user的外键关联关系,配置一对一
private User user;
}
我们看到,UserInfo 实体对象里面添加了 注解,这时我们写一个测试用例跑一下看看有什么效果:
Hibernate: create table user (id bigint not null, address varchar(255), email varchar(255), name varchar(255), sex varchar(255), primary key (id))
Hibernate: create table user_info (id bigint not null, ages integer, telephone varchar(255), user_id bigint, primary key (id))
Hibernate: alter table user_info add constraint FKn8pl63y4abe7n0ls6topbqjh2 foreign key (user_id) references user
那么双向关联应该怎么配置呢?我们保持 UserInfo 不变,在 User 实体对象里面添加这一段代码即可。
private UserInfo userInfo;
完整的 User 实体对象就会变成如下模样。
public class User {
private Long id;
private String name;
private String email;
private UserInfo userInfo;//变化之处
private String sex;
private String address;
} -
@interface OneToOne
源码解读1
2
3
4
5
6
7
8
9
10
11
12
13
14public OneToOne {
//表示关系目标实体,默认该注解标识的返回值的类型的类。
Class targetEntity() default void.class;
//cascade 级联操作策略,就是我们常说的级联操作
CascadeType[] cascade() default {};
//数据获取方式EAGER(立即加载)/LAZY(延迟加载)
FetchType fetch() default EAGER;
//是否允许为空,默认是可选的,也就表示可以为空;
boolean optional() default true;
//关联关系被谁维护的一方对象里面的属性名字。 双向关联的时候必填
String mappedBy() default "";
//当被标识的字段发生删除或者置空操作之后,是否同步到关联关系的一方,即进行通过删除操作,默认flase,注意与CascadeType.REMOVE 级联删除的区别
boolean orphanRemoval() default false;
} -
mappedBy 注意事项
-
只有关联关系的维护方才能操作两个实体之间外键的关系。被维护方即使设置了维护方属性进行存储也不会更新外键关联。
-
mappedBy 不能与 @JoinColumn 或者 @JoinTable 同时使用,因为没有意义,关联关系不在这里面维护。
-
此外,mappedBy 的值是指另一方的实体里面属性的字段,而不是数据库字段,也不是实体的对象的名字。也就是维护关联关系的一方属性字段名称,或者加了 @JoinColumn / @JoinTable 注解的属性字段名称。如上面的 User 例子 user 里面 mappedBy 的值,就是 UserInfo 里面的 user 字段的名字。
-
-
CascadeType用法
-
在 CascadeType 的用法中,CascadeType 的枚举值只有五个,分别如下:
- CascadeType.PERSIST 级联新建
- CascadeType.REMOVE 级联删除
- CascadeType.REFRESH 级联刷新
- CascadeType.MERGE 级联更新
- CascadeType.ALL 四项全选
-
其中,默认是没有级联操作的,关系表不会产生任何影响。此外,JPA 2.0 还新增了 CascadeType.DETACH,即级联实体到 Detach 状态。
1
2
3
4
5public class UserInfo {
...
private User user;
}
-
-
orphanRemoval 属性用法
- orphanRemoval 表示当关联关系被删除的时候,是否应用级联删除,默认 false。
1
2
3
4
5public class UserInfo {
private User user;
....其他没变的代码省了
} -
主键和外键都是同一个字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14我们假设 user 表是主表,user_info 的主键是 user_id,并且 user_id=user 是表里面的 id
public class UserInfo implements Serializable {
private Long userId;
private Integer ages;
private String telephone;
private User user;
}
这里的做法很简单,我们直接把 userId 设置为主键,在 上面添加 注解即可。 注解的作用是把关联关系实体里面的 ID(默认)值 copy 到 标注的字段上面(这里指的是 user_id 字段)。
Hibernate: create table user (id bigint not null, address varchar(255), email varchar(255), name varchar(255), sex varchar(255), primary key (id))
Hibernate: create table user_info (ages integer, telephone varchar(255), user_id bigint not null, primary key (user_id))
Hibernate: alter table user_info add constraint FKn8pl63y4abe7n0ls6topbqjh2 foreign key (user_id) references user -
@OneToOne 延迟加载,我们只需要 ID 值
- 在 @OneToOne 延迟加载的情况下,我们假设只想查下 user_id,而不想查看 user 表其他的信息,因为当前用不到,可以有以下几种做法。
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56第一种做法:还是 User 实体不变,我们改一下 UserInfo 对象,如下所示:
package com.example.jpa.example1;
import lombok.*;
import javax.persistence.*;
public class UserInfo{
private Long id;
private Integer ages;
private String telephone;
private User user;
}
从上面这段代码中,可以看到做的更改如下:
id 字段我们先用原来的
上面我们添加 注解
里面的 fetch = FetchType.LAZY 设置延迟加载
public class UserInfoRepositoryTest {
private UserInfoRepository userInfoRepository;
void init() {
User user = User.builder().name("jackxx").email("123456@126.com").build();
UserInfo userInfo = UserInfo.builder().ages(12).user(user).telephone("12345678").build();
userInfoRepository.saveAndFlush(userInfo);
}
/**
* 测试用User关联关系操作
*
* @throws JsonProcessingException
*/
public void testUserRelationships() throws JsonProcessingException {
UserInfo userInfo1 = userInfoRepository.getOne(1L);
System.out.println(userInfo1);
System.out.println(userInfo1.getUser().getId());
}
}
接下来介绍第二种做法,这种做法很简单,只要在 UserInfo 对象里面直接去掉 关联关系,新增下面的字段即可。
private Long userId;
第三做法是利用 Hibernate,它给我们提供了一种字节码增强技术,通过编译器改变 `class` 解决了延迟加载问题。这种方式有点复杂,需要在编译器引入 hibernateEnhance 的相关 jar 包,以及编译器需要改变 class 文件并添加 lazy 代理来解决延迟加载。我不太推荐这种方式,因为太复杂,你知道有这回事就行了。
@JoinCloumns & JoinColumn
1 | public JoinColumn { |
@ManyToOne& @OneToMany
-
@ManyToOne 代表多对一的关联关系,而 @OneToMany 代表一对多,一般两个成对使用表示双向关联关系。而 JPA 协议中也是明确规定:维护关联关系的是拥有外键的一方,而另一方必须配置 mappedBy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public ManyToOne {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default EAGER;
boolean optional() default true;
}
public OneToMany {
Class targetEntity() default void.class;
//cascade 级联操作策略:(CascadeType.PERSIST、CascadeType.REMOVE、 CascadeType.REFRESH、CascadeType.MERGE、CascadeType.ALL)
// 如果不填,默认关系表不会产生任何影响。
CascadeType[] cascade() default {};
// 数据获取方式EAGER(立即加载)/LAZY(延迟加载)
FetchType fetch() default LAZY;
// 关系被谁维护,单项的。注意:只有关系维护方才能操作两者的关系。
String mappedBy() default "";
// 是否级联删除。和CascadeType.REMOVE的效果一样。两种配置了一个就会自动级联删除
boolean orphanRemoval() default false;
}
我们看到上面的字段和 里面的基本一样,用法是一样的,不过需要注意以下几点:
一定是维护外键关系的一方,所以没有 mappedBy 字段;
删除的时候一定不能把 One 的一方删除了,所以也没有 orphanRemoval 的选项;
的 Lazy 效果和 的一样,所以和上面的用法基本一致;
的 Lazy 是有效果的。
@ManyToMany
1 | 假设 user 表和 room 表是多对多的关系 |
@JoinTable
1 |
|
利用 @ManyToOne 和 @OneToMany 表达多对多的关联关系
1 | 我们新建一张表 user_room_relation 来存储双方的关联关系和额外字段,实体如下: |
- @ManyToMany 的最佳实践
- 上面我们介绍的 @OneToMany 的最佳实践同样适用,我为了说明方便,采用的是双向关联,而实际生产一般是在中间表对象里面做单向关联,这样会让实体之间的关联关系简单很多。
- 与 @OneToMany 一样的道理,不要用级联删除和 orphanRemoval=true。
- FetchType 采用默认方式:fetch = FetchType.LAZY 的方式。
Jackson与JPA
Jackson核心模块
-
jackson-core:核心包,提供基于“流模式”解析的相关 API,它包括 JsonPaser 和 JsonGenerator。Jackson 内部实现正是通过高性能的流模式 API 的 JsonGenerator 和 JsonParser 来生成和解析 json。
-
jackson-annotations:注解包,提供标准注解功能,这是我们必须要掌握的基础语法。
-
jackson-databind:数据绑定包,提供基于“对象绑定”解析的相关 API( ObjectMapper ) 和“树模型”解析的相关 API(JsonNode);基于“对象绑定”解析的 API 和“树模型”解析的 API 依赖基于“流模式”解析的 API。如下图中一些标准的类型转换:
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
31
32
33
34
35
36
37package com.example.jpa.example1;
import com.fasterxml.jackson.annotation.*;
import lombok.*;
import javax.persistence.*;
import java.time.Instant;
import java.util.*;
public class UserJson {
private Long id;
private String name;
private Instant createDate;
private Date updateDate;
private String email;
private String sex;
public UserJson( { String email)
System.out.println("其他业务逻辑");
this.email = email;
}
private Map<String,Object> other = new HashMap<>();
public Map<String, Object> getOther() {
return other;
}
} -
@JsonAutoDetect JsonAutoDetect.Visibility 类包含与 Java 中的可见性级别匹配的常量,表示 ANY、DEFAULT、NON_PRIVATE、NONE、PROTECTED_AND_PRIVATE和PUBLIC_ONLY。
-
Jackson 默认不是所有的属性都可以被序列化和反序列化。默认的属性可视化的规则如下:
- 若该属性修饰符是 public,该属性可序列化和反序列化。
- 若属性的修饰符不是 public,但是它的 getter 方法和 setter 方法是 public,该属性可序列化和反序列化。因为 getter 方法用于序列化,而 setter 方法用于反序列化。
- 若属性只有 public 的 setter 方法,而无 public 的 getter 方法,该属性只能用于反序列化。
1
2
3
4ObjectMapper mapper = new ObjectMapper();
// PropertyAccessor 支持的类型有 ALL,CREATOR,FIELD,GETTER,IS_GETTER,NONE,SETTER
// Visibility 支持的类型有ANY,DEFAULT,NON_PRIVATE,NONE,PROTECTED_AND_PUBLIC,PUBLIC_ONLY
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); -
反序列化最重要的方法
1
2
3public <T> T readValue(String content, Class<T> valueType)
public <T> T readValue(String content, TypeReference<T> valueTypeRef)
public <T> T readValue(String content, JavaType valueType)1
2
3
4
5String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(userJson);
//单个对象的写法:
UserJson user = objectMapper.readValue(json,UserJson.class);
//返回List的返回结果的写法:
List<User> personList2 = mapper.readValue(jsonListString, new TypeReference<List<User>>(){});
Jackson 与 JPA 常见的问题
-
死循环问题如何解决
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21第一种情况:我们在写 ToString 方法,特别是 JPA 的实体的时候,很容易陷入死循环,因为实体之间的关联关系配置是双向的,我们就需要 ToString 的时候把一方排除掉,如下所示:
第二种情况:在转化JSON的时候,双向关联也会死循环。按照我们上面讲的方法,这是时候我们要想到通过 或者字段上面配置 ,如下:
private List<UserAddress> address;
此外,通过 和 注解也可以解决死循环。
public class UserAddress {
private User user;
....}
public class User implements Serializable {
private List<UserAddress> address;
...}
JPA 实体 JSON 序列化的常见报错
No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.example.jpa.example1.User$HibernateProxy$MdjeSaTz["hibernateLazyInitializer"])
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.example.jpa.example1.User$HibernateProxy$MdjeSaTz["hibernateLazyInitializer"])-
常见报错解决方法
-
解决方法一:引入 Hibernate5Module
1
2
3
4
5
6
7
8
9ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Hibernate5Module());
String json = objectMapper.writeValueAsString(user);
System.out.println(json);
这样子就不会报错了。
Hibernate5Module 里面还有很多 Feature 配置,例如FORCE_LAZY_LOADING,强制 lazy 里面加载就不会有上面的问题了。但是这个会有性能问题,我不建议使用。
还有 USE_TRANSIENT_ANNOTATION,利用 JPA 的 注解配置,这个默认是开启的。所以基本上 feature 默认配置都是 ok 的,不需要我们动手,只要知道这回事就行了。 -
解决方法二:关闭 SerializationFeature.FAIL_ON_EMPTY_BEANS 的 feature
1
2
3
4
5ObjectMapper objectMapper = new ObjectMapper();
//直接关闭SerializationFeature.FAIL_ON_EMPTY_BEANS objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
String json = objectMapper.writeValueAsString(user);
System.out.println(json)
因为是 lazy,所以 empty 的 bean 的时候不报错也可以。 -
解决方法三:对象上面排除“hibernateLazyInitializer”“handler”“fieldHandler”等
1
2
public class User implements Serializable {}
-
-
ObjectMapper 实战经验推荐配置项
1 | ObjectMapper objectMapper = new ObjectMapper(); |
时间类型的最佳实践,如何返回 ISO 格式的标准时间
1 | 有的时候我们会发现,默认的 ObjectMapper 里面的 module 提供的时间转化格式可能不能满足我们的要求,可能要进行扩展,如下: |
@Cacheable
-
应用到读取数据的方法上,就是可以缓存的方法,如查找方法:先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public Cacheable {
String[] value() default {};
//cache的名字。可以根据名字设置不同cache处理类。redis里面可以根据cache名字设置不同的失效时间。
String[] cacheNames() default {};
//缓存的key的名字,支持spel
String key() default "";
//key的生成策略,不指定可以用全局的默认的。
String keyGenerator() default "";
//客户选择不同的CacheManager
String cacheManager() default "";
//配置不同的cache resolver
String cacheResolver() default "";
//满足什么样的条件才能被缓存,支持SpEL,可以去掉方法名、参数
String condition() default "";
//排除哪些返回结果不加入缓存里面去,支持SpEL,实际工作中常见的是result ==null等
String unless() default "";
//是否同步读取缓存、更新缓存
boolean sync() default false;
}
//利用SPEL表达式只有当name参数长度小于32的时候再进行缓存,排除notNeedCache的对象
public Book findBook(String name)
@CachePut
- 调用方法时会自动把相应的数据放入缓存,它与 @Cacheable 不同的是所有注解的方法每次都会执行,一般配置在 Update 和 insert 方法上。其源码里面的字段和用法基本与 @Cacheable 相同,只是使用场景不一样.
@CacheEvict
-
删除缓存,一般配置在删除方法上面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14public CacheEvict {
//与@Cacheable相同的部分咱我就不重复叙述了。
......
//是否删除所有的实体对象
boolean allEntries() default false;
//是否方法执行之前执行。默认在方法调用成功之后删除
boolean beforeInvocation() default false;
}
所有Cache注解的组合配置方法,源码如下:
public Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
org.springframework.cache.annotation.CachingConfigurerSupport
-
通过此类可以自定义 Cache 里面的 CacheManager、CacheResolver、KeyGenerator、CacheErrorHandler
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
26public class CachingConfigurerSupport implements CachingConfigurer {
// cache的manager,主要是管理不同的cache的实现方式,如redis还是ehcache等
public CacheManager cacheManager() {
return null;
}
// cache的不同实现者的操作方法,CacheResolver解析器,用于根据实际情况来动态解析使用哪个Cache
public CacheResolver cacheResolver() {
return null;
}
//cache的key的生成规则
public KeyGenerator keyGenerator() {
return null;
}
//cache发生异常的回调处理,一般情况下我会打印个warn日志,方便知道发生了什么事情
public CacheErrorHandler errorHandler() {
return null;
}
}