【发布时间】: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°解
我尚未对其进行测试,因此它可能有不正确的语法,但我们的想法是将Post 和PostVote 实体包装在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