Inheritance with Metamodel Java - java

In my application I have some entities which are extended from User class. User has firstName and lastName fields. There is a search for each entity extended from User. To implement search I'm using Criteria API. As we are talking about the same params, queries for them look very similar. So I decided to create generic class to gather everything in one place
public abstract class NameSpecificationManager<Entity, SearchDto extends BasicSearchDto, MetaModel extends User_>
extends SpecificationManager<Entity, SearchDto> {
protected final SpecificationBuilder<Entity> entityByFirstName = (final String firstName) ->
(root, query, cb) -> cb.like(root.get(MetaModel.firstName), getParameterPattern(firstName));
protected final SpecificationBuilder<Entity> entityByLastName = (final String lastName) ->
(root, query, cb) -> cb.like(root.get(MetaModel.lastName), getParameterPattern(lastName));
}
Here is SpecificationBuilder:
public interface SpecificationBuilder<Entity> {
Specification<Entity> getSpecOfSearchParam(String searchParam);
}
The problem is that MetaModel.lastName and MetaModel.firstName are underlined by Intellij (it says that "Cannot resolve method with ...User.firstName"). May be it's because it's not clear what exact extension of User class we are expecting. May be there is a way to avoid this. Thanks in advance.

Related

Get only selected columns from DB with multiple filtering criteria spring boot 2 JPA

I'm trying to create a spring boot 2 web application which will fetch data from the db based on the filtering criteria passed to it, but will only fetch certain columns.
Here is my employee class:
#Entity
#Table("table=emplooyee")
class Employee{
#column("name="fname")
String fname;
#column("name="lname")
String lname;
#column("name="phoneNo")
String phoneNo;
#column("name="address")
String address;
}
There are 25 more such fields in my entity and in the db.
From the front-end the user should be able to choose a filtering criteria such as: fname, lname, phoneNo, address etc. He may specify any combination like fname and phoneNo, or lname and address or may not specify anything in which I have to do a select *. In a way, I want multiple filtering criteria. I expect these filters to come as request parameters from the front end.
My repository is:
public interface EmployeeRepository extends JpaRepository<Employee,Long>, JpaSpecificationExecutor<Employee>{
}
So far, I've looked into specifications which is pretty cool.
So I created a specification,
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
public class EmployeeSpecs {
public static Specification<Employee> hasFname(String fname){
return new Specification<Employee>() {
#Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.get("fname"),fname);
}
};
}
public static Specification<Employee> hasLname(String lname){
return new Specification<Employee>() {
#Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.get("lname"), lname);
}
};
}
public static Specification<Employee> hasAddress(String address){
return new Specification<Employee>() {
#Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.get("address"), address);
}
};
}
public static Specification<Employee> hasPhone(String phone){
return new Specification<Employee>() {
#Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.get("phone"), phone);
}
};
}
}
Now, from my service, I plan to do:
this.employeeRepository.findAll(EmployeeSpecs.hasFName(requestParameterFname).and(EmployeeSpecs.hasLName(requestParameterLname))).forEach(e->list.add(e));
However, this would fetch all the 25 columns in my db.
My front end application has 6 pages, each requiring different columns to be displayed but a combination of these specifications as where clauses.
I tried looking into the concept of projection, but figured out that currently SpringBoot does not support Specification with Projection.
Is there a way to get only selected columns and have multiple filtering criteria? Any thoughts on being able to dynamically append the passed request parameters to my query and fetching only relevant fields?
Should I create separate entities so that I get only those fields from my repository and then a new specification for each of those each time? Won't this create too many unnecessary entities and specification files?
The other way I can think of is that, I'll have to manually extract those columns. This would sound stupid as I already know that I need to do a 'select column1, column2, column3 from db where condition1 = true and condition2= true' but I'm still doing a select *.
Can anyone please guide on what's the best approach to take in this case? What would look the most clean way of achieving this? Should I be writing a query manually, like a native query?
In a nutshell, I want the following:
Multiple filtering criteria - any combination possible, ie. multiple conditions to be passed to the 'where' clause of my sql select statement.
Only selected columns, not all - but different use cases require different columns.
Spring Data doesn't have any special feature or this. So you would need to create a custom method, where you combine the Predicate from the Specification with a selection list.
The custom method might look somewhat like this:
Employee findBySpecAndColumns(Specification spec, List<String> columns) {
// create select list as described here, but from the list of columns or whatever you use to specify which columns you want to select: https://www.objectdb.com/java/jpa/query/jpql/select#SELECT_in_Criteria_Queries
// use spec.toPredicate(...) to create the where clause
// execute the query.
// transform the result to the form you need/want.
}
See also:
How to specify the select list using the Criteria API.
I wonder though, if this is worth the effort. I'd expect that selecting 25 columns for data to be displayed on a single page probable doesn't make much difference from selecting 4 columns from the same table.
You can use GraphQL or QueryDSL
Example using queryDSL
QMenuItemRelation entity = new QMenuItemRelation("entity");
QMenuItem menuItem = new QMenuItem("menuItem");
QMenuItemRelationPrice menuItemRelationPrice = new QMenuItemRelationPrice("menuItemRelationPrice");
return queryFactory.select(Projections.constructor(
MenuItemScalesExportDTO.class,
entity.menuItem.id,
entity.menuItem.name,
entity.menuItem.barcode,
entity.menuItem.unitType,
menuItemRelationPrice.price))
.from(entity)
.where(entity.active.eq(true), entity.menu.id.eq(menuId), entity.menuItem.usedByScales.eq(true))
.leftJoin(entity.menuItem, menuItem)
.leftJoin(menuItemRelationPrice).on(entity.eq(menuItemRelationPrice.menuItemRelation))
.orderBy(entity.id.desc())
.fetch();
You also can use Projections.bean if you want to map with getter/setter instead of constructor.
DTO
public class MenuItemScalesExportDTO implements Serializable {
private UUID id;
private String name;
private String code;
private String unit;
private List<PriceDTO> price;
private BigDecimal unitPrice;
public MenuItemScalesExportDTO(UUID id, String name, String code, String unit, List<PriceDTO> price) {
this.id = id;
this.name = name;
this.code = code;
this.unit = unit;
this.price = price;
}

how to write a single jpa specification for multiple entities

I am working on a Spring Boot - App that has multiple entities having some identical columns for filtering.
Currently, I have the same query defined in multiple repositories, so after doing some research, I've stumbled across an article about JPA - Specifications: https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/
So I made a generic class to build JPA-Specifications:
public final class GenericSpecifications<T>
{
public Specification whereNameLikeAndDateGreatherThan(String fieldName, String fieldDate, String name, LocalDate date)
{
return (root, query, builder) -> builder.lessThan(root.get(columnName), date);
}
}
So in the service I can use:
repository.findAll(whereNameLikeAndDateGreatherThan(Person_.name, Person_.date, "Max", LocalDate.now());
In this way, I have one query/specification in a central place and I don't need to write/maintain the same query on all repositories.
However, I have more complex queries, where I need to filter over multiple columns.
This means that my methods, in my GenericSpecification-Class, become too bloated, since I need to pass multiple column names and the search-values, so I could end up with methods with 6 or more parameters.
I could define an Abstract-Entity class extended by all other entities.This abstract entity would have all the common fields in order to be sure that all the entities have the same columns.
Then I can use these names for filtering, so I don't have to pass the field/coulmn-names at all.
But, I am not sure if this is the cleanest approach to my problem.
Do you know if there is a better way to do this?
I think the cleanest approach is to use inheritance, but in the specification creator, not the entities. So for example something like (didn't try if it compiles so it probably doesn't, but should give the idea):
class BasicSpecificationBuilder<T> {
public Specification<T> stringEqual(String fieldName, String value) {
// root is Root<T> here, don't know if this needs to be specified
return (root, query, builder) ->
builder.equal(root.<String>get(fieldName), value);
}
}
public Specification<T> dateAfter(String fieldName, LocalDate value) {
return (root, query, builder) ->
builder.<LocalDate>greaterThan(root.get(fieldName), value);
}
}
// extend per entity type and required queries
class ContractSpecificationBuilder<Contract> extends BasicSpecificationBuilder<Contract> {
public Specification<Contract> contractsCreatedAfter(String partner, LocalDate date) {
return (root, query, builder) ->
stringEqual(Contract_.partnerName, partner)
.and(
dateAfter(Contract_.closeDate, date));
}
}
class EmployeeSpecificationBuilder<Employee> extends BasicSpecificationBuilder<Employee> {
public Specification<Employee> employeesJoinedAfter(String name, LocalDate date) {
return (root, query, builder) ->
stringEqual(Employee_.name, name)
.and(
dateAfter(Employee_.entryDate, date));
}
}
This way you have a collection of builder methods in the base class you can reuse, and queries that don't explode because they're separated per entity. There may be a little code duplication as in the example above - if there's too many of those, you can refactor these common combinations into the base class.
class BasicSpecificationBuilder<T> {
public Specification<T> stringEqualAndDateAfter(String stringField, String stringValue, String dateField, LocalDate dateValue) {
public Specification<Employee> employeesJoinedAfter(String name, LocalDate date) {
return (root, query, builder) ->
stringEqual(stringField, name)
.and(
dateAfter(dateField, date));
}
}
class ContractSpecificationBuilder<Contract> extends BasicSpecificationBuilder<Contract> {
public Specification<Contract> contractsCreatedAfter(String partner, LocalDate date) {
return stringEqualAndDateAfter(Contract_.partnerName, partner, Contract_.closeDate, date);
}
}
That's a matter of taste and code quality settings (we had a code duplication measure in SonarQube with a limit, but I don't think this would have crossed the limit).
Since these are all factory methods, you can do pretty much the same thing with classes providing static methods and the "base" class containing the basic methods as static utility methods. I kind of dislike the syntax for generic static methods though.
That's all assuming you read the Baeldung intro on how to use Specification and didn't like that approach.

Filter child object in Spring Data Query

I have a following domain model:
Playlist -> List<PlaylistItem> -> Video
#Entity
class Playlist{
// id, name, etc
List<PlaylistItem> playlistItems;
// getters and setters
}
#Entity
class PlaylistItem{
// id, name, etc.
Video video;
// getters and setters
}
#Entity
class Video{
// id, name, etc.
boolean isDeleted;
// getters and setters
}
And my repository:
public interface PlaylistRepository extends JpaRepository<Playlist, Long> {
List<Playlist> findAll();
}
Now, how do I return a playlist with only existing videos, ie, if there are three videos in the database assigned to that playlist item and one of those videos has isDeleted set to true, then I need to get only two items instead.
All you have to do is declare this method on your PlaylistRepository interface:
List<Playlist> findByPlaylistItemsVideoIsDeleted(boolean isDeleted);
And call it like this:
playListRepository.findByPlaylistItemsVideoIsDeleted(false);
That will return all playlist with videos that are not removed.
You may have already resolved this issue, but I thought I would contribute this in hopes it might help you, or anyone else visiting this page.
Using Spring JPA Specifications, you would:
Enable your PlaylistRepository to use JPA Specifications
Write the Specification as a reusable method
Make use of the Specification as the query
Here are the details.
1. Implement JpaSpecificationExecutor
Update PlaylistRepository to implement JpaSpecificationExecutor. This adds find* methods that accept Specification<T> parameters to your PlaylistRepository.
public interface PlaylistRepository extends JpaRepository<Playlist, Long>,
JpaSpecificationExecutor<Playlist> {
}
2. Create the Specification
Create a class with a static method for use in creating a reusable Specification.
public final class PlaylistSpecifications {
private PlaylistSpecifications() {}
public static Specification<Playlist> hasExistingVideos() {
return (root, query, cb) -> {
return cb.equal(root.join("playlistItems").join("video")
.get("isDeleted"), false);
};
}
}
Using root.join (and subsequent joins) is similar to using JOIN in SQL. Here, we are joining on the fields of classes, instead of on columns of tables.
3. Issue the Query
I don't know how you plan to issue your query, but below is an example of how it could be done in a "service" class:
#Service
public class PlaylistService {
#Autowired
private PlaylistRepository playlistRepository;
public List<Playlist> findPlaylistsWithExistingVideos() {
Specification<Playlist> spec = PlaylistSpecifications.hasExistingVideos();
return playlistRepository.findAll(spec);
}
}
Hope this helps!
Maksim, you could use the #query annotation like this :
public interface PlaylistRepository extends JpaRepository<Playlist, Long> {
#Query("select playlist from Playlist playlist
fetch join playlist.playlistItems itens
fetch join itens.video as video
where video.isDeleted = false")
List<Playlist> findAll();
}
Or even better way :
public interface PlaylistRepository extends JpaRepository<Playlist, Long> {
#Query("select playlist from Playlist playlist
fetch join playlist.playlistItems itens
fetch join itens.video as video
where video.isDeleted = :hasVideo ")
List<Playlist> findPlayList(#Param("hasVideo") boolean hasVideo);
}
You can look into Spring Data Specifications. You use them by calling repository.findAll(s);
Specifications allow you add on arbitrary conditions to your query, including the filter you want to add. Another nice thing about Specifications is that they can be type-safe. See here:
http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications

Sorting by parent entity using Specifications

I’m dealing with an issue which to my understanding looks unsupported on Spring Data JPA.
I got a grid (using JqGrid plugin for jQuery) on the view which sends parameters to the server, they are parsed and then a dynamic query generated through Specifications is executed.
The issue comes when I want to order a column which doesn’t belong to the root entity.
Eg. Transaction, Card and Account are my entities and grid displays last4digits as a way for the user to identify the card. As you can imagine last4digits belongs to Card. I query transactions per account.
Using specifications I can filter by that attribute, joining tables and so on but sorting fails as findAll() implementation assumes properties from Sort class belongs to the root entity.
Code example:
JQGridRule panFirst6DigitsRule = FilterUtils.findSearchOrFilterRule(settings, Card_.panFirst6Digits.getName());
JQGridRule panLast4DigitsRule = FilterUtils.findSearchOrFilterRule(settings, Card_.panLast4Digits.getName());
if(panFirst6DigitsRule != null) {
filterPan1 = TransactionSpecs.withPanFirst6Digits(panFirst6DigitsRule.getData(),
panFirst6DigitsRule.getOp(), gridGroupOp);
}
if(panLast4DigitsRule != null) {
filterPan2 = TransactionSpecs.withPanLast4Digits(panLast4DigitsRule.getData(),
panLast4DigitsRule.getOp(), gridGroupOp);
}
Specification<Transaction> joinSpec = TransactionSpecs.withAccountId(account.getAccountId());
Specification<Transaction> activeSpec = BaseSpecs.withEntityStatus(true);
Page<Transaction> results = transactionRepository.findAll(
Specifications.where(joinSpec).and(filterSpec).and(filterPan1).and(filterPan2).and(activeSpec), springPageable);
springPageable variable contains a Sort for last4Digits column generated this way*:
List<Order> sortOrders = new ArrayList<Order>();
Order sortOrder = new Order(Direction.ASC, "panLast4Digits");
sortOrders.add(sortOrder);
sort = new Sort(sortOrders);
*There are missing code parsing parameters and creating more Order objects
Does someone know how to implement that kind of sort over an attribute which belongs to a parent entity/class?
Thanks in advance
Version 1.4.3 for Spring-data-jpa and 4.2.8 for Hibernate
EDIT
Showing how Specification for panLast4Digits is generated
public static Specification<Transaction> withPanLast4Digits(final String panLast4Digits, final JQGridSearchOp op, final JQGridGroupOp whereOp) {
Specification<Transaction> joinSpec = new Specification<Transaction>() {
#Override
public Predicate toPredicate(Root<Transaction> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Join<Transaction, Card> join = joinCards(root, JoinType.INNER);
return FilterUtils.buildPredicate(cb, join.get(Card_.panLast4Digits), op, panLast4Digits, null, whereOp);
}
};
return joinSpec;
}
private static Join<Transaction, Card> joinCards(Root<Transaction> root, JoinType joinType) {
Join<Transaction, Card> join = getJoin(root, Transaction_.parentCard, joinType);
// only join if not already joined
if (join == null) {
join = root.join(Transaction_.parentCard, joinType);
}
return join;
}
protected static <C, T> Join<C, T> getJoin(Root<C> root, Attribute<? super C, T> attribute, JoinType joinType) {
Set<Join<C, ?>> joins = root.getJoins();
for (Join<C, ?> join : joins) {
if (join.getAttribute().equals(attribute) && join.getJoinType().equals(joinType)) {
return (Join<C, T>) join;
}
}
return null;
}
Also I have updated to spring-data-jpa 1.6.0 and hibernate 4.3.5
the attribute for Sorting is "yourChildentity.attribute"
In your Case you can use the PagingAndSortingRepository this way:
let's assume you have two entities : an Account and a Card
#Entity
public class Account{
// Autogeneration and Ill just assume that your id is type long
private Long id;
#ManyToOne
#JoinColumn(name="CARD_ID")
private Card creditCard;
//getters and setters
}
#Entity
public class Card{
//Id and other attributes.
private String panLast4Digits;
//getters and Setters
}
Repository interface :
#Repository
public interface AccountRepository extends PagingAndSortingRepository<Account, Long>,
JpaSpecificationExecutor<Account>{
}
Service Layer :
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface AccountService{
//you can specify other arguments the one that you want to filter by
Page<Account> filter(Pageable pageable);
}
Service Implementation:
#Service
public calss AccountServiceImpl implements AccountService{
#Resource//or #Autowired
private AccountRepository repository;
#Override
public Page<Account> filter(Pageable pageable){
//Filter using Specifications if you have other arguments passed in the signature of the method.
return repository.findAll(pageable);//if you have specifications than return repository.findAll(yourspecification,pageable);
}
Now the call to service throw an endpoint or a Controller:
just a mthod to see how to sort throw child entity parameter :
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort.Direction;
// method
#Resource
private AccountService service;
public Page<Account> consumeMyService(){
// 0 : for Page 1
// 12 for page size
// Soting throw Child enntiy Account , by attribute panLast4Digits
PageRequest pageable = new PageRequest(0,
12, Direction.ASC, "mycard.panLast4Digits");
Page<Account> service.filter(pageable);
}
You must register you beans by configuring Jpa:repositories for the repository interfaces, and context:component-scan for service implementation
this answer may be useful too.

Filtering database rows with spring-data-jpa and spring-mvc

I have a spring-mvc project that is using spring-data-jpa for data access. I have a domain object called Travel which I want to allow the end-user to apply a number of filters to it.
For that, I've implemented the following controller:
#Autowired
private TravelRepository travelRep;
#RequestMapping("/search")
public ModelAndView search(
#RequestParam(required= false, defaultValue="") String lastName,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
Page<Travel> travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
mav.addObject("page", page);
mav.addObject("lastName", lastName);
return mav;
}
This works fine: The user has a form with a lastName input box which can be used to filter the Travels.
Beyond lastName, my Travel domain object has a lot more attributes by which I'd like to filter. I think that if these attributes were all strings then I could add them as #RequestParams and add a spring-data-jpa method to query by these. For instance I'd add a method findByLastNameLikeAndFirstNameLikeAndShipNameLike.
However, I don't know how should I do it when I need to filter for foreign keys. So my Travel has a period attribute that is a foreign key to the Period domain object, which I need to have it as a dropdown for the user to select the Period.
What I want to do is when the period is null I want to retrieve all travels filtered by the lastName and when the period is not null I want to retrieve all travels for this period filtered by the lastName.
I know that this can be done if I implement two methods in my repository and use an if to my controller:
public ModelAndView search(
#RequestParam(required= false, defaultValue="") String lastName,
#RequestParam(required= false, defaultValue=null) Period period,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
Page travels = null;
if(period==null) {
travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
} else {
travels = travelRep.findByPeriodAndLastNameLike(period,"%"+lastName+"%", pageable);
}
mav.addObject("page", page);
mav.addObject("period", period);
mav.addObject("lastName", lastName);
return mav;
}
Is there a way to do this without using the if ? My Travel has not only the period but also other attributes that need to be filtered using dropdowns !! As you can understand, the complexity would be exponentially increased when I need to use more dropdowns because all the combinations'd need to be considered :(
Update 03/12/13: Continuing from M. Deinum's excelent answer, and after actually implementing it, I'd like to provide some comments for completeness of the question/asnwer:
Instead of implementing JpaSpecificationExecutor you should implement JpaSpecificationExecutor<Travel> to avoid type check warnings.
Please take a look at kostja's excellent answer to this question
Really dynamic JPA CriteriaBuilder
since you will need to implement this if you want to have correct filters.
The best documentation I was able to find for the Criteria API was http://www.ibm.com/developerworks/library/j-typesafejpa/. This is a rather long read but I totally recommend it - after reading it most of my questions for Root and CriteriaBuilder were answered :)
Reusing the Travel object was not possible because it contained various other objects (who also contained other objects) which I needed to search for using Like - instead I used a TravelSearch object that contained the fields I needed to search for.
Update 10/05/15: As per #priyank's request, here's how I implemented the TravelSearch object:
public class TravelSearch {
private String lastName;
private School school;
private Period period;
private String companyName;
private TravelTypeEnum travelType;
private TravelStatusEnum travelStatus;
// Setters + Getters
}
This object was used by TravelSpecification (most of the code is domain specific but I'm leaving it there as an example):
public class TravelSpecification implements Specification<Travel> {
private TravelSearch criteria;
public TravelSpecification(TravelSearch ts) {
criteria= ts;
}
#Override
public Predicate toPredicate(Root<Travel> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
Join<Travel, Candidacy> o = root.join(Travel_.candidacy);
Path<Candidacy> candidacy = root.get(Travel_.candidacy);
Path<Student> student = candidacy.get(Candidacy_.student);
Path<String> lastName = student.get(Student_.lastName);
Path<School> school = student.get(Student_.school);
Path<Period> period = candidacy.get(Candidacy_.period);
Path<TravelStatusEnum> travelStatus = root.get(Travel_.travelStatus);
Path<TravelTypeEnum> travelType = root.get(Travel_.travelType);
Path<Company> company = root.get(Travel_.company);
Path<String> companyName = company.get(Company_.name);
final List<Predicate> predicates = new ArrayList<Predicate>();
if(criteria.getSchool()!=null) {
predicates.add(cb.equal(school, criteria.getSchool()));
}
if(criteria.getCompanyName()!=null) {
predicates.add(cb.like(companyName, "%"+criteria.getCompanyName()+"%"));
}
if(criteria.getPeriod()!=null) {
predicates.add(cb.equal(period, criteria.getPeriod()));
}
if(criteria.getTravelStatus()!=null) {
predicates.add(cb.equal(travelStatus, criteria.getTravelStatus()));
}
if(criteria.getTravelType()!=null) {
predicates.add(cb.equal(travelType, criteria.getTravelType()));
}
if(criteria.getLastName()!=null ) {
predicates.add(cb.like(lastName, "%"+criteria.getLastName()+"%"));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
}
Finally, here's my search method:
#RequestMapping("/search")
public ModelAndView search(
#ModelAttribute TravelSearch travelSearch,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
TravelSpecification tspec = new TravelSpecification(travelSearch);
Page<Travel> travels = travelRep.findAll(tspec, pageable);
PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
mav.addObject(travelSearch);
mav.addObject("page", page);
mav.addObject("schools", schoolRep.findAll() );
mav.addObject("periods", periodRep.findAll() );
mav.addObject("travelTypes", TravelTypeEnum.values());
mav.addObject("travelStatuses", TravelStatusEnum.values());
return mav;
}
Hope I helped!
For starters you should stop using #RequestParam and put all your search fields in an object (maybe reuse the Travel object for that). Then you have 2 options which you could use to dynamically build a query
Use the JpaSpecificationExecutor and write a Specification
Use the QueryDslPredicateExecutor and use QueryDSL to write a predicate.
Using JpaSpecificationExecutor
First add the JpaSpecificationExecutor to your TravelRepository this will give you a findAll(Specification) method and you can remove your custom finder methods.
public interface TravelRepository extends JpaRepository<Travel, Long>, JpaSpecificationExecutor<Travel> {}
Then you can create a method in your repository which uses a Specification which basically builds the query. See the Spring Data JPA documentation for this.
The only thing you need to do is create a class which implements Specification and which builds the query based on the fields which are available. The query is build using the JPA Criteria API link.
public class TravelSpecification implements Specification<Travel> {
private final Travel criteria;
public TravelSpecification(Travel criteria) {
this.criteria=criteria;
}
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
// create query/predicate here.
}
}
And finally you need to modify your controller to use the new findAll method (I took the liberty to clean it up a little).
#RequestMapping("/search")
public String search(#ModelAttribute Travel search, Pageable pageable, Model model) {
Specification<Travel> spec = new TravelSpecification(search);
Page<Travel> travels = travelRep.findAll(spec, pageable);
model.addObject("page", new PageWrapper(travels, "/search"));
return "travels/list";
}
Using QueryDslPredicateExecutor
First add the QueryDslPredicateExecutor to your TravelRepository this will give you a findAll(Predicate) method and you can remove your custom finder methods.
public interface TravelRepository extends JpaRepository<Travel, Long>, QueryDslPredicateExecutor<Travel> {}
Next you would implement a service method which would use the Travel object to build a predicate using QueryDSL.
#Service
#Transactional
public class TravelService {
private final TravelRepository travels;
public TravelService(TravelRepository travels) {
this.travels=travels;
}
public Iterable<Travel> search(Travel criteria) {
BooleanExpression predicate = QTravel.travel...
return travels.findAll(predicate);
}
}
See also this bog post.

Categories

Resources