Spring boot service not executing deleteBy from repository - java

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.

Related

Foreign key assigned to NULL in JPA one-to-many relationship

I have the following pair of entity classes:
#Entity(name="metadata")
public class Metadata {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
// Note: Hibernate CascadeType.ALL is also being used elsewhere in this class, hence
// the fully qualified class name used below
#OneToMany(cascade = javax.persistence.CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "metadata")
private List<Attachment> attachments;
// getters and setters
}
#Entity(name="attachment")
public class Attachment {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "metadata_id", referencedColumnName = "id", nullable = false, updatable = false)
private Metadata metadata;
// getters and setters
}
For completeness, here is how I am building the Metadata object:
Metadata metadata = modelMapper.map(req, Metadata.class);
List<Attachment> attachments = new ArrayList<>();
// the files come as a parameter to a Spring controller endpoint (FYI)
for (MultipartFile file : files) {
Attachment attachment = new Attachment();
attachment.setContents(file.getBytes());
attachment.setFilename(file.getOriginalFilename());
attachments.add(attachment);
}
metadata.setAttachments(attachments);
metadata.setDraft(isDraft);
myJPARepository.save(metadata);
What I observe when creating a Metadata entity and then calling save() from my JPA repository is that all data does get correctly written to my database (Postgres). However, the join column metadata_id is always NULL. At first, I thought this might have been caused due to the referencedColumnName attribute not being set (whose default is ""). However, adding this in as you see above did remedy the problem.
Does anyone know why the join column metadata_id is always appearing as NULL in the database table?
You need to synch both of your object, as of now you are creating metadata object and adding attachment to it and you have cascade so that will save both entities into their respective table.
But, since you have bidirectional relationship, you are only synching one side of relation ship here only, you need to set the same metadata object to each attachment object as well, then hibernate will be able to link the foreign key.
Instead of setter I would suggest use a add function on metadata object something like this
public void addAttachment(Attachment attachment) {
attachments.add(attachment);
attachment.setMetadata(this);
}
This way both the object would be in synch, use that inside in your for loop, you may have to initialise your collection inside metadata object before doing that or you can first check in above add function that if attachments list is null then create one and then add.

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

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.

Hibernate won't set foreign key to value of parent's primary key on insert

I have 3 tables that have a hierarchical relationship:
Page (Grandmother)
public class Page extends BaseDAO {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "page_id", unique = true, nullable = false)
public Integer getPageId() {
return this.pageId;
}
#OneToMany(fetch = FetchType.EAGER, mappedBy = "page", cascade=CascadeType.ALL, orphanRemoval=true)
#NotFound(action = NotFoundAction.IGNORE)
public Set<PageWell> getPageWells() {
return this.pageWells;
}
}
PageWell (Mother)
public class PageWell extends BaseDAO {
#Id
#Column(name = "page_well_id", unique = true, nullable = false)
public int getPageWellId() {
return this.pageWellId;
}
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "page_id", nullable = false)
public Page getPage() {
return this.page;
}
#OneToMany(fetch = FetchType.EAGER, mappedBy = "pageWell", cascade=CascadeType.ALL)
public Set<PageComponentAttribute> getPageComponentAttributes() {
return this.pageComponentAttributes;
}
}
PageComponentAttribute (Daughter)
public class PageComponentAttribute extends BaseDAO {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "page_component_attribute_id", unique = true, nullable = false)
public Integer getPageComponentAttributeId() {
return this.pageComponentAttributeId;
}
#ManyToOne(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
#JoinColumn(name = "page_well_id", nullable = false)
public PageWell getPageWell() {
return this.pageWell;
}
}
The primary keys for all three tables are AutoIncrement in MySQL. The expected behavior is that when I save the Page, all PageWell objects get saved, and all PageComponentAttribute objects also get saved.
For some reason, it is working correctly for the Grandmonther -> Daughter relationship. But in the case of the Mother -> Daughter relationship, the Daughter's foreign key is set to 0 every time. This was obviously causing a constraint violation. I have temporarily removed the FK constraint on that relationship, and the record makes it into the table, but the FK is still 0.
My save code looks like this:
Page page = getPage(request); //getPage() finds an instance of page, or creates and persists a new instance if none exists.
Set<PageWell> wells = page.getPageWells();
wells.clear(); //delete all related PageWell objects so we can re-create them from scratch
page = pageHome.merge(page);
wells = page.getPageWells();
PageWell pageWell;
// Now create a new PageWell and set up bi-directonal mapping with Page. This part works great.
pageWell = new PageWell();
pageWell.setPage(page);
wells.add(pageWell);
// Now do the exact same thing with the PageComponentAttribute objects
PageComponentAttribute pca = new PageComponentAttribute();
pca.setPageWell(pageWell);
pca.getPageWell().getPageComponentAttributes().add(pca);
// Now save the Page
page = pageHome.merge(page);
When I check the database, the FK in the PageComponentAttribute table is set to 0. Again, I have temporarily removed the FK constraint from MySQL just to allow the record to save without an exception, but besides that, what am I doing wrong?
I would try to do one of the things, or all:
1) Remove the cascade from the #ManyToOne. In general thats not a good idea to have it configured like that. It essentially makes sense only for #OneToMany and #OneToOne.
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "page_well_id", nullable = false)
public PageWell getPageWell() {
return this.pageWell;
}
2) Try using the Hibernate cascade configuration instead of the JPA one:
#OneToMany(fetch = FetchType.EAGER, mappedBy = "pageWell")
#Cascade(CascadeType.ALL)
public Set<PageComponentAttribute> getPageComponentAttributes() {
return this.pageComponentAttributes;
}
There may be some small differences, see: article
3) Not sure why you invoke merge twice on the page entity. I would just stick to one at the very end.
4) Last workaround that comes to my mind would be performing an explicit flush here:
pageWell = new PageWell();
pageWell.setPage(page);
wells.add(pageWell);
session.flush();
and then:
PageComponentAttribute pca = new PageComponentAttribute();
pca.setPageWell(pageWell);
pca.getPageWell().getPageComponentAttributes().add(pca);
session.merge(pageWell);
In theory, pageWell should have the primary already generated because of the flush and it should not be 0 anymore.
I wish i had a testing environment right now to test this properly.
In the unlikely chance that someone has made the same bone-headed mistake I've made, the problem was that the PageWell entity's primary key didn't have a Generation strategy. I added that and it fixed my problem.
#GeneratedValue(strategy = IDENTITY)

Spring Data REST + JPA remove from OneToMany collection [not owner side]

Currently we have an issue (a well known one) with Spring Data JPA + Spring Data REST (Hibernate as JPA implementation) when trying to update the collection (relation) which is a not the owning side.
The mapping is the following:
#Entity(name = Product.NAME)
public class Product {
...
#OneToMany(mappedBy = "baseProduct", fetch = FetchType.LAZY, targetEntity = Variant.class)
List<Variant> getVariants() {
...
and on the other variant side:
#Entity(name = Variant.NAME)
public class Variant extends Product {
...
#ManyToOne(fetch = FetchType.LAZY, targetEntity = Product.class)
#JoinColumn(name = "baseproduct_id", referencedColumnName = "id")
Product getBaseProduct() {
...
}
all is good on the Java side if you use Spring Data JPA only, however if you want to update the "product" by updating its collection of variants and send PATCH request to https://localhost:8112/storefront/rest/product/21394435410197232 containing the payload of the new collection only (having 2 out of the 3 items):
{"variants":["22801810293768080","22801810293768096"]}
I get no exceptions or anything but since the owning side is the other side nothing is persisted and I got the old 3 items again.
I know that I can fix this by setting
#JoinColumn(name = "baseproduct_id", referencedColumnName = "id")
on both sides and not use mappedBy anywhere, however I have heard there is a performance implication which I am not sure how big it is (we got 100+ entities having #OneToMany) and I wonder is there better workaround via #PreUpdate listener or something ?
You have to synchronize both sides of the bidirectional association, and also add on orphanRemoval and Cascade.
So, your mapping becomes:
#OneToMany(
mappedBy = "baseProduct",
fetch = FetchType.LAZY,
targetEntity = Variant.class
cascade = CascadeType.ALL,
orphanRemoval = true)
List<Variant> getVariants() {
And the two add/remove methods:
public void addVariant(Variant variant) {
getVariants().add(variant);
variant.setBaseProuct(this);
}
public void removeVariant(Variant variant) {
variant.setBaseProuct(null);
this.getVariants().remove(variant);
}
You need to implement equals and hashCode methods in the Variant child entity for the add and remove methods to work effectively.

JPA cascading merge when it shouldn't

I'm having a hard time understanding this JPA behavior which to me doesn't seem to follow the specification.
I have 2 basic entities:
public class User {
#Id
#Column(name = "id", unique = true, nullable = false, length = 36)
#Access(AccessType.PROPERTY)
private ID id;
#OrderBy("sequence ASC")
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = { CascadeType.REMOVE })
private final Set<UserProfile> userprofiles = new HashSet<UserProfile>(0);
//Ommiting rest of fields since they aren't relevant
}
public class UserProfile {
#Id
#Column(name = "id", unique = true, nullable = false, length = 36)
#Access(AccessType.PROPERTY)
private ID id;
#NotNull
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "userID", nullable = false, foreignKey = #ForeignKey(name = "FK_UserProfile_User"))
private User user;
//Ommiting rest of fields since they aren't relevant
}
As you can see I only have cascading set to REMOVE, the behavior will be the same if I don't have cascade set at all.
Now if I call:
User user = new User();
user.setId(UUIDGenerator.generateId());
UserProfile userProfile = new UserProfile();
userProfile.setId(UUIDGenerator.generateId());
userProfile.setUser(user);
user.getUserProfiles().add(userProfile);
em.merge(user);
merge will throw an exception.
I see Hibernate is executing a SQL query against the UserProfile table:
select userprofil0_.userProfileID as userProf1_4_0_, userprofil0_.profileID as profileI3_4_0_, userprofil0_.sequence as sequence2_4_0_, userprofil0_.userID as userID4_4_0_ from UserProfile userprofil0_ where userprofil0_.userProfileID=?
And then it will throw an exception
org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find com.mytest.domain.UserProfile with id 6aaab891-872d-41e6-8362-314601324847;
Why is this query even called?
Since I don't have cascade type set to MERGE in userprofiles my expectation would be that JPA/Hibernate would simply ignore the entities inside userprofiles set and only insert/update the user record, doesn't this go against the JPA specs?
If I change cascadetype to MERGE things will work as expected and both User and UserProfile will be added to the database, so no problem there. What puzzles me is why is Hibernate querying the database and erroring out about an entity that's not supposed to be merged at all since I don't have it set to cascade.
This is more of an academic scenario that I ran into, of course I could simply clear the userprofiles set and things would work, but I'm trying to understand why the above behavior happens since I'm probably missing some crucial piece of information about how merge works. It seems it will always try to attach all entities to the session regardless cascade type being set or not.
Why is this query even called?
It's because you are trying to merge the entity, in JPA merge() is used to make the entity managed/attached. To "merge" User, JPA needs to still maintian the references it holds(UserProfile). In your case its not trying to persist UserProfile its trying to get a reference to it to merge User. Read here
If you use persist rather than merge this should not happen.

Categories