JPA Criteria Subquery on JoinTable - jpa

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.

Related

Handling reduntant columns with hibernate/jpa/spring data

i'm kinda struggling mapping the following schema with hibernate
table_a (A1_ID,A2_ID) --> PK = (A1_ID, A2_ID)
table_b (A1_ID, A2_ID, B1_ID) --> PK =(A1_ID, A2_ID, B1_ID)
where table_b's A1_ID and A2_ID should be foreingkey referencing respective table_A's columns
There is a one-to-many from TABLE_A to TABLE_B where TABLE_B's primary key is partially shared with TABLE_A's primary key
What I've tried so far
#Data
#Entity
#Table(name = "table_a")
#IdClass(TableA.TableAKey.class)
public class TableA {
#Id
#Column(name = "A1_ID)
private String a1_id;
#Id
#Column(name = "A2_ID)
private String a2_id;
#OneToMany(mappedBy = "tableA",fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<TableB> tableB;
#Data
static class TableAKey implements Serializable {
private String a1_id;
private String a2_id
}
}
**CHILD ENTITY**
#Data
#Entity
#Table(name = "table_b")
#IdClass(TableB.TableBKey.class)
public class TableB {
#Id
#Column(name = "B1_ID)
private String b1_id;
#Id
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumns({
#JoinColumn(name = "a1_id", insertable = false, updatable = false),
#JoinColumn(name = "a2_id", insertable = false, updatable = false)
)}
private TableA tableA;
#Column(name = "A1_ID)
private String a1_id;
#Column(name = "A2_ID)
private String a2_id;
#Data
static class TableAKey implements Serializable {
private String b1_id;
private TableA tableA;
}
}
I was expecting i could be able to do something like this:
TableA tableA = new TableA();
t.setA1_id("a1id");
t.setA2_id("a2id");
TableB tableB = new TableB();
tableB.setB1Id("b1Id");
tableA.setTableB(Arrays.asList(tableB));
tableARepository.save(tableA);
And the code above I was expecting to "magically" perform the following insert at DB
INSERT INTO table_A (A1_ID,A2_ID) VALUES ('a1id',a2id');
INSERT INTO table_B (A1_ID,A2_ID, B1_ID) VALUES ('a1id',a2id','b1id')
but instead i get a "the column index is out of range: n, number of columns n-1".
I also tried with some embeddedId approach, using referenceColumnName but nothing.
Am I doing something wrong in the mapping or in the object creation process?
The problem is a lot similar to the following
https://hibernate.atlassian.net/browse/HHH-14340

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.

JPQL: How to join via #ElementCollection with #MapKeyJoinColumn

I have problems creating the correct JPQL query for joining through the following tables:
While between GROUPS and USERS there is a conventional #ManyToMany mapping table, DOCUMENTS_GROUPS is what causes the trouble. As you can see in the following entity, I want the relationship between DOCUMENTS and GROUPS to be mapped as a Map containing the access_mode (which works just fine except for the query):
#Entity
#Table(name = "DOCUMENTS")
#NamedQueries({
#NamedQuery(
name = "Documents.findAccessibleByUser",
query = "SELECT d FROM Document d INNER JOIN d.groups g INNER JOIN KEY(g).members m WHERE m.id = :userId"
)
})
public class Document {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#ElementCollection
#CollectionTable(name = "DOCUMENTS_GROUPS", joinColumns = {#JoinColumn(name = "document_id")})
#MapKeyJoinColumn(name = "group_id")
#Column(name = "access_mode")
#Enumerated(EnumType.STRING)
private Map<Group, AccessMode> groups = new HashMap<>();
/* ... */
}
With Group being rather normal:
#Entity
#Table(name = "GROUPS")
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column(length = 255)
private String name;
#ManyToMany
#JoinTable(name = "USERS_GROUPS", //
joinColumns = {#JoinColumn(name = "group_id")}, //
inverseJoinColumns = {#JoinColumn(name = "user_id")} //
)
private Set<User> members = new HashSet<>();
/* ... */
}
My question is now: How do I need to modify the second JOIN in my JPQL query?
SELECT d FROM Document d
INNER JOIN d.groups g
INNER JOIN KEY(g).members m
WHERE m.id = :userId
is syntactically wrong (unexpected KEY after INNER JOIN).
Of course, I have already tried a plain INNER JOIN g.members m, but since we're dealing with a Map<Group, AccessMode>, this fails with cannot dereference scalar collection element: members.
I was facing the same problem with a simple key-value Map<String, String> like:
#Entity Item.java
#ElementCollection
#MapKeyColumn(name = "name")
#Column(name = "value")
#CollectionTable(indexes = #Index(columnList = "value"))
private Map<String, String> attributes = new HashMap<>();
Joining the attributes was possible:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr");
but not querying fields:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr WHERE attr.value = 'something'");
I debugged the Hibernate internals and found out that the alias attr is already resolved to the value (e.attributes.value), so the only thing you can do here is:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr WHERE attr = 'something'");
But I did not find any documentation or JPQL examples pointing that out. The behaviour is is useless in my case, because I want to have conditions for both key and value. Thats why I migrated to a foreign entity collection with key mapping and composite primary key. Its way more complicated but works as expected.
The composite key entity to prevent single primary keys
#Embeddable
public class ItemAttributeName implements Serializable {
private String name;
#ManyToOne
#JoinColumn(nullable = false)
private Item item;
// Empty default constructor is important
public ItemAttributeName() {
}
public ItemAttributeName(Item item, String name) {
this.item = article;
this.name = name;
}
}
The real attribute entity
#Entity
public class ItemAttribute {
#EmbeddedId
private ItemAttributeName id;
private String value;
// Empty default constructor is important
public ItemAttribute() {
}
public ItemAttribute(Item item, String name) {
this.id = new ItemAttributeName (item, name);
}
public String getValue() {
return value;
}
}
#Entity Item.java
#OneToMany(mappedBy = "id.item",cascade = CascadeType.PERSIST)
#MapKeyColumn(name = "name")
public Map<String, ItemAttribute> attributes = new HashMap<>();
Creating entities:
Item item = new Item ();
ItemAttribute fooAttribute = new ItemAttribute(item, "foo");
fooAttribute.setValue("356");
item.attributes.put("foo", fooAttribute);
Querying entities:
Query query = em.createQuery("SELECT i FROM Item i JOIN i.attributes attr WHERE attr.id.name = 'foo' AND attr.value='bar'");
List<Item> resultList = query.getResultList();
System.out.println(resultList.get(0).attributes.get("foo").getValue());
Prints out: bar

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 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.