Avoid duplicate code in criteria builder query in spring data jpa - spring

I am using Spring Boot (1.5.14.RELEASE) and Spring data Jpa with Java 1.8. I want to avoid duplicate code.
Below query fetches employee details. It's working fine.
Class EmployeeDAO:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<EmployeeDto> cq = cb.createQuery(EmployeeDto.class);
Root<EmployeeInfo> root = cq.from(EmployeeInfo.class);
Join<EmployeeInfo, SalaryInfo> SalaryType = root.join("SalaryInfo");
Join<EmployeeInfo, CompanyInfo> Company = root.join("CompanyInfo");
cq.select(cb.construct(EmployeeDto.class,
root.get("FirstName"),
SalaryType.get("Salary"),
Company.get("CompanyName")))
.where(specification.toPredicate(root, cq, cb))
.orderBy(cb.asc(root.get("FirstName")));
Another function in same class in also making the almost 90% same criteria builder query as shown below:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<EmployeeDto> cq = cb.createQuery(EmployeeDto.class);
Root<EmployeeInfo> root = cq.from(EmployeeInfo.class);
Join<EmployeeInfo, SalaryInfo> SalaryType = root.join("SalaryInfo");
Join<EmployeeInfo, CompanyInfo> Company = root.join("CompanyInfo");
Join<EmployeeInfo, UserInfo> User = root.join("UserInfo");
cq.select(cb.construct(EmployeeDto.class,
root.get("FirstName"),
SalaryType.get("Salary"),
Company.get("CompanyName"),
User.get("Age")))
.where(specification.toPredicate(root, cq, cb))
.orderBy(cb.asc(root.get("FirstName")));
The code in both function is same except that below code is making join with UserInfo table to get user age. All other code is duplicate. Can you tell me how can I avoid this duplicate code.

Something like this:
public class EmployeeDAO {
EntityManager em;
Specification specification;
public void get(boolean withUsers) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<EmployeeDto> cq = cb.createQuery(EmployeeDto.class);
Root<EmployeeInfo> root = cq.from(EmployeeInfo.class);
Join<EmployeeInfo, SalaryInfo> salaryType = root.join("SalaryInfo");
Join<EmployeeInfo, CompanyInfo> company = root.join("CompanyInfo");
List<Selection> sels = new ArrayList<>();
Collections.addAll(sels,
root.get("FirstName"),
salaryType.get("Salary"),
company.get("CompanyName")
);
if (withUsers) {
Join<EmployeeInfo, UserInfo> user = root.join("UserInfo");
sels.add(user.get("Age"));
}
cq.select(cb.construct(EmployeeDto.class,
sels.toArray(new Selection[0])
))
.where(specification.toPredicate(root, cq, cb))
.orderBy(cb.asc(root.get("FirstName")));
}
}

Related

Criteria API deprecated, how to convert ti CriteriaQuery instead?

I am migrating an old existing project to Springboot.
I have following method:
public List<T> findAll(Class<?> clazz) {
Criteria search = entityManager.unwrap(Session.class).createCriteria(clazz);
List<T> entities = search.list();
return entities;
}
And another one:
public County findByName(String name) {
Criteria search = entityManager.unwrap(Session.class).createCriteria(County.class).add(Restrictions.eq("county", name));
County county = (County) search.uniqueResult();
return county;
}
This works but when i run the project I get following warning: Hibernate's legacy org.hibernate.Criteria API is deprecated; use the JPA javax.persistence.criteria.CriteriaQuery instead
How can i convert it to using javax.persistence.criteria.CriteriaQuery and still be able to receive the same result? I have never been using both of these APIs.
Thank for the help!
find all:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Country> cq = cb.createQuery(Country.class);
Root<Country> rootCountry= cq.from(Country.class);
cq.select(rootCountry);
List<Country> result = entityManager.createQuery(cq).getResultList();
find by name:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Country> cq = cb.createQuery(Country.class);
Root<Country> rootCountry= cq.from(Country.class);
cq.select(rootCountry);
cq.where(cb.equal(rootCountry.get(Country_.name),NAME))
// if you don't use metamodel (although it is recommended) -> cq.where(cb.equal(rootCountry.get("name"),NAME))
Country result = entityManager.createQuery(cq).getSingleResult();
Can you read more about this in this link https://docs.jboss.org/hibernate/entitymanager/3.5/reference/en/html/querycriteria.html

Unexpected end of subtree with Criteria Query using isEmpty

I've ran into a problem while developing a Spring Boot application with Criteria API.
I'm having a simple Employer entity, which contains a set of Job.ID (not entities, they're pulled out using repository when needed). Employer and Job are in many to many relationship. This mapping is only used on a purpose of finding Employee with no jobs.
public class Employer {
#ElementCollection
#CollectionTable(
name = "EMPLOYEE_JOBS"
joinColumns = #JoinColumn(name = "EMP_ID")
#Column(name = "JOB_ID")
private final Set<String> jobs = new HashSet<>(); //list of ids of jobs for an employee
}
Then I have a generic function, which returns a predicate (Specification) by a given attributePath and command for any IEntity implementation.
public <E extends IEntity> Specification<E> createPredicate(String attributePath, String command) {
return (r, q, b) -> {
Path<?> currentPath = r;
for(String attr : attributePath.split("\\.")) {
currentPath = currentPath.get(attr);
}
if(Collection.class.isAssignableFrom(currentPath.getJavaType())) {
//currentPath points to PluralAttribute
if(command.equalsIgnoreCase("empty")) {
return b.isEmpty((Expression<Collection<?>>)currentPath);
}
}
}
}
If want to get list of all employee, who currently have no job, I wish I could create the predicate as follows:
Specification<Employer> spec = createPredicate("jobs", "empty");
//or if I want only `Work`s whose were done by employer with no job at this moment
Specification<Work> spec = createPredicate("employerFinished.jobs", "empty");
This unfortunately does not works and throws following exception:
org.hibernate.hql.internal.ast.QuerySyntaxException:
unexpected end of subtree
[select generatedAlias0 from Employer as generatedAlias0
where generatedAlias0.jobs is empty]
Is there a workaround how to make this work?
This bug in Hibernate is known since September 2011, but sadly hasn't been fixed yet. (Update: this bug is fixed as of 5.4.11)
https://hibernate.atlassian.net/browse/HHH-6686
Luckily there is a very easy workaround, instead of:
"where generatedAlias0.jobs is empty"
you can use
"where size(generatedAlias0.jobs) = 0"
This way the query will work as expected.

JPA Criteria api - Total records for concrete query within pagination

I am programming function for pagination in my repository layer. Function receive as parameters spring's pageable object and some value like this:
public Page<Foo> filterFoo(Pageable pageable, String value) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Foo> fooQuery = cb.createQuery(Foo.class);
Root<Foo> foo = fooQuery .from(Foo.class);
fooQuery .where(adding predicate for match value);
List<Foo> result = entityManager.createQuery(fooQuery )
.setFirstResult((pageable.getPageNumber() - 1) * pageable.getPageSize())
.setMaxResults(pageable.getPageSize())
.getResultList();
return new PageImpl<>(result, pageable, xxxx);
}
Function return spring's PageImpl object filled with my result. To PageImpl I also need set total count of objects which suit predicates. This count number have to be of course without maxResult and firstResult. Is possible create another database call with my fooQuery to get total database records for that query without limit? What is the best practise to use pageable and criteria api in JPA? Thank you in advice.
Because generated SQL uses aliases - you may need make separate query for get total count of rows.
For example:
CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
countQuery.select(cb.count(countQuery.from(Foo.class)));
if (Objects.nonNull(filters)) {
countQuery.where(filters);
}
return new PageImpl<>(result, pageable, em.createQuery(countQuery).getSingleResult());
where filters is equal to your adding predicate for match value expression.
Also, you may use a TupleQuery with custom SQL function for calculate count of rows in one select query.
Like this:
public class SqlFunctionsMetadataBuilderContributor implements MetadataBuilderContributor {
#Override
public void contribute(MetadataBuilder metadataBuilder) {
metadataBuilder.applySqlFunction(
"count_over",
new SQLFunctionTemplate(
StandardBasicTypes.LONG,
"(count(?1) over())"
)
);
}
}
and Criteria:
public Page<Foo> findAll(Specification<Foo> specification, Pageable pageable) {
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Foo.class> fooRoot = cq.from(Foo.class);
cq.select(cb.tuple(fooRoot, cb.function("count_over", Long.class, fooRoot.get("id"))));
Predicate filters = specification.toPredicate(fooRoot, cq, cb);
if (Objects.nonNull(filters)) {
cq.where(filters);
}
TypedQuery<Tuple> query = em.createQuery(cq);
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
List<Tuple> result = query.getResultList();
if (result.isEmpty()) {
return new PageImpl<>(List.of());
}
return new PageImpl<>(
result.stream().map(tuple -> (Foo) tuple.get(0)).collect(toUnmodifiableList()),
pageable,
(long) result.get(0).get(1)
);
}
See more about SQLFunction: https://vladmihalcea.com/hibernate-sql-function-jpql-criteria-api-query/ and Custom SQL for Order in JPA Criteria API

Select values from related entities by related entity id

I am trying to select child element (many to one) modsVersionses of parent element Mods with Hibernate 5. In the same time, modsVersionses should be filtered by modsVersionses.id.
I've tried something like this:
Session s = sessionFactory.getCurrentSession();
CriteriaBuilder criteriaBuilder = s.getEntityManagerFactory().getCriteriaBuilder();
CriteriaQuery<Mods> criteriaQuery = criteriaBuilder.createQuery(Mods.class);
Root<Mods> root = criteriaQuery.from(Mods.class);
criteriaQuery.select(root.get("modsVersionses"));
criteriaQuery
.where(criteriaBuilder.equal(root.get("id"), id)).where(criteriaBuilder.equal(root.get("modsVersionses").get("id"), versionId));
List<Mods> resultList = s.createQuery(criteriaQuery).getResultList();
Mods mod = resultList.get(0);
But I got IllegalStateException on code .where(criteriaBuilder.equal(root.get("modsVersionses").get("id"), versionId)) .
How to correctly filter child elements by id?
Thank you!
Update 1: With #JB Nizet help I made changes and my code now is:
Session s = sessionFactory.getCurrentSession();
CriteriaBuilder cb = s.getEntityManagerFactory().getCriteriaBuilder();
CriteriaQuery<Mod> cq = cb.createQuery(Mod.class);
SetJoin<Mod, ModVersion> modsVersionsesNode = cq.from(Mod.class).join(Mod_.modsVersionses);
cq.where(
cb.equal(
modsVersionsesNode.get(ModVersion_.id), versionId
)
);
List<Mod> mods = s.createQuery(cq).getResultList();
But I still can't understand, how to select not Mod object, but to select only ModVersion?
Solved, thanks to #JB Nizet :
Session s = sessionFactory.getCurrentSession();
CriteriaBuilder cb = s.getEntityManagerFactory().getCriteriaBuilder();
CriteriaQuery<ModVersion> cq = cb.createQuery(ModVersion.class);
Root<Mod> root = cq.from(Mod.class);
SetJoin<Mod, ModVersion> join = root.join(Mod_.modsVersionses);
cq.select(
join
);
cq.where(
cb.equal(
join.get(ModVersion_.id), versionId
)
);
List<ModVersion> mods = s.createQuery(cq).getResultList();

How to execute a JPAQuery with pagination using Spring Data and QueryDSL

I have this request working good with queryDSL :
Iterable<AO> query_result = new JPAQuery(entityManager).from(ao)
.leftJoin( ao.lots , lot )
.leftJoin( ao.acs , ac )
.where(where).distinct()
.list(ao);
But what is its equivalent if we use it with spring data jpa
ao_respository.findAll(Predicate arg0, Pageable arg1);
Because i want to return a Page and just with querydsl it doesn't implement Page without spring data jpa.
I try to put my where in Predicate arg0 but i got this exception
Undeclared path 'lot '. Add this path as a source to the query to be able to reference it
where lot is declared as QLot lot = QLot.lot;
I created my own Page class and executed the query like this:
JPAQuery query = new JPAQuery(entityManager).from(ao)
.leftJoin( .. ).fetch()
.leftJoin( .. ).fetch()
...
.where(where)
MaPage<AO> page = new MaPage<AO>();
page.number = pageNumber+1;
page.content = query.offset(pageNumber*pageSize).limit(pageSize).list(ao);
page.totalResult = query.count();
My Page class:
public class MaPage<T> {
public List<T> content;
public int number;
public Long totalResult;
public Long totalPages;
...
}
It works but I got this warning
nov. 21, 2014 6:48:54 AM
org.hibernate.hql.internal.ast.QueryTranslatorImpl list WARN:
HHH000104: firstResult/maxResults specified with collection fetch;
applying in memory!
Returning a Page:
JPAQuery query =
...
.orderBy(getOrderSpecifiers(pageable, MyEntity.class))
.limit(pageable.getPageSize())
.offset(pageable.getOffset());
long total = query.fetchCount();
List<MyEntity> content = query.fetch();
return new PageImpl<>(content, pageable, total);
And I created this function to get OrderSpecifier:
private OrderSpecifier[] getOrderSpecifiers(#NotNull Pageable pageable, #NotNull Class klass) {
// orderVariable must match the variable of FROM
String className = klass.getSimpleName();
final String orderVariable = String.valueOf(Character.toLowerCase(className.charAt(0))).concat(className.substring(1));
return pageable.getSort().stream()
.map(order -> new OrderSpecifier(
Order.valueOf(order.getDirection().toString()),
new PathBuilder(klass, orderVariable).get(order.getProperty()))
)
.toArray(OrderSpecifier[]::new);
}
If you have a working, complex query in querydsl and you want to use springdata pagination, you have to:
make your querydsl/repository method return Page<T>
Page<YourEntity> yourSelect(Pageable aPageable)
use querydsl offset and limit to page your result set
List<YourEntity> theResultList = jpaQueryFactory
.select(<whatever complext jpaquery you like>)
.offset(aPageable.getOffset())
.limit(aPageable.getPageSize())
.fetch();
provide a LongSuplier counting all available results with respect to your query and use PageableExecutionUtils to return the result as Page
final long theCount = jpaQueryFactory
.selectFrom(<your select to count all results>)
.fetchCount();
return PageableExecutionUtils.getPage(theResultList, aPageable, () -> theCount);

Resources