Self-referencing ManyToMany relations in Hibernate - java

I am looking for a way to model a relation between two or more objects of the same class in Hibernate. For example: I want to create a situation where I want to model relations between persons. In the database, I have two tables:
Person:
Id
Name
Relation:
Parent Id
Child Id
I tried to model this in Hibernate as a Person have two ManyToMany relations (Getter/Setter annotations are Lombok):
#Getter
#Setter
#Builder
#AllArgsConstructor
#NoArgsConstructor
#Entity
#Table(name = "persons")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id
#Column(name="name")
private String name;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "relations",
joinColumns = {#JoinColumn(name = "parent_id", nullable = false)},
inverseJoinColumns = {#JoinColumn(name = "child_id", nullable = false)}
)
private Set<Person> children = new HashSet<>();
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "relations",
joinColumns = {#JoinColumn(name = "child_id", nullable = false)},
inverseJoinColumns = {#JoinColumn(name = "parent_id", nullable = false)}
)
private Set<Person> parents = new HashSet<>();
}
This gave me the following problems:
With fetch type "LAZY" Hibernate complains about not having a Session when calling person.getChildren() or person.getParents()
With fetch type "EAGER" Hibernate returns null for the sets, which causes nullpointers when trying to add children or parents. The other thing I am worried about is that possible endless recursive eager fetching.
To get around this, I've tried to model the Relation as a class, so that I can use JPA queries in the PersonRepository to find Children and Parents without having to mess with the intricacies of ManyToMany :
public interface PersonRepository extends JpaRepository<Person, Long> {
#Query(
"select p from Person p join Relation r on p.id = r.childId where r.childId = :childId"
)
List<Person> findParents(Long childId);
}
This caused Hibernate to complain that Relation does not have an #Id field. I do not want to add that because the relation table in the database should model the relation, and not have an id of its own.
When trying to search online for similar structures in Hibernate I usually find classic many-to-many examples in the documentation and questions like this one where there is a relation between two different classes instead of a relation between two objects of the same class.
I'm looking for a way to model a relation between two or more objects of the same class in Hibernate. I want to be able to fetch relations lazily, or fetch them afterwards for performance reasons.
It would be nice if the relation table could have an extra field "type" which indicates the type of relation between Persons (child, parent, nephew, friend) so that there is room for new relation types without too much changes to database and code.

Not sure I understand you correctly, but I had a similar case once.
What I did was to persist the child/parent without its relations and updated them with their relations afterwards (still in the same transaction).
private void insertEntity(final AbstractEntity entity) {
insertedEntities.add(entity.getId());
final List<AbstractEntity> relations = entity.onBeforeInsertion();
for (final AbstractEntity relation : relations) {
if (!insertedEntities.contains(relation.getId())) {
insertEntity(relation);
}
}
final RelationBundle relationBundle = new RelationBundle();
entity.onInsertion(relationBundle);
immediatelySaveNewEntityThroughProxy(entity);
for (final AbstractEntity relation : relations) {
entity.onRelationInsertion(relation, relationBundle);
}
dao.saveOrUpdate(entity);
}
private void immediatelySaveNewEntityThroughProxy(final DocumentEntity entity) {
proxiedAccess().immediatelySaveNewEntity(entity);
}
private MyConsumer proxiedAccess() {
return applicationContext.getBean(getClass());
}
#Transactional(propagation = Propagation.REQUIRES_NEW)
public void immediatelySaveNewEntity(final DocumentEntity entity) {
try {
if (!dao.entityExistsFromId((int) entity.getId())) {
dao.save(entity);
}
} catch (final Exception e) {
LOGGER.error("Error saving entity: {}", entity.getId(), e);
}
}

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?

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.

Load lazy collection in ManyToMany relationship

I would like to query all products for a company. The products should be loaded with the list of countries. I managed to write a Spring JPA repository method to query what I want but I wonder why I need a DISTINCT clause.
If I run the following query, I get one product per country. So if a product has 3 countries, the query will return the same 3 rows. Can you explain why?
#EntityGraph(attributePaths = "countries", type = EntityGraph.EntityGraphType.LOAD)
List<Product> findByCompanyIdOrderByIdAsc(Long companyId);
So to fix that issue I added a Distinct clause which return what I want.
#EntityGraph(attributePaths = "countries", type = EntityGraph.EntityGraphType.LOAD)
List<Product> findDistinctByCompanyIdOrderByIdAsc(Long companyId);
I have the same issue if I run a JPQL Select p from Product LEFT JOIN FETCH p.countries WHERE p.company.id = ?1 which is equivalent to findByCompanyIdOrderByIdAsc.
The entities:
public class Product implements Serializable {
#ManyToOne
#JsonIgnore
private Company company;
#ManyToMany
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#JoinTable(name = "product_country",
joinColumns = #JoinColumn(name="product_id", referencedColumnName="id"),
inverseJoinColumns = #JoinColumn(name="country_id", referencedColumnName="id"))
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id",
resolver = EntityIdResolver.class, scope = Country.class)
#JsonIdentityReference(alwaysAsId = true)
private Set<Country> countries = new HashSet<>();
}
public class Country implements Serializable {
}

Hibernate Unidirectional #ManyToMany

I have a many-to-many relationship between accounts and groups. The data for the groups table comes pre-populated and there is no adding additional groups.
Using Hibernate, my desire is to:
Add an account to the account table,
Add a record to the account_group table, and
NOT add a record to the group table
It seems to me, no matter what I do, I always get a duplicate record in the group table. This is my problem. Here is my code:
Account.java
#Entity
#Table(name = "account", schema = "admin")
public class Account {
#Id
#GeneratedValue
private UUID id;
...
#ManyToMany(fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST, CascadeType.MERGE })
#JoinTable(name = "account_grouping", schema = "admin", joinColumns = #JoinColumn(name = "account_id"), inverseJoinColumns = #JoinColumn(name = "grouping_id"))
private Set<Grouping> groupings;
...
// getters and setters
}
Grouping.java
#Entity
#Table(name = "grouping", schema = "admin")
public class Grouping {
...
#Id
#GeneratedValue
private UUID id;
...
// getters and setters
}
AccountDao.java
#Repository
public class AccountDao extends AbstractJpaDao<Account, String> {
...
#Transactional
public boolean create(Account newAccount) {
try {
getEntityManager().merge(newAccount);
return true;
} catch(Exception e) {
System.out.println("Danger, Will Robinson!: " + e);
return false;
}
}
}
I have looked up many questions and websites regarding Hibernate #ManyToMany examples, but I must be missing something. Thank you for reading and for your help.
Unfortunately I do not have enough rep to make a comment, so I'll throw my guess as an answer. Make sure the Group entity has an ID when selecting a Group for your Account before saving your entity. Otherwise the Group entity is treated as a new Object and will be saved into the grouping table.

Annotation to join columns with OR condition instead of AND condition

I have 2 java classes, Relation and Person, which both are present in my database.
Person:
#Entity
#Table(name = "persons")
public class Person {
#Id
#Column
private int id;
#Column
private String name;
#OneToMany(fetch = FetchType.EAGER)
#JoinColumns({
#JoinColumn(name = "slave_id", referencedColumnName="id"),
#JoinColumn(name = "master_id", referencedColumnName="id")
})
private List<Relation> relations;
//Getters and setters
}
Relation:
#Entity
#Table(name = "relations")
public class Relation {
#Id
#Column
private int id;
#Column
private int child_id;
#Column
private int parent_id;
#Column
private String type;
//Getters and setters
}
Each Person has a list of relations (or not), the relation should be added to the list when the child_id or the parent_id of the relation is equal to the id of the person.
TL;DR:
When relation.child_id OR relation.parent_id = person.id => add relation to list of relations to the person
The issue I am facing is that this annotation:
#JoinColumns({
#JoinColumn(name = "child_id", referencedColumnName="id"),
#JoinColumn(name = "parent_id", referencedColumnName="id")
})
creates following SQL (just the necessary part):
relations relations6_
on this_.id=relations6_.slave_id
and this_.id=relations6_.master_id
What is the correct annotation in Java Hibernate to generate an SQL statement saying OR instead of AND
Some of the options that you could utilize:
Database views. Create the view that does custom join for you and map the entity to the view.
Join formula. I managed to make them work only on many-to-one associations. Nevertheless, you could make the association bidirectional and apply the formula in the Relation entity.
#Subselect. This is a kind of Hibernate view, suitable if you can't afford to create a real database view or change the db schema to better suit the entity model structure.
This and this answer could also be helpful.
Also, you can always use two separate associations for slaves and masters:
public class Person {
#OneToMany
#JoinColumn(name = "slave_id"),
private List<Relation> slaves;
#OneToMany
#JoinColumn(name = "master_id"),
private List<Relation> masters;
public List<Relation> getRelations() {
List<Relation> result = new ArrayList<>(slaves);
result.addAll(masters);
return result;
}
}
However, keep in mind that joining all of them in a single query requires full Cartesian product between masters and slaves.
You can use #FilterDef and #Filter annotations.

Categories