Understanding how spring-data handles #EntityGraph - java

(I made a SSCCE for this question.)
I have 2 simple entities : Employee and Company. Employee has a #ManyToOne relationship with Company with default fetch strategy (eager).
I want to be able to load the Employee without the Companywithout changing the fetch strategy defined in the Employee because I need to do that for only one use case.
JPA's entity graph seems to be intended for this purpose.
So I defined a #NamedEntityGraphon the class Employee:
#Entity
#NamedEntityGraph(name = "employeeOnly")
public class Employee {
#Id
private Integer id;
private String name;
private String surname;
#ManyToOne
private Company company;
//Getters & Setters
And a EmployeeRepository like this :
public interface EmployeeRepository extends CrudRepository<Employee, Integer> {
#EntityGraph(value = "employeeOnly", type = EntityGraph.EntityGraphType.FETCH)
List<Employee> findByCompanyId(Integer companyId);
}
Despite the use of #EntityGraph, I can see in the logs that the Company is still loaded by hibernate :
2016-11-07 23:16:08.738 DEBUG 1029 --- [nio-8080-exec-2] org.hibernate.SQL : select employee0_.id as id1_1_, employee0_.company_id as company_4_1_, employee0_.name as name2_1_, employee0_.surname as surname3_1_ from employee employee0_ left outer join company company1_ on employee0_.company_id=company1_.id where company1_.id=?
2016-11-07 23:16:08.744 DEBUG 1029 --- [nio-8080-exec-2] org.hibernate.SQL : select company0_.id as id1_0_0_, company0_.name as name2_0_0_ from company company0_ where company0_.id=?
Why? How to avoid that?

Hibernate did not support handling non-lazy attributes as lazy, even with entity graphs. There was an issue for this: HHH-8776, but it's fixed now.
Previously, the only solution for the time being was to make the association lazy.

Modified Answer
per-specification, the fetch type for #ManyToOne is EAGER by default. But even through we set:
#ManyToOne(fetch = FetchType.LAZY)
private Company company;
You will get the same result. The problem because the way spring-data-jpa create HQL/JPQL for you. So adding #ManyToOne(fetch = FetchType.LAZY) won't work is not enough. To solve this, use#ManyToOne(fetch = FetchType.LAZY) and #Query annotation in your repository:
Employee.java :
#ManyToOne(fetch = FetchType.LAZY)
private Company company;
EmployeeRepository.java
#Query("from Employee e where e.company.id = :companyId")
List<Employee> findByCompanyIdUsingQuery(#Param("companyId") Integer companyId);
In the test, this is SQL that generated by your loadByCompanyId() (which is generate left outer join):
select employee0_.id as id1_1_, employee0_.company_id as company_4_1_, employee0_.name as name2_1_, employee0_.surname as surname3_1_ from employee employee0_ left outer join company company1_ on employee0_.company_id=company1_.id where company1_.id=?
And this is SQL generated by method that use #Query annotation:
select employee0_.id as id1_1_, employee0_.company_id as company_4_1_, employee0_.name as name2_1_, employee0_.surname as surname3_1_ from employee employee0_ where employee0_.company_id=?
You could check the latest code in my repository.
HTH.

Seems a bug in Hibernate.
#Dragan Bozanovic you are right. I only see one workaround in this case.
Set fetch = lazy
#Entity
#NamedEntityGraph(name = "Employee.withCompany" , attributeNodes = #NamedAttributeNode("company"))
public class Employee {
#Id
private Integer id;
private String name;
private String surname;
#ManyToOne(fetch = FetchType.LAZY)
private Company company;
Introduce new method for loading company eagerly
public interface EmployeeRepository extends CrudRepository<Employee, Integer> {
List<Employee> findByCompanyId(Integer companyId);
#Query("select e from Employee e left join e.company c where c.id = :companyId")
#EntityGraph(value = "Employee.withCompany", type = EntityGraph.EntityGraphType.FETCH)
List<Employee> findByCompanyIdFetchingCompany(#Param("companyId") Integer companyId);
}
And use following two interchangeably where required
#RequestMapping(value = "/by-company/{id}")
public void loadByCompanyId(#PathVariable Integer id) {
employeeService.loadByCompanyId(id);
}
#RequestMapping(value = "/by-company/eager-company/{id}")
public void loadByCompanyIdFetchingCompany(#PathVariable Integer id) {
employeeService.loadByCompanyIdFetchingCompany(id);
}
First one (for lazy loading) http://localhost:8080/employees/by-company/42
select employee0_.id as id1_1_, employee0_.company_id as company_4_1_, employee0_.name as name2_1_, employee0_.surname as surname3_1_ from employee employee0_ left outer join company company1_ on employee0_.company_id=company1_.id where company1_.id=?
Second (eager loading) http://localhost:8080/employees/by-company/eager-company/42
select employee0_.id as id1_1_0_, company1_.id as id1_0_1_, employee0_.company_id as company_4_1_0_, employee0_.name as name2_1_0_, employee0_.surname as surname3_1_0_, company1_.name as name2_0_1_ from employee employee0_ left outer join company company1_ on employee0_.company_id=company1_.id where company1_.id=?

I was under impression you have to specify fields within your Graph definition.
https://docs.oracle.com/javaee/7/tutorial/persistence-entitygraphs001.htm (43.1.2.1 Fetch Graphs)
A fetch graph consists of only the fields explicitly specified in the
EntityGraph instance, and ignores the default entity graph settings.

Maybe not so cool solution is using a native query.
#Query(value = "select * from employee where company_id= ?1", nativeQuery = true)
List<Employee> findByCompanyId(Integer companyId);
I tested it and it was giving expected results, without being forced to set fetch = FetchType.LAZY

though the association is lazy by specifying (fetch=FetchType.Lazy) the company is exposed with a separate query call because you are exposing the Entity Employee which has property company. If you don't want the company to be exposed then either use #JsonIgnore, which is not very much recommended. And here comes one of the reasons why we make used of DTO so that we can expose only the required fields. You can create an EmployeeDTO with employee specific fields keep the association lazy which will ensure that separate query is not executed to fetch company details and map the entity to DTO either explicitly or you can make use of MapSturct Api

Related

Make request with subcondition for child list via Spring Data JPA

Assume I have the next data model:
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Item> items;
... getters, setters, equals and hashcode.
}
#Entity
public class Item {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#ManyToOne
#JoinColumn(name = "user")
private User user;
private Boolean deleted;
... getters, setters, equals and hashcode.
}
I need to query a certain user by id with non-deleted items. Can I do it via Spring Data Repositories?
I tried something like:
public interface UserRepository extends CrudRepository<User, Long> {
#Query("from User u left join u.items i on i.deleted = false where u.id = ?1")
List<User> findUserWithNonDeletedItems(Long userId);
}
But this approach generates 2 separate SQL queries:
select user0_.id as id1_1_0_, items1_.id as id1_0_1_, items1_.deleted as deleted2_0_1_, items1_.user as user3_0_1_ from user user0_ left outer join item items1_ on user0_.id=items1_.user and (items1_.deleted=0) where user0_.id=?
select items0_.user as user3_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.deleted as deleted2_0_1_, items0_.user as user3_0_1_ from item items0_ where items0_.user=?
And as result, I receive user with deleted items in the item list.
Nothing wrong with creation of two separete queries. One is to get users from user table, other is to get items of related user from items table.
join clause is used to combine columns from one or more tables.
join u.items i on i.deleted = false is not a proper use. It should be on the where clause.
You should change the query this way:
#Query("from User u left join u.items i where i.deleted = false and u.id = ?1")
List<User> findUserWithNonDeletedItems(Long userId);

JpaRepository makes two identical queries to database while retreiving simple entity

I have a very primitive Entity:
#Entity
#Table(name = "person")
public class Person {
#Id
private Integer id;
#Column(name = "name")
private String name;
#Column(name = "address")
private String address;
}
I use a simple JpaRepository<Person, Integer> with no overriding methos. Whenever I invoke repository.findAll()method I get the following log:
org.hibernate.SQL :
select
person0_.id as id1_2_,
person0_.address as address2_2_,
person0_.name as name3_2_
from
person person0_
Hibernate:
select
person0_.id as id1_2_,
person0_.address as address2_2_,
person0_.name as name3_2_
from
person person0_
The same result I get invoking repository.getOne(). Repository while trying to get a very simple data uses two identical queries one after another with seemingly no particular reason. Is this just how it visualise or is it actually making two queries instead of one? Why is that?

JPA Many To Many remove entity

I use Hibernate (via JPA). There is a method to remove an entity:
public void delete(final ID id) { entityManager.createQuery(String.format("delete from %s e where e.id = :id", entityClass.getSimpleName()))
.setParameter("id", id).executeUpdate();
}
I remove the entity (with many2many relation):
Hibernate logs:
Hibernate: delete from author_to_book where (author_id) in (select id from author where id=?)
Hibernate: delete from author where id=?
Who is responsible for removing associations from the binding table? After all, my code specifies the removal from the main table only.
How it works?
Mapping:
#Entity
public class Author extends BaseEntity implements IAuthor {
#Column
private String name;
#JoinTable(name = "author_to_book",
joinColumns = {#JoinColumn(name = "author_id")} ,
inverseJoinColumns = {#JoinColumn(name = "book_id")}
)
#ManyToMany(targetEntity = Book.class, fetch = FetchType.LAZY)
#OrderBy("title ASC")
private Set<IBook> books = new HashSet<>();
Book entity does not have a mapping to Author entity
Using string to construct a Query SQL is not a good idea for JPA. JPQL or Criteria API should be used. Usually the owning side of an association is responsible for removing rows from join table. For bi-directional relations, the rows in join tables can be deleted from the other side too. It really depends on JPA implementation. As far as I know, CMobileCom JPA can manage join tables from both sides.
Disclaimer: I am a developer of CMobileCom JPA, a light-weight JPA implementation for both Android and Java JDBC.

Spring data findAll() does not fetch eagerly

I have two entities with unidirectional one to many relationship.
#Entity
public class Basket {
#Id
#GeneratedValue
private Long id;
private int capacity;
}
#Entity
public class Item {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToOne
private Basket basket;
}
I save couple of objects:
Basket basket1 = new Basket(100);
Basket basket2 = new Basket(200);
Basket basket3 = new Basket(300);
basketRepository.save(asList(basket1, basket2, basket3));
Item item1 = new Item("item1", basket1);
Item item11 = new Item("item11", basket1);
Item item2 = new Item("item2", basket2);
Item item22 = new Item("item22", basket2);
Item item3 = new Item("item3", basket3);
Item item33 = new Item("item33", basket3);
itemRepository.save(asList(item1, item11, item2, item22, item3, item33));
// Loading one item. Basket fetched eagerly.
itemRepository.findOne(1L);
// Loading many items. Baskets are not loaded (n+1 select problem).
itemRepository.findAll();
#ManyToOne annotation uses eager fetch by default.
When I load one Item using findOne(), Hibernate generates query with left outer join and Basket is fetched in the same query.
However when I use findAll(), Hibernate first fetches all Items and then executes N selects (one per each Basket), so that it leads to (n+1) select problem. Why Hiberante doesn't eagerly fetch Basket objects with findAll() method and how to fix this?
From JPA 2.0 spec, #ManyToOne are by default EAGER.
Now, when you use findAll() it is equivalent to firing a JPQL query like entityManager.createQuery(...) and it by default loads the items first and subsequently for each item it loads the basket entity and causing N+1 problem.
You can follow one of the two approaches:
Override the default query used by specifying #Query annotation on the findAll method and use the query with join like select i from Item i left join fetch i.basket.
Use #NamedEntityGraph with name say basket on Item class and specify which part of the Item graph needs to be loaded eagerly. On the findAll method, use #EntityGraph(value = "basket"). Note that as per spring jpa entity graph, we can also use attributePath to define ad-hoc entity graphs via #EntityGraph without the need of having to explicitly add #NamedEntityGraph to your domain types.
You can override findAll method with #Query annotation in your repository. Below is the sample code
public interface ItemRepository extends CrudRepository<Item, Long> {
#Override
#Query("select item from Item item left join fetch item.basket")
Iterable<Item> findAll();
}
Then you can log your sql queries to see that only one query is made
Hibernate: select item0_.id as id1_1_0_, basket1_.id as id1_0_1_, item0_.basket_id as basket_i3_1_0_, item0_.name as name2_1_0_, basket1_.capacity as capacity2_0_1_ from item item0_ left outer join basket basket1_ on item0_.basket_id=basket1_.id
And before it was
2018-03-09 13:26:52.269 INFO 4268 --- [ main] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select item0_.id as id1_1_, item0_.basket_id as basket_i3_1_, item0_.name as name2_1_ from item item0_
Hibernate: select basket0_.id as id1_0_0_, basket0_.capacity as capacity2_0_0_ from basket basket0_ where basket0_.id=?
Hibernate: select basket0_.id as id1_0_0_, basket0_.capacity as capacity2_0_0_ from basket basket0_ where basket0_.id=?
Hibernate: select basket0_.id as id1_0_0_, basket0_.capacity as capacity2_0_0_ from basket basket0_ where basket0_.id=?

JPA 2.1 - How to use dynamic join conditions with the Criteria API to fill OneToMany associations?

I'm using Glassfish 4.1 and JPA 2.1 powered by EclipseLink + Postgresql 9.4.1.
Let's assume we have a car rental company. A customer can rent a car, but the customer can rent
the same car only once. Now the goal is to return a list of all cars. However, for each car in the list
we want to tell the user whether the user ever rented this car before of not. This additional information
(for the UI) can be either a (transient?) boolean flag. In our case, I guess simply filling a corresponding association with the right data
fits exactly what we want (see code below). However, I am not very sure how to use a flag instead - any advice here? Anyway...
We have to use the Criteria API, as there are
many other dynamic filters which we need (irrelevant for this question), so using a NamedQuery with JPQL or
even a NamedNativeQuery is not possible and not in our favor.
In other words:
The list of cars should contain all available cars
Each car in the list ever rented by user 123456 should also have the corresponding rental (the length of this list would always be one then)
The Criteria API should generate exactly 1 native SQL query which uses the correct JOIN conditions
The association "rentals" for each car should be either empty or filled with exactly one Rental instance of the given user
Instead of the given association it would be possibe to use a boolean flag instead, i.e. "alreadyRented" - any idea?
I know how to do this outside of JPA directly on the DB. But I want to use JPA for this. Any I want JPA to fill
the association automatically using a single SELECT + LEFT JOIN query, however, things are not not as easy as I thought...
Any idea? Would you suggest a different data model?
Here is our Car Entity:
#Entity
public class Car {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(nullable=false)
#NotNull
private String manufacturer; //simplified
#OneToMany(mappedBy="car", fetch=FetchType.LAZY)
private List<Rental> rentals;
//...
}
According to this mapping, the "rentals" attribute holds a list of all rentals ever made for a given car. Please note that this list is not per user!
And here is the Rental Entity, which basically holds data for all rentals for a given car (again, this is simplified).
#Entity
#Table(
name="RENTALS",
uniqueConstraints={
#UniqueConstraint(columnNames={"CUSTOMER_ID", "CAR_ID"})
}
)
public class Rental {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne(optional=false, fetch= FetchType.EAGER)
#JoinColumn(name="CUSTOMER_ID", nullable=false, updatable=false)
#NotNull
private Customer customer;
#ManyToOne(optional=false, fetch=FetchType.EAGER)
#JoinColumn(name="CAR_ID", nullable=false, updatable=false)
#NotNull
private Car car;
#Temporal(javax.persistence.TemporalType.TIMESTAMP)
#Column(nullable=false)
#NotNull
private Date fromDate;
#Temporal(javax.persistence.TemporalType.TIMESTAMP)
#Column(nullable=false)
#NotNull
private Date toDate;
//...
}
And here is finally the Customer Entity, which is used in our Rental Entity:
#Entity
public class Customer {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(nullable=false)
#NotNull
private String firstName;
#Column(nullable=false)
#NotNull
private String lastName;
//...
}
And here is finally my EJB, which uses the injected EntityManager to access the DB:
#Stateless
#Local
public class CarBean {
#PersistenceContext(unitName = "myPU")
private EntityManager em;
//...
public List<Car> getCarsForCustomer(Long userId) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Car> q = cb.createQuery(Car.class);
Root<Car> rootCar = q.from(Car.class);
List<Predicate> predicates = new ArrayList<Predicate>();
//...
//can't just do this because we need a different/dynamic JOIN condition!!
//rootCar.fetch("rentals", JoinType.LEFT);
//now let's try to create the dynamic join condition:
Predicate criteria = cb.conjunction();
Join<Car,Rental> rental = rootCar.join("rentals", JoinType.LEFT);
criteria = cb.and(criteria, cb.equal(rental.get("customer").get("id"), userId) );
rental.on(criteria);
q.select(rootCar).where(predicates.toArray(new Predicate[]{}));
return em.createQuery(q).getResultList();
}
}
All this will generate the following native SQL statement:
SELECT t1.ID, t1.MANUFACTURER
FROM CAR t1
LEFT OUTER JOIN RENTALS t0
ON ((t0.CAR_ID = t1.ID) AND (t0.CUSTOMER_ID = 123456))
As you can see from the generated statement the joined RENTALS are not part of the result set. Even if it would be part of the result set I'm not sure if JPA would use them to fill the rentals association.
Using a Fetch Join is not possible, as we cannot dynamically choose the join columns/conditions. However, when I uncomment the Fetch Join (see code) then I get the following native SQL statement that uses two JOINS which I don't want:
SELECT
t1.ID, t1.MANUFACTURER, t0.ID, t0.FROMDATE, t0.TODATE, t0.CAR_ID, t0.CUSTOMER_ID
FROM CAR t1
LEFT OUTER JOIN RENTALS t0 ON (t0.CAR_ID = t1.ID)
LEFT OUTER JOIN RENTALS t2 ON ((t2.CAR_ID = t1.ID) AND (t2.CUSTOMER_ID = 123456))
So the big question is how can I fill the rentals association by using "dynamic" join conditions? What am I doing wrong?

Categories

Resources