Spring repository filter by using an object - spring

So I'm making use of PaginAndSortingRepository<> in Spring. Now I have an object that is bound to this repository. Let's call it AuditLogEntry. So my becomes PagingAndSortingRepository<AuditLogEntry, Long>. I need to apply some filters to this repository, so I created a new model called AuditLogEntryFilter. This is not bound to the AuditLogEntry itself, but I want to use this object to filter out my AuditLogEntry's. The first approach that I've came up with myself is make methods for each of the filter's properties that can be different. This becomes very complicated when the filter' size is expanding. The thing I want to do is this:
List<AuditLogEntry> filterByAuditLogEntryFilter(AuditLogEntryFilter filter);
Does anybody has an idea how to accomplish this? I already looked into make an implementation of the PagingAndSortingRepository, but then I have to implement all of the other methods. Which I don't want to do.
Update solution:
Thanks to #Nikolay Rusev, I come up with the following solution:
In my controller, I call my repository like this:
#RequestMapping(value = "/auditentries", method = RequestMethod.POST)
public String processFilter(final AuditLogEntryFilter filter, BindingResult bindingResult, ModelMap
modelMap, Pageable pageable) {
Page<AuditLogEntry> auditLogEntryPage = auditLogEntryRepository.findAll(auditLogEntryFilterSpec(filter), pageable);
modelMap.addAttribute("filter", filter);
return "audit";
}
Then I made a spec with roughly the following code:
public static Specification<AuditLogEntry> auditLogEntryFilterSpec(AuditLogEntryFilter filter) {
return new Specification<AuditLogEntry>() {
#Override
public Predicate toPredicate(Root<AuditLogEntry> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicateList = new ArrayList<Predicate>();
if(filter.getDateFrom() != null || filter.getDateTo() != null) {
if(filter.getDateFrom() != null && filter.getDateTo() == null)
{
Predicate auditLogEntryFromDatePredicate = criteriaBuilder.greaterThan(root.get("timestamp"), filter.getDateFrom());
predicateList.add(auditLogEntryFromDatePredicate);
}
if(filter.getDateFrom() == null && filter.getDateTo() != null) {
Predicate auditLogEntryToDatePredicate = criteriaBuilder.lessThan(root.get("timestamp"), filter.getDateTo());
predicateList.add(auditLogEntryToDatePredicate);
}
if(filter.getDateFrom() != null && filter.getDateTo() != null) {
Predicate auditLogEntryFromDatePredicate = criteriaBuilder.greaterThan(root.get("timestamp"), filter.getDateFrom());
Predicate auditLogEntryToDatePredicate = criteriaBuilder.lessThan(root.get("timestamp"), filter.getDateTo());
predicateList.add(auditLogEntryFromDatePredicate);
predicateList.add(auditLogEntryToDatePredicate);
}
}
Predicate[] predicates = new Predicate[predicateList.size()];
predicateList.toArray(predicates);
return criteriaBuilder.and(predicates);
}
};
}
Now it retrieves the right results according to the filters.

May be you need to look at spring data specifications. here is the spring documentation: http://docs.spring.io/spring-data/jpa/docs/1.5.0.RELEASE/reference/html/jpa.repositories.html#specifications
here is a good articles about it:
http://info.michael-simons.eu/2014/09/24/creating-specification-instances-for-spring-data-jpa-with-spring-mvc/
http://blog.kaczmarzyk.net/2014/03/23/alternative-api-for-filtering-data-with-spring-mvc-and-spring-data/
hope it helps

Related

Spring Data JPA - #Query with null values based on user input

The user has a form:
checkbox with fields status and priority
submit btn
Objective: Query DB based on these values.
If one of them is null or false, the criteria should be ignored.
Ex: status is checked and priority is not, I want to query only based on status.
The code below will never execute with one of them being false. I also read about Query by Example but could not find a solution.
Null values can indeed be ignored, but need to be previously defined.
I thought about replacing null with something similar to *.
Ex: WHERE c.status = *. It didn't work.
#Query(value = "SELECT * FROM tickets c WHERE c.status = :status AND c.priority= :priority",
nativeQuery = true)
List<Ticket> findByFilter(#Param("status") String status,
#Param("priority") String priority);
Do you have any idea how can I do this?
Thank you
Thank you Simon for pointing me in the right direction.
This was the web page that helped me:
https://dimitr.im/writing-dynamic-queries-with-spring-data-jpa
My situation:
Created a specification class:
public final class DbFilterSpecification {
public static Specification<Ticket> statusContains(String expression) {
return (root, query, builder) -> builder.like(root.get("status"), contains(expression));
}
public static Specification<Ticket> priorityContains(String expression) {
return (root, query, builder) -> builder.like(root.get("priority"), contains(expression));
}
private static String contains(String expression) {
return MessageFormat.format("%{0}%", expression);
}
}
Created a method inside the service layer:
public List<Ticket> findAllWithSpecification(String status, String priority) {
Specification<Ticket> specification = Specification
.where(status == null ? null : DbFilterSpecification.statusContains(status))
.and(priority == null ? null : DbFilterSpecification.priorityContains(priority));
return ticketRepository.findAll(specification);
}

if/else doese not work in my POST Method of SpringBoot rest api

I use Spring Boot rest api with MongoDB.
In the POST Method, if there is not scoreID and there is not a player with specific date in my collection, because at the same time a player cannot play different games and bring score, then if the specific player and gamecode exist, create a score.
In fact, in the POST Method I used Nested IF-ELSE conditions.
But, in the Postman when I execute POST Request with this data:
{
"scoreid":"s11",
"score":1000,
"player":"sahari",
"gamecode":"g12",
"date":"2020-01-01"
}
always, I recieve an error, in the Postman, 400 Bad Request!, which i defined in the last line of my IF-ELSE statements.
I do not know, what is my mistake and why my program doese not execute IF conditions correct.
The POST Method:
//Create Score
#PostMapping
public ResponseEntity<?> createScore(#RequestBody #JsonView(Views.class) #Valid Score score) {
String p = srepo.findByPlayerName(score.getPlayer());
String g = srepo.findByGameCode(score.getGamecode());
String scoreid = srepo.findByScoreid(score.getScoreid());
Query query = new Query();
query.addCriteria(new Criteria().andOperator(Criteria.where("player").is(score.getPlayer()),
Criteria.where("date").is(score.getDate())));
if((scoreid != null)) {
return ResponseEntity.status(409).body("Conflict!"); }
else
if(mongoTemplate.exists(query, Score.class))
return ResponseEntity.status(409).body("There is not Possible at same time one player brings different Scores!");
else
if((p!= null)&&(g!= null))
{
history = new ArrayList<History>();
h = new History();
h.setScore(score.getScore());
h.setDate(score.getDate());
history.add(h);
hrepo.save(h);
score.setHistory(history);
srepo.insert(score);
return ResponseEntity.ok(score);
}
else
{
return ResponseEntity.status(400).body("Bad Request!");
}
}
The Score Repository:
#Repository
public interface ScoreRepository extends MongoRepository<Score, String>{
#Query("{'scoreid':?0}")
public String findByScoreid(String scoreid);
#Query("{'Player.nickname':?0}")
public String findByPlayerName(String player);
#Query("{'Games.code':?0}")
public String findByGameCode(String game);
}
The problem is not for my IF-ELSE statements.The problem is in the Score Repository
I must return a List instead of String for findByPlayerName and findByGameCode and for findByScoreid which is for checking duplicate in the POST Method I must return type of Score

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

The best way to modify a WebAPI OData QueryOptions.Filter

I am using the OData sample project at http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/working-with-entity-relations. In the Get I want to be able to change the Filter in the QueryOptions of the EntitySetController:
public class ProductsController : EntitySetController<Product, int>
{
ProductsContext _context = new ProductsContext();
[Queryable(AllowedQueryOptions=AllowedQueryOptions.All)]
public override IQueryable<Product> Get()
{
var products = QueryOptions.ApplyTo(_context.Products).Cast<Product>();
return products.AsQueryable();
}
I would like to be able to find properties that are specifically referred to. I can do this by parsing this.QueryOptions.Filter.RawValue for the property names but I cannot update the RawValue as it is read only. I can however create another instance of FilterQueryOption from the modified RawValue but I cannot assign it to this.QueryOptions.Filter as this is read only too.
I guess I could call the new filter's ApplyTo passing it _context.Products, but then I will need to separately call the ApplyTo of the other properties of QueryOptions like Skip and OrderBy. Is there a better solution than this?
Update
I tried the following:
public override IQueryable<Product> Get()
{
IQueryable<Product> encryptedProducts = _context.Products;
var filter = QueryOptions.Filter;
if (filter != null && filter.RawValue.Contains("Name"))
{
var settings = new ODataQuerySettings();
var originalFilter = filter.RawValue;
var newFilter = ParseAndEncyptValue(originalFilter);
filter = new FilterQueryOption(newFilter, QueryOptions.Context);
encryptedProducts = filter.ApplyTo(encryptedProducts, settings).Cast<Product>();
if (QueryOptions.OrderBy != null)
{
QueryOptions.OrderBy.ApplyTo<Product>(encryptedProducts);
}
}
else
{
encryptedProducts = QueryOptions.ApplyTo(encryptedProducts).Cast<Product>();
}
var unencryptedProducts = encryptedProducts.Decrypt().ToList();
return unencryptedProducts.AsQueryable();
}
and it seems to be working up to a point. If I set a breakpoint I can see my products in the unencryptedProducts list, but when the method returns I don't get any items. I tried putting the [Queryable(AllowedQueryOptions=AllowedQueryOptions.All)] back on again but it had no effect. Any ideas why I am not getting an items?
Update 2
I discovered that my query was being applied twice even though I am not using the Queryable attribute. This meant that even though I had items to return the List was being queried with the unencrypted value and therefore no values were being returned.
I tried using an ODataController instead:
public class ODriversController : ODataController
{
//[Authorize()]
//[Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]
public IQueryable<Products> Get(ODataQueryOptions options)
{
and this worked! Does this indicate that there is a bug in EntitySetController?
You would probably need to regenerate ODataQueryOptions to solve your issue. Let's say if you want to modify to add $orderby, you can do this like:
string url = HttpContext.Current.Request.Url.AbsoluteUri;
url += "&$orderby=name";
var request = new HttpRequestMessage(HttpMethod.Get, url);
ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Product>("Product");
var options = new ODataQueryOptions<Product>(new ODataQueryContext(modelBuilder.GetEdmModel(), typeof(Product)), request);

How to extend LINQ select method in my own way

The following statement works fine if the source is not null:
Filters.Selection
.Select(o => new GetInputItem() { ItemID = o.ItemId })
It bombs if "Filters.Selection" is null (obviously). Is there any possible way to write my own extension method which returns null if the source is null or else execute the "Select" func, if the source is not null.
Say, something like the following:
var s = Filters.Selection
.MyOwnSelect(o => new GetInputItem() { ItemID = o.ItemId })
"s" would be null if "Filters.Selection" is null, or else, "s" would contain the evaluated "func" using LINQ Select.
This is only to learn more about LINQ extensions/customizations.
thanks.
You could do this:
public static IEnumerable<U> SelectOrNull<T,U>(this IEnumerable<T> seq, Func<T,U> map)
{
if (seq == null)
return Enumerable.Empty<U>(); // Or return null, though this will play nicely with other operations
return seq.Select(map);
}
Yes have a look at the Enumerable and Queryable classes in the framework, they implement the standard query operators.
You would need to implement a similar class with the same Select extension methods matching the same signatures, then if the source is null exit early, you should return an empty sequence.
Assuming you're talking about LINQ to Objects, absolutely:
public static class NullSafeLinq
{
public static IEnumerable<TResult> NullSafeSelect<TSource, TResult>
(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
// We don't intend to be safe against null projections...
if (selector == null)
{
throw new ArgumentNullException("selector");
}
return source == null ? null : source.Select(selector);
}
}
You may also want to read my Edulinq blog post series to learn more about how LINQ to Objects works.

Resources