【问题标题】:JPA: fetch posts with vote cast by a specific userJPA:获取特定用户投票的帖子
【发布时间】:2022-02-01 19:49:32
【问题描述】:

我需要加载Post 实体以及代表特定用户(当前登录的用户)投票的PostVote 实体。这是两个实体:

发布

@Entity
public class Post implements Serializable {
    public enum Type {TEXT, IMG}

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "section_id")
    protected Section section;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "author_id")
    protected User author;

    @Column(length = 255, nullable = false)
    protected String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    protected String content;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    protected Type type;

    @CreationTimestamp
    @Column(nullable = false, updatable = false, insertable = false)
    protected Instant creationDate;
    
    /*accessor methods*/
}  

投票后

@Entity
public class PostVote implements Serializable {

    @Embeddable
    public static class Id implements Serializable{

        @Column(name = "user_id", nullable = false)
        protected int userId;

        @Column(name = "post_id", nullable = false)
        protected int postId;

        /* hashcode, equals, getters, 2 args constructor */
    }

    @EmbeddedId
    protected Id id;

    @ManyToOne(optional = false)
    @MapsId("postId")
    protected Post post;

    @ManyToOne(optional = false)
    @MapsId("userId")
    protected User user;

    @Column(nullable = false)
    protected Short vote;

    /* accessor methods */
}

所有关联都是单向的@*ToOne。我不使用@OneToMany 的原因是因为集合太大,需要在访问之前进行适当的分页:不将@*ToManyassociation 添加到我的实体意味着防止任何人天真地做for (PostVote pv : post.getPostVotes()) 之类的事情。

对于我现在面临的问题,我提出了各种解决方案:没有一个对我来说完全令人信服。


1°解

我可以将@OneToMany 关联表示为只能通过键访问的Map。这样就不会因迭代集合而导致问题。

@Entity
public class Post implements Serializable {
    [...]

    @OneToMany(mappedBy = "post")
    @MapKeyJoinColumn(name = "user_id", insertable = false, updatable = false, nullable = false)
    protected Map<User, PostVote> votesMap;

    public PostVote getVote(User user){
        return votesMap.get(user);
    }
    
    [...]
}  

这个解决方案看起来非常酷,并且足够接近 DDD 原则(我猜?)。但是,在每个帖子上调用 post.getVote(user) 仍然会导致 N+1 选择问题。如果有一种方法可以有效地为会话中的后续访问预取一些特定的PostVotes,那么它会很棒。 (可能例如调用from Post p left join fetch PostVote pv on p = pv.post and pv.user = :user,然后将结果存储在一级缓存中。或者可能涉及EntityGraph


2°解

一个简单的解决方案可能如下:

public class PostVoteRepository extends AbstractRepository<PostVote, PostVote.Id> {
    public PostVoteRepository() {
        super(PostVote.class);
    }

    public Map<Post, PostVote> findByUser(User user, List<Post> posts){
        return em.createQuery("from PostVote pv where pv.user in :user and pv.post in :posts", PostVote.class)
                .setParameter("user",user)
                .setParameter("posts", posts)
                .getResultList().stream().collect(Collectors.toMap(
                        res -> res.getPost(),
                        res -> res
                ));
    }
}

服务层负责调用PostRepository#fetchPosts(...)PostVoteRepository#findByUser(...),然后将结果混合在一个DTO中发送到上面的表示层。

这是我目前正在使用的解决方案。但是,我不觉得有一个 ~50 个参数长 in 子句可能是一个好主意。此外,为PostVote 设置一个单独的Repository 类可能有点矫枉过正,并破坏了 ORM 的用途。


3°解

我尚未对其进行测试,因此它可能有不正确的语法,但我们的想法是将PostPostVote 实体包装在VotedPost DTO 中。

public class VotedPost{
    private Post post;
    private PostVote postVote;

    public VotedPost(Post post, PostVote postVote){
        this.post = post;
        this.postVote = postVote;
    }

    //getters
}  

我通过这样的查询获得对象:

select new my.pkg.VotedPost(p, pv) from Post p 
left join fetch PostVote pv on p = pv.post and pv.user = :user  

与基于Object[]Tuple 查询结果的解决方案相比,这给了我更多的类型安全性。看起来比解决方案 2 更好,但以有效的方式采用解决方案 1 将是最好的。

一般来说,解决此类问题的最佳方法是什么?我使用 Hibernate 作为 JPA 实现。

【问题讨论】:

    标签: java database hibernate jpa orm


    【解决方案1】:

    我可以想象使用@OneToMany 的标准双向关联是一种可维护但高性能的解决方案。

    为了减轻n+1 的选择,可以使用例如:

    • @EntityGraph,指定要加载哪些关联数据(例如,一个 user 与所有 posts 和所有关联 votes 在一个选择查询中)
    • 休眠@BatchSize,例如在遍历user 的所有posts 时一次为多个posts 加载votes,而不是对每个votes 的每个votes 的每个集合进行一个查询@ 987654335@

    当谈到限制用户以较低性能的方式执行访问时,我认为应该由 API 来记录可能的性能影响并为不同的用例提供性能替代方案。

    (作为 API 的用户,可能总能找到以性能最低的方式实现事物的方法:)

    【讨论】:

    • @BatchSize 是一个非常好的功能。知道这实际上是在后台使用IN(根据您提供的链接)使其基本上是解决方案#2,具有解决方案#1的语法,这太棒了。
    • 我可能会考虑将一些逻辑从*Repository 类移动到实体类(例如,getPostsFromSection(Section) 变成Section#getPosts())。考虑到我需要分页,BatchSize 仍然是一个可行的选择吗?
    • 集合上的 BatchSize 可以一次加载多个(不同父实体的)集合。而且在任何情况下,一个集合中的所有子集合都会一次加载,因此 BatchSize 可能无法真正与分页相比。
    • (但我不确定我是否完全理解您在上一条评论中描述的具体用例,也许值得在专门问题中由更广泛的受众讨论)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-22
    相关资源
    最近更新 更多