Spring Data JPA中的一对一,一对多,多对多查询

这几天学习一下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 teachers;

@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 userFriends;
}

@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 users;
}

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 teachers;
}

@JoinTable中的name需要填写中间关系表的表明

joinColumns中的name和inverseJoinColumns中的name需要填关系表TEACHER_USER_RELATION的实体的字段名
如果关系表TEACHER_USER_RELATION的实体中的字段使用了@Column(name),那么需要填写注解中指定的名字,即表字段名。
referencedColumnName填写的是本实体类中的关联的字段。

使用@OrderBy进行排序

在一对多或者多对多映射中,如果想要对得到的集合进行排序,可以使用@OrderBy注解,@OrderBy中只需要指定想要排序的字段以及排序的方向即可:

/**
 * 一对多
 */
@OneToMany(cascade = {CascadeType.ALL},fetch = FetchType.LAZY)
@JoinColumn(name = "userId",referencedColumnName = "id")
@OrderBy("friend_id DESC")
private Set userFriends;

上面的代码中,Set中的UserFriend会使用Friend进行倒序排序。

值得注意的是,即使用Set集合也可以保证有序性,在Hibernate内部使用了自定义集合PersistentSet,此集合是有序集合。

mappedBy元素说明

@OneToOne、@OneToMany、@ManyToMany中可以使用mappedBy元素定义被关联着和关联着的关系由谁去维护,即关系的操纵权在那一方,同时mappedBy不能和@JoinTable、@JoinColumn注解同时存使用

使用@EntityGraph和@NamedEntityGraph解决N+1问题

在常规的多对多和一对多查询时,会面临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 extends JpaRepository {
   @EntityGraph("UserEntity")
   List findByNameContaining(String name);
}

注解中的value是@NamedEntityGraph定义的name。

你可能感兴趣的:(spring-boot)