首页 > 解决方案 > JPA 与共享主键的单向@OneToOne 关系始终触发辅助查询,即使 fetchType 为 EAGER

问题描述

我正在构建一个博客系统,并希望为博客提供 upvote/downvote 功能。由于博客的投票数应该被持久化,所以我选择使用 MySQL 作为数据存储。我使用 Spring JPA(Hibernate) 来完成 ORM 工作。这是我的数据对象:

class Blog{
    // ...
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @OneToOne(optional = false, fetch = FetchType.EAGER)
    @PrimaryKeyJoinColumn
    private BlogVoteCounter voteCounter;    
}

和柜台类:

@Entity
public class BlogVoteCounter extends ManuallyAssignIdEntitySuperClass<Long> {
    @Id
    private Long id;

    private Integer value;
}

BlogVoteCounter我将from分开的原因Blog是我认为与voteCount博客的其他字段相比,该字段将被完全不同的频率修改,因为我想使用缓存来缓存Blog,按照本指南,我选择将它们分开。

但是,由于在VoteCount将 Blog 对象返回到前端时可能总是需要该字段,并且为了避免 n+1 问题,我BlogVoteCounter在 Blog 类中声明了 EAGER 获取类型的字段。

我已经看过这篇文章了。因此根据我个人的理解,我使用单向关系并且只OneToOneBlog侧面声明。

但是,当我检查查询时,发现 jpa 仍然会触发辅助查询以从数据库中检索,而无需在使用方法 onBlogVoteCounter时简单地使用连接。findAllBlogRepository

    select
        blogvoteco0_.id as id1_2_0_,
        blogvoteco0_.value as value2_2_0_ 
    from
        blog_vote_counter blogvoteco0_ 
    where
        blogvoteco0_.id=?

那么我应该如何配置,总是让BlogVoteCounter字段Blog被热切地获取。


的用法ManuallyAssignIdEntitySuperClass遵循 Spring JPA doc,因为我手动为BlogVoteCounter类分配 id。

@MappedSuperclass
public abstract class ManuallyAssignIdEntitySuperClass<ID> implements Persistable<ID> {

    @Transient
    private boolean isNew = true;

    @Override
    public boolean isNew() {
        return isNew;
    }

    @PrePersist
    @PostLoad
    void markNotNew(){
        this.isNew = false;
    }
}

并且BlogRepository源自JpaRepository

public interface BlogRepository extends JpaRepository<Blog, Long>{
    // ...
}

我通过 using 方法触发查询findAll,但 usingfindById或其他条件查询似乎没有区别。

标签: javahibernatejpaspring-data-jpa

解决方案


When to fetchvs How to fetchfetchType 定义何时获取关联(instantlyvs later when someone access)关联,但不定义如何获取关联(即第二个选择与连接查询)。因此,从 JPA 规范的角度来看,EAGER 意味着不要等到有人访问该字段来填充它,但 JPA 提供者可以自由使用 JOIN 或第二次选择,只要他们立即执行。

尽管他们可以自由地使用 join vs second select,但我仍然认为他们应该在 EAGER 的情况下为 join 进行优化。非常有兴趣找出不使用连接的逻辑推理

1. 生成的查询repository.findById(blogId);

    select
        blog0_.id as id1_0_0_,
        blog0_.vote_counter_id as vote_cou2_0_0_,
        blogvoteco1_.id as id1_1_1_,
        blogvoteco1_.value as value2_1_1_ 
    from
        blog blog0_ 
    inner join
        blog_vote_counter blogvoteco1_ 
            on blog0_.vote_counter_id=blogvoteco1_.id 
    where
        blog0_.id=?

2.更新映射

public class Blog {

    @Id
    private Long id;

    @ManyToOne(optional = false, cascade = ALL, fetch = FetchType.EAGER)
    @PrimaryKeyJoinColumn
    private BlogVoteCounter voteCounter;

    public Blog() {
    }

    public Blog(Long id, BlogVoteCounter voteCounter) {
        this.id = id;
        this.voteCounter = voteCounter;
    }

    public Long getId() {
        return id;
    }

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

    public BlogVoteCounter getVoteCounter() {
        return voteCounter;
    }

    public void setVoteCounter(BlogVoteCounter voteCounter) {
        this.voteCounter = voteCounter;
    }
}

3. 当前映射的问题

  • 根据您的映射,无法创建blogvotecounter因为它会导致chicken and egg问题。IE
  • blog 和 votecounter 需要共享同一个主键
  • 博客的主键是由数据库生成的。
  • 所以为了获取博客的主键并将其分配给投票计数器,您需要先存储博客
  • 但是@OneToOne 关系不是可选的,所以你不能先单独存储博客

4.变化

  • 要么需要使关系可选,因此可以先存储博客,获取 id,分配给 BlogVoteCounter 并保存计数器
  • 或者不要自动生成 ID 并手动分配 ID,以便可以同时保存博客和投票计数器。(我已经选择了这个选项,但你可以做第一个选项)

5.注意事项

  • 默认repository.findAll是生成 2 个查询,所以我重写了该方法以生成一个连接查询
public interface BlogRepository extends JpaRepository<Blog, Long> {

    @Override
    @Query("SELECT b from Blog b join fetch b.voteCounter ")
    List<Blog> findAll();
}
    select
        blog0_.id as id1_0_0_,
        blogvoteco1_.id as id1_1_1_,
        blog0_.vote_counter_id as vote_cou2_0_0_,
        blogvoteco1_.value as value2_1_1_ 
    from
        blog blog0_ 
    inner join
        blog_vote_counter blogvoteco1_ 
            on blog0_.vote_counter_id=blogvoteco1_.id

推荐阅读