Hibernate ManyToMany wtih column child removal - java

I have Many To Many with additional column. Here realization(getters/setters generated by lombok, removed for clarity):
public class User extends BaseEntity {
#OneToMany(mappedBy = "user",
orphanRemoval = true,
fetch = FetchType.LAZY
cascade = CascadeType.ALL,)
private List<UserEvent> attendeeEvents = new ArrayList<>();
}
#Table(
name = "UC_USER_EVENT",
uniqueConstraints = {#UniqueConstraint(columnNames = {"user_id", "event_id"})}
)
public class UserEvent extends BaseEntity {
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "event_id")
private Event event;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "user_id")
private User user;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "user_response_id")
private UserResponse userResponse;
}
public class Event extends BaseEntity {
#OneToMany(mappedBy = "event",
orphanRemoval = true,
fetch = FetchType.EAGER,
cascade = CascadeType.ALL)
private List<UserEvent> userEvents = new ArrayList<>();
}
I want this - when i delete Event, All "UserEvents" connected to it should be removed. And if I delete User, all "UserEvents" should be removed too.
I delete my event(eventRepository is Spring Jpa interface):
eventRepository.delete(event);
Then retrieving UserEvents from db:
List<UserEvent> userEvents = userEventsId.stream()
.map(id -> entityManager.find(UserEvent.class, id)).collect(Collectors.toList());
And there is collection with 2 items(this is count of UserEvents), but they all "null".
I can't understand what happening and how to do it right.
I need them deleted and when I check collection there should be 0, instead of 2.

The delete says marked for deletion, please try calling flush after deletion, and then find.
I guess find goes to the database, find the two rows, but when trying to instantiate them, find the entities marked for deletion and then you have this strange behaviour.
Recomendation: try to abstract more from the database and use less annotations. Learn the conventions of names for columns and tables (if you need to) and let JPA do its job.

Related

Hibernate Multiple #OneToMany bound to same entity type

I have yet another #OneToMany question. In this case, I'm trying to model a person having a list of excluded people they shouldn't be able to send items to. This is a Spring Boot app using JPA.
In the code below, the exclusions list populates properly but the excludedBy List does not. Because of this, I believe that is causing the deletion of a Person that is excluded by another person to fail because the Exclusion in excludedBy is not mapped on the object properly.
#Entity
#Table(name = "person")
public class Person {
#Id
#GeneratedValue
#Column(nullable = false)
Long id;
...
#OneToMany(mappedBy = "sender", cascade = { CascadeType.ALL })
List<Exclusion> exclusions = new ArrayList<>();
//This is not getting populated
#JsonIgnore
#OneToMany(mappedBy = "receiver", cascade = { CascadeType.ALL })
List<Exclusion> excludedBy = new ArrayList<>();
...
}
#Entity
#Table(name = "exclusions")
public class Exclusion {
#Id
#GeneratedValue
#Column(nullable = false)
Long id;
#ManyToOne
#JsonIgnore
Person sender;
#ManyToOne
Person receiver;
...
}
I would expect that this would have mapped the bidirectional relationship properly and as such the excludedBy List would be populated as well.
Any wisdom on this matter would be great!
1 - An #Id is by default not nullable, not required:
#Column(nullable = false)
2 - There is no need for an #Id in this class. Both sides of the exclusion are together unique. Not needed:
#Id
#GeneratedValue
Long id;
3 - An "Exclusion" requires both an excludedBy and an excluded, give them names that match and they are your #Id. It is a 2 way ManyToMany relationship.
#Entity
#Table(name = "exclusions")
public class Exclusion {
#Id
#ManyToMany // An ID so not optional, so no need for (optional = false)
Person excludedBy;
#Id
#ManyToMany // An ID so not optional, so no need for (optional = false)
Person excluded;
}
Entity Exclusion always knows both sides of the story.
#ManyToMany(mappedBy = "excludedBy", cascade = { CascadeType.ALL })
List<Exclusion> excluded = new ArrayList<>();
#ManyToMany(mappedBy = "excluded", cascade = { CascadeType.ALL })
List<Exclusion> excludedBy = new ArrayList<>();
Tip: JSON DTOs shouldn't be defined in your JPA DTOs, otherwise you can't change your internal data model independently of your external API model.
I had this problem in the past. Your key problem ist that your ORM Mapper hibernate does not know which of your database entries need to be assinged to exclusions and which are assiged to excludedBy. You need a discriminator and add the constraint in your select. I would propose a solution that looks something like this:
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "PRIMARY_KEX_IN_EXCLUSION_TABLE", referencedColumnName = "id")
#Where(clause = "is_excluded_by = 0")
private Set<Exclusion> exclusions;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "PRIMARY_KEX_IN_EXCLUSION_TABLE", referencedColumnName = "id")
#Where(clause = "is_excluded_by = 1")
private Set<Exclusion> excludedBy;
the value isExcludedBy needs to be a database column, part of your Entity and set in your code manually.
I think you also need to use Set instead of List when having multiple collections in one Entity. https://vladmihalcea.com/spring-data-jpa-multiplebagfetchexception/

After Hibernate migration from 5 to 5.5 adding entities to collections causes ConstraintViolationException

We have code like this (i simplified the code to make it more clear):
#Entity
#Table(name="storages")
class Storage {
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinTable(name = "storage_items", joinColumns = #JoinColumn(name = "storage_id"), inverseJoinColumns = #JoinColumn(name = "item_id"))
private Set<Item> items;
void putItemToStorage(Session session) {
Item item = new Item();
item.setStorage(this);
session.save(item);
items.add(item);
}
}
#Entity
#Table(name="items")
class Item {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "storage_id")
private Storage storage;
public void setStorage(Storage storage) {
this.storage = storage;
}
}
We call 'putItemToStorage' in a transaction, but in hibernate 5.5 it causes the following error, while in hibernate 5 same code worked like a charm:
> javax.persistence.PersistenceException:
> org.hibernate.exception.ConstraintViolationException: could not execute statement
> ...
> caused by org.postgresql.util.PSQLException: ERROR: insert or update on table
> "storage_items" violates foreign key constraint
> "fkla3c4upmtw4myssb3bfg2svkj" Detail: Key (storage_id)=(164) is not
> present in table "items".
So, hibernate 5 resolved both inserts into items table and storage_items table and worked as intended (adding both item to items table and linking the item to corresponding storage through joining table storage_items), but in hibernate 5.5 it no longer works. I spent quite time in google and documentation and can't find what was changed or what am I doing wrong.
I had similar error in other place, where I temporarily resolved it separating saving of an object and inserting the object into 2 separate transactions (it works, but it's definitely not a right solution), so, any help how to fix it correctly would be very much appreciated.
You should define mapping only on one side (the one that holds the relation), eg. like this:
#Entity
#Table(name="storages")
class Storage {
#OneToMany(mappedBy = "storage", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Item> items;
void putItemToStorage() {
Item item = new Item();
item.setStorage(this);
items.add(item);
}
}
#Entity
#Table(name="items")
class Item {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "storage_id", referencedColumnName = "id")
private Storage storage;
public void setStorage(Storage storage) {
this.storage = storage;
}
}
Notice that this approach will most likely result in 2 queries:
That will create Item record in DB with storage_id = null
That will update recods and set storage_id to value that it should be
To prevent it adjust annotations on field storage:
#Entity
#Table(name="items")
class Item {
#ManyToOne(optional = false)
#JoinColumn(name = "storage_id", referencedColumnName = "id", nullable = false, updatable = false)
private Storage storage;
}
You might also consider adding orphanRemoval = true to items in case you will want to delete record in DB.
#Entity
#Table(name="storages")
class Storage {
#OneToMany(mappedBy = "storage", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private Set<Item> items;
}
Your code then should looks like this:
// assumming within transaction context
var storage = // get from eg. EntityManager or JPA repository (in spring)
storage.putItemToStorage();
// There is no need to call EntityManager::persist or EntityManager::merge
// if you are withing transaction context
// and you are working with managed entity
// and have cascade = CascadeType.ALL

Spring Data JPA - Delete many to many entries

I am attempting to remove entries from a many to many relationship using Spring Data JPA. One of the models is the owner of the relationship and I need to remove entries of the non-owner entity. These are the models:
Workflow entity
#Entity(name = "workflows")
public class Workflow {
#Id
#Column(name = "workflow_id", updatable = false, nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID workflowId;
#ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
#JoinTable(name = "workflow_data",
joinColumns = #JoinColumn(name = "workflow_id", referencedColumnName = "workflow_id"),
inverseJoinColumns = #JoinColumn(name = "data_upload_id", referencedColumnName = "data_upload_id"))
private Set<DataUpload> dataUploads = new HashSet<>();
// Setters and getters...
}
DataUpload entity
#Entity(name = "data_uploads")
public class DataUpload {
#Id
#Column(name = "data_upload_id")
private UUID dataUploadId;
#ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }, mappedBy = "dataUploads")
private Set<Workflow> workflows = new HashSet<>();
// Setters and getters...
}
DataUpload repository
#Repository
public interface DataUploadsRepository extends JpaRepository<DataUpload, UUID> {
#Transactional
void delete(DataUpload dataUpload);
Optional<DataUpload> findByDataUploadId(UUID dataUploadId);
}
To delete data uploads, I am trying to execute a couple of query methods of the repository:
First version
dataUploadsRepository.deleteAll(workflow.getDataUploads());
Second version
workflow.getDataUploads().stream()
.map(DataUpload::getDataUploadId)
.map(dataUploadsRepository::findByDataUploadId)
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(dataUploadsRepository::delete);
Problem is that Spring Data JPA is not removing DataUploads nor entries of the association table workflow_data.
How can I tell Spring Data to remove from both data_uploads and workflow_data (association table)?
I would appreciate any help.
I found the solution for this problem. Basically, both entities (in my case) need to be the owner of the relationship and the data from the association table must be deleted first.
Workflow entity (relationship owner)
#Entity(name = "workflows")
public class Workflow {
#Id
#Column(name = "workflow_id", updatable = false, nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID workflowId;
#ManyToMany(cascade = { CascadeType.ALL })
#JoinTable(name = "workflow_data",
joinColumns = #JoinColumn(name = "workflow_id", referencedColumnName = "workflow_id"),
inverseJoinColumns = #JoinColumn(name = "data_upload_id", referencedColumnName = "data_upload_id"))
private Set<DataUpload> dataUploads = new HashSet<>();
// Setters and getters...
}
DataUpload entity (relationship owner)
#Entity(name = "data_uploads")
public class DataUpload {
#Id
#Column(name = "data_upload_id")
private UUID dataUploadId;
#ManyToMany
#JoinTable(name = "workflow_data",
joinColumns = #JoinColumn(name = "data_upload_id", referencedColumnName = "data_upload_id"),
inverseJoinColumns = #JoinColumn(name = "workflow_id", referencedColumnName = "workflow_id"))
private Set<Workflow> workflows = new HashSet<>();
// Setters and getters...
}
Notice that Workflow has ALL as cascade type, since (based on the logic I need), I want Spring Data JPA to remove, merge, refresh, persist and detach DataUploads when modifying workflows. On the other hand, DataUpload does not have cascade type, as I do not want Workflow instances (and records) to be affected due to DataUploads deletions.
In order to successfully delete DataUploads, the associate data should be deleted first:
public void deleteDataUploads(Workflow workflow) {
for (Iterator<DataUpload> dataUploadIterator = workflow.getDataUploads().iterator(); dataUploadIterator.hasNext();) {
DataUpload dataUploadEntry = dataUploadIterator.next();
dataUploadIterator.remove();
dataUploadsRepository.delete(dataUploadEntry);
}
}
dataUploadIterator.remove() deletes records from the association table (workflow_data) and then the DataUpload is deleted with dataUploadRepository.delete(dataUploadEntry);.
It has been a while since I have to fix these kind of mappings so I'm not going to give you a code fix, instead maybe give you another perspective.
First some questions like, do you really need a many to many? does it make sense that any of those entities exist without the other one? Can a DataUpload exist by itself?
In these mappings you are supposed to unassign the relationships on both sides, and keep in mind that you could always execute a query to remove the actual values (a query against the entity and the intermediate as well)
A couple of links that I hope can be useful to you, they explain the mappings best practices and different ways to do the deletion.
Delete Many, Delete Many to Many, Best way to use many to many.

Understanding Hibernate cascade behavior with multiple item references

I don't understand a certain Hibernate behavior regarding an object that should be persisted by a Cascade.ALL setting, but is then regarded as unsaved in another reference in the same transaction.
Example:
OrderProposal proposal = new OrderProposal();
ProposalLineItem proposalLine = new ProposalLineItem();
proposalLine.setProposal(proposal);
proposal.addLineItem(proposalLine); //to be saved by cascade.all via proposal
saveOrUpdate(proposal);
Order order = new Order();
OrderLineItem orderLine = new OrderLineItem(); //to be saved by cascade.all via order
orderLine.setProposalLine(proposalLine); //proposalLine is not mapped as cascaded from orderLine
proposalLine.setOrderLine(orderLine);
order.addLineItem(orderLine);
saveOrUpdate(order);
If this is run in a single transaction, Hibernate throws when processing the cascades of the order object:
org.hibernate.TransientPropertyValueException: object references an
unsaved transient instance - save the transient instance before flushing : orderLine.proposalLine
Do I have to save proposalLine explicitly for getting this to work?
EDITED
Here are the affected hibernate mappings:
#Entity
#Access(AccessType.FIELD)
#Proxy(lazy = false)
#OptimisticLocking(type = OptimisticLockType.VERSION)
public class ProposalLineItem {
#ManyToOne(optional = false)
#JoinColumn(name = "proposal_id")
private OrderProposal proposal;
#OneToOne(mappedBy = "proposalLine")
#NotFound(action = NotFoundAction.EXCEPTION)
private OrderLineItem orderLine;
}
#Entity
#OptimisticLocking(type = OptimisticLockType.VERSION)
#Proxy(lazy = false)
#Access(AccessType.FIELD)
public class OrderProposal {
#OneToMany(mappedBy = "proposal", orphanRemoval = true, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#OrderColumn(name = "listIndex", nullable = false)
private List<ProposalLineItem> lineItems;
}
#Entity
#Proxy(lazy = false)
#OptimisticLocking(type = OptimisticLockType.VERSION)
#Access(AccessType.FIELD)
public class OrderLineItem {
#OneToOne(optional = false)
#NotFound(action = NotFoundAction.EXCEPTION)
#JoinColumn(name = "proposal_line_id", nullable = false)
private ProposalLineItem proposalLine;
}
#Entity
#OptimisticLocking(type = OptimisticLockType.VERSION)
#Proxy(lazy = false)
#Access(AccessType.FIELD)
public class Order {
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
#JoinColumn(name = "order_id")
#IndexColumn(nullable = false, name = "listIndex", base = 0)
private List<OrderLineItem> lineItems;
}
Cascade is a convenient feature to save the lines of code needed to
manage the state of the other side manually.
Let us suppose one scenario,where one object depend on the other one and you want to delete it how you will delete it? You have to fire the 1 after other query manually if cascading not there. So in hibernate cascading attribute is mandatory where you are define relationship between objects so by defining it child class object also going to effect if you make any change to Parent class object.
Now you have mentioned cascade = “all” you are telling cascading will be apply for insert/update/delete
Cascading can be apply save/update/delete etc.
Now if cascade = “none” then only Parent class object will effected and not the child class.

Confused regarding JPA

I have two classes.
public class Invoice {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "invoice_id", unique = true)
private int invId;
#OneToMany(mappedBy = "invoiceList", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private List<Item> itemList;
#Column(name = "invoice_amt", nullable = false)
private Double invAmt;
}
And,
public class Item {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "item_id", unique = true)
private int itemId;
#ManyToOne(optional = false, targetEntity = Invoice.class)
#JoinColumn(name="invoice_id")
private List<Invoice> invoiceList;
}
I am new to JPA. So my understanding may not be accurate.
My understanding is that, if I save Invoice, the invoice_id of that instant should cascade down to invoice_id of all the items.
However, I see Item being saved but get null in place of invoice_id of the Item.
What am I missing?
UPDATE!!! UPDATE!!!
Ok so I changed the #ManyToOne to be a singular attribute and did objItem.setInvoice(objInvoice) and saved it. However, I still get NULL on invoice_id.
You are annotating a many-to-one relation, but use collections on both sides. This will not work. The one-side has to map the relation to a singular attribute. In your case, it would be
#ManyToOne
private Invoice invoice
Perhaps you rather need a many-to-many relation. In this case, you will need to change the annotations to #ManyToMany and get rid of the cascades (they tend not to work as expected from a many-side).
targetEntity attribute and the #JoinColumn annotation are redundant on the invoice attribute of Item.
In order for the Item to save the id of the related invoice, you first need to set the invoice attribute of the Item since item is the owning side (the one where the relation information is stored).
I'm not sure this is your only problem, but a 1:n relationship shouldn't have a List both ways. If you turn List<Invoice> into a simple Invoice object, you'll at least be closer to a solution. We can go from there if your code still fails.
public class Item {
#ManyToOne(optional = false, targetEntity = Invoice.class)
#JoinColumn(name = "invoice_id")
private Invoice invoice;
}
public class Invoice {
#OneToMany(mappedBy = "invoiceList", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private List<Item> itemList;
}

Categories