OneToMany和ManyToOne双向关联时,FetchType开关的意外行为影响

结论在前:

  1. JPA的数据库映射DAO中,避免使用lombok的@Data注解,推荐手写getter和setter方法,避免发生HHH000100: Fail-safe cleanup错误
  2. JPA定义关联关系时,尽量避免显示使用 FetchType.EAGER ,推荐使用默认的 FetchType.LAZY 以避免意外行为的发生(如下文的情况)

意外行为情况

例如post博客对象和comment评论对象,这两个对象关系是

ER图

ER

定义这两个数据库映射

Post.java

@Entity
@Table(name = "post")
public class Post{
    @Id
    @Column(name = "id")
    private Long id;

    @Column(name = "title")
    private String title;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinTable(
            name = "post_comment_ref",
            joinColumns = @JoinColumn(name = "post_id"),
            inverseJoinColumns = @JoinColumn(name = "comment_id")
    )
    private List<Comment> comments;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

		public List<Post> getComments() {
        return comments;
    }

    public void setComments(List<Post> comments) {
        this.comments = comments;
    }

}

Comment.java

@Entity
@Table(name = "comment")
public class Comment{
    @Id
    @Column(name = "id")
    private Long id;

    @Column(name = "content")
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinTable(
            name = "post_comment_ref",
            joinColumns = @JoinColumn(name = "comment_id"),
            inverseJoinColumns = @JoinColumn(name = "post_id")
    )
    private Post post;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getContent() {
        return title;
    }

    public void setContent(String content) {
        this.content = content;
    }

		public Post getPost() {
        return post;
    }

    public void setPost(Post post) {
        this.post = post;
    }
}

这里描述一对多关系使用的是@JoinTable这个注解,需要数据多一张post和comment主键关联的表来存储这个关联关系,因此在每次关联查询需要使用join联表查询,这里因为fetch开关的不同遇到了坑, 坑触发的条件为:

  1. comment中 @ManyToOne的fetch设置为 FetchType.EAGER
  2. comment(id=1)没有关联的post数据,即post_comment_ref表中无主键的关联数据
  3. 通过commentDao.findById(1)查询时,返回为空,但主键为1的comment数据是存在的,只是没有与之关联的post数据

查看JPA打印的sql分析,其自动生成的sql为

select c.id , c.title , r.post_id , p.id , p.content from comment c 
left outer join post_comment_ref r on c.id = r.comment_id inner join post p on r.post_id = p.id where c.id = 1

其中 inner join 导致了comment本身的数据都未查到,而预期返回的结果是comment对象有数据,其post属性为null,即预期inner join 应该是 left join

此处为 FetchType.EAGER 导致,修改为 FetchType.LAZY ,行为符合预期,在查询comment时,只查询关联的post_id,只有调用 comment.getPost()才执行post表的单独查询

//comment = commentDao.findById(1).get()触发
select c.id , c.title , r.post_id from comment c 
left outer join post_comment_ref r on c.id = r.comment_id where c.id = 1

//comment.getPost()调用触发
//post_id其实为null
select p.id , p.content from  post p where c.id = null