这几天学习一下Spring Data JPA中的一对一、一对多、多对多映射。这些映射还分单向关联和双向关联,在双向关联时还需要考虑对象序列化为JSON字符串时的死循环问题。
单向关联 单向关联指的是实体类A中有一个实体类B变量,但是实体类B中没有实体类A变量,即为单向关联。
双向关联 双向关联指的是实体类A中有一个实体类B变量,而实体类B中也含有一个实体类A变量,即为双向关联。
值得注意的是:Spring Data JPA中属性的集合需要使用Set来保存,如果使用List会抛出异常。
在双向关联时,可能会存在对象序列化成JSON字符创时的死循环问题,因为A中包含B,B中也包含A,序列化A中的B时,因为B也含有A,A又含有B,所以会死循环。
特南克斯
首先你要理解这是双向关联,双向关联中你如果从数据库里面查询一个User对象,那么User对象里面有Role,Role里面又有User对象,那么你用syso输出User对象,如果toString方法里面包含有输出User.roles的话,那么是必然会造成死循环的。如果你用sping mvc等框架将后台数据返回给前台也是同理,也会造成返回的JSON数据死循环
使用Jackson时,可以使用@JsonIgnoreProperties注解来解决:
@JsonIgnoreProperties(value = { "users" }) @ManyToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY) @JoinTable( name = "TEACHER_USER_RELATION", joinColumns = @JoinColumn(name = "UserId",referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "TeacherId",referencedColumnName = "id") ) private Set |
@JsonIgnoreProperties(value = { “users” })注解排除了Teacher中的User字段,从而避免了死循环问题。
一对一映射需要@OneToOne注解和@JoinColumn注解配合使用,为了创建测试数据,首先定义一个User实体类,然后定义一个UserDetail实体类,其中User与UserDetail的关系是一对一的关系,即一个User对应一个UserDetail,一个UserDetail对应一个User。具体代码如下:
@Data @Entity @Table(name = "USER") public class User { ... /** * 一对一 */ @JoinColumn(name = "id",referencedColumnName = "id") @OneToOne(cascade = {CascadeType.ALL},fetch = FetchType.LAZY) private UserDetail userDetail; } |
在被关联的UserDetail代码如下:
@Entity @Data @Table(name = "USER_DETAIL") public class UserDetail { @Id @GeneratedValue @Column(name = "ID") private Long id; @Column(name = "CREDIT") private Float credit; @Column(name = "ENROLLMENT_DATE") private Date enrollmentDate; @Column(name = "USER_ID") private Long userId; } |
UserDetail中没有User类型成员变量,所以User与UserDetail的关联是单向关联,如果UserDetail中也含有一个User类型的变量,则为双向关联;(单向和双向关联只与是否包含对象有关,和是否使用注解无关。)
同时需要在User类型的变量名中增加@OneToOne(mappedBy=”userDetail”)注解,表示两者的关系由User实体去维护,如果配置了Cascade,对User的操作也会影响到UserDetail实体。
上面代码中的@OneToOne注解中,可以定义级联操作,包括级联新建、级联删除、级联更新、级联刷新。
@JoinColumn注解中的name元素为被关联对象的id,即UserDetail类的id,而referencedColumnName则为关联对象的id,即@JoinColumn所在实体类的id。
一对多需要使用@JoinColumn注解和@OneToMany配置使用,如果是双向关联,则还需要在被关联的实体类的成员变量中使用@ManyToOne。
为了创建测试环境,需要新建一个UserFriend实体类,一个User可以有一个或多个UserFriend。
User实体类中需要包含一个集合类型的UserFriend成员变量:
@Data @Entity @Table(name = "USER") public class User { ... /** * 一对多 */ @OneToMany(cascade = {CascadeType.ALL},fetch = FetchType.LAZY) @JoinColumn(name = "userId",referencedColumnName = "id") private Set |
@OneToMany注解和@JoinColumn注解需要配合使用,@OneToMany注解中同样可以指定级联操作和加载类型。
@JoinColumn注解中,name元素为被被关联实体类中的id,而referencedColumnName元素为关联实体类中的id,即@JoinColumn所在实体类的id。
UserFriend实体类代码如下:
@Entity @Data @Table(name = "USER_FRIEND") public class UserFriend { @Id @GeneratedValue @Column(name = "ID") private Long id; private Long userId; @Column(name = "FRIEND_ID") private Long friendId; } |
由于使用的是单向关联,UserFriend实体类没有对应的User成员变量,所以是单向关联,如果需要指定关系的维护方,需要在使用没有@JoinColumn的实体类上使用注解@OneToMany(mappedBy)。
多对多和一对一、一对多不同,需要引入两者之间的关系表,关系表负责维护两者之间的关系,起到至关重要的作用,Spring Data JPA中,需要使用@ManyToMany注解和@JoinTable注解配合使用。
为了创建多对多的测试环境,需要创建一个Teacher实体类:
@Entity @Data @Table(name = "TEACHER") public class Teacher { @Id @GeneratedValue @Column(name = "ID") private Long id; @Column(name = "NAME") private String name; @ManyToMany(mappedBy = "teachers") private List |
Teacher中,由于也包含User的集合,所以Teacher与User是双向关联,在Teacher中使用@ManyToMany(mappedBy)注解申明User类为双方关系的维护方,即删除User也会删除关联的Teacher和关系表中的数据,但删除Teacher不会删除User表中的数据。
User类中,需要加入@JoinTable和@ManyToMany注解:
public class User{ ... /** * 多对多 */ @JsonIgnoreProperties(value = { "users" }) @ManyToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY) @JoinTable( name = "TEACHER_USER_RELATION", joinColumns = @JoinColumn(name = "UserId",referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "TeacherId",referencedColumnName = "id") ) private Set |
@JoinTable中的name需要填写中间关系表的表明
joinColumns中的name和inverseJoinColumns中的name需要填关系表TEACHER_USER_RELATION的实体的字段名
如果关系表TEACHER_USER_RELATION的实体中的字段使用了@Column(name),那么需要填写注解中指定的名字,即表字段名。
referencedColumnName填写的是本实体类中的关联的字段。
在一对多或者多对多映射中,如果想要对得到的集合进行排序,可以使用@OrderBy注解,@OrderBy中只需要指定想要排序的字段以及排序的方向即可:
/** * 一对多 */ @OneToMany(cascade = {CascadeType.ALL},fetch = FetchType.LAZY) @JoinColumn(name = "userId",referencedColumnName = "id") @OrderBy("friend_id DESC") private Set |
上面的代码中,Set中的UserFriend会使用Friend进行倒序排序。
值得注意的是,即使用Set集合也可以保证有序性,在Hibernate内部使用了自定义集合PersistentSet,此集合是有序集合。
@OneToOne、@OneToMany、@ManyToMany中可以使用mappedBy元素定义被关联着和关联着的关系由谁去维护,即关系的操纵权在那一方,同时mappedBy不能和@JoinTable、@JoinColumn注解同时存使用。
在常规的多对多和一对多查询时,会面临N+1问题:
N+1问题指的是,如果一个User对应N个Friend,在查询某id的User时,会首先执行一条SQL语句查询该User,然后会执行N条SQL语句查询该User对应的N个Friend,过程中一共使用了N+1条语句,效率会非常低下,正确的做法是使用内连接和外链接,只需要一条语句。
Spring Data JPA中针对N+1问题有相应的优化,使用@EntityGraph和@NamedEntityGraph就可以解决N+1问题。
首先需要在User实体类上使用@NamedEntityGraph注解:
@Data @Entity @Table(name = "USER") @NamedEntityGraph( name = "UserEntity", attributeNodes = { @NamedAttributeNode("userDetail"), @NamedAttributeNode("userFriends"), @NamedAttributeNode("teachers"), } ) public class User { ... } |
上面代码中,name可以随便定义,在@EntityGraph中会对其进行引用,@NamedAttributeNode中的value元素即为需要解决N+1问题的字段。
在Repository中,对需要解决N+1问题的方法上使用的@EntityGraph注解就可以了:
public interface UserJpaRepository |
注解中的value是@NamedEntityGraph定义的name。