I have a method controller which gonna return all advertisements that are being filtered by passed paremetrs.
#GetMapping("/search/advertisements")
public List<Advertisement> getSpecicShortAdvertisements(
#RequestParam(value = "city", required = false) String city,
#RequestParam(value = "category", required = false) AdvertisementCategory category) {
return shortAdvertisementService.getSpecificShortAdvertisements(city,category);
}
So for example it can be: http://localhost:8080/search/advertisements?city=WARSZAWA&category=ANIMALSCARE
but it can be also http://localhost:8080/search/advertisements?category=ANIMALSCARE
If I do not send any parameters it shoudl return all advertisements.
It would be so hard to write query for every single parameter, so I've used Example, but do not know why it returns empty list all the time despite having data in DB.
It's being added for every run of application (initial data)
advertisementRepository.save(Advertisement.builder()
.advertisementCategory(AdvertisementCategory.ANIMALSCARE)
.dateTime(LocalDateTime.now())
.description("LAAAAAAAAAAAAAAAAAAA")
.featured(false)
.photos(null)
.title("TiTitle 2tle 1")
.city("KRAKOW")
.build());
advertisementRepository.save(Advertisement.builder()
.advertisementCategory(AdvertisementCategory.ANIMALSCARE)
.dateTime(LocalDateTime.now())
.description("LAAAAAAAAAAAAAAAAAAALA" +
"AALAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAALAAAAA" +
"AAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAALAAAAAAAAAAAAAAAAAAA")
.featured(false)
.photos(null)
.title("TTitle 2Title 2Title 2itle 2")
.city("WARSZAWA")
.build());
So as you can see if I send 'WARSZAWA' as an city it should return this advertisement, but it returns nothing.
Service method:
public List<Advertisement> getSpecificShortAdvertisements(String city, AdvertisementCategory category) {
Advertisement example = Advertisement.builder()
.advertisementCategory(category)
.city(city)
.build();
return advertisementRepository.findAll(Example.of(example));
}
Repo:
#Repository
public interface AdvertisementRepository extends JpaRepository<Advertisement, Long> {
}
//EDIT
SQL generated:
select advertisem0_.advertisementid as advertis1_0_, advertisem0_.advertisement_category as advertis2_0_, advertisem0_.city as city3_0_, advertisem0_.date_time as date_tim4_0_, advertisem0_.description as descript5_0_, advertisem0_.featured as featured6_0_, advertisem0_.title as title7_0_ from advertisement advertisem0_ where advertisem0_.featured=? and advertisem0_.advertisementid=0 and advertisem0_.city=?
If I made
public List<Advertisement> getSpecificShortAdvertisements(String city, AdvertisementCategory category) {
ExampleMatcher exampleMatcher = ExampleMatcher.matchingAny();
Advertisement example = Advertisement.builder()
.advertisementCategory(category)
.city(city)
.build();
return advertisementRepository.findAll(Example.of(example, exampleMatcher));
}
then it returns everything despite sending http://localhost:8080/search/advertisements?city=LALA which should not match anything
Related
I am using Spring for GraphQL to create a small microservice project which consists of 2 apps, a customer service and an order service.
My order service app is running on port 8081 and it contains an OrderData model:
public record OrderData(#Id Integer id, Integer customerId) {}
It also contains an OrderDataRepository interface:
#Repository
public interface OrderDataRepository extends ReactiveCrudRepository<OrderData, Integer> {
Flux<OrderData> getByCustomerId(Integer customerId);
}
And it exposes a single endpoint
#RestController
#RequestMapping(path = "/api/v1/orders")
public class OrderDataController {
private final OrderDataRepository orderDataRepository;
public OrderDataController(OrderDataRepository orderDataRepository) {
this.orderDataRepository = orderDataRepository;
}
#GetMapping
Flux<OrderData> getByCustomerId(#RequestParam Integer customerId) {
return orderDataRepository.getByCustomerId(customerId);
}
}
My customer service app defines the following graphql schema:
type Query {
customers: [Customer]
customersByName(name: String): [Customer]
customerById(id: ID): Customer
}
type Mutation {
addCustomer(name: String): Customer
}
type Customer {
id: ID
name: String
orders: [Order]
}
type Order {
id: ID
customerId: ID
}
And it exposes a few graphql endpoints for querying and mutating customer data, one of which is used to fetch customer orders by using a WebClient to call the endpoint exposed by my order service app:
#Controller
public class CustomerController {
private final CustomerRepository customerRepository;
private final WebClient webClient;
public CustomerController(CustomerRepository customerRepository, WebClient.Builder webClientBuilder) {
this.customerRepository = customerRepository;
this.webClient = webClientBuilder.baseUrl("http://localhost:8081").build();
}
// ...
#QueryMapping
Mono<Customer> customerById(#Argument Integer id) {
return this.customerRepository.findById(id);
}
#SchemaMapping(typeName = "Customer")
Flux<Order> orders(Customer customer) {
return webClient
.get()
.uri("/api/v1/orders?customerId=" + customer.id())
.retrieve()
.bodyToFlux(Order.class);
}
}
record Order(Integer id, Integer customerId){}
My question is how would I refactor this #SchemaMapping endpoint to use #BatchMapping and keep the app nonblocking.
I tried the following:
#BatchMapping
Map<Customer, Flux<Order>> orders(List<Customer> customers) {
return customers
.stream()
.collect(Collectors.toMap(customer -> customer,
customer -> webClient
.get()
.uri("/api/v1/orders?customerId=" + customer.id())
.retrieve()
.bodyToFlux(Order.class)));
}
But I get this error...
Can't resolve value (/customerById/orders) : type mismatch error, expected type LIST got class reactor.core.publisher.MonoFlatMapMany
... because the type of Customer has a orders LIST field and my orders service is returning a Flux.
How can I resolve this problem so I can return a Map<Customer, List<Order>> from my #BatchMapping endpoint and keep it nonblocking?
I assume it's a pretty simple solution but I don't have a lot of experience with Spring Webflux.
Thanks in advance!
I believe that what's missing from your method signature is an additional Mono wrapping it all. You should perform the necessary logic to transform what you return as well. For example, I have this #BatchMapping:
#BatchMapping(typeName = "Artista")
public Mono<Map<Artista, List<Obra>>> obras(List<Artista> artistas){
var artistasIds = artistas.stream()
.map(Artista::id)
.toList();
var todasLasObras = obtenerObras(artistasIds); // service method
return todasLasObras.collectList()
.map(obras -> {
Map<Long, List<Obra>> obrasDeCadaArtistaId = obras.stream()
.collect(Collectors.groupingBy(Obra::artistaId));
return artistas.stream()
.collect(Collectors.toMap(
unArtista -> unArtista, //K, the Artista
unArtista -> obrasDeCadaArtistaId.get(Long.parseLong(unArtista.id().toString())))); //V, the Obra List
});
}
You should replace my Artista for your Customer, and my Obra for your Order. Therefore you will return a Mono<Map<Customer, List>>
I am sorry for the long post, but I believe It is important to mention everything related to the issue.
I am dealing with a requirement for my webservice that sends out notifications for 20k+ users at a time. Since this is quite a heavy task, I thought that having it Async was probably the best approach as it will take some time to process the data. This feature is available for a vast majority of users on the platform, hence there can be multiple requests at once. The amount of users that will receive a notification can vary from 1k to 20k+. Since the request processing takes quite a long time - I basically create a notification, assign it to the correct talents and then send it out in waves. This feature alone seems to have a massive impact on performance when there is multiple concurrent requets for notifications active at the same time and I end up with an out of memory error. I am not sure if this can be optimized at all, or If I should just perhaps choose a completely different approach to everything. I apologize for the long post but I believe it important that I mention everything.
I designed the system to act as follows:
I receive a notification request, which is created in a separate table
I receive a token that indicates which users should get the notification
I fetch the users via a mapped class that is used as a predicate inside of a findAll method (QueryDSL)
I created a relational table taht contains the notificationId, talentId and an extra 'sent' column. Every talent that should receive the message is added to this table along with the notificationId
I have a #Scheduled method that picks up a portion of the notification/talent relations and sends out the notification periodically
My Async configuration class is as follows:
#Component
#Configuration
#EnableAsync
public class AsyncConfiguration implements AsyncConfigurer {
#Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(5);
executor.setCorePoolSize(5);
executor.setThreadNamePrefix("asyncExec-");
executor.initialize();
return executor;
}
#Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
System.out.println("Exception with message :" + ex.getMessage());
System.out.println("Method :" + method);
System.out.println("Number of parameters :" + params.length);
};
}
}
My user entity has the following mapping:
#OneToMany(mappedBy = "talent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TalentRoleNotificationRelations> roleNotifications;
My notification has the following mapping:
#OneToMany(mappedBy = "roleNotification", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TalentRoleNotificationRelations> roleNotifications;
And my relational table entity is as follows:
#Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class TalentRoleNotificationRelations implements Serializable {
private static final long serialVersionUID = 1L;
#EmbeddedId
private TalentRoleNotificationIdentity identity;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("talentId")
#JoinColumn(name = "talent_id")
private Talent talent;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("roleNotificationId")
#JoinColumn(name = "role_notification_id")
private RoleNotification roleNotification;
private Boolean sent;
}
And the composite key identity:
#Embeddable
#Builder
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode
#Data
public class TalentRoleNotificationIdentity implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "talent_id")
private String talentId;
#Column(name = "role_notification_id")
private String roleNotificationId;
}
This mapping was done following the guide here which indicates how a many to many relation with an extra column should be implemented.
And the actual process of creating the notification
Controller method:
#PostMapping(value = "notify/{searchToken}/{roleId}", produces = MediaType.APPLICATION_JSON_VALUE)
public RoleNotificationInfo notifyTalentsOfNewRole(#PathVariable String searchToken, #PathVariable String roleId) {
var roleProfileInfo = (RoleProfileInfo) Optional.ofNullable(searchToken)
.map(token -> searchFactory.fromToken(token, RoleProfileInfo.class))
.orElse(null);
return productionService.notifyTalentsOfMatchingRole(roleProfileInfo, roleId);
}
Service method: (This is where I believe the issue is, as well as a very bad use of a many to many table mapping?)
#Transactional(propagation = Propagation.REQUIRES_NEW)
#Async
public RoleNotificationInfo notifyTalentsOfMatchingRole(RoleProfileInfo roleProfileInfo, String roleId) {
var predicate = Optional.ofNullable(userService.getPredicate(roleProfileInfo)).orElse(new BooleanBuilder());
var role = roleDao.findById(roleId).orElseThrow(NoSuchRole::new);
var isNotificationLimitReached = isNotificationLimitReached(roleId);
if (!isNotificationLimitReached) {
var notificationBody = notificationBodyDao.findById(NotificationBodyIdentifier.MATCHING_ROLE)
.orElseThrow(NoSuchNotificationBody::new);
var newNotification = RoleNotification.builder()
.notificationBody(notificationBody)
.role(role)
.build();
roleNotificationDao.saveAndFlush(newNotification);
talentDao.findAll(predicate)
.forEach(talent -> {
var identity = TalentRoleNotificationIdentity.builder()
.talentId(talent.getId())
.roleNotificationId(newNotification.getId())
.build();
var talentRoleNotification = TalentRoleNotificationRelations.builder()
.identity(identity)
.roleNotification(newNotification)
.talent(talent)
.sent(false)
.build();
talentRoleNotification.setIdentity(identity);
talentRoleNotification.setTalent(talent);
talentRoleNotificationRelationsDao.save(talentRoleNotification);
});
return dtoFactory.toInfo(newNotification);
} else throw new RoleNotificationLimitReached();
}
Scheduled method that sends out the notifications:
#Transactional(propagation = Propagation.REQUIRES_NEW)
#Scheduled(fixedDelay = 5)
public void sendRoleMessage() {
var unsentNotificationIds = talentRoleNotificationRelationsDao.findAll(
QTalentRoleNotificationRelations.talentRoleNotificationRelations.sent.isFalse()
.and(QTalentRoleNotificationRelations.talentRoleNotificationRelations.talent.notificationToken.isNotNull()),
PageRequest.of(0, 50)
);
unsentNotificationIds.forEach(unsentNotification -> {
var talent = unsentNotification.getTalent();
var roleNotification = unsentNotification.getRoleNotification();
markAsSent(unsentNotification);
talentRoleNotificationRelationsDao.saveAndFlush(unsentNotification);
notificationPusher.push(notificationFactory.buildComposite(talent.getNotificationToken()), dtoFactory.toInfo(roleNotification));
});
}
The notificationPusher method itself:
#Override
public void push(PushMessageComposite composite, RoleNotificationInfo roleNotificationInfo) {
String roleId = roleNotificationInfo.getRoleId();
String title = "New matching role!";
var push = Message.builder()
.setToken(composite.getMeta().getDeviceToken())
.setAndroidConfig(AndroidConfig.builder()
.setNotification(AndroidNotification.builder()
.setTitle(title)
.setBody(roleNotificationInfo.getBody())
.setSound(composite.getMeta().getSound())
.build())
.build())
.setApnsConfig(ApnsConfig.builder()
.setAps(Aps.builder()
.setAlert(ApsAlert.builder()
.setTitle(title)
.setBody(roleNotificationInfo.getBody())
.build())
.setBadge(composite.getMeta().getBadge().intValue())
.setSound(composite.getMeta().getSound())
.build())
.build())
.putData("roleId", roleId)
.build();
Try.run(() -> FirebaseMessaging.getInstance().sendAsync(push).get())
.onFailure(e -> {
log.error("Firebase Cloud Messaging failed during sendNotification", e);
var talent = talentDao.findTalentByNotificationToken(composite.getMeta().getDeviceToken());
talent.setNotificationToken(null);
talentDao.save(talent);
});
}
And the dto factory mapper method:
#Transactional(propagation = Propagation.MANDATORY)
public RoleNotificationInfo toInfo(RoleNotification source) {
return RoleNotificationInfo.builder()
.id(source.getId())
.body(source.getNotificationBody().getBody())
.created(source.getCreated())
.roleId(source.getRole().getId())
.build();
}
I am unsure where the problem lies. I assume it is due to the high amount of users fetched from certain queries (20k+). I did some profiling, and this were the results:
Memory/CPU charts:
My question is, should this even be possible at all? Is there a different approach that would be much more efficient? Should I use an external service for this? Is the problem something very obvious that I do not see? I am not sure where to look. If anything in my code is unclear and needs further clarification, please let me know and I'll try to edit and format it the best way I can.
I've followed an open Course on Spring web. Written some code to list all orders from a database and return them through a rest api. This works perfectly. Now I'm writing some code to give the ID of the order in the request, find 0 or 1 orders and return them. However, when there is no Order find with the given ID, a nullpointerexception is given. I can't find out what is causing this. I'm assuming the .orElse(null) statement. Please advise
Controller:
#RequestMapping("api/V1/order")
#RestController
public class OrderController {
private final OrderService orderService;
#Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
#GetMapping(path = "{id}")
public Order getOrderById(#PathVariable("id") int id) {
return orderService.getOrderById(id)
.orElse(null);
}
}
Service:
#Service
public class OrderService {
private final OrderDao orderDao;
#Autowired
public OrderService(#Qualifier("oracle") OrderDao orderDao) {
this.orderDao = orderDao;
}
public Optional<Order> getOrderById(int orderNumber) {
return orderDao.selectOrderById(orderNumber);
}
}
Dao:
#Override
public Optional<Order> selectOrderById(int searchedOrderNumber) {
final String sql = "SELECT \"order\", sender, receiver, patient, orderdate, duedate, paymentref, status, netprice from \"ORDER\" where \"order\" = ?";
Order order = jdbcTemplate.queryForObject(sql, new Object[] {searchedOrderNumber}, (resultSet, i) -> {
int orderNumber = resultSet.getInt( "\"order\"");
String sender = resultSet.getString("sender");
String receiver = resultSet.getString("receiver");
String patient = resultSet.getString("patient");
String orderDate = resultSet.getString("orderdate");
String dueDate = resultSet.getString("duedate");
String paymentRef = resultSet.getString("paymentref");
String status = resultSet.getString("status");
int netPrice = resultSet.getInt("netprice");
return new Order(orderNumber,sender,receiver,patient,orderDate,dueDate,paymentRef,status,netPrice);
});
return Optional.ofNullable(order);
}
For the Jdbcexception, use general query instead of the queryForObject, or use try/catch to convert the Jdbc related exception, else Spring itself will handle these internally using ExceptionTranslater, ExceptionHandler etc.
To handle optional case in controllers, just throw an exception there, for example PostController.java#L63
And handle it in the PostExceptionHandler.
Editing based on comment about stack trace
For your error please check - Jdbctemplate query for string: EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
To solve problem associated with orderService.getOrderById(id) returning null you can return ResponseEntity.ResponseEntity gives you more flexibility in terms of status code and header. If you can change your code to return ResponseEntitythen you can do something like
#GetMapping(path = "{id}")
public ResponseEntity<?> getOrderById(#PathVariable("id") int id) {
return orderService
.getOrderById(id)
.map(order -> new ResponseEntity<>(order.getId(), HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
You can even write generic Exception handler using #ControllerAdvice and throw OrderNotFoundException as .orElse(throw new OrderNotFoundException);. Check more information here.
I am attempting to use the DataLoader feature within the graphql-java-kickstart library:
https://github.com/graphql-java-kickstart
My application is a Spring Boot application using 2.3.0.RELEASE. And I using version 7.0.1 of the graphql-spring-boot-starter library.
The library is pretty easy to use and it works when I don't use the data loader. However, I am plagued by the N+1 SQL problem and as a result need to use the data loader to help alleviate this issue. When I execute a request, I end up getting this:
Can't resolve value (/findAccountById[0]/customers) : type mismatch error, expected type LIST got class com.daluga.api.account.domain.Customer
I am sure I am missing something in the configuration but really don't know what that is.
Here is my graphql schema:
type Account {
id: ID!
accountNumber: String!
customers: [Customer]
}
type Customer {
id: ID!
fullName: String
}
I have created a CustomGraphQLContextBuilder:
#Component
public class CustomGraphQLContextBuilder implements GraphQLServletContextBuilder {
private final CustomerRepository customerRepository;
public CustomGraphQLContextBuilder(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
#Override
public GraphQLContext build(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
return DefaultGraphQLServletContext.createServletContext(buildDataLoaderRegistry(), null).with(httpServletRequest).with(httpServletResponse).build();
}
#Override
public GraphQLContext build(Session session, HandshakeRequest handshakeRequest) {
return DefaultGraphQLWebSocketContext.createWebSocketContext(buildDataLoaderRegistry(), null).with(session).with(handshakeRequest).build();
}
#Override
public GraphQLContext build() {
return new DefaultGraphQLContext(buildDataLoaderRegistry(), null);
}
private DataLoaderRegistry buildDataLoaderRegistry() {
DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry();
dataLoaderRegistry.register("customerDataLoader",
new DataLoader<Long, Customer>(accountIds ->
CompletableFuture.supplyAsync(() ->
customerRepository.findCustomersByAccountIds(accountIds), new SyncTaskExecutor())));
return dataLoaderRegistry;
}
}
I also have create an AccountResolver:
public CompletableFuture<List<Customer>> customers(Account account, DataFetchingEnvironment dfe) {
final DataLoader<Long, List<Customer>> dataloader = ((GraphQLContext) dfe.getContext())
.getDataLoaderRegistry().get()
.getDataLoader("customerDataLoader");
return dataloader.load(account.getId());
}
And here is the Customer Repository:
public List<Customer> findCustomersByAccountIds(List<Long> accountIds) {
Instant begin = Instant.now();
MapSqlParameterSource namedParameters = new MapSqlParameterSource();
String inClause = getInClauseParamFromList(accountIds, namedParameters);
String sql = StringUtils.replace(SQL_FIND_CUSTOMERS_BY_ACCOUNT_IDS,"__ACCOUNT_IDS__", inClause);
List<Customer> customers = jdbcTemplate.query(sql, namedParameters, new CustomerRowMapper());
Instant end = Instant.now();
LOGGER.info("Total Time in Millis to Execute findCustomersByAccountIds: " + Duration.between(begin, end).toMillis());
return customers;
}
I can put a break point in the Customer Repository and see the SQL execute and it returns a List of Customer objects. You can also see that the schema wants an array of customers. If I remove the code above and put in the resolver to get the customers one by one....it works....but is really slow.
What am I missing in the configuration that would cause this?
Can't resolve value (/findAccountById[0]/customers) : type mismatch error, expected type LIST got class com.daluga.api.account.domain.Customer
Thanks for your help!
Dan
Thanks, #Bms bharadwaj! The issue was on my side in understanding how the data is returned in the dataloader. I ended up using a MappedBatchLoader to bring the data in a map. The key in the map being the accountId.
private DataLoader<Long, List<Customer>> getCustomerDataLoader() {
MappedBatchLoader<Long, List<Customer>> customerMappedBatchLoader = accountIds -> CompletableFuture.supplyAsync(() -> {
List<Customer> customers = customerRepository.findCustomersByAccountId(accountIds);
Map<Long, List<Customer>> groupByAccountId = customers.stream().collect(Collectors.groupingBy(cust -> cust.getAccountId()));
return groupByAaccountId;
});
// }, new SyncTaskExecutor());
return DataLoader.newMappedDataLoader(customerMappedBatchLoader);
}
This seems to have done the trick because before I was issuing hundreds of SQL statement and now down to 2 (one for the driver SQL...accounts and one for the customers).
In the CustomGraphQLContextBuilder,
I think you should have registered the DataLoader as :
...
dataLoaderRegistry.register("customerDataLoader",
new DataLoader<Long, List<Customer>>(accountIds ->
...
because, you are expecting a list of Customers for one account Id.
That should work I guess.
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.