Most efficient way to implement Liked/Disliked status retrieval in JPA/Hibernate? - java

I'm currently implementing a doc with a like button like this:
The like button is associated with certain user account. When you press a like, it will stay liked for that user (similar to youtube video).
My entities and DTOs are below:
Doc.java:
#Entity(name = "Doc")
#Table(name = "doc")
#Data
public class Doc {
//Unrelated code reacted for clarity
#ManyToMany(cascade = {
CascadeType.MERGE,
CascadeType.PERSIST
})
#JoinTable(
name = "doc_user_dislike",
joinColumns = #JoinColumn(name = "doc_id"),
inverseJoinColumns = #JoinColumn(name = "user_id")
)
private Set<UserWebsite> dislikedUsers;
#ManyToMany(cascade = {
CascadeType.MERGE,
CascadeType.PERSIST
})
#JoinTable(
name = "doc_user_like",
joinColumns = #JoinColumn(name = "doc_id"),
inverseJoinColumns = #JoinColumn(name = "user_id")
)
private Set<UserWebsite> likedUsers;
}
User.java:
#Entity
#Table(name = "user_website")
#Data
public class UserWebsite {
//Unrelated code reacted for clarity
#ManyToMany(mappedBy = "likedUsers")
private Set<Doc> likedDocs;
#ManyToMany(mappedBy = "dislikedUsers")
private Set<Doc> dislikedDocs;
}
DocDetailsDTO.java (This will be sent to client).
#Data
public class DocDetailsDTO {
private Long id;
private Boolean isDisliked;
private Boolean isLiked;
}
I'm having some solutions:
Add a field called isLiked to Doc.java with #Formular combine with
#Transient and perform queries to DB.
Have another API which accept from Client a list of DocID, and a
UserID, then return a list of DocID that UserID liked.
Check if UserID exist in likedUsers list (not very efficient,
sometimes not feasible since I have to initialize that big
lazy-loaded list).
The question is: What is the most efficient way to retrieve liked/disliked status for many post at once (>10 doc but max 100 doc per request) for about thousand users (1000 CCU) at once ? Are above solutions already optimal ?
Any help is appreciated. Thanks for your time reading through the question.

If I understand the problem correctly, this approach is not correct. You want to determine if a given user likes specified documents, so the formula would need a user id parameter, which you have no way to pass to the formula. Even if somehow #Formula could be used, it leads to N+1 problem (extra query per each document). Plus, you use managed entities which means extra dirty checking at the end.
This one is optimal in my opinion - one query, capable of using projection (no managed entities).
As you notice, this will kill your application and database. Plus, again you use managed entities which means extra dirty checking at the end. Definitely don't use this one.

Related

JPA: fetch posts with vote cast by a specific user

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 #*ToManyassociation 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 PostVotes 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.
I could imagine the standard bi-directional association using #OneToMany being a maintainable yet performant solution.
To mitigate n+1 selects, one could use e.g.:
#EntityGraph, to specify which associated data is to be loaded (e.g. one user with all of it's posts and all associated votes within one single select query)
Hibernates #BatchSize, e.g. to load votes for multiple posts at once when iterating over all posts of a user, instead having one query for each collection of votes of each post
When it comes to restricting users to perform accesses in less performant ways, I'd argue that it should be up the API to document possible performance impacts and offer performant alternatives for different use-cases.
(As a user of an API one might always find ways to implement things in the least performant fashion:)

Spring boot service not executing deleteBy from repository

I'm doing a sort of a feed with posts and comments
Post entity
public class Post extends AuditEntity {
#Lob
private String text;
#Column
private int totalCmnts = 0;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "post_id")
private Set<Comment> comments;
#ManyToOne
#JoinColumn(name = "user_id", referencedColumnName = "id")
private MEUser MEUser;
}
Comment entity
public class Comment extends AuditEntity {
#Lob
private String text;
#ManyToOne
#JoinColumn(name = "user_id", referencedColumnName = "id")
private MEUser MEUser;
}
and i have a service to delete a comment where i fetch the post and the comment, i check if the current user can execute the operation then i delete the comment and decrement the total count of comments in post entity.
the problem in the log i can see the select operation and the update operation but the delet is not executed at all and still getting HTTP 200 with #transactional annotation in the service class
I tried an SQL query and it worked fine
DELETE FROM `db-engine`.comment WHERE comment.id = 2
Service
#Override
public void deleteComment(Long commentID, Long postID, UserPrincipal currentUser) {
Comment comment = commentsRepository.findById(commentID)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
Post post = postsRepository.findById(postID)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (comment.getMEUser().getId().equals(currentUser.getId())
|| currentUser.getAuthorities().contains(new SimpleGrantedAuthority(RoleName.ROLE_ADMIN.toString()))
|| currentUser.getAuthorities().contains(new SimpleGrantedAuthority(RoleName.ROLE_SUPER_ADMIN.toString()))) {
commentsRepository.deleteById(comment.getId());
post.setTotalCmnts(post.getTotalCmnts() - 1);
System.out.println(comment.getId());
System.out.println("hello");
}
}
The issue is that you are changing the post entity with post.setTotalCmnts(post.getTotalCmnts() - 1);. This means that Hibernate will notice the difference between your entity and the data in database and flush it (thus updating the post in the database).
Since you have #OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) on comments this means that Hibernate will cascade the persist operation to comments entities. In this list, you still have the Comment you want to delete and thus it is created again (there might be some Hibernate optimization here that understands this and actually does not delete it from the database instead).
You need to also remove the Comment you want to delete from the post comments list as follows:
commentsRepository.deleteById(comment.getId());
post.setTotalCmnts(post.getTotalCmnts() - 1);
post.setComments(post.getComments().remove(comment));
System.out.println(comment.getId());
System.out.println("hello");
Additionally, I would add a custom setComments() method that actually updates totalCmnts based on the given parameter, otherwise, you might have inconsistent data. Also, the getter should return a copy of the list and not the list itself, otherwise, you might have some pretty nasty bugs because you change the list elsewhere and this change gets persisted without you noticing it.

JPA many-to-many relationship causing infinite recursion and stack overflow error

I'm working on an EclipseLink project in which one user can "follow" another as can be done on social media sites. I have this set up with a User entity (referencing a table called users) which has a list of "followers" (users who follow that user) and another list of "following" (users that user is following). The relationship is defined in a separate table called followers which contains columns for the followed user's ID (user_id) and the following user's ID (follower_id).
My users model looks like this:
#Entity
#Table(name = "users")
#NamedQuery(name = "User.findAll", query = "SELECT u FROM USER u")
public class User {
// other attributes
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "follower", joinColumns = #JoinColumn(
name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(
name = "follower_id", referencedColumnName = "id"))
private List<User> followers;
#ManyToMany(mappedBy = "followers")
private List<User> following;
// other getters and setters
public List<User> getFollowers() {
return this.followers;
}
public List<User> getFollowing() {
return this.following;
}
}
The getFollowers() method seems to work fine, but when getFollowing() is called I get a bunch of console spam that culminates in a StackOverflowException:
com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion
(StackOverflowError) (through reference chain:
org.eclipse.persistence.indirection.IndirectList[0]-
>org.myproject.model.User["followers"]-
>org.eclipse.persistence.indirection.IndirectList[0]-
>org.myproject.model.User["following"]-
...
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase
.serializeFields(BeanSerializerBase.java:518)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize
(BeanSerializer.java:117)
at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer
.serializeContents(IndexedListSerializer.java:94)
at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer
.serializeContents(IndexedListSerializer.java:21)
...
Please let me know if I should provide more of the stack trace. Any hints?
Every time you have #OneToMany (a collection) you need to add #JsonIgnore to it or else it will cause an infinite loop which results in a stack overflow exception because it keeps looking up between the parent(the one side) and the child (the many side)
For more info on dealing with this kind of problems check this excellent article http://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion
public class A {
private String name;
#JsonIgnoreProperties(value = "linkToA") // remove field to avoid loop
private B linkToB;
}
public class B {
private String name;
#JsonIgnoreProperties(value = "linkToB") // remove field to avoid loop
private A linkToA;
}
I tried everything what I found in the web and it didn't work. None of any annotation.
But I found a solution after a huge fight with this issue.
First point you need to add to both entities (not the relational one) this annotation:
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "relationClass"})
public class YourClass { ...
}
Where "relationClass" is the name of the List/Set of your class for the relations:
For example:
#OneToMany(mappedBy = "yourClass", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private Set<RelationClass> relationClass;
You also need to specify "hibernateLazyInitializer", "handler" in the annotation or it will cause serialization problem.
After it if you see the table that define relation table and you will see rows in there, and in your JSON response will not be any loop anymore. So as I think the best solution is also create a repository for the relation tablee and access the data from there.
Hope it will help someone!
I think the problem in previous answers is the package of the annotation. In the article http://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion
#JsonManagedReference and #JsonBackReference works fine.
But the package should be com.fasterxml.jackson.annotation .
Sometimes another packages may be imported and does not solve the problem.
In addition adding
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
before the class definition of the model works fine.
You can also use lombok's EqualsAndHashcode annotation like below.
#EqualsAndHashcode(exclude ={"following"})
public class User{}

How to deal with unique field generated by hibernate?

Getting right to the point, I'm trying to build the following logic to hibernate relationships.
A Resource has many read groups.
A Resource has many write groups.
Both groups are Groups class.
What I did until now:
ResourcePage.class
public class ResourcePage {
/*
useless code
*/
private Set read;
private Set write;
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
targetEntity = Groups.class)
#JoinTable(name = "resourcepage_read_permissions")
public Set getRead() {
return read;
}
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
targetEntity = Groups.class)
#JoinTable(name = "resourcepage_write_permissions")
public Set getWrite() {
return write;
}
/*
useless code
*/
}
The tables is created as expected.
However, hibernate is generating an unique constraint to id of group and this is giving me a big problem because sometimes two different resources can be same group as read group.
How do you guys deal with it?
How can I make hibernate not generate this unique constraint?
Thanks a lot.
You need to use #ManyToMany instead of #OneToMany.

JPA Best Practice: Static Lookup Entities

Imagine, an Event entity references a Status Entity:
#Entity
#Table(name = "event")
public class Event()
{
#Id
#Column(name = "id", nullable = false)
private long id;
...
#ManyToOne
#JoinColumn(name = "status_code", nullable = false)
private Status status;
}
#Entity
#Table(name = "status")
public class Status()
{
#Id
#Column(name = "code", nullable = false)
private String code;
#Column(name = "label", nullable = false, updatable = false)
private String label;
}
Status is mapped to a small table 'status'. Status is a typical reference data / lookup Entity.
code label
----- --------------
CRD Created
ITD Initiated
PSD Paused
CCD Cancelled
ABD Aborted
I'm not sure if it is a good idea to model Status as an Entity. It feels more like an enumeration of constants...
By mapping Status as an Entity, I can use Status objects in Java code, and the Status values are equally present in the database. This is good for reporting.
On the other hand, if I want to set a particular Status to an Event, I can't simply assign the constant status I have in mind. I have to lookup the right entity first:
event.setStatus(entityManager.find(Status.class, "CRD"))
Can I avoid the above code fragment? I'm affraid for a performance penalty and it looks very heavy...
Do I have to tweak things with read-only attributes?
Can I prefetch these lookup entities and use them as constants?
Did I miss a crucial JPA feature?
...?
All opinions / suggestions / recommendations are welcome!
Thank you!
J.
You could use entityManager.getReference(Status.class, "CRD"), which might not fetch the entity from the database if it is only used to set a foreign key.
Can I avoid the above code fragment? I'm affraid for a performance penalty and it looks very heavy?
Well, you could use an enum instead. I don't really see why you don't actually.
But if you really want to use an entity, then it would be a perfect candidate for 2nd level caching and this would solve your performance concern.

Categories