Add optional query parameter using spring data mongodb repository - java

I want to add optional query parameters using spring data mongodb.
Controller code:
#RestController
private final ActionService actionService;
#RequestMapping(value = "/action/{id}", method = RequestMethod.GET)
public ResponseEntity<List<Action>> getActionList(#PathVariable("id") long id,
#RequestParam(value = "actionType", required = false) ActionType actionType,
#RequestParam(value = " ", required = false) String[] params) {
List<Action> actionList = actionService.getAction(id, actionType, params);
return new ResponseEntity<>(actionList, HttpStatus.OK);
}
ActionServiceImpl.java
private ActionRepository actionRepository;
public List<Action> getAction(long id, ActionType type, String... var1) {
return actionRepository.getByActionType(id, type, var1.length > 0 ? var1[0] : "", var1.length > 1 ? var1[1] : "", var1.length > 2 ? var1[2] : "");
}
ActionRepository.java
#Repository
public interface ActionRepository extends MongoRepository<Action, String> {
#Query(value = "{ 'id' : ?0 , 'actionType' : ?1 , 'param1' : ?2 , 'param2': ?3 , 'param3' : ?4 } ")
List<Action> getByActionType(long id, ActionType type, String var1, String var2, String var3);
}
Note: 'id' is mandatory field and action type and params are optional. I want to get data based on 'id' whether I pass action type/params or not. Currently, i am getting null pointer exception in 'ActionServiceImpl' as I am not passing params and action Type. 'Action Type' is enumeration.
Can someone help me to change ActionRepository #Query tag so, that I can get data based on id without passing actionType or params. e.g. if I pass action type then mongo query should give result based on 'id $or actionType'.

You cannot achieve this using #Query. Other possible alternatives are
Create two methods in Repository class. One which takes only id and other which takes id and other arguments. And in your service class, you can decide which one to call based on the data in hand. (Not Scalable)
Use QueryDsl. With this you can create search criteria based on data you have dynamically. Some helpful links
https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#core.extensions.querydsl
http://www.baeldung.com/queries-in-spring-data-mongodb
You can use Example. Here is the link for documentation.(This has some limitations)
In my personal experience using QueryDsl is the best way to tackle these cases and it can be easily extended for further changes in requirement.

Related

Error that 2 parameter(s) provided but only 1 parameter(s) present in query

Hi, I have an error and i dont know how to fix it. I see other similar issues here, but they haven't been useful to me. So, I hope you could help me.
I have this DAO:
public interface GoalDao extends PagingAndSortingRepository<Goal, Long> {
Slice<Goal> findByCompanyId(Long companyId, PageRequest of);
}
And this method in my service:
#Override
#Transactional(readOnly = true)
public Block<Goal> findCompanyGoals(Long userId, Long companyId, int page, int size)
throws InstanceNotFoundException, PermissionException {
Company company = permissionChecker.checkCompanyExistsAndBelongsToUser(companyId, userId);
Slice<Goal> slice = goalDao.findByCompanyIdOrderByIdDesc(company.getId(), PageRequest.of(page, size));
return new Block<>(slice.getContent(), slice.hasNext());
}
So, this line:
Slice<Goal> slice = goalDao.findByCompanyIdOrderByIdDesc(company.getId(), PageRequest.of(page, size));
It is throwing me the following error:
testException = org.springframework.dao.InvalidDataAccessApiUsageException: At least 2 parameter(s) provided but only 1 parameter(s) present in query.; nested exception is java.lang.IllegalArgumentException: At least 2 parameter(s) provided but only 1 parameter(s) present in query.
Thank you.
In your GoalDao try using Pageable instead of PageRequest. So it will look like this:
public interface GoalDao extends PagingAndSortingRepository<Goal, Long> {
Slice<Goal> findByCompanyId(Long companyId, Pageable pageable);
}
Not sure why, but it worked for me and I hope it will help you too.
To add more information to #Bisha's answer : It doesn't work with PageRequest because Spring accepts only two types (in a method signature) to apply sorting and pagination : Pageable & Sort.
Pageable and Sort are considered special parameters that are handled diffrently by Spring (It won't try to bind them to the query).
In your question's example, Spring treats your PageRequest method parameter (in the method signature) as a normal method parameter (even if it implements Pageable) and tries to bind it to the final query hence the error message.
You can see where the check is done in Spring's source code (spring-data-jpa:2.4.10 and spring-data-commons:2.4.10):
In org.springframework.data.jpa.repository.query.ParameterBinderFactory, Spring checks if a parameter should be bound to the query by calling parameter.isBindable :
private static List<ParameterBinding> getBindings(JpaParameters parameters) {
List<ParameterBinding> result = new ArrayList<>();
int bindableParameterIndex = 0;
for (JpaParameter parameter : parameters) {
if (parameter.isBindable()) {
result.add(new ParameterBinding(++bindableParameterIndex));
}
}
return result;
}
isBindable in (org.springframework.data.jpa.repository.query.JpaParameters) will ultimately call isBindable in org.springframework.data.jpa.repository.query.Parameter:
public boolean isBindable() {
return !isSpecialParameter();
}
isSpecialParameter will return false when the parameter type is PageRequest which means isBindable will true and Spring will try to bind the parameter to the query instead of using it for pagination.
isSpecialParameter will return false because (in your case) the list TYPES doesn't contain the class PageRequest :
public boolean isSpecialParameter() {
return isDynamicProjectionParameter || TYPES.contains(parameter.getParameterType());
}
TYPES is initialized in a static block in org.springframework.data.repository.query.Parameter and has (in your case) only two values Pageable and Sort :
static {
List<Class<?>> types = new ArrayList<>(Arrays.asList(Pageable.class, Sort.class));
// consider Kotlin Coroutines Continuation a special parameter. That parameter is synthetic and should not get
// bound to any query.
ClassUtils.ifPresent("kotlin.coroutines.Continuation", Parameter.class.getClassLoader(), types::add);
TYPES = Collections.unmodifiableList(types);
}

InvalidDataAccessApiUsageException: Parameter value [...] did not match expected type [java.util.UUID (n/a)]

I'm using Criteria API to build named queries using filters. It works on normal String comparisons but when filtering on UUID it throws the following error:
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [67279329-5096-4196-9E73-748B33122CE2] did not match expected type [java.util.UUID (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [67279329-5096-4196-9E73-748B33122CE2] did not match expected type [java.util.UUID (n/a)]
There are several questions addressing this issue but none of them worked, I tried the following:
adding #Column(name = "id", updatable = false, nullable = false) to the entity field
adding #Type(type="org.hibernate.type.UUIDCharType") to the entity field
adding #Type(type="uuid-char") to the entity field
Foo entity:
#Entity
//lombok stuff
public class Foo {
#Id
private UUID id;
private String name;
//...
}
SQL Variant:
CREATE TABLE foo
(
id UUID NOT NULL PRIMARY KEY,
name VARCHAR NOT NULL,
...
);
FooController:
#GetMapping(value = "/foo")
public ResponseEntity<List<Foo>> findFoos(#RequestParam Map<String, String> filterArguments) {
FooFilter filter = filterMapper.map(filterArguments);
FooSpecification spec = new FooSpecification(filter);
List<Foo> foos = fooRepo.findAll(spec);
//...
}
FooSpecification:
public class FooSpecification extends SpecificationHelper implements Specification<Foo> {
private final FooFilter filter;
public FooSpecification(FooFilter filter) {
this.filter = filter;
}
#Override
public Predicate toPredicate(Root<Foo> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
Predicate predicate = null;
predicate = createIdPredicate(root, filter, criteriaBuilder, predicate);
predicate = createNamePredicate(root, filter, criteriaBuilder, predicate);
// ...
return predicate;
}
private Predicate createIdPredicate(Root<Foo> foo, FooFilter filter, CriteriaBuilder cb, Predicate predicate) {
Predicate returnPredicate = predicate;
if (StringUtils.isNotBlank(filter.getId()))
returnPredicate = addAndPredicate(cb, predicate, cb.like(cb.upper(foo.get("id")), "%" + filter.getId().toUpperCase() + "%"));
return returnPredicate;
}
private Predicate createNamePredicate(Root<Foo> foo, FooFilter filter, CriteriaBuilder cb, Predicate predicate) {
Predicate returnPredicate = predicate;
if (StringUtils.isNotBlank(filter.getName()))
returnPredicate = addAndPredicate(cb, predicate, cb.like(cb.upper(foo.get("name")), "%" + filter.getName().toUpperCase() + "%"));
return returnPredicate;
}
}
addAndPredicate is a simple helper method that just uses criteriaBuilder.and(predicate,newPredicate)
FooFilter only has String fields
The problem is that your FooFilter contains only String fields, and you are trying to compare String id with a UUID object in createIdPredicate(). And that's why the exception is thrown. There are 2 solutions:
Either, you should replace filter.getId().toUpperCase() part in createIdPredicate() with UUID.fromString(filter.getId()) as suggested by #tentacle
Or, change your FooFilter filter so that the id would be of type java.util.UUID.
By the way, IMHO, comparing UUIDs with like is not a good idea, because one UUID can never be like another UUID, each UUID object is unique. Your predicate should check the equality of ids transferred by FooFilter and the one from DB.
I fixed the issue using typecasting provided by the Criteria API.
Before:
addAndPredicate(..., ..., cb.like(cb.upper(foo.get("id")), ...));
After:
addAndPredicate(..., ..., cb.like(cb.upper(foo.get("id").as(String.class)), ...));
From: https://docs.oracle.com/javaee/6/api/javax/persistence/criteria/Expression.html#as(java.lang.Class)
<X> Expression<X> as(java.lang.Class<X> type)
Perform a typecast upon the expression, returning a new expression
object. This method does not cause type conversion: the runtime type
is not changed. Warning: may result in a runtime failure.
I just had a similar trouble and #edward answer actually helped me a lot :)
One thing that I have changed is the use of cb.equal instead of the cb.like, because when searching for a specific id the equal way seems to be more efficient.
One other thing I've done is to parse only once my filter param with UUID.fromString(uuid_string_param) so that I just parse the param once and dont have to cast database UUID field to string in order to get the match.
Cheers.

Spring Boot handling multiple parameters in a get request

I am new to using Spring boot framework.
I want to create a #GetMapping where based on what user enters in the parameter either Property1 Name(String) or Protery2 Designation(String) or Property3 Salary(Integer) the method should be able to get the List of employees based on one or more properties.
I can create individual methods but I do not want to do that.
I want to do something like this:
#GetMapping("/employee")
public List<Employee> getEmployee(Params parameters)
{
// Filter the list based on parameters provided and return the list
}
Also, I am not understanding how to handle parameters
for example, if it is an integer there is only one column but if the user enters string there are two columns.
If the user does not specify the parameter name I have to handle that.
You can use #RequestParam Map<String, String> params to bind all parameters to one variable
E.g.
#RequestMapping(value="/params", method = RequestMethod.GET)
public ResponseEntity getParams(#RequestParam Map<String, String> params ) {
System.out.println(params.keySet());
System.out.println(params.values());
return new ResponseEntity<String>("ok", HttpStatus.OK);
}
You can define the three parameters using the #RequestParam annotation and check which one is non-empty:
#GetMapping("/employee")
public List<Employee> getEmployee(#RequestParam(defaultValue = "empty") String name, #RequestParam(defaultValue = "empty") String designation, ....
{
// check which one is not empty and perform logic
if (!name.equals("empty")) {
// do something
}
}
Regarding which parameter the user chooses: you can make a drop-down menu or a simple-radio selection, where the user chooses the search criteria himself (and where each criterion is mapped by a request parameter). For example:

JPA Query Methods: findTopX where X is a given (variable) number?

I'm creating a RESTful API. I need to to be able to send a number to the API specifying the amount of news items it needs to return.
I already looked for some things and according to the documentation it is possible to limit your results, but I can't find anything about how to make it variable. So my question is: Is this even possible to do or do I need to write my own custom query for this.
I already tried things like:
Iterable<NewsItem> findTopAmountByOrderByDatetimeDesc(Integer amount); where Amount would be filled in by the given Integer "amount", but maybe it is stupid to even think this would be possible although it would be a nice feature in my opinion.
What I have now (not variable, so it doesn't care about the given number):
NewsItemApi:
#RequestMapping(value = "/newsitems/amount",
produces = { "application/json" },
method = RequestMethod.GET)
NewsItemRepository:
Iterable<NewsItem> findTop2ByOrderByDatetimeDesc();
NewsItemApiController:
public ResponseEntity<Iterable> getLastNewsItems(#NotNull #ApiParam(value = "amount of news items to return", required = true) #Valid #RequestParam(value = "amount", required = true) Integer amount) {
return ResponseEntity.accepted().body(this.newsItemRepository.findTop2ByOrderByDatetimeDesc());
}
You can create repository and use #Query annotation to make custom requests, something like this:
public interface NewsRepository extends JpaRepository<News, Long> {
#Query(value = "SELECT * FROM NEWS ODER BY FIELD_YOU_WANT_TO_ODER_BY DESC LIMIT ?1", nativeQuery = true)
List<News> getLatestNews(Integer amount);
}
You would have to use #Query, something like:
#Query("select n from NewsItem n order by n.datetime desc limit :num", nativeQuery = true)
Iterable<NewsItem> findTopXByOrderByDatetimeDesc(#Param("num") int num);
Of course use limit keyword according to your database.

Spring jdbcTemplate passing constant value to an query method

Is it possible to mention an values as constant in the function. I have an entity say, EntityClass. Then a repository interface over it, say
interface EntityClassRepository extends CrudRepository<EntityClass, String> {
List<EntityClass> findById(String id);
}
Can I hard code some value in the method like,
List<EntityClass> findByIdAndActiveFlagAsY(String id);
To only query the ones where active flag column's value is 'Y'.
Is it possible?
No, spring data not support AS keywords inside method names, you can check all the Supported keywords inside method names :
So to solve your problem you can use custom query instead :
#Query("from EntityClass e WHERE id = :id AND ActiveFlag = 'Y'")
public List<EntityClass> findByIdAndActiveFlagAsY(#Param("id") String id);
Or you can use Equals keywords but you should to send the value in the method :
#Query("from EntityClass e WHERE id = :id AND ActiveFlag = :flag")
public List<EntityClass> findByIdAndActiveFlagEquals(#Param("id") String id, #Param("flag") String flag);

Categories

Resources