Spring Data JPA. How to get only a list of IDs from findAll() method - spring

I have a very complicated model. Entity has a lot relationship and so on.
I try to use Spring Data JPA and I prepared a repository.
but when I invoke a method findAll() with specification for the object a have a performance issue because objects are very big. I know that because when I invoke a method like this:
#Query(value = "select id, name from Customer ")
List<Object[]> myFindCustomerIds();
I didn't have any problems with performance.
But when I invoke
List<Customer> findAll();
I had a big problem with performance.
The problem is that I need to invoke findAll method with Specifications for Customer that is why I cannot use method which returns a list of arrays of objects.
How to write a method to finding all customers with specifications for Customer entity but which returns only an IDs.
like this:
List<Long> findAll(Specification<Customer> spec);
I cannot use in this case pagination.
Please help.

Why not using the #Query annotation?
#Query("select p.id from #{#entityName} p")
List<Long> getAllIds();
The only disadvantage I see is when the attribute id changes, but since this is a very common name and unlikely to change (id = primary key), this should be ok.

This is now supported by Spring Data using Projections:
interface SparseCustomer {
String getId();
String getName();
}
Than in your Customer repository
List<SparseCustomer> findAll(Specification<Customer> spec);
EDIT:
As noted by Radouane ROUFID Projections with Specifications currently doesn't work beacuse of bug.
But you can use specification-with-projection library which workarounds this Spring Data Jpa deficiency.

I solved the problem.
(As a result we will have a sparse Customer object only with id and name)
Define their own repository:
public interface SparseCustomerRepository {
List<Customer> findAllWithNameOnly(Specification<Customer> spec);
}
And an implementation (remember about suffix - Impl as default)
#Service
public class SparseCustomerRepositoryImpl implements SparseCustomerRepository {
private final EntityManager entityManager;
#Autowired
public SparseCustomerRepositoryImpl(EntityManager entityManager) {
this.entityManager = entityManager;
}
#Override
public List<Customer> findAllWithNameOnly(Specification<Customer> spec) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> tupleQuery = criteriaBuilder.createTupleQuery();
Root<Customer> root = tupleQuery.from(Customer.class);
tupleQuery.multiselect(getSelection(root, Customer_.id),
getSelection(root, Customer_.name));
if (spec != null) {
tupleQuery.where(spec.toPredicate(root, tupleQuery, criteriaBuilder));
}
List<Tuple> CustomerNames = entityManager.createQuery(tupleQuery).getResultList();
return createEntitiesFromTuples(CustomerNames);
}
private Selection<?> getSelection(Root<Customer> root,
SingularAttribute<Customer, ?> attribute) {
return root.get(attribute).alias(attribute.getName());
}
private List<Customer> createEntitiesFromTuples(List<Tuple> CustomerNames) {
List<Customer> customers = new ArrayList<>();
for (Tuple customer : CustomerNames) {
Customer c = new Customer();
c.setId(customer.get(Customer_.id.getName(), Long.class));
c.setName(customer.get(Customer_.name.getName(), String.class));
c.add(customer);
}
return customers;
}
}

Unfortunately Projections does not work with specifications. JpaSpecificationExecutor return only a List typed with the aggregated root managed by the repository ( List<T> findAll(Specification<T> var1); )
An actual workaround is to use Tuple. Example :
#Override
public <D> D findOne(Projections<DOMAIN> projections, Specification<DOMAIN> specification, SingleTupleMapper<D> tupleMapper) {
Tuple tuple = this.getTupleQuery(projections, specification).getSingleResult();
return tupleMapper.map(tuple);
}
#Override
public <D extends Dto<ID>> List<D> findAll(Projections<DOMAIN> projections, Specification<DOMAIN> specification, TupleMapper<D> tupleMapper) {
List<Tuple> tupleList = this.getTupleQuery(projections, specification).getResultList();
return tupleMapper.map(tupleList);
}
private TypedQuery<Tuple> getTupleQuery(Projections<DOMAIN> projections, Specification<DOMAIN> specification) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> query = cb.createTupleQuery();
Root<DOMAIN> root = query.from((Class<DOMAIN>) domainClass);
query.multiselect(projections.project(root));
query.where(specification.toPredicate(root, query, cb));
return entityManager.createQuery(query);
}
where Projections is a functional interface for root projection.
#FunctionalInterface
public interface Projections<D> {
List<Selection<?>> project(Root<D> root);
}
SingleTupleMapper and TupleMapper are used to map the TupleQuery result to the Object you want to return.
#FunctionalInterface
public interface SingleTupleMapper<D> {
D map(Tuple tuple);
}
#FunctionalInterface
public interface TupleMapper<D> {
List<D> map(List<Tuple> tuples);
}
Example of use :
Projections<User> userProjections = (root) -> Arrays.asList(
root.get(User_.uid).alias(User_.uid.getName()),
root.get(User_.active).alias(User_.active.getName()),
root.get(User_.userProvider).alias(User_.userProvider.getName()),
root.join(User_.profile).get(Profile_.firstName).alias(Profile_.firstName.getName()),
root.join(User_.profile).get(Profile_.lastName).alias(Profile_.lastName.getName()),
root.join(User_.profile).get(Profile_.picture).alias(Profile_.picture.getName()),
root.join(User_.profile).get(Profile_.gender).alias(Profile_.gender.getName())
);
Specification<User> userSpecification = UserSpecifications.withUid(userUid);
SingleTupleMapper<BasicUserDto> singleMapper = tuple -> {
BasicUserDto basicUserDto = new BasicUserDto();
basicUserDto.setUid(tuple.get(User_.uid.getName(), String.class));
basicUserDto.setActive(tuple.get(User_.active.getName(), Boolean.class));
basicUserDto.setUserProvider(tuple.get(User_.userProvider.getName(), UserProvider.class));
basicUserDto.setFirstName(tuple.get(Profile_.firstName.getName(), String.class));
basicUserDto.setLastName(tuple.get(Profile_.lastName.getName(), String.class));
basicUserDto.setPicture(tuple.get(Profile_.picture.getName(), String.class));
basicUserDto.setGender(tuple.get(Profile_.gender.getName(), Gender.class));
return basicUserDto;
};
BasicUserDto basicUser = findOne(userProjections, userSpecification, singleMapper);
I hope it helps.

Related

Spring JPA: find by multiple IDs with Pagination

How is it possible to apply the pagination to the below query:
#Repository
public interface PostRepository extends JpaRepository<Post, Long> {
#Query("select b from Building b where b.id in :ids" )
Page<Post> findByIds(#Param("ids") List<Long> postIdsList);
...
}
All the existing examples are based on the standard findAll method that accepts a Pageable object: public Page findAll(Pageable pageable);.
The questions are:
what the controller method signature should be
what the repository method parameters should be
how and what parameters should be passed into the controller method
should I always split the post IDs for every request
will Spring make a single query and keep all the found posts in memory or it will hit a query every time for every next/previous page? If so, how can it figure out the IDs to use to find the next/previous posts?
The initial implementation was as follows:
#RestController
class PostsController {
#Autowired
private PostService postService;
#GetMapping("/posts", params = "ids")
public List<Post> getPaginatedPosts(#RequestParam List<Long> ids) {
return postService.findPaginatedPosts(ids);
}
}
#Repository
#Repository
public interface PostRepository extends JpaRepository<Post, Long> {
#Query("select b from Building b where b.id in :ids" )
Page<Post> findByIds(#Param("ids") List<Long> postIdsList);
...
}
I omitted the code from the PostServiceImpl qui implements the PostService and just calls the PostRepository#findByIds method.
Try this:
#Repository
public interface PostRepository extends JpaRepository<Post, Long> {
#Query( "select o from Building b where id in :ids" )
Page<Post> findByIds(#Param("ids") List<Long> postIdsList,Pageable pageRequest);
...
}
In controller ask for pageSize and pageNo, if it is empty set a default value like pageNo = 0, pageSize=10.
pass these values to to service layer service should create pageable object call findByIds(ids, pagable); and return the page to controller.
you can refer this:
https://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-seven-pagination/
Here is the solution I came to you coupled with the above comments suggestions.
Define a repository either extending JpaRepository or PagingAndSortingRepositoryas follows:
#Repository
public interface PostRepository extends JpaRepository<Post, Long> {
#Query("select p from Post p where p.id in :ids" )
Page<Post> findByIds(#Param("ids") List<Long> postIdsList);
...
}
Create a service class and its implementation:
public interface PostService {
List<PostDTO> getPostsList(List<Long> ids, Pageable pageable);
...
}
#Service
#Slf4j
public class PostServiceImpl implements PostService {
...
#Autowired
private PostRepository postRepository;
...
#Override
public List<PostDTO> getPostsList(List<Long> ids, Pageable pageable) {
List<PostDTO> resultList = new ArrayList<>();
Page<Post> paginatedPosts = postRepository.findByIds(ids, pageable);
List<Post> posts = paginatedPosts.getContent();
posts.forEach(post -> resultList.add(convertToPostDTO(post)));
return resultList;
}
And finally, the PostsController part:
#RestController
#RequestMapping("/api")
class PostsController {
#Autowired
private PostService postService;
...
#GetMapping(value = "/posts", params = "ids")
public ResponseEntity <List<PostDTO>>getPostsList(#RequestParam List<Long> ids, Pageable pageable) {
List<PostDTO> postsList = postService.getPostsList(ids, pageable);
return new ResponseEntity<>(postsList, HttpStatus.OK);
}
The request should contain page and size URL parameters (by default, page is 0 and size is 20):
http://localhost:8080/api/posts?ids=1050,1049,1048,1043,1042,1041,1040,1039,1038&size=5&page=1&sort=id
In the above example, I had 9 records total and I put the parameters explicitly to limit the result list to 5 and display the second page only as well as to sort them by id.
If you don't provide them, the default values will be used (page = 0, size = 20).
To anyone coming here looking to pass a list of ids as a url-parameter like the question asker wants to do and the answer of belgoros explains:
Be aware of the url-max-length of 2048 characters.
So if your list of ids is long enough to require pagination, you probably also want to make the ids a body-parameter. This answer explains how to create body-parameters with spring: https://stackoverflow.com/a/22163492/7465516
I think this is important, because solutions that work on small data but unexpectedly fail on big data are the kind of thing that gets through testing and fails in production.
(I do not have the reputation to make this a comment, I hope this post is acceptable)
#Query( "select o from Building b where id in :ids", nativeQuery=true )
Page findByIds(#Param("ids") List postIdsList,Pageable pageRequest);

mapping entity to DTO with nested DTO

I have a relationnal (heavy) database model with a lot of table dependencies and foreign keys.
We have choosen to use DTOs in order to simplify data representation ton front and hide database mode complixity.
But we have DTO with nested DTO. And we have Mapper implementation classes to set data with small business/functional logic.
The question is if it is a good pratice that a mapper class calls mapper (etc.) or is it a best way to have a main class handling all mapper classes ? (Example 1 or2)
Example 1 :
#Component
public class ActorMapperImpl implements ActorMapper {
#Autowired
private InsurerMapper insurerMapper;
#Autowired
private PersonMapper personMapper;
#Autowired
private CorrespondentMapper correspondentMapper;
....
#Override
public ActorDto mapToDto(Acteur actor) {
final ActorDto actorDto;
if (actor != null) {
....
actorDto.setPerson(personMapper.personneToPersonDto(actor.getPersonne()));
if (actor.getInsurer() != null) {
actorDto.setInsurer(insurerMapper.entityToDto(actor.getInsurer()));
} else if (actor.getCorrespondantAssureur() != null) {
actorDto.setInsurer(correspondentMapper.correspondentToInsurerDto(actor.getCorrespondantAssureur()));
}
....
// intermediate
final Intermediaire intermediate = actor.getIntermediaire();
if (intermediate != null) {
.....
if (person != null) {
intermediateDto = personMapper.personneToPersonDto(person);
intermediateDto.setQuality(quality);
}
.....
}
.....
Example 2 :
#Service
public class FinancialSlipOrchestratorImpl implements FinancialSlipOrchestrator {
.....
#Autowired
private FinancialSlipMapper financialSlipMapper;
#Autowired
private PersonMapper personMapper;
..... some public / private methods
private FinancialSlipDto fullMapToDto(FinancialSlip financialSlip) {
.....
// Financial slip
var financialSlipDto = financialSlipMapper.mapToDto(financialSlip);
// person
financialSlipDto.setIssuerPerson(personMapper.personneToPersonDto(financialSlip.getIssuerPerson()));
....
// RIB
financialSlipDto.setRib(ribMapper.mapToDto(financialSlip.getRib()));
return financialSlipDto;
}
I would say that it's ok for one mapper to call another and think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(Acteur.class)
public interface ActorDto {
#IdMapping
Long getId();
String getName();
PersonDto getPerson();
default InsurerDto getInsurer() {
return getMainInsurer() != null ? getMainInsurer(): getCorrespondantAssureur();
}
#Mapping("insurer")
InsurerDto getMainInsurer();
InsurerDto getCorrespondantAssureur();
IntermediaireDto getIntermediaire();
#EntityView(Person.class)
interface PersonDto {
#IdMapping
Long getId();
String getName();
}
#EntityView(Insurer.class)
interface InsurerDto {
#IdMapping
Long getId();
String getName();
}
#EntityView(Intermediaire.class)
interface IntermediaireDto {
#IdMapping
Long getId();
String getName();
Integer getQuality();
PersonDto getPerson();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
ActorDto a = entityViewManager.find(entityManager, ActorDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
The best thing about this is, it will only fetch the data that is actually necessary.
If you use DTOs for flushing back changes as well, you will be delighted to hear that Blaze-Persistence Entity-Views also supports that in a very efficient manner. This will allow you to get rid of all those manually written mappers :)

Multiple aliases with QuerydslBinderCustomizer

I'm using QuerydslPredicate in my RestController on an entity which has a date object, I want to be able to query for a date before/after/between given dates, hoping to have something like
GET /problems?createdOnAfter=XXX
GET /problems?createdOnBefore=YYY
GET /problems?createdOnAfter=XXX&createdOnBefore=YYY
My entity has the date field createdOn and I was hoping I could customise bindings for an entity path using multiple aliases i.e. adding aliases createdOnAfter & createdOnBefore - it doesn't look like I can create multiple aliases though, e.g.
#Repository
public interface ProblemRepository extends JpaRepository<Problem, String>, QueryDslPredicateExecutor<Problem>,
QuerydslBinderCustomizer<QProblem> {
....
#Override
default void customize(QuerydslBindings bindings, QProblem root) {
bindings.bind(root.createdOn).as("createdOnAfter").first(TemporalExpression::after);
bindings.bind(root.createdOn).as("createdOnBefore").first(TemporalExpression::before);
}
}
The before alias is obviously overwriting the after one.
What's the correct approach to avoid having to manually create the predicates?
Why not using QueryDSL Predicate ? You could do :
#GetMapping("/problems")
#Timed
public ResponseEntity<List<ProblemDTO>> getAllProblems(
#RequestParam(required = false) LocalDateTime createdOnAfter,
#RequestParam(required = false) LocalDateTime createdOnBefore,
#ApiParam Pageable pageable) {
BooleanBuilder where = new BooleanBuilder();
if (startDate != null) {
where = where.and(problem.createdOn.after(createdOnAfter));
}
if (endDate != null) {
where = where.and(problem.createdOn.before(createdOnBefore));
}
Page<Donnee> page = problemRepository.findAll(where, pageable);
return new ResponseEntity<>(problemMapper.toDTO(page.getContent())), null, HttpStatus.OK);
}
Hope it helps,
Regards
How about adding new Entity properties and marking those as transient like this:
#Transient
private Instant createdOnBefore;
#Transient
private Instant createdOnAfter;
and then customise your repository class like this:
#Override
default void customize(final QuerydslBindings bindings, final QProblem root) {
bindings.bind(root.createdOnBefore).first((path, value) -> root.createdOn.goe(value));
bindings.bind(root.createdOnAfter).first((path, value) -> root.createdOn.loe(value));
}

How to orderBy a specified column in com.mysema.query.jpa.impl.JPAQuery?

I have written JPA query for User entity as
public List<User> findActiveUsers(String sortColumn) {
#Autowired
#PersistenceContext
EntityManager entityManager;
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
builder.and(user.status.eq("ACTIVE"));
JPAQuery jpaQuery = new JPAQuery(entityManager).from(user)
.where(builder);
jpaQuery.offset(100);
jpaQuery.limit(20);
//jpaQuery.orderBy([ORDER_SPECIFIER]);
return jpaQuery.list(user);
}
I want to get the orderBy as a String parameter to the function and from that I want to build the OrderSpecifier as required in jpaQuery.orderBy.
Is there a way to do it like this?
if(sortColumn ! null) {
jpaQuery.orderBy(user.getColumn(sortColumn).asc());
}
You have to create a PathBuilder for your entity, that will allow to get the property path by name. Something like
if(sortColumn != null) {
PathBuilder<User> userPathBuilder = new PathBuilder<User>(User.class, "user");
jpaQuery.orderBy(new OrderSpecifier(Order.ASC, userPathBuilder.get(sortColumn)));
}

Spring data JPA Specifications - #OneToMany dependency

i have a problem with getting List from entity Person using Spring data JPA specifications (because of pagination). I need to get all notes by person but dependency between these two entities is on Person side. I don't know how to create my Predicate because Note doesn't contain any attribute related to Person.
I simply can get List with Persons getter but i can't use this way because i need returned data paginated.
#Entity
public class Person implements Serializable {
#Id
private Long personId;
#OneToMany
#JoinColumn(name = "personId")
private List<Note> notes;
}
#Entity
public class Note implements Serializable {
#Id
private Long noteId;
}
Normally, I would write something like this, but i don't have an attribute person in Note and database can't be remapped at this stage.
public static Specification<Note> notesByPerson(final Long personId) {
return new Specification<Note>() {
#Override
public Predicate toPredicate(final Root<Note> root, final CriteriaQuery<?> query,
final CriteriaBuilder builder) {
final Path<Person> per = root.<Person> get("person");
return builder.equal(per.<Long> get("personId"), personId);
}
};
}
Thank you,
Zdend
Solved..
public static Specification<Note> notesByPerson(final Long personId) {
return new Specification<Note>() {
#Override
public Predicate toPredicate(final Root<Note> noteRoot, final CriteriaQuery<?> query,
final CriteriaBuilder cb) {
final Subquery<Long> personQuery = query.subquery(Long.class);
final Root<Person> person = personQuery.from(Person.class);
final Join<Person, Note> notes = person.join("notes");
personQuery.select(notes.<Long> get("noteId"));
personQuery.where(cb.equal(person.<Long> get("personId"), personId));
return cb.in(noteRoot.get("noteId")).value(personQuery);
}
};
}
I am not sure how to do that with Predicates, as I usually dont use them, but in JPQL (or HQL, which is similar), you can do something like this:
SELECT Note n FROM Person.notes WHERE XXXX
It is basically the same thing as doing this in SQL
SELECT n.noteId FROM person as p JOIN persons_notes pn ON pn.person=p.personId JOIN notes as n ON n.noteId=pn.noteId
I would venture a guess that the Predicate method has similar abilities as described above.

Resources