Hibernate makes extra query to full fill entity - java

I want to extract entity, that contains only a specific part of data from DB. But Hibernate creates extra query, that fully fill my entity.
EXAMPLE
Pseudo Query: Select * From ShopUnitDB WHERE prices.date BETWEEN 2022-05-02 AND 2022-05-30;
Pseudo Expected: ShopUnit{1, Name, prices=[{2000, 2022-05-25}, {4000, 2022-05-29}]}
Result: All data from ShopUnitPrice table, related to ShopUnitDB.
Console log:
Hibernate: select shopunitdb0_.id as id1_0_, shopunitdb0_.name as name2_0_, shopunitdb0_.parent_id as parent_i3_0_, shopunitdb0_.type as type4_0_ from shop_unit shopunitdb0_ inner join shop_unit_price prices1_ on shopunitdb0_.id=prices1_.unit_id where shopunitdb0_.id=? and (prices1_.date between ? and ?)
Hibernate: select prices0_.unit_id as unit_id4_1_0_, prices0_.id as id1_1_0_, prices0_.id as id1_1_1_, prices0_.date as date2_1_1_, prices0_.price as price3_1_1_, prices0_.unit_id as unit_id4_1_1_ from shop_unit_price prices0_ where prices0_.unit_id=?
Look, first query extracts correct entity - that contains only a part based on WHERE ... BETWEEN statement. Next it creates new query to extract all data of sub entity of ShopUnitDB.
I use Spring Data JPA Specification. Also tryed to use #Query in JpaRepository. No luck.
Entity 1:
#Entity
#Table(name = "shop_unit")
public class ShopUnitDB {
#Id
private UUID id;
#Column(name = "name")
private String name;
#Column(name = "parent_id")
#Nullable
private UUID parentId;
#Enumerated(EnumType.STRING)
private ShopUnitType type;
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "unit_id", referencedColumnName = "id")
private List<ShopUnitPrice> prices;
#Transient
private Set<ShopUnitDB> children;
...
public List<ShopUnit> convertToShopUnitStatistic() {
return prices.stream()
.map(price -> new ShopUnit(id, name, price.getDate(),
parentId, type, price.getPrice()))
.collect(Collectors.toList());
}
...
}
Entity 2:
#Entity
#Table(name = "shop_unit_price")
public class ShopUnitPrice {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(name = "unit_id")
private UUID unitId;
#Column(name = "date")
private LocalDateTime date;
#Column(name = "price")
private Integer price;
...
}
My Service, that call JpaRepository using Spring Data JPA Specification:
#Service
public class ShopUnitService {
private ShopUnitRepository jpa;
#Autowired
public ShopUnitService(ShopUnitRepository jpa) {
this.jpa = jpa;
}
public List<ShopUnit> getShopUnitStatistic(UUID uuid, LocalDateTime start, LocalDateTime end)
{
unitDB = jpa.findAll(where(idLike(uuid).and(joinPrices(start, end)))).get(0);
return unitDB.convertToShopUnitStatistic();
}
private Specification<ShopUnitDB> idLike(UUID id) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("id"), id);
}
private Specification<ShopUnitDB> joinPrices(LocalDateTime start, LocalDateTime end) {
return (root, query, criteriaBuilder) -> {
return criteriaBuilder.between(root.join("prices").get("date"), start, end);
};
}
}
Spring JpaRepository:
#Repository
public interface ShopUnitRepository extends JpaRepository<ShopUnitDB, UUID>, JpaSpecificationExecutor<ShopUnitDB> {}
UPDATE
spring-boot-starter-parent version: 2.6.8
application.properties
server.port=80
spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/goods_warehouse
spring.datasource.username=username
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
spring.jpa.show-sql=true

I see you are using fetch = FetchType.EAGER for OneToMany association (which is not recommended) in ShopUnitDB entity and criteria APIs to fetch the data. If you use JPQL/Criteria APIs to fetch data with fetchType=EAGER, it will fire 2 queries to retrieve data. Please refer this link for more details.
To fix your issue, I think you should either remove fetchType from OneToMany association in ShopUnitDB or make it fetchType = FetchType.LAZY

According to #Daniel Wosch's comment, i understood the main problem. To solve it, i used to use JPARepository method this #Query like this:
#Repository
public interface ShopUnitRepository extends JpaRepository<ShopUnitDB, UUID>, JpaSpecificationExecutor<ShopUnitDB> {
#Query("SELECT unit " +
"FROM ShopUnitDB unit JOIN FETCH unit.prices price " +
"WHERE unit.id = ?1 AND price.date BETWEEN ?2 AND ?3")
ShopUnitDB findByIdAndDate(UUID uuid, LocalDateTime dateStart, LocalDateTime dateEnd);
}
Note that i used JOIN FETCH *ShopUnit collection field* and filtering that data set.

Related

Why do we need bidirectional synchronized methods?

As stated in the topic. Why do we need bidirectional synchronized methods? What real world use case does it solve? What happens if I don't use them?
In Hibernate's User Guide:
Whenever a bidirectional association is formed, the application developer must make sure both sides are in-sync at all times.
The addPhone() and removePhone() are utility methods that synchronize both ends whenever a child element is added or removed.
Source - Hibernate User Guide
In one of Vlad's blog posts:
However, we still need to have both sides in sync as otherwise, we break the Domain Model relationship consistency, and the entity state transitions are not guaranteed to work unless both sides are properly synchronized.
Source - Vlad Mihalcea Blog
Lastly, in Vlad's book - High Performance Java Persistance, page 216:
For a bidirectional #ManyToMany association, the helper methods must be added to the entity that is more likely to interact with. In our case, the root entity is the Post, so the helper methods are added to the Post entity
However, if I use simple generated setters, Hibernate seems to work just fine as well. Furthermore, synchronized methods might lead to performance degredation.
Synchronized methods:
public void joinProject(ProjectEntity project) {
project.getEmployees().add(this);
this.projects.add(project);
}
Generates this:
Hibernate:
select
employeeen0_.id as id1_0_0_,
projectent2_.id as id1_2_1_,
teamentity3_.id as id1_3_2_,
employeeen0_.first_name as first_na2_0_0_,
employeeen0_.job_title as job_titl3_0_0_,
employeeen0_.last_name as last_nam4_0_0_,
employeeen0_.team_id as team_id5_0_0_,
projectent2_.budget as budget2_2_1_,
projectent2_.name as name3_2_1_,
projects1_.employee_id as employee1_1_0__,
projects1_.project_id as project_2_1_0__,
teamentity3_.name as name2_3_2_
from
employees.employee employeeen0_
inner join
employees.employee_project projects1_
on employeeen0_.id=projects1_.employee_id
inner join
employees.project projectent2_
on projects1_.project_id=projectent2_.id
inner join
employees.team teamentity3_
on employeeen0_.team_id=teamentity3_.id
where
employeeen0_.id=?
Hibernate:
select
projectent0_.id as id1_2_,
projectent0_.budget as budget2_2_,
projectent0_.name as name3_2_
from
employees.project projectent0_
where
projectent0_.id=?
Hibernate:
select
employees0_.project_id as project_2_1_0_,
employees0_.employee_id as employee1_1_0_,
employeeen1_.id as id1_0_1_,
employeeen1_.first_name as first_na2_0_1_,
employeeen1_.job_title as job_titl3_0_1_,
employeeen1_.last_name as last_nam4_0_1_,
employeeen1_.team_id as team_id5_0_1_
from
employees.employee_project employees0_
inner join
employees.employee employeeen1_
on employees0_.employee_id=employeeen1_.id
where
employees0_.project_id=?
Hibernate:
insert
into
employees.employee_project
(employee_id, project_id)
values
(?, ?)
Notice additional select for Employee right after Projects were fetched. If I use simply employeeEntity.getProjects().add(projectEntity);, it generates:
Hibernate:
select
employeeen0_.id as id1_0_0_,
projectent2_.id as id1_2_1_,
teamentity3_.id as id1_3_2_,
employeeen0_.first_name as first_na2_0_0_,
employeeen0_.job_title as job_titl3_0_0_,
employeeen0_.last_name as last_nam4_0_0_,
employeeen0_.team_id as team_id5_0_0_,
projectent2_.budget as budget2_2_1_,
projectent2_.name as name3_2_1_,
projects1_.employee_id as employee1_1_0__,
projects1_.project_id as project_2_1_0__,
teamentity3_.name as name2_3_2_
from
employees.employee employeeen0_
inner join
employees.employee_project projects1_
on employeeen0_.id=projects1_.employee_id
inner join
employees.project projectent2_
on projects1_.project_id=projectent2_.id
inner join
employees.team teamentity3_
on employeeen0_.team_id=teamentity3_.id
where
employeeen0_.id=?
Hibernate:
select
projectent0_.id as id1_2_,
projectent0_.budget as budget2_2_,
projectent0_.name as name3_2_
from
employees.project projectent0_
where
projectent0_.id=?
Hibernate:
insert
into
employees.employee_project
(employee_id, project_id)
values
(?, ?)
No more fetching of employee.
Full code.
Controller.
#RestController
#RequestMapping(path = "${application.endpoints.projects}", produces = MediaType.APPLICATION_JSON_VALUE)
#Validated
public class ProjectsEndPoint {
#PostMapping("add-employee")
#ApiOperation("Add employee to project")
public void addEmployeeToProject(#RequestBody #Valid EmployeeProjectRequest request) {
LOGGER.info("Add employee to project. Request: {}", request);
this.projectsService.addEmployeeToProject(request);
}
}
EmployeeProjectRequest.
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public record EmployeeProjectRequest(
#NotNull #Min(0) Long employeeId,
#NotNull #Min(0) Long projectId) {
}
ProjectService.
#Service
public class ProjectsService {
private final ProjectRepo projectRepo;
private final EmployeeRepo repo;
public ProjectsService(ProjectRepo projectRepo, EmployeeRepo repo) {
this.projectRepo = projectRepo;
this.repo = repo;
}
#Transactional
public void addEmployeeToProject(EmployeeProjectRequest request) {
var employeeEntity = this.repo.getEmployee(request.employeeId())
.orElseThrow(() -> new NotFoundException("Employee with id: %d does not exist".formatted(request.employeeId())));
var projectEntity = this.projectRepo.getProject(request.projectId())
.orElseThrow(() -> new NotFoundException("Project with id: %d does not exists".formatted(request.projectId())));
//This line can be changed with employeeEntity.joinProject(projectEntity);
employeeEntity.getProjects().add(projectEntity);
}
}
ProjectRepo.
#Repository
public class ProjectRepo {
private final EntityManager em;
public ProjectRepo(EntityManager em) {
this.em = em;
}
public Optional<ProjectEntity> getProject(Long id) {
var result = this.em.createQuery("SELECT p FROM ProjectEntity p where p.id = :id", ProjectEntity.class)
.setParameter("id", id)
.getResultList();
return RepoUtils.fromResultListToOptional(result);
}
}
EmployeeRepo.
#Repository
public class EmployeeRepo {
private final EntityManager em;
public EmployeeRepo(EntityManager em) {
this.em = em;
}
public Optional<EmployeeEntity> getEmployee(Long id) {
var employees = this.em.createQuery("""
SELECT e FROM EmployeeEntity e
JOIN FETCH e.projects p
JOIN FETCH e.team t
WHERE e.id = :id""", EmployeeEntity.class)
.setParameter("id", id)
.getResultList();
return Optional.ofNullable(employees.isEmpty() ? null : employees.get(0));
}
}
EmployeeEntity.
#Entity
#Table(name = "employee", schema = "employees")
public class EmployeeEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
#Enumerated(EnumType.STRING)
private JobTitle jobTitle;
#ManyToOne(fetch = FetchType.LAZY)
private TeamEntity team;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#JoinTable(schema = "employees", name = "employee_project",
joinColumns = #JoinColumn(name = "employee_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "project_id", referencedColumnName = "id"))
private Set<ProjectEntity> projects = new HashSet<>();
public EmployeeEntity() {
}
public void joinProject(ProjectEntity project) {
project.getEmployees().add(this);
this.projects.add(project);
}
public void leaveProject(ProjectEntity project) {
project.getEmployees().remove(this);
this.projects.remove(project);
}
... Getters and Setters ...
}
ProjectEntity.
Entity
#Table(name = "project", schema = "employees")
public class ProjectEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal budget;
#ManyToMany(fetch = FetchType.LAZY, mappedBy = "projects")
private Set<EmployeeEntity> employees = new HashSet<>();
public ProjectEntity() {
}
... Getters and Setters ...
}
If there are really many elements on the Many side, then you probably should not use OneToMany at all. Fetching large collections implies using some kind of pagination\filtering, but OneToMany loads the whole set.
First of all, you need to update an owning entity(where FK resides) to store it in the DB. And what Vlad and Hibernate guide mean about consistency, refers to updating entity objects inside current session. Those objects have transitions during lifecycle, and when you have bidirectional association, if you don't set inverse side, then that inverse side entity won't have the field updated, and would be inconsistent with an owning side entity(and probably with the DB ultimately, after TX commits) in the current session.
Let me illustrate on OneToMany example.
If we get 2 managed entities Company and Employee:
set employee.company = X -> persist(employee) -> managed List<Employee> company.employees gets inconsistent with db
And there might be different types of inconsistencies, like getting from company.employees field after and arising side-effects(guess it was not empty, but just without employee you just added), and if there is Cascade.ALL, you might miss or falsely remove\update\add entities through broken relationships, because your entities are in a ambigious state, and hibernate deals with it in a defensive but sometimes unpredictable way:
Delete Not Working with JpaRepository
Also, you might find interesting this answer: https://stackoverflow.com/a/5361587/2924122

JPA Criteria API Specification for Many to Many

I have got three classes as mentioned below. I am trying to create a specification to filter data where there is a match in the linked table.
public class Album {
private Long id;
private List<AlbumTag> albumTags;
}
public class Tag {
private Long id;
private String category;
}
public class AlbumTag{
private Long id;
private Album album;
private Tag tag;
}
In the schema given above what I am trying to find is a list of all albums from Album table with the link in AlbumTag. The SQL that I want to achieve, doesn't have to be same, is below
select *
from Album A
where (A.Id in (select [AT].AlbumId
from AlbumTag [AT]))
What I have tried so far which is not working, of course, is below
public class AlbumWithTagSpecification implements Specification<Album> {
#Override
public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
final Subquery<Long> personQuery = cq.subquery(Long.class);
final Root<Album> album = personQuery.from(Album.class);
final Join<Album, AlbumTag> albumTags = album.join("albumTags");
personQuery.select((albumTags.get("album")).get("id"));
personQuery.where(cb.equal(album.get("id"), (albumTags.get("album")).get("id")));
return cb.in(root.get("id")).value(personQuery);
}
}
Using spring boot and spring data JPA, you can prefer entity relationship to fetch the data.
1.Annotate the domain class with the entity relationship which given below:
#Entity
#Table(name="Album")
public class Album {
#Id
#Column(name="id")
private Long id;
#OneToMany(targetEntity = AlbumTag.class, mappedBy = "album")
private List<AlbumTag> albumTags;
//getter and setter
}
#Entity
#Table(name="Tag")
public class Tag {
#Id
#Column(name="id")
private Long id;
#Column(name="category")
private String category;
//getter and setter
}
#Entity
#Table(name="AlbumTag")
public class AlbumTag{
#Id
#Column(name="id")
private Long id;
#ManyToOne(optional = false, targetEntity = Album.class)
#JoinColumn(name = "id", referencedColumnName="id", insertable = false, updatable = false)
private Album album;
#ManyToOne(optional = false, targetEntity = Tag.class)
#JoinColumn(name = "id", referencedColumnName="id", insertable = false, updatable = false)
private Tag tag;
//getter and setter
}
2.use the spring data to fetch the details using the below:
Album album = ablumRepository.findOne(1); // get the complete details about individual album.
List<AlbumTag> albumTags = ablum.getAlbumTags(); // get the all related albumTags details for particular album.
I hope this will help you to solve it.
Subqueries in JPA only really work with CriteriaBuilder.exists() so i would try:
public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
final Subquery<Long> subQuery = cq.subquery(Long.class);
final Root<AlbumTag> albumTag = subQuery.from(AlbumTag.class);
// it doesn't really matter what we select
subQuery.select(cb.literal(1));
subQuery.where(cb.equal(root.get("id"), (albumTag.get("album")).get("id")));
return cb.exists(subQuery);
}
which is equivalent to
select *
from Album A
where exists(
select 1 from AlbumTag AT
where AT.AlbumId = A.Id
)
Well, I wouldn't go for in operation in this case - it just complicates the query and the specification. The problem you described is actually matter of joining records from Table A with related records from Table B so the query in your case would be like:
SELECT a from Album a join AlbumTag at on a.id = at.albumId - as you needed it will return all albums that have album tags. Inner join explained
So in your case I would create this "factory" method that would create for you this specification.
public static Specification<Album> withTags() {
return new Specification<Album>() {
#Override
public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return root.join("albumTags").getOn();
}
};
}
Also I would suggest you to have a look at static metamodel library from hibernate - link to introduction. It generates for you static model from your entity classes that helps you avoid creating queries/specifications using hardcoded strings.
creteria query for join tables
CriteriaQuery<Album> query = cb.createQuery(Album.class);
Root<Album> album = query.from(Teacher.class);
Join<Album, AlbumTag> tag = teacher.join("id");
query.select(tag).where(cb.equal(album.get("album")));
List<Album> results = em.createQuery(query).getResultList();
for (Album al : results) {
System.out.println("album-->+al.get(name));
}
This looks like a classic many to many example. The three classes you have map directly to the tables you would expect in the database. JPA is an Object Relational Mapping (ORM) library which means we can structure the classes in a more OO style and map to the underlying relational database.
The AlbumTag class can be omitted and the #ManyToMany relationship added to both Album and Tag.
public class Album {
private Long id;
#ManyToMany
#JoinTable(name="AlbumTag",
joinColumns=
#JoinColumn(name="album", referencedColumnName="id"),
inverseJoinColumns=
#JoinColumn(name="tag", referencedColumnName="id"))
private List<Tag> tags;
}
public class Tag {
private Long id;
private String category;
#ManyToMany(mappedBy="tags")
private List<Album> albums;
}
To find albums by Tag you would first retrieve the Tag from the repository using something like findById(1l); or findByCategory("Rock"); and then simply call getAlbums() on the Tag object.
Note: One slight difference here is that the AlbumTag table would have only two columns (album and tag). The extra id column on AlbumTag is unnecessary since the combination of album and tag would be a unique id and you would never need to find by id in this table anyway.
Since you are using spring-data-jpa you should really take advantage of the features it provides.
My first question is related to your entity classes. I do not understand why is it necesary to store a list of album tags in the album class. Since you have a join table this information is reduntant.
Secondly you should adnotate your entity clases:
#Entity
public class Album {
#Id
#Column
private Long id;
}
#Entity
public class Tag {
#Id
#Column
private Long id;
#Column
private String category;
}
#Entity
#Table
public class AlbumTag{
#Id
#Column
private Long id;
#ManyToOne
#JoinColumn
private Album album;
#ManyToOne
#JoinColumn
private Tag tag;
}
Next you should create repositories for your entity classes.
interface AlbumRepository extends JpaRepository<Album, Long>{
#Query
("select DISTINCT(a) from AlbumTag at "+
"join at.album a "
"where at.tag is not null")
List<Album> findAlbumWithTag();
}
Then simply call the repository function which will return a list of albums which have at least one tag.

Hibernate Criteria One to Many issues

I am trying to use Hibernate Criteria api to fetch only the topics based on the USER_ID but have no idea how to do it using the criteria.
My Tables are "topic_users" (below)
and "topics" table (below)
I know how to do it using SQL, this would be something like:
SELECT TOPICNAME
FROM topic_users INNER JOIN topics on topic_users.TOPICS_TOPICS_ID = topics.TOPICS_ID
WHERE topic_users.USER_ID = 1
This will return all TOPICNAME of USER_ID 1 which is exactly what I want but how I can do this with Hibernate Criteria. So far I have this in my Repository class (see below) but this will only return a highly nested JSON array. I could loop through the objects, use a DTO and build my response or try the Hibernate createSQLQuery method that will let me call a native SQL statement directly (haven't tried that yet)...but I am trying to learn the Criteria so I hope anyone can answer my query.
#Repository("userTopicsDao")
public class UserTopicsDaoImpl extends AbstractDao<Integer, UserTopics>implements UserTopicsDao {
#Override
public List<UserTopics> findMyTopics(int userId) {
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("userId", userId));
List<UserTopics> userTopicsList = (List<UserTopics>)crit.list();
return userTopicsList;
}
and my TOPIC_USERS Entity where I have mapped the TOPICS
#Entity
#Table(name="TOPIC_USERS")
public class UserTopics {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="TOPICUSER_ID")
private Integer id;
#Column(name="USER_ID")
private Integer userId;
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "TOPICS_ID")
private Set<Topics> topicsUser;
//getter and setters
Ok starting from the ground up.. you entity classes should look like this:
#Entity
#Table(name="TOPIC_USERS")
public class UserTopics {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="TOPICUSER_ID")
private Integer id;
#Column(name="USER_ID")
private Integer userId;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "TOPICS_TOPICS_ID")
private Topics topics;
Your Topics class should look like this:
#Entity
#Table(name="TOPICS")
public class Topic {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="TOPICUS_ID")
private Integer id;
#Column(name="TOPICNAME")
private Integer topicName;
#OneToMany(mappedBy = "topics")
private Set<UserTopics> userTopics;
Finally the Criteria:
Version 1) You get entire entity:
Criteria c = session.createCriteria(Topics.class, "topics");
c.createAlias("topics.userTopics", "userTopics");
c.add(Restrictions.eq("userTopics.userId", userId));
return c.list(); // here you return List<Topics>
Version 2) You project only the topicname:
Criteria c = session.createCriteria(Topics.class, "topics");
c.createAlias("topics.userTopics", "userTopics");
c.add(Restrictions.eq("userTopics.userId", userId));
c.setProjection(Projections.property("topics.topicName"));
List<Object[]> results = (List<Object[]>)c.list();
// Here you have to manually get the topicname from Object[] table.
}

Spring JPA Join Efficiency - Create a query for each iteration

I have a simple 2 JPA entities which I have a Join Between them:
Primary entity Country:
public class Country implements Serializable {
#Id
#Column(name = "MCC")
private String mcc;
......
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "mcc", referencedColumnName = "mcc")
private List<CountryInfo> countryInfo;
Joint entity CountryInfo:
public class CountryInfo implements Serializable {
#Id
#Column(name = "id")
private Long id;
#Column(name = "mcc")
private String mcc;
#Column(name = "continent")
private String continent;
When I've turned on my configuration to dump the queries being executed, I've noticed that for each Country found, another call is done on the CountryInfo entity with the mcc specified..
This is obviously slow since rather than creating 1 call with a JOIN, it is executing N + 1 queries (where N = count of Country).
I've already seen this tutorial https://zeroturnaround.com/rebellabs/how-to-use-jpa-correctly-to-avoid-complaints-of-a-slow-application/ and changed accordingly but it is still calling N + 1 queries..
Is there a way to overcome this?
EDIT
In order to get the data I have a Repository:
#RepositoryRestResource(exported = false)
public interface CountryRepository extends JpaRepository<E212MCC, Long>,
JpaSpecificationExecutor<E212MCC> {
}
And then call with some specifications:
List<E212MCC> countries = this.countryRepository.findAll(specifications);
Since you are using Specifications you could try with specification that performs fetch join operation (I am assuming that you are using JPA meta model):
private Specification<Country> joinContryInfo() {
return (root, query, cb) -> {
root.fetch(Country_.countryInfo);
// here you can fetch more entities if you need...
return null;
};
}
And then, just add it to your specification object:
Specifications.where(joinCountryInfo())
If you are not using meta model then just replace Country_.countryInfo with "countryInfo" string.
If you are using CountryInfo fields for searching, you can omit joinContryInfo() specification and prepare join and search query in one specification:
private Specification<Country> continentEqual(String param) {
return (root, query, cb) -> {
Join<Country,CountryInfo> join = (Join) root.fetch(Country_.countryInfo);
return cb.equal(join.get(CountryInfo_.continent), addWildCards(param));;
};
}

JPA Criteria multiselect with fetch

I have following model:
#Entity
#Table(name = "SAMPLE_TABLE")
#Audited
public class SampleModel implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#Column(name = "NAME", nullable = false)
#NotEmpty
private String name;
#Column(name = "SHORT_NAME", nullable = true)
private String shortName;
#ManyToOne(fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "MENTOR_ID")
private User mentor;
//other fields here
//omitted getters/setters
}
Now I would like to query only columns: id, name, shortName and mentor which referes to User entity (not complete entity, because it has many other properties and I would like to have best performance).
When I write query:
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<SampleModel> query = builder.createQuery(SampleModel.class);
Root<SampleModel> root = query.from(SampleModel.class);
query.select(root).distinct(true);
root.fetch(SampleModel_.mentor, JoinType.LEFT);
query.multiselect(root.get(SampleModel_.id), root.get(SampleModel_.name), root.get(SampleModel_.shortName), root.get(SampleModel_.mentor));
query.orderBy(builder.asc(root.get(SampleModel_.name)));
TypedQuery<SampleModel> allQuery = em.createQuery(query);
return allQuery.getResultList();
I have following exception:
Caused by: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=generatedAlias1,role=com.sample.SampleModel.model.SampleModel.mentor,tableName=USER_,tableAlias=user1_,origin=SampleModel SampleModel0_,columns={SampleModel0_.MENTOR_ID ,className=com.sample.credential.model.User}}]
at org.hibernate.hql.internal.ast.tree.SelectClause.initializeExplicitSelectClause(SelectClause.java:214)
at org.hibernate.hql.internal.ast.HqlSqlWalker.useSelectClause(HqlSqlWalker.java:991)
at org.hibernate.hql.internal.ast.HqlSqlWalker.processQuery(HqlSqlWalker.java:759)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.query(HqlSqlBaseWalker.java:675)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.selectStatement(HqlSqlBaseWalker.java:311)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.statement(HqlSqlBaseWalker.java:259)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.analyze(QueryTranslatorImpl.java:262)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:190)
... 138 more
Query before exception:
SELECT DISTINCT NEW com.sample.SampleModel.model.SampleModel(generatedAlias0.id, generatedAlias0.name, generatedAlias0.shortName, generatedAlias0.mentor)
FROM com.sample.SampleModel.model.SampleModel AS generatedAlias0
LEFT JOIN FETCH generatedAlias0.mentor AS generatedAlias1
ORDER BY generatedAlias0.name ASC
I know that I can replace fetch with join but then I will have N+1 problem. Also I do not have back reference from User to SampleModel and I do not want to have..
I ran into this same issue, and found that I was able to work around it by using:
CriteriaQuery<Tuple> crit = builder.createTupleQuery();
instead of
CriteriaQuery<X> crit = builder.createQuery(X.class);
A little extra work has to be done to produce the end result, e.g. in your case:
return allQuery.getResultList().stream()
map(tuple -> {
return new SampleModel(tuple.get(0, ...), ...));
})
.collect(toList());
It's been a long time since the question was asked. But I wish some other guys would benefit from my solution:
The trick is to use subquery.
Let's assume you have Applicant in your Application entity (one-to-one):
#Entity
public class Application {
private long id;
private Date date;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "some_id")
private Applicant applicant;
// Other fields
public Application() {}
public Application(long id, Date date, Applicant applicant) {
// Setters
}
}
//...............
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Application> cbQuery = cb.createQuery(Application.class);
Root<Application> root = cbQuery.from(Application.class);
Subquery<Applicant> subquery = cbQuery.subquery(Applicant.class);
Root subRoot = subquery.from(Applicant.class);
subquery.select(subRoot).where(cb.equal(root.get("applicant"), subRoot));
cbQuery.multiselect(root.get("id"), root.get("date"), subquery.getSelection());
This code will generate a select statement for Application, and select statements for Applicant per each Application.
Note that you have to define an appropriate constructor corresponding to your multiselect.
I got the same problem using EclipseLink as the JPA provider : I just wanted to return the id of a mapped entity («User» in Gazeciarz's example).
This can be achieved quite simply by replacing (in the query.multiselect clause)
root.get(SampleModel_.mentor)
with something like
root.get(SampleModel_.mentor).get(User_.id)
Then, instead of returning all the fields of User, the request will only return the its id.
I also used a tuple query but, in my case, it was because my query was returning fileds from more than one entity.

Categories