I am currently studying Springboot so I am developing a personal project. In this Project a Have an entity named Campaign and I want to build a endpoint to list all campaigns in the database filtering, sorting and paginating
Some examples of requests:
# Pagination
GET domain/api/v1/campaigns (return the first page with all campaigns, default sorting and page size 20)
GET domain/api/v1/campaigns?page=2&size=50 (return the second page with all campaigns, default sorting and page size 50)
GET domain/api/v1/campaigns?page=2&size=100 (return the second page with all campaigns, default sorting and page size 50 which is the max)
# SORTING
GET domain/api/v1/campaigns?sort=name,asc (return the first page with all campaigns, sorting by name and page size 20)
GET domain/api/v1/campaigns?sort=name,asc&sort=startDate,dsc (return the first page with all campaigns, sorting by name and startDate and page size 20)
# Filtering
GET domain/api/v1/campaigns?status=open (return the first page with all campaigns with "open" status, default sorting and page size 20)
GET domain/api/v1/campaigns?status=open&status=new (return the first page with all campaigns with "open" and "new" status,default sorting and page size 20)
GET domain/api/v1/campaigns?status=open&startdate=2023-01-12 (return the first page with all campaigns with "open" status and startDate 2023-01-12, default sortin and page size 20)
# Combined
GET domain/api/v1/campaigns?status=open&status=new&page=2&size=10&sort=name,asc (returns page 2 with size 10 sorting by name the campaigns with statuses "open" and "new")
Which is the best way to implement this? A single Controller method that delegates to the correct service method based on the parameters? Collecting the parameters on a HashMap and create a generic Query for the database that deals with the parameters? Many different controllers that receive a diffent set of parameters?
Here is mt Campign Model and empty Repository, Service And Controller
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "CAMPAIGNS")
public class Campaign {
@Id
@GeneratedValue
private Long id;
private String name;
@Column(name = "start_date")
private LocalDateTime startDate;
@Column(name = "end_date")
private LocalDateTime endDate;
private String status;
}
public interface CampaignRepository extends JpaRepository<Campaign, Long> {
}
@Service
@AllArgsConstructor
public class CampaignService {
private final CampaignRepository campaignRepository;
@RestController
@AllArgsConstructor
@RequestMapping("/api/v1/campaigns")
public class CampaignController {
private final CampaignService campaignService;
}
CodePudding user response:
You could use the annotation @RequestParam in your controller. It would be something like:
@GetMapping("/api/v1/campaigns")
@ResponseBody
public String CampaignController(@RequestParam(name = "page", required = false, defaultValue = "0") Integer page, @RequestParam (name = "size", required = false, defaultValue = "50") Integer size) {
return "page: " page;
}
CodePudding user response:
For paging and sorting, the Spring Framework comes with good support:
public interface CampaignRepository extends PagingAndSortingRepository<Campaign, Long> {
}
This provides you the possibility to append a Pageable to the methods. E.g. if you want to page through results filtered by status:
public interface CampaignRepository extends PagingAndSortingRepository<Campaign, Long> {
List<Campaign> findAllByStatusIn(Collection<String> status, Pageable pageRequest);
}
Your Service can then easily prepare such a pageable, for example:
public List<Campaign> listByStatusSorted(Collection<String> status, int page, int pageSize, String sortProperty) {
PageRequest pageRequest = PageRequest.of(page, pageSize, Sort.by(sortProperty).ascending());
return campaignRepository.findAllByStatusIn(status, pageRequest);
}
Sorting ascending or descending can also be provided from the controller to the service of course. But in essence I would have one or two service methods. I think this is a matter of taste where you'd rather have another service method instead of adding ever more parameters.
I would try to mostly use one controller endpoint with default values. Something along the lines of:
public List<Campaign> listCampaigns(@RequestParam(value = "page", required = false, defaultValue = "0") int page, @RequestParam(value = "pageSize", required = false, defaultValue = "20") int pageSize, ...) {
...
}
Going by this, absolutely none of
Collecting the parameters on a HashMap and create a generic Query for the database that deals with the parameters
is needed. By using the naming conventions all is done for you. In fact, the repository I provided above should already work without any further code - just inject it in the service. Further reading on the power of Spring Data Repositories.
CodePudding user response:
It looks like you are trying to build a dynamic query. In this case, not only can you accomplish this with a single controller method, but you will also only need one service method with the use of Specifications. Additionally, you can retrieve certain pages and limit its size with the Page<T>
interface and the PageRequest
class
CampaignController.java
:
// receive all possible arguments
public Page<Campaign> filterCampaigns(..., Pageable pageable) {
return campaignService.getCampaignsByCriteria(..., pageable.getPageNumber());
}
You can pass all possible URL parameters to the controller method. To execute specifications, CampaignRepository
will need to extend the JpaSpecificationExecutor<T>
interface. Paginating campaigns will require another interface PagingAndSortingRepository<T, ID>
CampaignRepository.java
public interface CampaignRepository extends
JpaRepository<Campaign, Long>, JpaSpecificationExecutor<Campaign>,
PagingAndSortingRepository<Campaign, Long> {
}
Following this, create a helper class that defines these specifications. Here's an example for finding by status, but you can have as many specifications as necessary
CampaignSpecification.java
public static Specification<Campaign> hasStatus(String campaignStatus) {
return startDate == null ? null : (root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.get("status"), campaignStatus);
}
If the status argument is not provided, it will return null. Now, you can call this execute this specification in the service.
CampaignService.java
public Page<Campaign> getCampaignsByCriteria(String status,
LocalDateTime startDate,
int pageNumber) {
PageRequest campaignPageRequest = PageRequest
.of(pageNumber, 2, Sort.by("startDate").ascending());
return campaignRepository.findAll(Specification.where(
CampaignSpecification.hasStatus(status).
and(CampaignSpecification.hasSomethingElse(...)))
, campaignPageRequest)
)
}
You can chain as many specifications as you need. The PageRequest
class allows you to specify the current page, the maximum number of items for each page, as well as perform sorting
Further reading:
Spring Data Specification: https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/
Page interface: https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html
PageRequest: https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/PageRequest.html