JPA: Join Fetch results to NULL on empty many side - java

I have a one to many relationship between User and GameMap. One user can have many maps.
User class:
// LAZY LOADED
#OneToMany(cascade = CascadeType.ALL, mappedBy = "creater")
private final List<GameMap> maps = new ArrayList<>();
However, sometimes I need to eager load the maps. To avoid the LazyInitializationException after closing Session, I have two variants of retrieving Users.
User Repository:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findById( Long id );
#Query("SELECT u FROM User u JOIN FETCH u.maps WHERE u.id = (:id)")
public User findByIdEagerFetch( #Param("id") Long id );
}
Problem:
However, the JPQL JOIN FETCH variant to eager load in one go the user and his maps returns a NULL user if there are no maps for this user in the table.
Question:
How can I rewrite the JPQL statement in order to retrieve the user and optionally(!) all his maps but if there are no maps, than thats okay, but dont return a NULL user.

A FETCH JOIN actually will resolve to an inner join in SQL. This means that any records/entities in the User table which have no maps will be removed from the result set. You need the LEFT keyword on your FETCH JOIN to get all the results, even those without a map.
#Query("SELECT u FROM User u LEFT JOIN FETCH u.maps WHERE u.id = (:id)")
public User findByIdEagerFetch( #Param("id") Long id );

Related

Query using specifications gives error when ordering on joined column that is not in SELECT DISTINCT

I get following error when doing a rather complicated query: for SELECT DISTINCT, ORDER BY expressions must appear in select list
In the query I need to find all distinct Requests that have an ExploitationSite that contains a search term in their dutch or french name. The result has to be ordered by the Activity's dutch name and limited to the first 10 for pagination.
To do this query I use the Page <T> findAll(Specification<T> spec, Pageable pageable) method of JpaSpecificationExecutor.
This will result in a SELECT DISTINCT query which has to be ORDERed BY a property that is not in SELECT. (details below)
I tried to fetch the activities eagerly in the hope it would place those differently in the SELECT. I did my best trying to get the DISTINCT in a subquery and then have the ORDER BY + LIMIT around that, but I did not succeed in that.
Has someone an idea how I can get this query to work?
The (simplified) Request entity
#Entity
#Table(name = "request_requests")
#History("Request")
public class Request extends EqualByStateObject {
#GeneratedValue
#Id
private int id;
#Embedded
private RequestNumber requestNumber;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "fk_request")
private List<ExploitationSite> exploitationSites = new ArrayList<>();
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(unique = true, name = "fk_activity")
private Activity activity;
...
}
The Specification (I have to use distinct here because since a Request contains a List of ExploitationSites it was possible I got the same request multiple times back if multiple ExploitationSites contained the search term)
public class ExploitationSiteSpecification extends EqualByStateObject implements Specification<Request> {
private final String exploitationSiteName;
protected ExploitationSiteSpecification(String exploitationSiteName) {
this.exploitationSiteName = exploitationSiteName;
}
#Override
public Predicate toPredicate(Root<Request> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
query.distinct(true);
ListJoin<Object, Object> exploitationSites = root.joinList("exploitationSites");
return criteriaBuilder.or(
criteriaBuilder.like(
criteriaBuilder.lower(exploitationSites.get("name").get("dutchName")), "%" + exploitationSiteName.toLowerCase() + "%"),
criteriaBuilder.like(
criteriaBuilder.lower(exploitationSites.get("name").get("frenchName")), "%" + exploitationSiteName.toLowerCase() + "%")
);
}
}
The Pageable
public Pageable getPageable() {
Sort sort = Sort.by(Sort.Order.asc("activity.name.dutchName"));
PageRequest.of(0, 10, sort);
}
This results in a generated query like this one
select distinct request0_.id as id1_23_,
request0_.fk_activity as fk_acti15_23_,
request0_.request_number as request12_23_
from request_requests request0_
inner join request_exploitation_sites exploitati1_ on request0_.id=exploitati1_.fk_request
left outer join request_activity activity2_ on request0_.fk_activity=activity2_.id
where lower(exploitati1_.dutch_name) like $1
or lower(exploitati1_.french_name) like $2
order by activity2_.dutch_name asc limit $3
which then gives the for SELECT DISTINCT, ORDER BY expressions must appear in select list error
Assuming you put the distinct because the join with exploitationSites would return multiple rows, the following two options would work without using distinct.
right after the join you could do an additional fetch
ListJoin<Object, Object> exploitationSites = root.joinList("exploitationSites");
root.fetch("exploitationSites")
this would result in hibernate to create an additional join of ExploitationSites as well as selecting additional columns
select request0_.id as id1_23_,
request0_.fk_activity as fk_acti15_23_,
request0_.request_number as request12_23_,
exploitati3_.id as exploitati3_id,
exploitati3_.name as exploitati3_name,
...
from request_requests request0_
inner join request_exploitation_sites exploitati1_ on request0_.id=exploitati1_.fk_request
left outer join request_activity activity2_ on request0_.fk_activity=activity2_.id
inner join request_exploitation_sites exploitati3_ on request0_.id=exploitati3_.fk_request
where lower(exploitati1_.dutch_name) like $1
or lower(exploitati1_.french_name) like $2
order by activity2_.dutch_name asc limit $3
use fetch in the first place and cast it to Join
Join<Object, Object> exploitationSites = (Join<Object, Object>) root.fetch("exploitationSites");
By casting the Fetch to a Join you can still use where clauses.
Note that this will also select additional columns, but won't do an additional join in the resulting query.
In both cases the fetch will result in a join fetch which hibernate internally will remove duplicates from the parent entity (see https://stackoverflow.com/a/51177569)

Making query combining, one to many relationships in JPA with JPQL query

I have a database. with a one to many relationships.
pet 1--* event.
I want to make a query, that selects all pets, that has had an event on a given date. (I'm using the SQL date format)
As of now I just want to be able to get all entities, for a hardcoded date.
here is the reference in my PetEntity table
#OneToMany
private List<EventEntity> events = new ArrayList();
and in my EventEntity
#ManyToOne
PetEntity pet;
I'm using a pattern where I use a repository to handle the data layer, and then a facade to handle any logic(if any)
So far I have made a method like this.
public Set<PetEntity> getPetsWithEvents(Date date){
EntityManager em = emf.createEntityManager();
Set<PetEntity> entities = new HashSet<>();
List<EventEntity> eventEntities=
em.createQuery("SELECT e from EventEntity e where e.date =: date", EventEntity.class).setParameter("date", date).getResultList();
for(EventEntity entity: eventEntities){
entities.add(entity.getPet());
}
return entities;
}
}
Is there a way to simply method this method into using one query, instead of looping through the vent and finding each pet?
As the others already mentioned, you should be able to select pet join event.
The JPQL will be something like below:
SELECT p FROM PetEntity p join p.events e
WHERE e.date =: date

Hibernate doesn't return proper objects in inner join

I'm working on Spring MVC application that uses Hibernate. One of my requests in SQL looks like this:
SELECT work_order.* FROM work_order
inner join user
on
work_order.user_id = user.id
and
user.user_name = 'Jenna'
and I do get a result as a row of workorders. When I'm trying to do the same with Hibernate I get five objects(which is correct) but can't convert into WorkoOrder objects.
Here's my Hibernate request:
List<WorkOrder> workorders = (List<WorkOrder>)currentSession.createQuery(
"from WorkOrder w inner join w.user as u where user_name=:tempName")
.setParameter("tempName", tempName).getResultList();
Where tempName is a parameter. I do get objects, but can't cast them to Workorder, probably because Hibernate returns Workorders and Users combined. How to fix this so only Workorders will be returned?
Update: User is mapped in WorkOrder
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name="user_id", nullable = true)
private User user;
Your HQL does not have a select clause. If you want only the WorkOrder entities from the join you form, then you'll need to tell Hibernate so:
currentSession.createQuery(
"select w from WorkOrder w inner join w.user as u where u.user_name=:tempName")

Spring: How to get data from two table in JPA

In the following code I want to get the data from Order table and also from User table How can I modify my query so that I can achieve this ? user_id is foreign key in order table
public interface OrderRepository extends JpaRepository<Order, Long> {
#Query("Select o from Order o where o.customer.id= :customerId and o.orderStatus='DELIVERED'")
List<Order> orderHistory(#Param("customerId") long customerId);
}
To fetch the Customer with Order, do the join fetch.
The JOIN FETCH expression is not a regular JOIN and it does not define a JOIN variable. Its only purpose is specifying related objects that should be fetched from the database with the query results on the same round trip. Using this query improves the efficiency of iteration over the result Country objects because it eliminates the need for retrieving the associated Capital objects separately.
http://www.objectdb.com/java/jpa/query/jpql/from
public interface OrderRepository extends JpaRepository<Order, Long> {
#Query("Select o from Order o inner join fetch o.customer as customer left join fetch o.user as user where customer.id= :customerId and o.orderStatus='DELIVERED'")
List<Order> orderHistory(#Param("customerId") long customerId);
}
You want to put customerId and order fields into param?
I think list the order fields in params is enough.
of course sql statement must be right.

HQL left join with condition

This may seem basic but it's late and I'm having trouble with the following.
class Group {
#Id
String id;
}
class Participation {
#Id
String id;
#ManyToOne
#JoinColumn(name = "GROUP_ID")
Group group;
#ManyToOne
#JoinColumn(name = "USER_ID")
User user;
}
class User {
#Id
String id;
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
Set<Participation> participations;
}
Class diagram
So
Participation -->1 Group
and User 1<-->N Participation
How can I retrieve all Groups with, for a given User, the associated Participation (or null is there is none)? I've been playing with join fetches but to no avail so far...
Many thanks,
CN
PS. I can do this in SQL thus :
select g.d, p.id
from group as g
left join participation as p
on p.group_id = g.id and p.user_id = 2;
(Probably some typo in the HQL itself but the idea should be correct)
What you are asking, based on your SQL and description, is find out all Participation (and its corresponding Group) based on User, which is simply
select p.id, p.group.id from Participation p where p.user.id = :userId
To make it better, you should fetch the entities instead:
from Participation p left join fetch p.group where p.user.id = :userId
There were some confusions in understanding what you were trying to do:
You want all groups (regardless of condition). And, for a given user, you want to find all groups and participations that user is involved.
Though it should be possible using Right-outer-join:
select g, p from Participation p
right outer join p.group g
where p.user.id=:userId
Or, in later version of Hibernate (>= 5.1 ?), it allow explicit join (haven't tried before, you may give it a try) (Replace with with on if you are using JPQL):
select g, p from Group g
left outer join Participation p
with p.group = g
left outer join p.user u
where u.id = :userId
Or you may use other techniques such as subquery etc. However I would rather separate them into simpler queries in your case and do a simple aggregation in your code.
The basic idea is to have
Query for all groups: from Groups
Query for all participations for a user: from Participation p join fetch p.group where p.user.id=:userId
Then you can aggregate them easily to the form you want, e.g. Map<Group, List<Participation>>, or even more meaningful value object.
The benefit is the data access query is simpler and more reusable (esp if you are wrapping them in DAO/Repository finder method). One more round trip to DB shouldn't cause any obvious performance impact here.
You need to map the participation relationship in the Group entity. If the relationship between Participation and Group is 1..N:
class Group {
String id
List<Participation> participations
}
The JPQL can be:
SELECT g.id, p.id FROM Group g
JOIN g.participations p
JOIN p.user user
WHERE user.id = :idUser
You can receive this information as List<Object[]> (Object[0] is group id and Object[1] is participation id) or use SELECT NEW.
Without map the Group/Participation relationship, you can do:
SELECT g.id, p.id FROM Group g, Participation p
JOIN p.user user
JOIN p.group groupp
WHERE g.id = groupp.id
AND user.id = :idUser
But you can't do a LEFT JOIN using this strategy. The query above behaviours like a JOIN.
It's normal with Hibernate you map the side of a relationship that you would like to make a query. So, I recommend the first approach, mapping the relationship.

Categories