QueryDsl invert manyToMany join - java

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?

Related

How to use Hibernate to combine efficient queries on #ManyToMany association?

I have 2 entities with a ManyToMany association between them - FeedbackApp & FeedbackAppProfile and each of them has a tenant-id FK to Tenant entity.
FeedbackApp entity:
public class FeedbackApp {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "tenant_id")
private Tenant tenant;
/*
Marked as the owner side.
*/
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "feedbackApp_profile_mapping",
joinColumns = #JoinColumn(name = "feedbackApp_id"),
inverseJoinColumns = #JoinColumn(name = "profile_id"))
Set<FeedbackProfile> profiles;
}
The FeedbackProfile entity:
public class FeedbackProfile {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "tenant_id")
private Tenant tenant;
#ManyToMany(mappedBy = "profiles", fetch = FetchType.EAGER)
Set<FeedbackApp> feedbackApps;
}
The feedbackApp_profile_mapping join table has 2 columns and looks like this:
My question: I need to create a query that gets all feedback apps for a specific feedback profile and tenant id. Is it possible to get it with Hibernate/JPA OR I have to manually query my join table?
Let Jpa worry about the optimal Sql query to generate. Your Jpa/criteria/specification query should be something like
select fp.feedbackApps from FeedbackProfile fp LEFT JOIN FETCH fp.feedbackApps where fp.id=:feedback_profile_id and fp.tenant.id=:tenant_id
Since you are asking about efficiency, better remove fetch = FetchType.EAGER from the two many-to-many mappings and use join fetch or named entity graphs to do the joins only when you need to.
Thanks to #GabiM direction, I created a join fetch query which did the job for what I needed:
#Query(value = "SELECT f FROM FeedbackApp f JOIN FETCH f.profiles p WHERE p.id = ?1 AND p.tenant.id = ?2")
Set<FeedbackApp> getFeedbackAppsByProfileId(long profileId, long tenantId);

Hibernate: Limit result size of child objects

I've got a parent entity which has many child objects, most of which are collections. See below.
#Entity
#Table(name = "A")
public class A {
#Id
#Column(name = "ID")
private long id;
#OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
#JoinColumn(name = "A_ID", nullable = false)
private Set<B> setB;
#OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
#JoinColumn(name = "A_ID", nullable = false)
private Set<C> setC;
#OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
#JoinColumn(name = "A_ID", nullable = false)
private Set<D> setD;
// Skipping more collections as they are not needed for the example
// Standard getters and setters
}
Now classes B, C and D have A_ID and 5 more String columns. Please consider them B1,B2,B3,B4,B5 and so on.
I also have a CrudRepository for A
#Repository
public interface ARepository extends CrudRepository<A, Long> {
Optional<A> findById(Long id);
}
I want to fetch the complete A object with an id but the other child collections that A have (setB, setC, setD) contains approx thousands of rows for each A_ID. I want to put a filter to fetch only first 100 rows for a given A_ID.
I have tried putting #Where(clause = "ROWNUM < 101") on the collections but it does not work as in the query the table name gets prefixed to the ROWNUM.
I also took a look at Criteria and Criterion but I am unable to find any working solution.
Since there are many collection of Objects in the parent class. So using native queries for each object would be too much rework.
Can anyone please help me with this. Do comment if you need more information.
Thanks
That's not so easy. You would have to fetch the collections manually or use a library like Blaze-Persistence Entity-Views on top that supports this out of the box. Also see here for a similar question: Hibernate - Limit size of nested collection
In your particular case, this could look like the following:
#EntityView(A.class)
public interface ADto {
#IdMapping
long getId();
#Limit(limit = "100", order = "id asc")
Set<BDto> getSetB();
#Limit(limit = "100", order = "id asc")
Set<C> getSetC();
#Limit(limit = "100", order = "id asc")
Set<D> getSetD();
}

Spring + Hibernate: How do I efficiently chain two link tables and include resulting data in single entity? (user-role-right)

Short version
I have a basic setup where a user table is linked to a role table and a role table is linked to a right. These are both Many-to-Many relations. The roles are a dynamic entity and not of interest for the application (only for visual aspects). When I fetch a user I want to return the data in the user table including a list of the names of all rights.
To clarify, this is what I want the solution to do:
I managed to get the rights in my user object and return them, but it's inefficient due to the extra query calls hibernate makes after the original query was called.
Detailed version
Let me first give you some information on how to entities are linked and what the code looks like. My (simplified) database table structure looks like this:
User.java
#Entity
#Table(name = "user")
public class User {
#Id
#Column(name = "user_id", columnDefinition = "user_id")
private Long userId;
#Transient
private List<String> rights
#ManyToMany
#JoinTable(
name = "user_role",
joinColumns = #JoinColumn(name = "user_id", referencedColumnName = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id", referencedColumnName = "role_id"))
#JsonIgnore
private List<Role> roles;
//Getters and setters
}
Role.java
#Entity
#Table(name = "role")
public class Role {
#Id
#Column(name = "role_id", columnDefinition = "role_id")
private Long roleId;
#ManyToMany
#JoinTable(
name = "user_role",
joinColumns = #JoinColumn(name = "role_id", referencedColumnName = "role_id"),
inverseJoinColumns = #JoinColumn(name = "user_id", referencedColumnName = "user_id"))
#JsonIgnore
private List<Employee> employees;
#ManyToMany
#JoinTable(
name = "role_right",
joinColumns = #JoinColumn(name = "role_id", referencedColumnName = "role_id"),
inverseJoinColumns = #JoinColumn(name = "right_id", referencedColumnName = "right_id"))
#JsonIgnore
private List<Right> rights;
//Getters and setters
}
Right.java
#Entity
#Table(name = "right")
public class Right {
#Id
#Column(name = "right_id", columnDefinition = "right_id")
private Long rightId;
#Column(name = "name", columnDefinition = "name")
private String name;
#ManyToMany
#JoinTable(
name = "role_right",
joinColumns = #JoinColumn(name = "right_id", referencedColumnName = "right_id"),
inverseJoinColumns = #JoinColumn(name = "role_id", referencedColumnName = "role_id"))
#JsonIgnore
private List<Role> roles;
//Getters and setters
}
It's important to know that I use the Java Specifications API to join the tables:
return (root, query, cb) -> {
query.distinct(true);
Join rolesJoin = root.join("roles", JoinType.LEFT);
Join rightsJoin = rolesJoin.join("rights", JoinType.LEFT);
return cb.conjunction();
};
This creates the correct query:
select <columns go here>
from employee user0_
left outer join user_role roles1_ on user0_.user_id=roles1_.user_id
left outer join role role2_ on roles1_.role_id=role2_.role_id
left outer join role_right rights3_ on role2_.role_id=rights3_.role_id
left outer join right right4_ on rights3_.right_id=right4_.right_id
Everything looked to good to me till now. But when I tried to fetch the names of all roles, there where more than two queries (count for page and the original one) being executed
//The original code uses lambda for this
for(Role role : user.getRoles()){
for(Right right: role.getRights()){
user.addRight(right.getName());
}
}
The extra query looks like:
select <column stuff>
from role_right rights0_
inner join right right1_ on rights0_.right_id=right1_.right_id
where rights0_.role_id=?
This makes the call very inefficient to me. In this case it's a single user, but with multiple users it adds up.
Is there a way to have a single query put the names of all rights in the user entity, without adding extra query executions?
Things I tried so far:
Using #SecondaryTable to directly define column from the Right table in my User entity. I could not get to first link the Role to the User and then use fields from the Role table to link the Right table. So in the end I would have to #SecondaryTable annotation on top of my User object and define columns of the Right object below.
Using #Formula in the User entity to insert a native call into the query. This did also not work as the annotation did not understand how to map everything into a list of rights.
There might be other options here, or I did something horribly wrong with implementing the ones above. But for now I don't which way to go in finding a solution for my problem. If someone could tell me, that would be great.
Thanks in advance,
Robin
You are using Root.join which does just the joining of tables for the purposes of the query; lazy associations in the loaded entities will still not be initialized.
As I see, your intention is to initialize the lazy collections as well. For that you have to use Root.fetch (defined in the interface method inherited from the FetchParent):
Create a fetch join to the specified collection-valued attribute using
the given join type.
However, your intention is not a good practice; do not join multiple collections in one query, otherwise the query result set will explode with full Cartesian product between the joined collections. Your result set contains <num of users> * <num of roles per user> * <num of rights per role> rows. So, each user data is repeated <num of roles per user> * <num of rights per role> times in the generated result set.
The approach I find to be the best and most straightforward is to specify batch size on lazy associations.

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.

Hibernate criteria many to many

I have two entities with many to many relationship:
#Entity
#Table(name = "items")
public class Item implements Comparable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "item_id")
private Integer itemId;
#ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
#JoinTable(name = "items_criteria",
joinColumns = #JoinColumn(name = "item_id"),
inverseJoinColumns = #JoinColumn(name = "filter_criterion_id"))
private List<FilterCriterion> filterCriteria;
}
and
#Entity
#Table(name = "filter_criteria")
public class FilterCriterion {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "filter_criterion_id")
private Integer filterCriterionId;
#ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
#JoinTable(name = "items_criteria",
joinColumns = #JoinColumn(name = "filter_criterion_id"),
inverseJoinColumns = #JoinColumn(name = "item_id"))
private List<Item> items;
}
I need to write function in ItemDao class that returns List of Items which have all elements of the collection given as an argument. In the example below I use Restrictions.in so the result contains even those Items which contain for example only one FilterCriterion from List given as argument. I need to have in the result only those Items, which contain all of the elements in argument List.
public List<Item> getItems(List<FilterCriterion> currentFilterCriteria) {
Criteria criteria = ht.getSessionFactory().getCurrentSession().createCriteria(Item.class);
List<Integer>currentFilterCriteriaId = new ArrayList<Integer>();
for(FilterCriterion criterion : currentFilterCriteria){
currentFilterCriteriaId.add(criterion.getFilterCriterionId());
}
if(!currentFilterCriteriaId.isEmpty()){
criteria.createAlias("filterCriteria", "f");
criteria.add(Restrictions.in("f.filterCriterionId", currentFilterCriteriaId));
}
return criteria.list();
}
First of all, you'll have to fix your mapping. You don't have a bidirectional ManyToMany association here, but two, unrelated, unidirectional ManyToMany associations. One side must be the inverse side by using the mappedBy attribute:
#ManyToMany(mappedBy = "filterCriteria")
private List<Item> items;
Now to your question, one way of doing this is to use such a query. I'll let you translate it to Criteria if you really want to. I'd use HQL instead, since it's so much easier and maintainable:
select i from Item i
where :criteriaIdSetSize = (select count(c.id) from Item i2
inner join i2.filterCriteria c
where c.id in :criteriaIdSet
and i2 = i)
You should use a Set to hold your criteria IDs rather than a list though, to make sure it doesn't contain duplicates (which would make the result incorrect).

Categories