JPA criteria multiple joins for same target class - jpa

I'm trying to migrate an SQL query to JPA criteria.
The query is used to display the results of a search form and I'm currently blocked on the migration of the JOIN statement.
Here are my simplified Parent and Child entities.
public class Parent {
#OneToOne(cascade = ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "published_id")
private Child published;
#OneToOne(cascade = ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "draft_id")
private Child draft;
#OneToOne(cascade = ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "cancelled_id")
private Child cancelled;
#OneToMany(cascade = ALL)
#JoinColumn(name = "parent_id")
#Builder.Default
private Set<Child> historic = new HashSet<>();
}
public class Child {
#ManyToOne(cascade = {PERSIST, MERGE}, fetch = LAZY)
#JoinColumn(name = "parent_id")
private Parent parent;
#OneToOne(cascade = ALL)
#JoinColumn(name = "organisation_id")
#ToString.Include
private Organisation organisation;
....
}
Here's the part of the query I'm blocking on :
SELECT DISTINCT ON (parent.id) {child.*},{parent.*}
FROM parent
JOIN child
ON parent.draft_id = child.id
OR parent.cancelled_id = child.id
OR parent.published_id = child.id
OR child.historic_parent_id = parent.id
JOIN organisation o ON o.id = child.organisation_id
.....
WHERE child.name = 'Didier'
I've tried to extract the different statements related to each JOIN but I need a way to merge them together so It can be used in the where clause.
Join<Parent, Child> draft = parentRoot.join(Parent_.draft, JoinType.LEFT);
Join<Parent, Child> cancelled = parentRoot.join(Parent_.cancelled, JoinType.LEFT);
Join<Parent, Child> published = parentRoot.join(Parent_.published, JoinType.LEFT);
SetJoin<Parent, Child> historic = parentRoot.join(Parent_.historic, JoinType.LEFT);
Is there a way of migrating the JOIN statement to criteria?
Thank you.

Related

JPA Specification: Select all entities which have at least one param with attribute from list

I have 2 entities with relationship ManyToMany
#Entity
#Table
public class TranslationUnit implements Serializable {
#Id
private Long id;
#ManyToMany(mappedBy = "translationUnit", fetch = FetchType.EAGER)
private Set<Category> categories = new HashSet<>();
}
#Entity
#Table
public class Category implements Serializable {
#ManyToMany
#JoinTable(name = "category_translation_unit",
joinColumns = #JoinColumn(name = "categories_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "translation_units_id", referencedColumnName = "id"))
private Set<TranslationUnit> translationUnits = new HashSet<>();
}
In Category I have 1 field, which should be used for filtering:
String name;
I need to be able to specify list of Category names (List), and select those TranslationUnits which have at least one Category with specified name.
I have several other filtering options, which should be used together, and I successfully built Specifications for them. But I've stuck with this one.
Please help.
P.S. One of my existing Specifications looks like this:
Specification idSpec = (Specification) (r, q, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (!filterRequest.getTranslationUnitIds().isEmpty())
predicates.add(r.get(TranslationUnit_.id).in(filterRequest.getTranslationUnitIds()));
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
Good day. You could use IN for filtering translation units by category names list. I believe, it will look like this using Criteria API:
Root<TranslationUnit> itemsRoot = ...;
Join join = itemsRoot.join("categories");
List<Predicate> predicates = new ArrayList<>();
predicates(join.get("name").in(categoryNamesList));

JPQL to filter by OneToMany relationship that uses a join table

I'm trying to write a JPQL to fetch Users with some Roles by filtering by Role.Name.
public class User
#ManyToMany
#JoinTable(
name = "UsersRoles",
joinColumns = {#JoinColumn(name = "UsersId", referencedColumnName = "UsersId")},
inverseJoinColumns = {#JoinColumn(name = "RolesName", referencedColumnName = "Name")})
#BatchSize(size = 20)
private Set<Role> roles = new HashSet<>();
}
public class Role {
#Id
#Column(name = "Name", length = 50)
private String name;
}
I tried the following on #Query in the jpa repository
SELECT U FROM User U INNER JOIN Role R WHERE R.name = ?1
But that is not working and it generates sql like the following.
select
...
from Users user0_
inner join Roles role1_ on
where role1_.Name = ?
The joining condition is not added.

JPA2 Criteria : Many to Many Join Table, how to do the restriction on the Join table

I've a very basic Many to Many relationship between 2 entities.
Let say Car :
public class Car {
#Id
#Column(name = "ID")
private Long id;
#ManyToMany
#JoinTable(name = "CAR_GARAGE",
joinColumns = { #JoinColumn(name = "CAR_ID", nullable = false) },
inverseJoinColumns = { #JoinColumn(name = "GARAGE_ID", nullable = false) })
private List<Garage> listGarages;
}
And Garage :
public class Garage {
#Id
#Column(name = "ID")
private Long id;
#ManyToMany
#JoinTable(name = "CAR_GARAGE",
joinColumns = { #JoinColumn(name = "GARAGE_ID", nullable = false) },
inverseJoinColumns = { #JoinColumn(name = "CAR_ID", nullable = false) })
private List<Car> listCars;
}
I need to do a query to retrieve all the CAR from one garage:
public Long getCarFromGarage(final String pGarageId) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Car> crit = builder.createQuery(Car.class);
// jointure
Root<Car> root = crit.from(Car.class);
Join<TarifEntiteFac, Garage> garageJoin = root.join("listGarages");
crit.where(builder.equal(garageJoin.get("id"), pIdentifiant));
return em.createQuery(crit).getResultList();
}
This works fine but the SQL generated is something like this :
SELECT c.id
FROM CAR c
INNER JOIN CAR_GARAGE cg ON c.id = cg.CAR_ID
INNER JOIN GARAGE g on cg.GARAGE_ID = g.ID
WHERE g.ID = :pGarageId
Is there a way in JPA to generate this instead :
SELECT c.id
FROM CAR c
INNER JOIN CAR_GARAGE cg ON c.id = cg.CAR_ID
INNER JOIN GARAGE g on cg.GARAGE_ID = g.ID
WHERE cg.GARAGE_ID = :pGarageId
To save myself an extra join.
I do not see a way how to force only single join with many-to-many relationship, unless there is some query hint or provider specific option that turns this optimization on.
However, you may generate single join also by mapping the intermediary table as an entity and adding additional onetomany mapping to this entity.

JPA Criteria Subquery on JoinTable

How do I create an efficient JPA Criteria query to select a list of entities only if they exist in a join table? For example take the following three tables:
create table user (user_id int, lastname varchar(64));
create table workgroup (workgroup_id int, name varchar(64));
create table user_workgroup (user_id int, workgroup_id int); -- Join Table
The query in question (what I want JPA to produce) is:
select * from user where user_id in (select user_id from user_workgroup where workgroup_id = ?);
The following Criteria query will produce a similar result, but with two joins:
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
cq.select(root);
Subquery<Long> subquery = cq.subquery(Long.class);
Root<User> subroot = subquery.from(User.class);
subquery.select(subroot.<Long>get("userId"));
Join<User, Workgroup> workgroupList = subroot.join("workgroupList");
subquery.where(cb.equal(workgroupList.get("workgroupId"), ?));
cq.where(cb.in(root.get("userId")).value(subquery));
getEntityManager().createQuery(cq).getResultList();
The fundamental problem seems to be that I'm using the #JoinTable annotation for the USER_WORKGROUP join table instead of a separate #Entity for the join table so it doesn't seem I can use USER_WORKGROUP as a Root in a criteria query.
Here are the entity classes:
#Entity
public class User {
#Id
#Column(name = "USER_ID")
private Long userId;
#Column(name = "LASTNAME")
private String lastname;
#ManyToMany(mappedBy = "userList")
private List<Workgroup> workgroupList;
}
#Entity
public class Workgroup {
#Id
#Column(name = "WORKGROUP_ID")
private Long workgroupId;
#Column(name = "NAME")
private String name;
#JoinTable(name = "USER_WORKGROUP", joinColumns = {
#JoinColumn(name = "WORKGROUP_ID", referencedColumnName = "WORKGROUP_ID", nullable = false)}, inverseJoinColumns = {
#JoinColumn(name = "USER_ID", referencedColumnName = "USER_ID", nullable = false)})
#ManyToMany
private List<User> userList;
}
As far as I know, JPA essentially ignores the join table. The JPQL that you do would be
select distinct u from user u join u.workgroupList wg where wg.name = :wgName
for the Criteria query, you should be able to do:
Criteria c = session.createCriteria(User.class, "u");
c.createAlias("u.workgroupList", "wg");
c.add(Restrictions.eq("wg.name", groupName));
c.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
there's no need to worry about the middle join table.

JPA join between a class that has two fields of the same type

I have the following class:
#Entity
#Table(name = "entity")
public class Entity extends Model
{
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "entity_owner", joinColumns = #JoinColumn(name = "entity_id"), inverseJoinColumns = #JoinColumn(name = "user_id"))
private Set<UserAccount> owners;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "entity_assignee", joinColumns = #JoinColumn(name = "entity_id"), inverseJoinColumns = #JoinColumn(name = "user_id"))
private Set<UserAccount> assignees;
}
I have a user account that I want to be able to being back all the Entity objects that have the user in either the owners or the assignees.
I tried this, which almost works, but seems to being back some sort of cartesian result:
String query = "SELECT r FROM models.Entity r LEFT JOIN r.assignees a LEFT JOIN r.owners n ";
query += "WHERE a.id = 1 OR n.id = 1";
Can anyone tell me what I am doing wrong?
If the UserAccount is in both owners and assignees, seems like the query will duplicate a result, you can use DISTINCT to filter duplicated results:
SELECT DISTINCT r FROM ...
When there is instance of UserAccount already available for filtering results,
MEMBER OF expression can be used:
SELECT DISTINCT(e)
FROM SomeEntity e
WHERE :someUser MEMBER OF e.assignees OR
:someUser MEMBER OF e.owners
UserAccount ua ...
em.createQuery(jpql).setParameter("someUser", ua)
If because of some reason query similar to original query is preferred, then adding DISTINCT is enough.