本篇内容将会带领读者详细了解一下 javax.persistence 下面的 Entity 里面常用注解有哪些,而学习的基本条件是要对 Java 的注解有基本的了解,前提要明白注解是干什么用的。
虽然 Spring Data JPA 已经对数据的操作封装的很好了,约定大于配置的思想,帮我们默认了很多东西。JPA(Java 持久性 API)是存储业务实体关联的实体的来源,它显示了如何定义一个面向普通 Java 对象(POJO)作为一个实体,以及如何与管理关系实体,提供了一套标准。因此,javax.persistence 下面的有些注解我们还是必须要去了解的,便于更好的提高工作效率。
(1)javax.persistence 我们打开源码位于 hibernate-jpa-**.jar 包里面。
依赖关系,通过 Intellij Idea 的 Maven 插件直接分析一下其依赖,也可以用$ mvn dependency:tree
分析,如下:
[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:2.0.0.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.0.0.RELEASE:compile
[INFO] | | \- org.springframework:spring-jdbc:jar:5.0.4.RELEASE:compile
[INFO] | +- org.hibernate:hibernate-core:jar:5.2.14.Final:compile
[INFO] | | +- org.javassist:javassist:jar:3.22.0-GA:compile
(2)我们也通过前面说到的 Intellij Idea 的 Diagram 来看一下此模块的类的关键关系。
(3)下图显示了 JPA 的类的层次结构,它显示核心类和 JPA 接口。
(4)下表描述了每个在上述架构的显示单元。
单元 | 描述 |
---|---|
EntityManagerFactory | 这是一个 EntityManager 的工厂类,它创建并管理多个 EntityManager 实例 |
EntityManager | 这是一个接口,它管理的持久化操作的对象,它的工作原理类似工厂的查询实例 |
Entity | 实体是持久性对象,是存储在数据库中的记录 |
EntityTransaction | 它与 EntityManager 是一对一的关系,对于每一个 EntityManager,操作是由 EntityTransaction 类维护 |
Persistence | 这个类包含静态方法来获取 EntityManagerFactory 实例 |
Query | 该接口由每个 JPA 供应商,能够获得符合标准的关系对象 |
上述的类和接口用于存储实体到数据库的一个记录,帮助程序员通过减少自己编写代码来将数据存储到数据库中,使他们能够专注于更重要的业务活动代码,如数据库表映射的类编写代码。
下面我们主要介绍一下,在 Entity 里面常用的注解有哪些,还有很多没有介绍到的,可以直接到包的源码里面进行查找和分析。
先看一个 Blog 的案例其中实体的配置如下:
@Entity
@Table(name = "user_blog", schema = "test")
public class UserBlogEntity {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "title", nullable = true, length = 200)
private String title;
@Basic
@Column(name = "create_user_id", nullable = true)
private Integer createUserId;
@Basic
@Column(name = "blog_content", nullable = true, length = -1)
@Lob
private String blogContent;
@Basic(fetch = FetchType.LAZY)
@Column(name = "image", nullable = true)
@Lob
private byte[] image;
@Basic
@Column(name = "create_time", nullable = true)
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
@Basic
@Column(name = "create_date", nullable = true)
@Temporal(TemporalType.DATE)
private Date createDate;
@Transient
private String transientSimple;
......
}
下面对上面类中用到的注解来一一解释一下。
(1)@Entity 用于定义对象将会成为被 JPA 管理的实体,将字段映射到指定的数据库表中,源码如下:
public @interface Entity {
//可选,默认是次实体类的名字,全局唯一。
String name() default "";
}
(2)@Table 用于指定数据库的表名:
public @interface Table {
//表的名字,可选。如果不填写,系统认为好实体的名字一样为表名。
String name() default "";
//此表的catalog,可选
String catalog() default "";
//此表所在schema,可选
String schema() default "";
//唯一性约束,只有创建表的时候有用,默认不需要。
UniqueConstraint[] uniqueConstraints() default { };
//索引,只有创建表的时候使用,默认不需要。
Index[] indexes() default {};
}
(3)@Id 定义属性为数据库的主键,一个实体里面必须有一个,并且必须和 @GeneratedValue 配合使用和成对出现。
(4)@IdClass 利用外部类的联合主键,源码:
public @interface IdClass {
//联合主键的类
Class value();
}
作为符合主键类,要满足以下几点要求。
- 必须实现 Serializable 接口。
- 必须有默认的 public 无参数的构造方法。
- 必须覆盖 equals 和 hashCode 方法。equals 方法用于判断两个对象是否相同,EntityManger 通过 find 方法来查找 Entity 时,是根据 equals 的返回值来判断的。本例中,只有对象的 name 和 email 值完全相同时或同一个对象时则返回 true,否则返回 false。hashCode 方法返回当前对象的哈希码,生成 hashCode 相同的概率越小越好,算法可以进行优化。
(5)@IdClass 用法
1)假设 UserBlog 的联合主键是 createUserId 和 title,新增一个 UserBlogKey 的类。
UserBlogKey.class
import java.io.Serializable;
public class UserBlogKey implements Serializable {
private String title;
private Integer createUserId;
public UserBlogKey() {
}
public UserBlogKey(String title, Integer createUserId) {
this.title = title;
this.createUserId = createUserId;
}
.....//get set 方法我们略过
}
2)UserBlogEntity.java 稍加改动,实体类上需要加 @IdClass 注解和两个主键上都得加 @Id 注解,如下。
@Entity
@Table(name = "user_blog", schema = "test")
@IdClass(value = UserBlogKey.class)
public class UserBlogEntity {
@Column(name = "id", nullable = false)
private Integer id;
@Id
@Column(name = "title", nullable = true, length = 200)
private String title;
@Id
@Column(name = "create_user_id", nullable = true)
private Integer createUserId;
......//不变的部分我们省略
}
3)UserBlogRepository 我们做的改动:
public interface UserBlogRepository extends JpaRepository<UserBlogEntity,UserBlogKey>{
}
4)使用的时候:
@RequestMapping(path = "/blog/{title}/{createUserId}")
@ResponseBody
public Optional<UserBlogEntity> showBlogs(@PathVariable(value = "createUserId") Integer createUserId,@PathVariable("title") String title) {
return userBlogRepository.findById(new UserBlogKey(title,createUserId));
}
(6)@GeneratedValue 主键生成策略:
public @interface GeneratedValue {
//Id的生成策略
GenerationType strategy() default AUTO;
//通过Sequences生成Id,常见的是Orcale数据库ID生成规则,这个时候需要配合@SequenceGenerator使用
String generator() default "";
}
GenerationType 一共有以下四个值:
public enum GenerationType {
//通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。
TABLE,
//通过序列产生主键,通过 @SequenceGenerator 注解指定序列名, MySql 不支持这种方式;
SEQUENCE,
//采用数据库ID自增长, 一般用于mysql数据库
IDENTITY,
//JPA 自动选择合适的策略,是默认选项;
AUTO
}
(7)@Basic 表示属性是到数据库表的字段的映射。如果实体的字段上没有任何注解,默认即为 @Basic。
public @interface Basic {
//可选,EAGER(默认):立即加载;LAZY:延迟加载。(LAZY主要应用在大字段上面)
FetchType fetch() default EAGER;
//可选。这个字段是否可以为null,默认是true。
boolean optional() default true;
}
(8)@Transient 表示该属性并非一个到数据库表的字段的映射,表示非持久化属性。JPA 映射数据库的时候忽略它,与 @Basic 相反的作用。
(9)@Column 定义该属性对应数据库中的列名。
public @interface Column {
//数据库中的表的列名;可选,如果不填写认为字段名和实体属性名一样。
String name() default "";
//是否唯一。默认flase,可选。
boolean unique() default false;
//数据字段是否允许空。可选,默认true。
boolean nullable() default true;
//执行insert操作的时候是否包含此字段,默认,true,可选。
boolean insertable() default true;
//执行update的时候是否包含此字段,默认,true,可选。
boolean updatable() default true;
//表示该字段在数据库中的实际类型。
String columnDefinition() default "";
//数据库字段的长度,可选,默认255
int length() default 255;
}
(10)@Temporal 用来设置 Date 类型的属性映射到对应精度的字段。
- @Temporal(TemporalType.DATE)映射为日期 // date (只有日期)
- @Temporal(TemporalType.TIME)映射为日期 // time (是有时间)
- @Temporal(TemporalType.TIMESTAMP)映射为日期 // date time (日期+时间)
(11)@Enumerated 这个注解很好用,直接映射 enum 枚举类型的字段。
1)看源码:
public @interface Enumerated {
//枚举映射的类型,默认是ORDINAL(即枚举字段的下标)。
EnumType value() default ORDINAL;
}
public enum EnumType {
//映射枚举字段的下标
ORDINAL,
//映射枚举的Name
STRING
}
2)看例子:
//有一个枚举类,用户的性别
public enum Gender {
MAIL("男性"), FMAIL("女性");
private String value;
private Gender(String value) {
this.value = value;
}
}
//实体类@Enumerated的写法如下
@Entity
@Table(name = "tb_user")
public class User implements Serializable {
@Enumerated(EnumType.STRING)
@Column(name = "user_gender")
private Gender gender;
.......................
}
这时候插入两条数据,数据库里面的值是 MAIL/FMAIL,而不是“男性”/女性。
如果我们用 @Enumerated(EnumType.ORDINAL),这时候数据库里面的值是 0,1。但是实际工作中,不建议用数字下标,因为枚举里面的属性值是会不断新增的,如果新增一个,位置变化了就惨了。
(12)@Lob 将属性映射成数据库支持的大对象类型,支持以下两种数据库类型的字段。
- Clob(Character Large Ojects)类型是长字符串类型,java.sql.Clob、Character[]、char[] 和 String 将被映射为 Clob 类型。
- Blob(Binary Large Objects)类型是字节类型,java.sql.Blob、Byte[]、byte[]和实现了 Serializable 接口的类型将被映射为 Blob 类型。
- 由于 Clob,Blob 占用内存空间较大一般配合 @Basic(fetch=FetchType.LAZY) 将其设置为延迟加载。
(13)@SqlResultSetMapping、@EntityResult、@ColumnResult 配合 @NamedNativeQuery 一起使用的。
在实际工作中不建议这样配置,因为必要性比较少,如果这种配置多了会把 @entity 用配置 XML 思路。看一个案例简单说明一下:
@NamedNativeQueries({
@NamedNativeQuery(name = "getUsers",
query = "select id,username,usertype from t_xfw_operator order by id desc",
resultSetMapping = "usersMap")
})
@SqlResultSetMappings({
@SqlResultSetMapping(name = "usersMap",
entities = {},
columns = {
@ColumnResult(name = "id"),
@ColumnResult(name="username"),
@ColumnResult(name="usertype")
})
})
@Entity
@Table(name = "operator")
public class Operator {
......
}
(1)源码语法
public @interface JoinColumn {
//目标表的字段名,必填
String name() default "";
//本实体的字段名,非必填,默认是本表ID
String referencedColumnName() default "";
//外键字段是否唯一
boolean unique() default false;
//外键字段是否允许为空
boolean nullable() default true;
//是否跟随一起新增
boolean insertable() default true;
//是否跟随一起更新
boolean updatable() default true;
}
(2)用法:@JoinColumn 主要配合 @OneToOne、@ManyToOne、@OneToMany 一起使用,单独使用没有意义。
(3)@JoinColumns 定义多个字段的关联关系。
(1)源码语法:
public @interface OneToOne {
//关系目标实体,非必填,默认该字段的类型。
Class targetEntity() default void.class;
//cascade 级联操作策略
1. CascadeType.PERSIST 级联新建
2. CascadeType.REMOVE 级联删除
3. CascadeType.REFRESH 级联刷新
4. CascadeType.MERGE 级联更新
5. CascadeType.ALL 四项全选
6. 默认,关系表不会产生任何影响
CascadeType[] cascade() default {};
//数据获取方式EAGER(立即加载)/LAZY(延迟加载)
FetchType fetch() default EAGER;
//是否允许为空
boolean optional() default true;
//关联关系被谁维护的。 非必填,一般不需要特别指定。
//注意:只有关系维护方才能操作两者的关系。被维护方即使设置了维护方属性进行存储也不会更新外键关联。1)mappedBy不能与@JoinColumn或者@JoinTable同时使用。2)mappedBy的值是指另一方的实体里面属性的字段,而不是数据库字段,也不是实体的对象的名字。既是另一方配置了@JoinColumn或者@JoinTable注解的属性的字段名称。
String mappedBy() default "";
//是否级联删除。和CascadeType.REMOVE的效果一样。两种配置了一个就会自动级联删除
boolean orphanRemoval() default false;
}
(2)用法 @OneToOne 需要配合 @JoinColumn 一起使用。注意:可以双向关联,也可以只配置一方,看实际需求。
案例:假设一个部门只有一个员工,Department 的内容如下:
@OneToOne
@JoinColumn(name="employee_id",referencedColumnName="id")
private Employee employeeAttribute = new Employee();
注意:
employee_id
指的是 Department 里面的字段,而 referencedColumnName="id" 指的是 Employee 表里面的字段。
如果需要双向关联,Employee 的内容如下:
@OneToOne(mappedBy="employeeAttribute")
private Department department;
当然了也可以不选用 mappedBy 和下面效果是一样的:
@OneToOne
@JoinColumn(name="id",referencedColumnName="employee_id")
private Department department;
@OneToMany & @ManyToOne 可以相对存在,也可只存在一方。
(1)@OneToMany 源码语法:
public @interface 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;
}
public @interface ManyToOne {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default EAGER;
boolean optional() default true;
}
@ManyToOne 与 OneToMany 的源码稍有区别仔细体会。
(2)使用案例,也是必须要和 @JoinColumn 配合使用才有效。
@Entity
@Table(name="user")
public class User implements Serializable{
@OneToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY,mappedBy="user")
private Set<role> setRole;
......}
@Entity
@Table(name="role")
public class Role {
@ManyToOne(cascade=CascadeType.ALL,fetch=FetchType.EAGER)
@JoinColumn(name="user_id")//user_id字段作为外键
private User user;
......}
一般和 @OneToMany 一起使用。
(1)源码语法:
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface OrderBy {
/**
* 要排序的字段,格式如下:
* orderby_list::= orderby_item [,orderby_item]*
* orderby_item::= [property_or_field_name] [ASC | DESC]
* 字段可以是实体属性,也可以数据字段,默认ASC。
*/
String value() default "";
}
(2)用法案例:
@Entity
@Table(name="user")
public class User implements Serializable{
@OneToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY,mappedBy="user")
@OrderBy("role_name DESC")
private Set<role> setRole;
......}
@JoinTable 是指如果对象与对象之间有个关联关系表的时候,就会用到这个,一般和 @ManyToMany 一起使用。
(1)源码语法:
public @interface JoinTable {
//中间关联关系表明
String name() default "";
//表的catalog
String catalog() default "";
//表的schema
String schema() default "";
//主链接表的字段
JoinColumn[] joinColumns() default {};
//被联机的表外键字段
JoinColumn[] inverseJoinColumns() default {};
......
}
(2)假设 Blog 和 Tag 是多对多的关系,有个关联关系表blog_tag_relation
,表中有两个属性blog_id
和tag_id
,那么 Blog 实体里面的写法如下:
@Entity
public class Blog{
@ManyToMany
@JoinTable(
name="blog_tag_relation",
joinColumns=@JoinColumn(name="blog_id",referencedColumnName="id"),
inverseJoinColumn=@JoinColumn(name="tag_id",referencedColumnName="id")
private List<Tag> tags = new ArrayList<Tag>();
)
}
(1)源码语法:
public @interface ManyToMany {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default LAZY;
String mappedBy() default "";
}
@ManyToMany 表示多对多,和 @OneToOne、@ManyToOne 一样也有单向双向之分,单项双向和注解没有关系,只看实体类之间是否相互引用。 主要注意的是当用到 @ManyToMany 的时候一定是三张表,不要想着偷懒,否则会发现有很多麻烦。
(2)案例:一个博客可以拥有多个标签,一个标签也可以使用在多个博客上,Blog 和 Tag 就是多对多关系。
//第一种:单向多对多
@Entity
public class Blog{
@Id
@Column(name = "id")
private Integer id;
@ManyToMany(cascade=CascadeType.ALL)
@JoinTable(
name="blog_tag_relation",
joinColumns=@JoinColumn(name="blog_id",referencedColumnName="id"), inverseJoinColumn=@JoinColumn(name="tag_id",referencedColumnName="id")
private List<Tag> tags = new ArrayList<Tag>();
......}
//多对多中间关联关系表
@Entity
public class BlogTagRelation{
@Column(name = "blog_id")
private Integer blogId;
@Column(name = "tag_id")
private Integer tagId;
......}
//标签表
@Entity
public class Tag{
@Id
@Column(name = "id")
private Integer id;
......}
//第二种:双向多对多
@Entity
public class Blog{
@ManyToMany(cascade=CascadeType.ALL)
@JoinTable(
name="blog_tag_relation", joinColumns=@JoinColumn(name="blog_id",referencedColumnName="id"),
inverseJoinColumn=@JoinColumn(name="tag_id",referencedColumnName="id")
private List<Tag> tags = new ArrayList<Tag>();
}
@Entity
public class Tag{
@ManyToMany(mappedBy="BlogTagRelation")
private List<Blog> blogs = new ArrayList<Blog>();
}
说明:BlogTagRelation 为中间关联关系表
blog_tag_relation
对应的实体。
当使用 @ManyToMany、@ManyToOne、@OneToMany、@OneToOne 关联关系的时候,当 FetchType 怎么配置 LAZY 或者 EAGER,SQL 真正执行的时候都是有一条主表查询和 N 条子表查询组成的。这种查询效率一般比较低下,子对象有多少个就会执行 N+1 条 SQL。
有时候我们需要用到 Left Join 或者 Inner Join 来查询来提高效率,只能通过 @Query 的 JQPL 语法去实现,还有后面将介绍的 Criteria API 可以做到。Spring Data JPA 为了简单的提高查询率,引入了 EntityGraph 的概念,可以解决 N+1 条 SQL 的问题。
JPA 2.1 推出来的 @EntityGraph、 @NamedEntityGraph 用来提高查询效率,很好的解决了 N+1 条 SQL 的问题,两者需要配合起来使用,缺一不可。@NamedEntityGraph 配置在 @Entity 上面,而 @EntityGraph 配置在 Repository 的查询方法上面,我们看一下实例。
(1)先在 Entity 里面定义 @NamedEntityGraph,其他都不变,其中 @NamedAttributeNode 可以有多个,也可以有一个。
@NamedEntityGraph(name = "UserInfoEntity.addressEntityList", attributeNodes = {
@NamedAttributeNode("addressEntityList"),
@NamedAttributeNode("userBlogEntityList")})
@Entity(name = "UserInfoEntity")
@Table(name = "user_info", schema = "test")
public class UserInfoEntity implements Serializable {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@OneToOne(optional = false)
@JoinColumn(referencedColumnName = "id",name = "address_id",nullable = false)
private UserReceivingAddressEntity addressEntityList;
@OneToMany
@JoinColumn(name = "create_user_id",referencedColumnName = "id")
private List<UserBlogEntity> userBlogEntityList;
......
}
(2)只需要在查询方法上加 @EntityGraph 注解即可,其中 value 就是 @NamedEntityGraph 中的 Name,在实例配置如下:
public interface UserRepository extends JpaRepository<UserInfoEntity, Integer>{
@Override
@EntityGraph(value = "UserInfoEntity.addressEntityList")
List<UserInfoEntity> findAll();
}
所有的注解要么全配置在字段上,要么全配置在 get 方法上,不能混用,混用启动的时候就会启动不起来,然后启动的时候报错,如果是第一次使用很难找到原因,但是看语法上面这样配置又没有问题。而作者喜欢把注解都放在 get 方法上面,如下面的例子,因为 Intellij IDEA 工具生成的 Entity 类,相关的注解都在 get 方法上面,这样不需要做出任何调整,案例如下:
/**
* 看一下作者实际工作中发短信的Entity如下:
*/
@Entity
@Table(name = "message_request")
@Include(rootLevel = true, type = "messageRequest")
@Setter
@ToString
public class MessageRequest extends DefaultSimpleAuditable {
public MessageRequest(String uuid) {
this.uuid = uuid;
}
public MessageRequest() {
}
/**
* 下面有哪些手机号
*/
private List<MessageRequestTelephone> messageRequestTelephone = Lists.newArrayList();
/**
* uuid(自动产生) 流水号
*/
private String uuid;
/**
* 短信内容,不包含签名,走模板,不用这个了
*/
@Deprecated
private String content;
/**
* 哪个供应商,如果不指定,按照系统顺序发放
*/
private String messageVendorCode;
/**
* 自己的模板code
*/
private String templateCode;
/**
* abc,def,5模板参数,分割
*/
private String templateParams;
/**
* 重试次数
*/
private Integer retryTimes;
/**
* 【爱中国】,客户端传递的
*/
private List<MessageRequestSign> messageRequestSigns;
/**
* 状态枚举
*/
private MessageRequestStatus status;
/**
* 状态最后的异常
*/
private String statusRemark;
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(referencedColumnName = "id", name = "message_request_id")
@JsonManagedReference
public List<MessageRequestSign> getMessageRequestSigns() {
return messageRequestSigns;
}
@Transient
@Exclude
@JsonIgnore
public String getDomesticSign() {
if (messageRequestSigns == null) {
return null;
}
List<MessageRequestSign> list = messageRequestSigns.stream().filter(s -> s.getNationCode().isDomestic()).collect(Collectors.toList());
if (list == null || list.isEmpty()) {
return null;
}
return list.stream().findFirst().get().getSign();
}
@Transient
@Exclude
@JsonIgnore
public String getAbroadSign() {
if (messageRequestSigns == null) {
return null;
}
List<MessageRequestSign> list = messageRequestSigns.stream().filter(s -> s.getNationCode().isAbroad()).collect(Collectors.toList());
if (list == null || list.isEmpty()) {
return null;
}
return list.stream().findFirst().get().getSign();
}
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(referencedColumnName = "id", name = "message_request_id")
@JsonManagedReference
public List<MessageRequestTelephone> getMessageRequestTelephone(){
return messageRequestTelephone;
}
@Basic
@Column(name = "uuid")
public String getUuid() {
return uuid;
}
@Basic
@Column(name = "content")
public String getContent() {
return content;
}
@Basic
@Column(name = "message_vendor_code")
public String getMessageVendorCode() {
return messageVendorCode;
}
@Basic
@Column(name = "template_code")
public String getTemplateCode() {
return templateCode;
}
@Basic
@Column(name = "template_params")
public String getTemplateParams() {
return templateParams;
}
//不加@Column注解,用默认的字段名字匹配规则
public Integer getRetryTimes() {
return retryTimes;
}
@Enumerated(value = EnumType.STRING)
public MessageRequestStatus getStatus() {
return status;
}
public String getStatusRemark() {
return statusRemark;
}
@PostLoad
public void postLoadDefaultValue() {
if (status == null) {
status = MessageRequestStatus.WAITING;
}
}
}
(1)JSON 序列化的双向关联会死循环,解决方法有两种:
- 利用 @JsonIgnore 在另外一方忽略掉(最简单,但是不推荐,这样会改变业务场景)。
- 利用 @JsonManagedReference 和 @JsonBackReference 注解来表明 back 关系,来解决 Jack Son 序列化的时候所产生的双向关联死循环。
@JsonBackReference 和 @JsonManagedReference:这两个标注通常配对使用,通常用在父子关系中。@JsonBackReference 标注的属性在序列化(serialization,即将对象转换为 JSON 数据)时,会被忽略(即结果中的 JSON 数据不包含该属性的内容)。@JsonManagedReference 标注的属性则会被序列化。在序列化时,@JsonBackReference 的作用相当于 @JsonIgnore,此时可以没有 @JsonManagedReference,但在反序列化(deserialization,即 JSON 数据转换为对象)时,如果没有 @JsonManagedReference,则不会自动注入 @JsonBackReference 标注的属性(被忽略的父或子);如果有 @JsonManagedReference,则会自动注入自动注入 @JsonBackReference 标注的属性。
正如上面的 MessageRequest 类中描述一样,我们来开看一下 MessageRequestTelephone 的关键源码如下:
@Entity
@Table(name = "message_request_telephone")
@ToString(exclude = "messageRequest")
public class MessageRequestTelephone extends DefaultSimpleAuditable implements Serializable {
/**
* 当我们使用messageRequestId的时候,对应的@JoinColumn要注意配置insertable = false, updatable = false
*/
private Long messageRequestId;
@JsonBackReference
@ManyToOne
@JoinColumn(name = "message_request_id", referencedColumnName = "id", nullable = false, insertable = false, updatable = false)
public MessageRequest getMessageRequest() {
return messageRequest;
}
......
}
(2)同样的当覆盖 toString() 方法的时候需要注意一下,也会有双向关联死循环的问题。解决方法一样的,toString 的时候在一方排除掉即可。
(1)虽然我们使用关联查询,但是在实际工作中,所有的关联查询,表上一般是不需要建立外键约束的,为了提高操作效率,但是每个外键的字段上都会加上一般索引。
(2)不同的关联关系的配置,@JoinClumn 里面的(name、referencedColumnName)代表的意思是不一样的,很容易弄混。我们有两种方法应对:
- 打印出 SQL,我们请求的时候看一下 SQL 正确不正确,一般配置如下,key 在 spring 里面,将 SQL 参数都打印出来观察仔细。
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.type=trace
logging.level.org.hibernate.type.descriptor.sql=trace
- 我们现在表上建立外键约束,然后利用 Intellij IDEA 自动生成关联关系注解,当生成完代码的时候再把表上的外键约束删除掉,操作界面如下:
而更过的关于 IDEA 使用的请看作者的其他章节内容。
当我们使用关联操作的时候建议大家 cascade = CascadeType.PERSIST 配置成这个,这样 insert 的时候能帮我们不少忙,它的原理是两条都先 insert,然后再来个 update 把关联关系更新上去,读者自己打印出 SQL,也可以看得出来。如下:
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(referencedColumnName = "id", name = "message_request_id")
@JsonManagedReference
public List<MessageRequestTelephone> getMessageRequestTelephone() {
return messageRequestTelephone;
}
级联更新、级联删除的时候比较危险,建议考虑清楚,或者完全掌握的时候再去使用,否则生产产生事故还是比较糟糕的。