How to filter one-to-many mapping based on entity fields in JPA? - jpa

I can represent this query:
SELECT * FROM group g JOIN user u ON user.group_id = group.id
via the following in JPA:
#EntityGraph(attributePaths = {"users.posts.comments"})
Optional<Group> findEagerlyFetchedById(UUID id);
But how do I filter out some users based on a field? I want to get the group with the given groupId, with user rows (and children of those) but only for users that are authenticated. As in, how do I represent the following SQL query in JPA?
SELECT * FROM group g JOIN user u ON user.group_id = group.id WHERE user.isAuthenticated = true
I currently have the query below but it takes an all-or-nothing approach. If a single user has matching isAuthenticated field then it returns the group along with all users regardless of whether that field is true for that user. Also, if no users are authenticated, then the group isn't returned at all.
#EntityGraph(attributePaths = {"users.posts.comments"})
#Query("SELECT g FROM Group g JOIN g.users gu WHERE gu.isAuthenticated = :isAuthenticated AND g.id = :groupId")
Optional<Group> findEagerlyFetchedByUserAuthed(UUID groupId, boolean isAuthenticated);
For reference these are the entity definitions:
Group:
#Entity
public class Group {
private UUID id;
#OneToMany(
mappedBy = "groups",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<User> users = Sets.newHashSet();
}
User:
#Entity
public class User {
private UUID id;
private Boolean isAuthenticated;
#ManyToOne( fetch = FetchType.LAZY )
private Group group;
}

From what i am understanding is that you want to select all groups that have authenticated users.
As I understand your problem with "then it returns the group along with all users regardless of whether" is that the database loads all the users, into the java/context even if this is not needed. The Problem for this is probably that even though the users are fetched lazy
#OneToMany(
mappedBy = "groups",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<User> users = Sets.newHashSet();
java has to evaluate weather or not or not the entry in the set is unique. Depending on your implementation of equals for Group or User (not shown in your example) it might be possible that the value of all fields is called, therefore requiring the Set to be fully loaded. A solution for this could be replacing the Set with a List.
private Set<User> users = new ArrayList<>();
Depending on your toString() implementation of the classes it could also just be a problem with debugging since most debuggers call the toString() implementation when trying to display an Object inside the debugger.
The second problem I understand you are approaching is "Also, if no users are authenticated, then the group isn't returned at all." I dont know how to help with that since your SQL clearly states
" ....g.users gu WHERE gu.isAuthenticated = :isAuthenticated ..."
this will always just return groups with authenticated users. Here i cant understand what your problem is. That is what i thought was your goal.
A practical approach that might help you could be selecting the Users and then accessing the groups (in Java via streams).
#Query("SELECT u FROM Users u WHERE u.isAuthenticated = :isAuthenticated)
List<Users> findEagerlyFetchedByUserAuthed(boolean isAuthenticated);
or trying to do a sub select of users first and then joining with something like this:
#Query(
"SELECT group
FROM from group
where groupid IN (SELECT u.groupId
FROM Users u
WHERE u.isAuthenticated = :isAuthenticated))
Optional<Group> findEagerlyFetchedByUserAuthed(UUID groupId, boolean isAuthenticated);
My syntax here is probably not 100% correct but i hope you got the idea.
Lastly it might be better to use
List<Group> findEagerlyFetc...
instead of
Optional<Group> findEagerlyFetc....

#EntityGraph with #Query not working properly.
Use JPA method naming query with #EntityGraph
#EntityGraph(attributePaths = {"users.posts.comments"})
Optional<Group> findByIdAndUsers_IsAuthenticated(UUID groupId, boolean isAuthenticated);
Note: To resolve ambiguity we can use _ inside your method name to manually define traversal points.

Related

EclipseLink ManyToOne - CriteriaBuilder Generated Query is Wrong

I have an Entity with a ManyToOne Relationship to the Primary Key of another entity. When I create a query that references this Foreign Key eclipseLink always creates a join instead of simply accessing the Foreign Key.
I have created a highly simplified example to show my issue:
#Entity
public class House {
#Id
#Column(name = "H_ID")
private long id;
#Column(name = "NAME")
private String name;
#ManyToOne
#JoinColumn(name = "G_ID")
private Garage garage;
}
#Entity
public class Garage{
#Id
#Column(name = "G_ID")
private long id;
#Column(name = "SPACE")
private Integer space;
}
I created a query that should return all houses that either have no garage or have a garage with G_ID = 0 using the CriteriaBuilder.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<House> query = cb.createQuery(House.class);
Root<House> houseRoot = query.from(House.class);
Path<Long> garageId = houseRoot.get(House_.garage).get(Garage_.id);
query.where(cb.or(cb.equal(garageId , 0), cb.isNull(garageId)));
TypedQuery<House> typedQuery = entityManager.createQuery(query);
List<House> houses = typedQuery.getResultList();
The generated query is:
SELECT h.NAME, h.G_ID FROM HOUSE h, GARAGE g WHERE (((h.G_ID= 0) OR (g.G_ID IS NULL)) AND (g.G_ID = h.G_ID));
I don't understand why
The or condition first references table HOUSE and then GARAGE (instead of HOUSE)
The join is created in the first place.
The correct query should look like this in my understanding:
SELECT h.NAME, h.G_ID FROM HOUSE h WHERE (((h.G_ID= 0) OR (h.G_ID IS NULL));
Or if a join is made it should take into account that the ManyToOne relationship is nullable and therefore do a LEFT OUTER JOIN.
SELECT h.NAME, h.G_ID FROM HOUSE h LEFT OUTER JOIN GARAGE g ON (h.G_ID = g.G_ID ) WHERE (h.G_ID = 0) OR (g.G_ID IS NULL);
(Note both these queries would work correctly in my more complicated setup. I also get the same error when only wanting to retrieve all houses that have no garage.)
How can I achieve this (while still using the CriteriaBuilder and ideally not having to change the DB Model)?
(Please let me know any additional information that might be required, I'm very new to this topic and came across this issue while migrating an existing application.)
-- edit --
I have found a solution to my problem that will result in slightly different behaviour (but in my application that part of the code I had to migrate didn't make much sense in the first place). Instead of using
Path<Long> garageId = houseRoot.get(House_.garage).get(Garage_.id);
I use
Path<Garage> garage = houseRoot.get(House_.garage);
And then as expected table Garage isn't joined anymore. (I assume the code previously must have been some kind of hack to get the desired behaviour from openJPA)
I don't understand why
The or condition first references table HOUSE and then GARAGE (instead of HOUSE)
I believe this is implementation specific; in any case, it shouldn't have any bearing on the results.
The join is created in the first place.
By saying Path<Long> garageId = houseRoot.get(House_.garage).get(Garage_.id) you're basically telling EclipseLink: 'join Garage to House, we're gonna need it'. That you then access Garage_.id (and not, for example, Garage_.space) is inconsequential.
If you don't want the join, simply map the G_ID column one more time as a simple property: #Column(name = "G_ID", insertable = false, updatable = false) private Long garageId. Then refer to House_.garageId in your query.
Or if a join is made it should take into account that the ManyToOne relationship is nullable and therefore do a LEFT OUTER JOIN.
Path.get(...) always defaults to an INNER JOIN. If you want a different join type, use Root.join(..., JoinType.LEFT), i. e. houseRoot.join(House_.garage, JoinType.LEFT).get(Garage_.id).
One solution that results in the same behaviour is:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<House> query = cb.createQuery(House.class);
Root<House> houseRoot = query.from(House.class);
Path<Garage> garage = houseRoot.get(House_.garage);
Path<Long> garageId = garage.get(Garage_.id);
query.where(cb.or(cb.equal(garageId , 0), cb.isNull(garage)));
TypedQuery<House> typedQuery = entityManager.createQuery(query);
List<House> houses = typedQuery.getResultList();
This results in the following SQL:
SELECT H_ID, NAME, G_ID FROM HOUSE WHERE ((G_ID = 0) OR (G_ID IS NULL));

Constraint the number of rows returned by JOIN FETCH in JPA

I'm having a Product object with a list of related products (which are also product objects). The field of related products is annotated like this:
public class Product {
#JoinTable(name = "RELATED_PRODUCT", joinColumns = {
#JoinColumn(name = "PRODUCT_ID", referencedColumnName = "id", nullable = false)}, inverseJoinColumns = {
#JoinColumn(name = "RELATED_PRODUCT_ID", referencedColumnName = "id", nullable = false)})
#ManyToMany(fetch = FetchType.LAZY)
List<Product> relatedProducts;
}
As you can see the list is fetched lazy, which is what I want in most cases. In some cases however, I want the list of related products to be filled immediatly. I created a query for this with a LEFT JOIN FETCH. However, I want only the related products to be added that have a certain rating, let's say a rating of > 3.
I tried the following:
SELECT DISTINCT p FROM Product p LEFT JOIN FETCH p.comparableProducts cp WHERE p.id = :id AND cp.rating > 3 AND CURRENT_DATE BETWEEN p.commenceDate AND p.removeDate
But this doesn't work. It always returns back ALL related products in the database, not just the ones that have a rating above 3. How is this fixable?
The easiest way to solve this problem is to load related products separately instead of trying to fit them into relatedProducts field.
It also makes perfect sense from object oriented point of view. I suppose you have something like "Product page" that contains the selected product and "recommended products". If so, such a page is a separate concept that deserves its own class:
public class ProductPage {
private Product product;
private List<Product> recommendedProducts;
...
}
Then you can fill such a class either by a single query:
SELECT DISTINCT p, cp FROM Product p LEFT JOIN p.comparableProducts cp WHERE p.id = :id AND cp.rating > 3 AND CURRENT_DATE BETWEEN p.commenceDate AND p.removeDate
or by two separate queries.
Unfortunately, this approach doesn't allow you to receive an instance of ProductPage directly from JPA, you need to write conversion code manually.

QueryDsl - OR statement not working

I have the following QueryDSL query:
QCustomer customer = QCustomer.customer;
BooleanBuilder builder = new BooleanBuilder();
builder.or(customer.person.name.containsIgnoreCase(query));
builder.or(customer.company.name.containsIgnoreCase(query));
return builder;
And I expect to get results from Persons that contains the name = query and/or Companies that contains the query parameter. But I get nothing.
This is my Customer class mapping:
#OneToOne(orphanRemoval = false, optional = true, cascade = CascadeType.ALL)
private Company company;
#OneToOne(orphanRemoval = false, optional = true, cascade = CascadeType.ALL)
private Person person;
Did someone knows what I'm missing here?
I expect to get a query like this:
select o
from Customer
where o.person.name like '%:name%' or o.company.name like '%:name%'
This is the generated query:
select
count(customer0_.uid) as col_0_0_
from
Customer customer0_
cross join
Person person1_
cross join
Company company2_
where
customer0_.person_uid=person1_.uid
and customer0_.company_uid = company2_.uid
and (lower(person1_.name) like ? escape '!' or lower(company2_.name) like ? escape '!') limit ?
It uses a count because it's the first query that Spring Data use to paginate the result.
The query looks ok. Most probably you get wrong results because the implicit property based joins make the joins inner joins.
Using left joins you might get the results you need.
QPerson person = QPerson.person;
QCompany company = QCompany.company;
BooleanBuilder builder = new BooleanBuilder();
builder.or(person.name.containsIgnoreCase(str));
builder.or(company.name.containsIgnoreCase(str));
query.from(customer)
.leftJoin(customer.person, person)
.leftJoin(customer.company, company)
.where(builder);

Troubles with JPA criteria API and multiple subqueries

I am struggling with the JPA Criteria API for formulating a query for my data structure. Ok, my entities are as follows. I have users and groups (both share a common base class OrgEntity). Logically, users can be members in multiple groups of course. Finally, I have an entity representing a task, which has a list of potential owners (that can be either single users or whole groups). The domain model is summarized below and is given, so I cannot change it.
#Entity
#Inheritance(strategy=InheritanceType.JOINED)
abstract public class OrgEntity {
#Id
public String name;
...
}
#Entity
public class User extends OrgEntity {
public String displayName;
#ManyToMany(mappedBy="members")
public List<Group> groups;
...
}
#Entity
public class Group extends OrgEntity {
#ManyToMany
public List<User> members;
...
}
#Entity
public class Task {
#Id
public String uuid;
#ManyToMany
public List<OrgEntity> potentialOwners;
...
}
The starting point for my query is a single instance of User. I want to know all the tasks where the user is a potential owner (regardless if the user is directly contained in the potentialOwners collection or member of a group that is contained in potentialOwners).
My first attempt using a named query was as follows
SELECT DISTINCT t FROM Task AS t JOIN t.potentialOwners po
WHERE (po IN (SELECT g FROM User u JOIN u.groups g WHERE u = :user)
OR po IN (SELECT u FROM User u WHERE u = :user))
It works, but I don't know if this is the most efficient way to do this. Any suggestions?
However, I have no idea how to implement this using the criteria API. Can somebody please help me with that.
Thanks
Ok, I finally figured out how to do it. If you are interested in my solution, here it is. u is the User object, basically the query parameter and em is the EntityManager instance.
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
// specifies the result value of the query
CriteriaQuery<Task> cq = criteriaBuilder.createQuery(Task.class);
// start with the navigation at the task entity
Root<Task> from = cq.from(Task.class);
// join the potential owner organizational entities
Join<Task,OrgEntity> potentialOwners = from.join("potentialOwners");
// select the tasks but remove duplicates
CriteriaQuery<Task> select = cq.select(from).distinct(true);
// definition for subquery1: fetch the user instance
Subquery<User> subquery1 = cq.subquery(User.class);
// start at the User entities
Root<User> from1 = subquery1.from(User.class);
// select the whole user
subquery1.select(from1);
// based on the specified user
subquery1.where(criteriaBuilder.equal(from1, u));
// definition for subquery2: fetch all groups for given user
Subquery<Group> subquery2 = cq.subquery(Group.class);
// we start at the User entity
Root<User> from2 = subquery2.from(User.class);
// join to Group entities via the groups collection
Join<User, Group> groups = from2.join("groups");
// select the group entities only
subquery2.select(groups).distinct(true);
// and finally restrict to all groups of the specified user
subquery2.where(criteriaBuilder.equal(from2, u));
// order in descending order based on the unique task id
select.orderBy(criteriaBuilder.desc(from.get("uuid")));
// here we restrict to those tasks that have the potential
// owners either in the result set of subquery2 or subquery1
// additionally I've tried to filter for another restriction
// in the task (based on a like statement of the uuid)
select.where(criteriaBuilder.and(
criteriaBuilder.or(
criteriaBuilder.in(potentialOwners).value(subquery2),
criteriaBuilder.in(potentialOwners).value(subquery1)),
criteriaBuilder.like(from.<String>get("uuid"), "1%")));
TypedQuery<Task> typedQuery = em.createQuery(select);
List<Task> resultList = typedQuery.getResultList();

JPA OneToMany relations and performace

I have two entities: parent Customer and child Order.
Each Customer has 1,000,000 Orders for example, so it is not needed in any given time to load a Customer with all Orders but I want to have this ability to make join query on these two entities in JPA.
So because of this, I must create #OneToMany relationship for making join queries.
My question is: how to get query without making joinColumn because even in Lazy mode it is possible to load 1,000,000 objects!
I just want to get query on these object with where restrictions like native join.
If you don't want the #OneToMany relationship implicitly set in your Customer class than you don't have to. You can execute JPQL queries (in very precise manner) without the marked relationship.
Assume you have:
#Entity
public class Customer {
// all Customer-related fields WITHOUT #OneToMany relationship with Order
}
#Entity
public class Order {
#ManyToOne
private Customer owner;
}
Then if you want to get all Orders for particular Customer you can execute a simple JPQL query like that:
// Customer customer = ...
// EntityManager em = ...
String jpql = "SELECT o FROM Order o WHERE o.owner = :customer";
TypedQuery<Order> query = em.createQuery(jpql, Order.class);
query.setParameter("customer", customer);
List<Order> orders = query.getResultList();
In this way you can execute the code only when you're really sure you want to fetch Customer's orders.
I hope I've understood your problem correctly.
EclipseLink has support for QueryKeys, that allow you to define fields or relationships for querying that are not mapped. Currently there in no annotation support for query keys, but you can define them using the API and a DescriptorCustomizer.
Also you do not need the OneToMany to query on it, just use the inverse ManyToOne to query,
i.e.
Select distinct c from Customer c, Order o where o.customer = c and o.item = :item
Or,
Select distinct o.customer from Order o join o.customer c where o.customer = c and o.item = :item