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

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

Related

Handling multiple possible #RequestParam values when making request

I have an endpoint to get all Posts, I also have multiple #RequestParams used to filter and search for values etc.
The issue I'm having is that when trying to filter based on specific #RequestParams, I would need to have multiple checks to see whether that specific parameter is passed when calling the endpoint, so in my Controller I have something like this. The parameters are optional, I also have parameters for Pagination etc, but I left it out below.
I have these criteria:
#RequestParam(required=false) List<String> brand - Used to filter by multiple brands
#RequestParam(required=false) String province - Used to filter by province
#RequestParam(required=false) String city - Used to filter by city
// Using these 2 for getting Posts within a certain price range
#RequestParam(defaultValue = "0", required = false) String minValue - Used to filter by min price
#RequestParam(defaultValue = "5000000", required = false) String maxValue - Used to filter by max price
I also have this in my Controller when checking which of my service methods to call based on the parameters passed.
if(query != null) {
pageTuts = postService.findAllPosts(query, pagingSort);
} else if(brand != null) {
pageTuts = postService.findAllByBrandIn(brand, pagingSort);
} else if(minValue != null && maxValue != null) {
pageTuts = postService.findAllPostsByPriceBetween(minValue, maxValue, pagingSort);
} else if(brand != null & minValue != null & maxValue != null) {
pageTuts = postService.findAllPostsByPriceBetween(minValue, maxValue, pagingSort);
} else {
// if no parameters are passed in req, just get all the Posts available
pageTuts = postService.findAllPosts(pagingSort);
}
// I would need more checks to handle all parameters
The issue is that I'm struggling to find out, if I need this condition for each and every possible parameter, which will be a lot of checks and Repository/Service methods based on that parameter.
For example in my Repository I have abstract methods like these:
Page<Post> findAllByProvince(String province, Pageable pageable);
Page<Post> findAllByCity(String city, Pageable pageable);
Page<Post> findAllByProvinceAndCity(String province, String city, Pageable pageable);
Page<Post> findAllByBrandInAndProvince(List<String> brand, String province, Pageable pageable);
And I'd need much more so I could handle the other potential values, ie. findAllByPriceBetween(), findAllByCityAndPriceBetween(), findAllByProvinceAndPriceBetween()...
So I'd like some suggestions on how to handle this?.
Edit
Managed to get it working by overriding the toPredicate method as shown by #M. Deinum with some small tweaks according to my use case.
#Override
public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<>();
// min/max is never not set as they have default values
predicates.add(builder.between(root.get("price"), params.getMinValue(), params.getMaxValue()));
if (params.getProvince() != null) {
predicates.add(builder.equal(root.get("province"), params.getProvince()));
}
if (params.getCity() != null) {
predicates.add(builder.equal(root.get("city"), params.getCity()));
}
if (!CollectionUtils.isEmpty(params.getBrand())) {
Expression<String> userExpression = root.get("brand");
Predicate p = userExpression.in(params.getBrand());
predicates.add(p);
}
return builder.and(predicates.toArray(new Predicate[0]));
}
Create an object to hold your variables instead of individual elements.
Move the logic to your service and pass the object and pageable to the service
Ditch those findAll methods from your repository and add the JpaSpecificationExecutor in your extends clause.
In the service create Predicate and use the JpaSpecificationExecutor.findAll to return what you want.
public class PostSearchParameters {
private String province;
private String city;
private List<String> brand;
private int minValue = 0;
private int maxValue = 500000;
//getters/setters or when on java17+ use a record instead of class
}
Predicate
public class PostSearchParametersSpecification implements Specification {
private final PostSearchParameters params;
PostSearchParametersPredicate(PostSearchParameters params) {
this.params=params;
}
#Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<>();
// min/max is never not set as they have default values
predicates.add(builder.between(root.get("price", params.getMinValue(), params.getMaxValue());
if (params.getProvince() != null) {
predicates.add(builder.equal(root.get("province"), params.getProvince());
}
if (params.getCity() != null) {
predicates.add(builder.equal(root.get("city"), params.getCity());
}
if (!CollectionUtils.isEmpty(params.getBrand()) {
predicates.add(builder.in(root.get("brand")).values( params.getBrand());
}
return builder.and(predicates.toArray(new Predicate[0]));
}
}
Repository
public interface PostRepository extends JpaRepository<Post, Long>, JpaSpecificationExecutor<Post> {}
Service method
public Page<Post> searchPosts(PostSearchParameters params, Pageable pageSort) {
PostSearchParametersSpecification specification =
new PostSearchParametersSpecification(params)
return repository.findAll(specification, pageSort);
}
Now you can query on all available parameters, adding one is extending/modifying the predicate and you are good to go.
See also the Spring Data JPA Reference guide on Specifications

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 use OpenSessionInViewInterceptor?

Consider entity
public class User {
...
#OneToMany(cascade = CascadeType.ALL)
List<SocialCredential> credentialsList = new ArrayList<SocialCredential> ();
}
with DAO Implementation method
#Transactional
#Override
public User getUser(long id){
Session s = sessionFactory.getCurrentSession();
User u = (User) s.get(User.class, id);
return u;
}
and Controller
#Controller
public class DummyController {
#Autowired
UserDAO userDAO;
public void anyMethodAccessedByGetORPost(){
User u= userDAO.getUser(1L);
}
}
A simple query for entity User automatically fires query to initialize entity list of SocialCredential ? Ultimately it leads to LazyInitializationException.I came to know to know about OpenSessionInViewInterceptor which can solve the issue.How can I apply the same. I am already following http://www.jroller.com/kbaum/entry/orm_lazy_initialization_with_dao but with no success so far.
A simple query for entity User automatically fires query to initialize entity list of SocialCredential ?
It depends on underlying persistence API's default fetch type.
Refer this question
Ultimately it leads to LazyInitializationException -- This is probably you are trying access credentialsList collection after session has been closed.
Replace DAO's getUser(Long id) method with below code may solve LazyInitializationException.
#Transactional
#Override
public User getUser(long id){
Session s = sessionFactory.getCurrentSession();
User u = (User) s.get(User.class, id);
if (u != null) {
u.getCredentialsList(); //it loads the SocialCredentials before session closes.
}
return u;
}

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.

Spring data + Mongodb + query single value?

how to query a field instead of a whole object? I am trying to do something like that, want to see is that possible?
public BigInteger findUserIDWithRegisteredEmail(String email){
Query query = Query.query(Criteria.where("primaryEmail").is (email));
query.fields().include("_id");
return (BigInteger) mongoTemplate.find(query, BigInteger.class);
}
In method
find(Query query, Class<YourCollection> entityClass)
entityClass should be the corresponding collection, not the type of id.
If you are just trying to get id use
Query query = Query.query(Criteria.where("primaryEmail").is (email));
query.fields().include("_id");
mongoTemplate.find(query, <YourCollection>.class).getId();
If you only include _id, all the other fields will be null in your result.
If you want to avoid serialization, this is one way you could handle it:-
final List<String> ids = new ArrayList<String>();
mongoTemplate.executeQuery(query, "collectionName", new DocumentCallbackHandler() {
#Override
public void processDocument(DBObject dbObject) throws MongoException, DataAccessException {
ids.add(dbObject.get("_id").toString());
}
});

Resources