I need to load the Post
entities along with the PostVote
entity that represents the vote cast by a specific user (The currently logged in user). These are the two entities:
Post
@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*/
}
PostVote
@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 */
}
All the associations are unidirectional @*ToOne
. The reason I don't use @OneToMany
is because the collections are too large and need proper paging before being accessed: not adding the @*ToMany
association to my entities means preventing anyone from naively doing something like for (PostVote pv : post.getPostVotes())
.
For the problem i'm facing right now I've come with various solutions: none of them looks fully convincing to me.
1° solution
I could represent the @OneToMany
association as a Map
that can only be accessed by key. This way there is no issue caused by iterating over the collection.
@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);
}
[...]
}
This solution looks very cool and close enough to DDD principles (i guess?). However, calling post.getVote(user)
on each post would still cause a N+1 selects problem. If there was a way to efficiently prefetch some specific PostVote
s for subsequent accesses in the session then it would be great. (Maybe for example calling from Post p left join fetch PostVote pv on p = pv.post and pv.user = :user
and then storing the result in the L1 cache. Or maybe something that involves EntityGraph
)
2° solution
A simplistic solution could be the following:
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
));
}
}
The service layer takes the responsability of calling both PostRepository#fetchPosts(...)
and then PostVoteRepository#findByUser(...)
, then mixes the results in a DTO to send to the presentation layer above.
This is the solution I'm currently using. However, I don't feel like having a ~50 parameters long in
clause might be a good idea. Also, having a separate Repository
class for PostVote
may be a bit overkill and break the purpose of ORMs.
3° solution
I haven't tested it so it might have an incorrect syntax, but the idea is to wrap the Post
and PostVote
entity in a VotedPost
DTO.
public class VotedPost{
private Post post;
private PostVote postVote;
public VotedPost(Post post, PostVote postVote){
this.post = post;
this.postVote = postVote;
}
//getters
}
I obtain the object with a query like this:
select new my.pkg.VotedPost(p, pv) from Post p
left join fetch PostVote pv on p = pv.post and pv.user = :user
This gives me more type safeness than the the solutions based on Object[]
or Tuple
query results. Looks like a better alternative than the solution 2 but adopting the solution 1 in a efficient way would be the best.
What is, generally, the best approach in problems like this? I'm using Hibernate as JPA implementation.