購物車系統一定會有商品,我們建立Product.java,來創造Product的Entity。
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private Integer price;
private String image;
private String category;
public Product() {
}
public Product(Long id, String name, String description, Integer price, String image, String category) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.image = image;
this.category = category;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
}
下一個是ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
}
接著是ProductService.java
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product addProduct(Product product) {
return productRepository.save(product);
}
public String deleteProduct(Long id){
productRepository.deleteById(id);
return "Product deleted successfully";
}
public Product getProductById(Long id) throws Exception{
Optional<Product> opt = productRepository.findById(id);
if(opt.isPresent()){
return opt.get();
}
throw new Exception("Product not found");
}
}
最後是ProductController.java,完成新增、刪除、查詢商品的功能。
@RestController
@RequestMapping("/api/product")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping("/")
public ResponseEntity<Product> addProduct(@RequestBody Product product) {
Product createdProduct = productService.addProduct(product);
return new ResponseEntity<>(createdProduct, HttpStatus.CREATED);
}
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteProduct(@PathVariable("id") Long id) {
return new ResponseEntity<>(productService.deleteProduct(id), HttpStatus.OK);
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable("id") Long id) throws Exception {
return new ResponseEntity<>(productService.getProductById(id), HttpStatus.OK);
}
}
以上的部分大家應該都很熟悉了,不多做解釋。
測試一下能否運作。
首先測試新增商品。
先登入取得有效的token,設定到Authorization,再來POST http://localhost:8080/api/product/,request body的內容如下
{
"name": "Anime Inspired Novelty Bag Paimon is my Emergency Food Traveller Gift Anime Lover Gift",
"description": "The words on bag Paimon is my Emergency food. If you are a fan of paimon, you will love this bag. It would be a great addition to your fan collection. Make a funny gift.",
"price": 118,
"image": "https://cdn.pixabay.com/photo/2020/10/17/03/55/woman-5661078_1280.png",
"category": "Bags"
}
這是個範例,左邊的name、description、price、image、category是固定的,冒號的右邊可以自行修改成想要的內容。
token沒過期的話,就能得到201 Created的回應。
再來測試查詢商品,GET http://localhost:8080/api/product/1,和新增商品的結果一樣,狀態碼變成200 OK。
最後是刪除商品,回傳Product deleted successfully的訊息。
我們參考以下知名的電商網站,發現它們不會一次把全部的商品都呈現在網頁上,而是分成許多頁,一頁只顯示十幾個。另外,這些網站都有篩選過濾產品的功能。
將這些功能加入到專案中,我們從Controller開始修改,讓它接收前端提供的過濾用參數(商品類別、最低價、最高價、排序方式、頁數、一頁有幾個產品),經過處理後把分頁完成的結果交給前端使用。
//UserController.java
//根據條件篩選並分頁商品
@GetMapping("/")
//value用來定位URL中的參數,required = false代表可以不填,required = true是強制要填資料
public ResponseEntity<Page<Product>> findProductByFilter(@RequestParam(value = "category", required = false) String category,
@RequestParam(value = "minPrice", required = false) Integer minPrice,
@RequestParam(value = "maxPrice", required = false) Integer maxPrice,
@RequestParam(value = "sort", required = false) String sort,
@RequestParam(value = "pageNumber", required = true) Integer pageNumber,
@RequestParam(value = "pageSize", required = true) Integer pageSize){
Page<Product> filteredProductsPage = productService.getProductsByFilter(category, minPrice, maxPrice, sort, pageNumber, pageSize);
return new ResponseEntity<>(filteredProductsPage, HttpStatus.OK);
}
來到Service完成Controller設定的功能,取得分頁和過濾後的產品。
//UserService.java
public Page<Product> getProductsByFilter(String category, Integer minPrice, Integer maxPrice,
String sort, Integer pageNumber, Integer pageSize) {
取得第pageNumber(頁數是從0開始),每頁有pageSize個產品
Pageable pageable = PageRequest.of(pageNumber, pageSize);
從資料庫取得符合條件的產品
List<Product> products = productRepository.findProductsByFilter(category, minPrice, maxPrice, sort);
設定從哪裡開始取資料。
取得的指定頁數前面有多少資料,數值等於pageNumber*pageSize。
int startIndex = (int) pageable.getOffset();
設定從哪裡結束
如果剩餘的資料>=pageSize,就只取其中的pageSize筆。
如果剩餘的資料<pageSize,將剩下的資料全部取得。
int endIndex = Math.min((startIndex + pageable.getPageSize()), products.size());
從過濾後的產品列表,截取對應頁數和數量的產品
List<Product> pageContent = products.subList(startIndex, endIndex);
回傳內容、分頁資訊(頁碼、一頁有幾筆資料)、符合過濾條件的產品數量
return new PageImpl<>(pageContent, pageable, products.size());
}
在Repository實現過濾的功能,這次Spring Data JPA無法幫我們了,因為條件太多了,只能自行編寫SQL語句篩選符合的資料,每一行的SQL語句添加了解釋,幫助大家理解它們的作用。
//ProductRepository.java
@Query("SELECT p FROM Product p " +
OR前的部分:如果指定了類別,就只找這個類別的商品
OR後:沒有指定類別,那就把所有商品都找出來。
"WHERE (p.category = :category OR :category LIKE '') " +
如果指定了最低價格,就只找價格比這個最低價格高的商品
"AND (:minPrice IS NULL OR p.price >= :minPrice) " +
如果指定了最高價格,就只找價格比這個最高價格低的商品
如果使用者兩個都沒指定,那就把所有商品都找出來。
"AND (:maxPrice IS NULL OR p.price <= :maxPrice) " +
依據指定的排序方式,對商品價格進行排序。
"ORDER BY " +
如果sort是price_low,按照價格由低到高(ASC)排序
"CASE :sort WHEN 'price_low' THEN p.price END ASC, " +
sort是price_high,按照價格由高到低(DESC)排序
price_low和price_high是我們自己設定的,不是SQL規定的語句
"CASE :sort WHEN 'price_high' THEN p.price END DESC")
public List<Product> findProductsByFilter(@Param("category") String category,
@Param("minPrice") Integer minPrice,
@Param("maxPrice") Integer maxPrice,
@Param("sort") String sort);
目前我們資料庫中的產品太少了,看不出分頁的作用,需要更多的產品,一筆一筆加入的效率不高,必須想辦法提升,改良成一次可以接受多個建立產品的版本。
修改UserController.java
@PostMapping("/")
public ResponseEntity<List<Product>> addProducts(@RequestBody Product[] products) {
List<Product> createdProducts = new ArrayList<>();
for(Product product : products) {
Product p = productService.addProduct(product);
createdProducts.add(p);
}
return new ResponseEntity<>(createdProducts, HttpStatus.CREATED);
}
我們就能快速擴充資料庫中的產品,在這邊提供request body給大家使用。
[
{
"name": "Anime Inspired Novelty Bag Paimon is my Emergency Food Traveller Gift Anime Lover Gift",
"description": "The words on bag Paimon is my Emergency food. If you are a fan of paimon, you will love this bag. It would be a great addition to your fan collection. Make a funny gift.",
"price": 118,
"image": "https://cdn.pixabay.com/photo/2020/10/17/03/55/woman-5661078_1280.png",
"category": "Bags"
},{
"name": "Stereo Gaming Headset, Noise Cancelling Over Ear Headphones with Mic, LED Light, Bass Surround, Soft Memory Earmuffs",
"description": "Surrounding Stereo Subwoofer Clear sound operating strong brass",
"price": 219,
"image": "https://cdn.pixabay.com/photo/2015/09/09/20/30/headphones-933157_1280.jpg",
"category": "Headset"
},{
"name": "RGB Gaming Mouse, 8000 DPI Wired Optical Gamer Mouse with 11 Programmable Buttons & 5 Backlit Modes, Software Supports DIY Keybinds Rapid Fire Button",
"description": "Pentakill, 5 DPI Levels - Geared with 5 redefinable DPI levels (default as: 500/1000/2000/3000/4000)",
"price": 199,
"image": "https://cdn.pixabay.com/photo/2022/08/14/16/39/mouse-7386247_1280.jpg",
"category": "Mice"
},
{
"name": "Gaming Chair, Computer Chair with Footrest and Lumbar Support, Height Adjustable Game Chair with 360°-Swivel Seat and Headrest and for Office or Gaming",
"description": "Most Comfortable And Relaxing: Equipped with headrest and lumbar pillow.",
"price": 1899,
"image": "https://images.pexels.com/photos/7862491/pexels-photo-7862491.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
"category": "Chair"
},
{
"name": "18-Piece Service for 6 Dinnerware Set, Triple Layer Glass and Chip Resistant, Lightweight Round Plates and Bowls Set, Country Cottage",
"description": "18-PIECE SET: Includes (6) 10-1/4-inch dinner plates, (6) 6-3/4-inch appetizer plates and (6) 18-oz soup/cereal bowls. This set has everything you need for a full table service of 6, and it comes with the classic Corelle style.",
"price": 699,
"image": "https://cdn.pixabay.com/photo/2014/11/11/10/22/plate-526603_1280.jpg",
"category": "Dinnerware"
},
{
"name": "Night Light, Night Lights Plug into Wall 4-Pack, Nightlight Plug in Night Light, Dusk to Dawn Night Lamp Led Night Light for Kids Bedroom, Bathroom, Hallway Warm White",
"description": "Night Lights Plug into Wall : 0.5 watts, 50 lumen, 3000k warm white Night Light. 4pcs long-life LED, if lights up to 7hrs each day, only 0.7 Kwh per year.",
"price": 99,
"image": "https://cdn.pixabay.com/photo/2022/11/08/08/52/lamp-7578025_1280.jpg",
"category": "Light"
},
{
"name": "Men's Non Slip Running Shoes Ultra Light Breathable Casual Walking Shoes Fashion Sneakers Mesh Workout Sports Shoes",
"description": "Memory Foam Insole: The memory foam insole is comfortable to touch,absorbs the impact force in motion, reduces the burden on the body. It feels like you are walking on the clouds.",
"price": 199,
"image": "https://cdn.pixabay.com/photo/2014/06/18/18/42/running-shoe-371625_1280.jpg",
"category": "Shoe"
},
{
"name": "Mens Riggs Workwear Advanced Comfort Five Pocket Jeans",
"description": "For long days on the job, you want a work jean that keeps you comfortable. Constructed with a gusseted crotch and improved fit in the seat, thigh, and knee, this jean gives greater range of motion.",
"price": 217,
"image": "https://images.pexels.com/photos/603022/pexels-photo-603022.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
"category": "Jean"
},{
"name": "Wireless Bluetooth Headphones Over Ear,5.0 Foldable Gaming Wireless Headset Noise Cancelling Earphones Built-in Mic Macaron Bass Call Headset Hi-Fi Stereo Headsets Earbuds for Cell Phone PC Laptop",
"description": "Large capacity Long battery life, built-in large-capacity lithium battery, long standby time, 6 hours of talk on a single charge.",
"price": 99,
"image": "https://images.pexels.com/photos/205926/pexels-photo-205926.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
"category": "Headset"
},
{
"name": "75% Keyboard with Color Multimedia Display Mechanical Gaming Keyboard, Hot Swappable Keyboard, Gasket Mount RGB Custom Keyboard, Pre-lubed Stabilizer for Mac/Win, Black Kanagawa Theme",
"description": "Vibrant Color Multimedia Screen: The DIY multimedia display screen design in the upper right corner of the keyboard",
"price": 799,
"image": "https://images.pexels.com/photos/1772123/pexels-photo-1772123.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
"category": "Keyboard"
},
{
"name": " Joker Hot Swap RGB Tri-Mode Mechanical Keyboard,Wireless Bluetooth 5.0/2.4G/Wired Type-C PBT Sublimation Keycaps 65% Gaming Keyboard",
"description": "65% Compact Hot-swappable keyboard Gateron Mechanical 3 pins switches can be hotswapped at wish and replaced by other switches.",
"price": 699,
"image": "https://cdn.pixabay.com/photo/2023/04/10/10/30/keyboard-7913431_1280.jpg",
"category": "Keyboard"
}
]
目前第一筆資料的id是2
可以看到第一筆資料的id變成7
下面就不放結果的圖了,因為內容看起來都很相似。