Θα δημιουργήσουμε το category-sidebar component, το οποίο περιέχει δύο φίλτρα για τα προϊόντα. Ο χρήστης θα μπορεί, να αναζητήσει προϊόντα συγκεκριμένων εταιριών ή/και μέσα σε ένα συγκεκριμένο διάστημα τιμών(10-20ευρώ).
Για την αρχικοποίηση των τιμών των φίλτρων, ο client θα ζητάει τον αριθμό των προϊόντων της κάθε εταιρίας, σε μια κατηγορία. Αν επιλεχθεί κάποιο φίλτρο από τα διαστήματα τιμών μετά, τότε στέλνει μια παράμετρο range, για να πάρει τον αριθμό των προϊόντων της κάθε εταιρίας, σε ένα διάστημα τιμών.
Για την αρχικοποίηση των τιμών των φίλτρων, ο client θα ζητάει τον αριθμό των προϊόντων ανά διάστημα τιμών. Αν επιλεχθεί κάποιο φίλτρο από τις εταιρίες μετά, τότε στέλνει μια παράμετρο brand, για να πάρει τον αριθμό των προϊόντων στο συγκεκριμένο διάστημα τιμών, που ανήκουν σε αυτές τις εταιρίες.
Αρχικά θα χρειαστούμε μια μέθοδο, η οποία θα επιστρέφει τις εταιρίες των προϊόντων μιας κατηγορίας.
Προσθέτουμε την ακόλουθη μέθοδο στο ProductRepository. Θα επιστρέφει τη λίστα των εταιριών, πραγματοποιώντας το ερώτημα JPQL "select DISTINCT prod.brand from Product prod where prod.category.id = :id".
package springeshop.repositories;
@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {
....
@Query("select DISTINCT prod.brand from Product prod where prod.category.id = :id")
List<Brand> findCategoryBrands(@Param("id") int id);
}
Προσθέτουμε την μέθοδο στο ProductService και ProductServiceImpl.
package springeshop.service;
public interface ProductService{
...
List<Brand> findCategoryBrands(@Param("id") int id);
}
package springeshop.service;
@Service("productService")
@Transactional
public class ProductServiceImpl implements ProductService{
...
@Override
public List<Brand> findCategoryBrands(@Param("id") int id){
return productRepository.findCategoryBrands(id);
}
}
Δημιουργούμε έναν BrandApiController στο springeshop.controller
package springeshop.controller;
@RestController
@RequestMapping("/api")
public class BrandApiController{
public static final Logger logger = LoggerFactory.getLogger(BrandApiController.class);
@Autowired
private ProductService productService;
@Autowired
private CategoryService categoryService;
...
Προσθέτουμε τη μέθοδο getCategoryBrands στον BrandApiController. Εάν δεν υπάρχει κατηγορία με αυτό το όνομα, επιστρέφουμε status 404 NOT FOUND.
@RequestMapping(value = "/brands", method = RequestMethod.GET)
public ResponseEntity<?> getCategoryBrands(@RequestParam((@value = "category") String category{
Category requestedCategory = categoryService.findByName(getCorrectCategoryName(category));
if(requestedCategory == null){
logger.error("Category with name {} not found.", name);
return new ResponseEntity<>(new ErrorMessage("Category with name " + name + " not found"), HttpStatus.NOT_FOUND);
}
Εάν η λίστα τον εταιριών είναι κενή, επιστρέφουμε status 204 NO CONTENT, αλλιώς τις ταξινομούμε σε αύξουσα αλφαβητική σειρά και επιστρέφουμε την λίστα με status 200.
List<Brand> brands = productService.findCategoryBrands(requestedCategory.getId());
if(brands.isEmpty())){
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
brands.sort((Brand brand1, Brand brand2) -> brand1.getName().compareTo(brand2.getName()));
return new ResponseEntity<List<Brand>>(brands, HttpStatus.OK);
Παράδειγμα αιτήματος με Postman
Θα δημιουργήσουμε τις μεθόδους, οι οποίες θα μας επιστρέφουν τον αριθμό των προϊόντων
Προσθέτουμε τις ακόλουθες μέθοδους στο ProductRepository, οι οποίες θα χρησιμοποιηθούν, όταν αρχικοποιηθούν τα φίλτρα. Τα ερωτήμα είναι πάνω από τις υπογραφές των μεθόδων.
....
@Query("select Count (prod) from Product prod where prod.category.id = :categoryid and prod.brand.id = :brandid")
int findNumberOfProductsOfBrandInCategory(@Param("categoryid") int categoryid, @Param("brandid") int brandid);
@Query("select Count (prod) from Product prod where prod.category = :category and prod.price between :min and :max")
int findNumberOfProductsWithinPriceRange(@Param("category") Category category, @Param("min") double min, @Param("max") double max);
Προσθέτουμε τις ακόλουθες μεθόδους στο ProductService.
....
int findNumberOfProductsOfBrandInCategory(int categoryid, int brandid);
int findNumberOfBrandProductsWithinSpecificRangesInCategory(Category category, Brand brand, List<double[]< priceRanges);
int findNumberOfProductsWithinPriceRange(Category category, double min, double max);
int findNumberOfSpecificBrandsProductsWithinPriceRange(Category category, double min, double max, List<Brand< brands);
Προσθέτουμε τις υλοποιήσεις των μεθόδων, όταν δεν είναι επιλεγμένα τα αντίστοιχα φίλτρα
package springeshop.service;
@Service("productService")
@Transactional
public class ProductServiceImpl implements ProductService{
@Autowired
private EntityManager entityManager;
...
@Override
public int findNumberOfProductsOfBrandInCategory(int categoryid, int brandid) {
return productRepository.findNumberOfProductsOfBrandInCategory(categoryid, brandid);
}
@Override
public int findNumberOfProductsWithinPriceRange(Category category, double min, double max) {
return productRepository.findNumberOfProductsWithinPriceRange(category, min, max);
}
}
Η ακόλουθη μέθοδος findNumberOfSpecificBrandsProductsWithinPriceRange(), επιστρέφει τον αριθμό των προϊόντων ανά διάστημα τιμών, όταν ο χρήστης έχει επιλέξει μία ή περισσότερες εταιρίες. Θα χρησιμοποιήσουμε το Criteria API, επειδή δεν γνωρίζουμε από πριν, πόσες εταιρίες θα είναι επιλεγμένες.
Δημιουργούμε το query root
@Override
public int findNumberOfSpecificBrandsProductsWithinPriceRange(Category category, double min, double max, List<Brand> brands){
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> criteriaQuery = criteriaBuilder.createQuery(Product.class);
Root<Product> productsRoot = criteriaQuery.from(Product.class);
Με το παρακάτω for, δημιουργούμε συνθήκες του WHERE, "prod.brand = :brand." και τα προσθέτουμε σε ένα array.
List<Predicate> brandPredicateList = new ArrayList<>();
for(int i=0; i < brands.size(); i++){
brandPredicateList.add(criteriaBuilder.equal(productsRoot.get("brand"), brands.get(i)));
Predicate[] brandsPredicateArray = new Predicate[brandPredicateList.size()];
brandPredicateList.toArray(brandsPredicateArray);
Δημιουργούμε τις συνθήκες "prod.category = :category" και "product.price between :min and :max"
Predicate categoryPredicate = criteriaBuilder.equal(productsRoot.get("category"), category);
Predicate priceRangePredicate = criteriaBuilder.between(productsRoot.get("price"), min, max);
Predicate brandsPredicate = criteriaBuilder.or(brandsPredicateArray);
Δημιουργούμε το τελικό ερώτημα "select Count (prod) from Product prod where prod.category = :category and prod.price between :min and ( prod.brand = :brand1 or prod.brand2 .....)", το εκτελεί ο entityManager και επιστρέφουμε τον αριθμο των αποτελεσμάτων.
criteriaQuery.where(criteriaBuilder.and(categoryPredicate, priceRangePredicate, brandsPredicate));
int number = 0;
number = entityManager.createQuery(criteriaQuery).getResultList().size();
return number;
Η ακόλουθη μέθοδος findNumberOfBrandProductsWithinSpecificRangesInCategory, επιστρέφει τον αριθμό των προϊόντων ανά εταιρία, όταν ο χρήστης έχει επιλέξει ένα ή περισσότερα διαστήματα τιμών.
Δημιουργούμε το query root
@Override
public findNumberOfBrandProductsWithinSpecificRangesInCategory(Category category, Brand brand, List<double[]> priceRanges) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> criteriaQuery = criteriaBuilder.createQuery(Product.class);
Root<Product> productsRoot = criteriaQuery.from(Product.class);
Με το παρακάτω for, δημιουργούμε συνθήκες του WHERE, "prod.price between :range1 and :range2" και τα προσθέτουμε σε ένα array.
List<Predicate> priceRangePredicateList = new ArrayList<>();
for(double[] range : priceRanges){
priceRangePredicateList.add(criteriaBuilder.between(productsRoot.get("price"), range[0], range[1]));
}
Predicate[] priceRangePredicateArray = new Predicate[priceRangePredicateList.size()];
priceRangePredicateList.toArray(priceRangePredicateArray);
Δημιουργούμε τις συνθήκες "prod.category = :category" και "prod.brand = :brand"
Predicate categoryPredicate = criteriaBuilder.equal(productsRoot.get("category"), category);
Predicate brandsPredicate = criteriaBuilder.equal(productsRoot.get("brand"), brand);
Predicate priceRangePredicate = criteriaBuilder.or(priceRangePredicateArray);
Δημιουργούμε το τελικό ερώτημα "select Count (prod) from Product prod where prod.category = :category and prod.brand = :brand ( prod.price between :range1 and :range 2 or prod.price between :range3 and :range 4.....)", το εκτελεί ο entityManager και επιστρέφουμε τον αριθμο των αποτελεσμάτων.
criteriaQuery.where(criteriaBuilder.and(categoryPredicate, brandsPredicate, priceRangePredicate));
int number = 0;
number = entityManager.createQuery(criteriaQuery).getResultList().size();
return number;
Δημιουργούμε το τελικό ερώτημα "select Count (prod) from Product prod where prod.category = :category and prod.brand = :brand ( prod.price between :range1 and :range 2 or prod.price between :range3 and :range 4.....)", το εκτελεί ο entityManager και επιστρέφουμε τον αριθμο των αποτελεσμάτων.
criteriaQuery.where(criteriaBuilder.and(categoryPredicate, brandsPredicate, priceRangePredicate));
int number = 0;
number = entityManager.createQuery(criteriaQuery).getResultList().size();
return number;
Θα προσθέσουμε τις κατάλληλες μεθόδους στον CategoryApiController. Πρώτα θα δημιουργήσουμε μια μέθοδο στο BrandService, η οποία θα παίρνει τις παραμέτρους των εταιριών και θα επιστρέφει μια λίστα από Brand entities.
package springeshop.service;
public interface BrandService{
...
List<Brand> findSpecificBrands(String[] brands);
}
package springeshop.service;
@Service("brandService")
@Transactional
public class BrandServiceImpl implements BrandService{
@Autowired
private EntityManager entityManager;
...
@Override
public List<Brand> findSpecificBrands(String[] brands) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Brand> criteriaQuery = criteriaBuilder.createQuery(Brand.class);
Root<Brand> productsRoot = criteriaQuery.from(Brand.class);
Predicate[] predicates = new Predicate[brands.length];
for(int i=0; i < predicates.length; i++){
predicates[i] = criteriaBuilder.equal(requestedBrands.get("name"), brands[i]);
}
criteriaQuery.where(criteriaBuilder.or(predicates));
return entityManager.createQuery(criteriaQuery).getResultList();
}
}
Προσθέτουμε κάποιες σταθερές στο Constants.java
package springeshop.util;
public final class Constants {
....
private static final double[] PRICE_RANGE_ZERO_TO_TEN = { 0.0 ,9.99};
private static final double[] PRICE_RANGE_TEN_TO_TWENTY = { 10.0 ,19.99};
private static final double[] PRICE_RANGE_TWENTY_TO_THIRTY = { 20.0 ,29.99};
private static final double[] PRICE_RANGE_THIRTY_TO_FIFTY = { 30.0 ,50.0};
}
Δημιουργούμε τις κλάσεις-μοντέλα ProductsPerPriceRange και ProductsPerBrand
public class ProductsPerPriceRange{
private double min;
private double max;
private int number;
private int rangeId;
public ProductsPerPriceRange() {}
//Getters - Setters ...
}
public class ProductsPerBrand{
private String brand;
private int number;
public ProductsPerBrand() {}
//Getters - Setters ...
}
Δημιουργούμε δύο βοηθητικές μεθόδους στον CategoryApiController, οι οποίες θα επιστρέφουν το minx και το max του κάθε εύρους τιμών.
public double getRangeMin(String range){
double min = 0.0;
&if(range.equals("1")){
min = Constants.PRICE_RANGE_ZERO_TO_TEN[0];
}else if(range.equals("2")){
min = Constants.PRICE_RANGE_TEN_TO_TWENTY[0];
else if(range.equals("3")){
min = Constants.PRICE_RANGE_TWENTY_TO_THIRTY[0];
}else if(range.equals("4")){
min = Constants.PRICE_RANGE_THIRTY_TO_FIFTY[0];
}
return min;
}
public double getRangeMax(String range){
double max = 0.0;
&if(range.equals("1")){
max = Constants.PRICE_RANGE_ZERO_TO_TEN[1];
}else if(range.equals("2")){
max = Constants.PRICE_RANGE_TEN_TO_TWENTY[1];
else if(range.equals("3")){
max = Constants.PRICE_RANGE_TWENTY_TO_THIRTY[1];
}else if(range.equals("4")){
max = Constants.PRICE_RANGE_THIRTY_TO_FIFTY[1];
}
return max;
}
Προσθέτουμε τη μέθοδο getCategoryRangeProductsNumber στον controller. Αν δεν υπάρχει η ζητούμενη κατηγορία, επιστρέφουμε status 404 NOT FOUND.
@RequestMapping(value = "/categories/{name}/ranges/{rangeid}/products/count", method = RequestMethod.GET)
public ResponseEntity<?> getCategoryRangeProductsNumber(@PathVariable((@value = "name") String name,
@PathVariable((@value = "rangeid") String rangeid,
@RequestParam((@value = "brand", required = false) String[] brands{
Category category = categoryService.findByName(getCorrectCategoryName(name));
if(requestedCategory == null){
logger.error("Category with name {} not found.", name);
return new ResponseEntity<>(new ErrorMessage("Category with name " + name + " not found"), HttpStatus.NOT_FOUND);
}
Εάν δεν είναι κάποιο φίλτρο επιλεγμένο καλούμε την findNumberOfProductsWithinPriceRange, αλλιώς βρίσκουμε τα αντίστοιχα Brand entities και το παιρνάμε στην findNumberOfSpecificBrandsProductsWithinPriceRange.
ProductsPerPriceRange ppNumber = new ProductsPerPriceRange();
if(brands == null){
ppNumber.setNumber(productService.findNumberOfProductsWithinPriceRange(category, getRangeMin(rangeid), getRangeMax(rangeid)));
}else{
List<Brand> brandPredicateList = new ArrayList<>();
brandList = brandService.findSpecificBrands(brands);
ppNumber.setNumber(productService.findNumberOfSpecificBrandsProductsWithinPriceRange(category, getRangeMin(rangeid), getRangeMax(rangeid), brandList));
}
Προσθέτουμε min, max του διαστήματος και το id του και το επιστρέφουμε με status 200 OK.
ppNumber.setMin(getRangeMin(rangeid));
ppNumber.setMax(getRangeMax(rangeid));
ppNumber.setRangeId(Integer.parseInt(rangeid));
return new ResponseEntity<ProductsPerPriceRang<(ppNumber, HttpStatus.OK);
Παράδειγμα αιτήματος με Postman
Προσθέτουμε τη μέθοδο getCategoryBrandProductsNumber στον controller. Αν δεν υπάρχει η κατηγορία ή η εταιρία, επιστρέφουμε status 404 NOT FOUND.
@RequestMapping(value = "/categories/{categoryname}/brands/{brandname}/products/count", method = RequestMethod.GET)
public ResponseEntity<?> getCategoryBrandProductsNumber(@PathVariable((@value = "categoryname") String categoryname,
@PathVariable((@value = "brandname") String brandname,
@RequestParam((@value = "range", required = false) String[] ranges{
Category category = categoryService.findByName(getCorrectCategoryName(categoryname));
if(category == null){
logger.error("Category with name {} not found.", categoryname);
return new ResponseEntity<>(new ErrorMessage("Category with name " + categoryname + " not found"), HttpStatus.NOT_FOUND);
}
if(!brandService.doesBrandExist(brandname)){
logger.error("Brand with name {} does not exist.", brandname);
return new ResponseEntity<>(new ErrorMessage("Brand with name {} does not exist" not found"), HttpStatus.NOT_FOUND);
}
Εάν δεν είναι κάποιο διάστημα τιμών καλούμε την findNumberOfProductsOfBrandInCategory, αλλιώς δημιουργούμε μια λίστα από διαστήματα τιμών, από την παράμετρο range και το παιρνάμε στην findNumberOfBrandProductsWithinSpecificRangesInCategory.
int productsNumber = 0;
if(ranges == null){
productsNumber = productService.findNumberOfProductsOfBrandInCategory(category.getId(), brandService.findByName(brandname).getId());
}else{
List<double[]> priceRangeList = new ArrayList<>();
for(String range : ranges){
double[] rangeValues = new double[2];
rangeValues[0] = getRangeMin(range);
rangeValues[1] = getRangeMax(range);
priceRangeList.add(rangeValues);
}
productsNumber = productService.findNumberOfBrandProductsWithinSpecificRangesInCategory(category, brandService.findByName(brandname), priceRangeList);
}
Προσθέτουμε τον αριθμό και το όνομα της εταιρίας και το επιστρέφουμε με status 200 OK.
ProductsPerBrand pNumber = new ProductsPerBrand();
pNumber.setNumber(productsNumber);
pNumber.setBrand(brandname);
return new ResponseEntity<ProductsPerBrand<(pNumber, HttpStatus.OK);
Παράδειγμα αιτήματος με Postman
Δημιουργούμε το component category-sidebar και το τοποθετούμε στο φάκελο components.
ng generate component category-sidebar
category.service.ts
category-sidebar.component.html
Το html αρχείο περιέχει δύο λίστες με checkboxes.
category-sidebar.component.css
Δημιουργούμε τα αντίστοιχα μοντέλα ProductsPerBrand και ProductsPerPriceRange.
Κάθε φορά που αλλάζει ο χρήστης κατηγορία, καλείται η μέθοδος ngOnChanges(), η οποία καλεί την updateSidebar(), για να πραγματοποίησει τα HTTP αιτήματα και να αρχικοποιήσει τα φίλτρα
category-sidebar.component.ts
Όταν ο χρήστης κάνει κλικ σε κάποια εταιρία (event), εκτελείται η μέθοδος onSelectedBrand(), η οποία ενημερώνει τον αριθμό των προϊόντων ανα διάστημα τιμών. Αντίστοιχα, όταν κάνει κλικ σε κάποιο διάστημα (event), εκτελείται η μέθοδος onSelectedPriceRange(), η οποία ενημερώνει τον αριθμό των προϊόντων ανα εταιρία.