Hibernate ManyToMany results in cartesian product - java

I am a newbie in hibernate/JPA and apologies for this noob question. I have follow relations with in entities
Manager has many to many relation with Worker
Worker has many to many relation with Task
Following is the ERD
Following are my java classes
#Entity
#table(name="manager")
public class Manager {
private long id;
private String name;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "manager_worker", joinColumns = { #JoinColumn(name = "manager_id") },
inverseJoinColumns = { #JoinColumn(name = "worker_id") })
private ArrayList<Worker> workers = new ArrayList<Worker>();
// ......getter setter
}
#Entity
#Table(name = "worker")
public class Worker {
#Id
private long id;
private String name;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "worker_task", joinColumns = { #JoinColumn(name = "worker_id") },
inverseJoinColumns = { #JoinColumn(name = "task_id") })
private ArrayList<Task> tasks = new ArrayList<Task>();
...........................
}
#Entity
#Table(name = "task")
public class Task {
#Id
private long id;
private String title;
.................
}
I have following data:
Manager M1 has worker W1 and W2
W1 has tasks TW1, TW2, TW3
W2 has tasks TW2 and TW2
When I get Manager object for Id M1, the result has cartesian product of Worker and Task i.e. W1 data is repeated for 3 times and W2 data is repeated for 2 times i.e. Manager.worker array list have 5 worker object instead of 2.
To load the data I am using Session.get() method
public E find(final K key) {
return (E) currentSession().get(daoType, key);
}
Can anyone please tell me how I can fix this and point me to any best practices that I should use in this case.
Thanks

This happens because both #ManyToMany relationships are eager loaded.
This will cause an outer join over all three tables. The result of this query are five rows where W1 appear three times and W2 two times. So far everything is correct.
But for some reason Hibernate just puts all workers in the collections even if they are duplicates. It's even described in the FAQ.
There are multiple options to resolve this issue:
change one of the relationships to lazy loading
use a Set instead of a List (you shouldn't use ArrayList as type for variables anyway especially when using Hibernate)
use #Fetch(FetchMode.SELECT) for tasks

Where is your targetEntity in the #ManyToMany relationship?
you should have something like:
#ManyToMany(targetEntity = Task.class)
#JoinTable(name = "worker_task", joinColumns = { #JoinColumn(name = "worker_id") },
inverseJoinColumns = { #JoinColumn(name = "task_id") })
private ArrayList<Task> tasks = new ArrayList<Task>();
I suggest you to change to #OneToMany and #ManyToOne relationship anyways. That concept is more compatible with the database design and is more understandable when a person looks at the ERD. So Manager has one-to-many relationship with Manager_Worker thus manager_worker has many-to-one relationship with manager. Keep the same for the rest of the entities.

Related

QueryDsl invert manyToMany join

Given we have the following entities that form a many-to-many relationship:
#Entity
public class A {
#Id
private Long id;
private String name;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(
name = "A_B",
joinColumns = #JoinColumn(name = "id_a"),
inverseJoinColumns = #JoinColumn(name = "id_b"))
private Set<B> listing;
}
#Entity
public class B {
#Id
private Long id;
}
I need to write a query that fetches B and applies some WHERE criteria on A side.
Since the relationsip is modeled from A entity's side it's very easy to write a query that joins these itsself:
new JPAQuery<>(entityManager)
.select(QB.b)
.from(QA.a)
.join(QA.a.listing,b)
.where(QA.a.name.eq("test"))
.fetch();
However since A_B table can be duplicated, this query can produce duplicate entries, which does not do for my scenario. So instead I need to start FROM B and JOIN A. And this is where I need help. I tried:
new JPAQuery<>(entityManager)
.select(QB.b)
.from(QB.b)
.join(QA.a).on(QA.a.listing.any().eq(QB.b))
.where(QA.a.name.eq("test"))
.fetch();
But that does not work as any() merely produces a subselect, instead of many to many join.
How do I write this query in Querydsl?

Java: Optimally handling many-to-many relation with huge data sets

here is my issue.
i have a huge amount of data in many-to-many tables that i have to update quite often and also request them with dynamic filters, order and request by location (postgis-db)...
shop entity:
public class Shop extends BaseEntity {
....
#ManyToOne
#JsonIdentityReference(alwaysAsId = true)
#JsonSerialize(using = IdSerializer.class)
#JoinColumn(name = "parent_id")
private Shop parent;
#ManyToMany(mappedBy = "shops")
#JsonIdentityReference(alwaysAsId = true)
#JsonIgnore
private List<Product> products = new ArrayList<>();
...
}
product entity:
public class Product extends BaseEntity {
...
#ManyToMany
#JsonIdentityReference(alwaysAsId = true)
#JoinTable(name = "product_shop",
joinColumns = #JoinColumn(name = "product_id"),
inverseJoinColumns = #JoinColumn(name = "shop_id"))
private List<Shop> shops = new ArrayList<>();
the product table consists of about 1 million rows and the shops of about 8000. hence, the many-to-many table is 8*10^9 rows long in the worst case which results in really long request times.
any idea to solve this performantly with keeping in mind, that i want to sort the shops according to a location field which.
current tech stack:
java 8
spring boot
hibernate / jpa
postgresql / postgis

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.

How can I make Bridge table from 2 Model classes using hibernate annotation configuration

How to make Bridge table using annotation using Hibernate/JPA configuration.
1: BookModel
2: UserModel
now I have to create a bridge table by these two by fields
book_id and user_id
You are trying to implement Many to Many relationship between your entities. So for this, If you have list of Books in User model, then you can annotate the list as following:
public class UserModel {
#ManyToMany(cascade = CascadeType.REMOVE)
#JoinTable(name = "book_user_table", joinColumns = { #JoinColumn(name = "book_id") }, inverseJoinColumns = { #JoinColumn(name = "user_id") })
private List<BookModel> books;
//Getters and setters
}
and in BookModel, if you have list of users, then you need to use #mappedBy() annotation like following:
#ManyToMany(mappedBy = "books")
private List<UserModel> users;
This will generate the 3rd table, which will have the name book_user_table, with your required columns. See this for detailed explanation: https://dzone.com/tutorials/java/hibernate/hibernate-example/hibernate-mapping-many-to-many-using-annotations-1.html
From what I understand from your quesrtion is, you want to know how to map book and user so that there is many to many association between these entities.
If so, you need to specify #ManyToMany on both the associations and make one of them as inverse and the other end with #JoinTable. An example mapping here. Snippet below.
#ManyToMany(cascade = { CascadeType.ALL })
#JoinTable(
name = "Employee_Project",
joinColumns = { #JoinColumn(name = "employee_id") },
inverseJoinColumns = { #JoinColumn(name = "project_id") }
)
Set<Project> projects = new HashSet<>();
On the inverse end,
#ManyToMany(mappedBy = "projects")
private Set<Employee> employees;

Hibernate ManyToMany wtih column child removal

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.

Categories