Spring JPA: find by multiple IDs with Pagination - spring-boot

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);

Related

Spring Data: can I create a method in the #Repository with a name what return list of object if String equals to constant?

Guess easier if I show you my example:
#Entity
class User {
Long id;
Status status;
}
enum Status {
NEW("N"), DELETED("D")
}
I have an AttributeConverter on Status so in DB the enum is stored with one character.
In my database I have entities like:
Table user
------------
Id Status
1 N
2 N
3 D
4 N
5 D
I want a method that list the Users with Status D. Something like this:
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByStatusEqualsD();
or
List<User> findByStatusEqualsDeleted();
problem is these are not working
}
I could write this:
List<User> findByStatus(Status status);
And call it as repo.findByStatus(Status.DELETED) but I want a method what returns only the deleted users.
If I call it as repo.findByStatus(Status.NEW) then it will return the new users.
I prefer to not write a #Query, I hope it is possible what I'm asking without doing it...
Thanks in advance.
Such behavior is not supported.
Method name is translated into JPQL expression (which is the same as used in #Query) with parameters in it (if needed) so you have to provide these. (https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation)
If you want query parameters to be hardcoded - #Query is what you need.
Alternatively you can have default method in your repository calling the parametrized one as mentioned here JpaRepository with Enum: findAllByOfferState_ACTIVE. NoSuchElementException
Easy,
You don't need a repo for that. Create a Service instead:
public interface UserDAOService{
List<User> getAllDeletedUsers();
}
And then just implement it with hardcoded findByStatus method from repo:
#Service
public class UserDAOServiceImpl implements UserDAOService{
private final UserRepository userRepository;
public UserDAOServiceImpl(UserRepository userRepository) {
this.userRepository= userRepository;
}
#Override
public List<Author> getAllDeletedUsers();
return userRepository.findByStatus(Status.DELETED);
}

Trying to lazy load and return a list inside a #transactional annotated method and getting a no session error

this works, but when I try to remove the courses.size i get no session error.
#Transactional
public List<Course> initiateCourses(Long id) {
Instructor instructor = instructorRepository.findById(2L).get();
List<Course> courses = instructor.getCourses();
courses.size();
return courses;
}
It works, but it feels like a hack.
also I found another way to load lazy collections.
#Repository
public interface InstructorRepository extends CrudRepository<Instructor, Long> {
#Query("SELECT p FROM Instructor p LEFT JOIN FETCH p.courses WHERE p.id = ?1")
Optional<Instructor> findByIdAndFetchCourseEagerly(Long id);
}
which one should I use performance wise? or is there a better way of fetching lazy initialized objects.
You can work with #EntityGraph to specify which parts of the object hierarchy should be fetched:
#Repository
public interface InstructorRepository extends CrudRepository<Instructor, Long> {
#EntityGraph(type = EntityGraphType.FETCH, attributePaths = {"courses"})
Optional<Instructor> findById(Long id);
}
See: EntityGraph (Spring Data JPA)
This proxy will not be initialized till you retrieve element from it or call size() method.
There are several ways to solve your problem without "magic":
Explicitly call Hibernate.init() and pass in it your collection-proxy before returning it from method.
Use entity graph as Peter Walser suggested

Spring data JPA + Native query - replace query string depending on profile

I have service:
#Service
public class MessageServiceImpl implements MessageService {
private final MessageRepository smevMessageRepository;
private final Environment environment;
public MessageServiceImpl(MessageRepository messageRepository, Environment environment) {
this.messageRepository= messageRepository;
this.environment = environment;
}
#Override
public List<Message> findReadyToSend() {
if (environment.acceptsProfiles("postgre")) {
return messageRepository.findReadyToSendPostgre();
}
return messageRepository.findReadyToSendOracle();
}
And It is my repository:
#Repository
public interface MessageRepository extends JpaRepository<Message, String> {
#Query(value = "select sm.* from MESSAGES sm ...", nativeQuery = true)
List<Message> findReadyToSendOracle();
#Query(value = "select sm.* from MESSAGES sm ...", nativeQuery = true)
List<Message> findReadyToSendPostgre();
If I start spring boot server with oracle profile I call findReadyToSendOracle method and if postgre profile - findReadyToSendPostgre method. It work. But this solution is bad. I think. Because I write hardcode for profile check. and my repository has 2 methods for different DB.
How to implement this correctly?
What are the problems you are facing while adapting to JPQL? Using native/custom functions? It might look way too difficult, but you might find a way using criteria + the function function from JPA 2.1+, take a look at this article.
On the other hand, I found an old workaround of mine here that might help. There is a simple way to solve that using a few shortcuts with the #Profile annotation and some extra interfaces.
If you provide an interface with your expected native query method that extends JpaRepository, like this:
#NoRepositoryBean
public interface MessageRepository extends JpaRepository<Message, String>{
List<Message> findByReady();
}
Note the #NoRepositoryBean, avoiding duplicate beans with profile specialization.
Then, just provide your implementations according to your needs:
#Repository
#Profile("oracle")
public interface MessageOracleRepository extends MessageRepository {
#Query(value = "select m.* from Message m where m.ready = false", nativeQuery = true)
List<Message> findByReady();
}
... and ...
#Repository
#Profile("mysql")
public interface MessageMySQLRepository extends MessageRepository {
#Query(value = "select m.* from Message m where m.ready = true", nativeQuery = true)
List<Message> findByReady();
}
Now you will only need to provide the desired profile, inject and use the correct native queries.
As you can see I simplified the queries, for the sake of simplicity. Take a look at this repository with the adapted code.

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

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.

How to use Or condition in PagingAndSortingRepository

I need to use two or more conditions on different fields in a Repository that extends PagingAndSortingRepository.
For example:
public interface PersonRepository extends PagingAndSortingRepository<Person,String> {
Page<Person> findByCreatedUser(String createdUserId, Pageable pageable);
}
This method should filter by createdUserId=Person.createdUserId or createdUserId=Person.userId.
How can this be done?
Never mind. I found the answer.
Page findByCreatedUserIdOrUserId(String createdUserId, String userId, Pageable pageable);
Just define a method and add the #Query annotation as outlined in the documentation:
public interface PersonRepository extends PagingAndSortingRepository<Person,String> {
#Query("......")
Page<Person> findByCreatedUser(String createdUserId, String userId, Pageable pageable);
}
http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.at-query

Resources