- Name: Nguyễn Quang Trực
- Student ID: ITCSIU23041
- Class: Group 2
- Spring Boot 3.3.x
- Spring Data JPA
- MySQL 8.0
- Thymeleaf
- Maven
- Import project into VS Code
- Create database:
product_management - Update
application.propertieswith your MySQL credentials - Run:
mvn spring-boot:run - Open browser: http://localhost:8080/products
- CRUD operations
- Search functionality
- Advanced search with filters
- Validation
- Sorting
- Pagination
- Statistics Dashboard
- REST API (Bonus)
product-management/
│
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/product_management/
│ │ │ ├── ProductManagementApplication.java (Main)
│ │ │ ├── entity/
│ │ │ │ └── Product.java
│ │ │ ├── repository/
│ │ │ │ └── ProductRepository.java
│ │ │ ├── service/
│ │ │ │ ├── ProductService.java
│ │ │ │ └── ProductServiceImpl.java
│ │ │ └── controller/
│ │ │ ├── ProductController.java
│ │ │ └── DashboardController.java
│ │ │
│ │ └── resources/
│ │ ├── application.properties
│ │ ├── static/
│ │ │ └── css/
│ │ └── templates/
│ │ ├── product-list.html
│ │ ├── product-form.html
│ │ └── dashboard.html
│ │
│ └── test/
│ └── java/
│
├── pom.xml (Maven dependencies)
└── README.md
Objective: Retrieve and display the complete list of products.
-
Request Initiation: The flow begins when a
GETrequest is made to the root path/products. -
Controller Layer (
ProductController.java): ThelistProductsmethod intercepts the request. It calls the service layer to fetch data and adds it to theModelfor rendering.// Web_lab_7/src/main/java/com/example/product_management/controller/ProductController.java @GetMapping public String listProducts(Model model) { // Retrieve all products from the service layer List<Product> products = productService.getAllProducts(); // Bind the list to the model with key "products" model.addAttribute("products", products); // Return the view name "product-list.html" return "product-list"; }
-
Service Layer (
ProductServiceImpl.java): ThegetAllProductsmethod delegates the data retrieval to the repository.// Web_lab_7/src/main/java/com/example/product_management/service/ProductServiceImpl.java @Override public List<Product> getAllProducts() { // Execute SELECT * FROM products via JPA return productRepository.findAll(); }
-
View Layer (
product-list.html): Thymeleaf iterates over theproductslist to render the table rows.<tr th:each="product : ${products}"> <td th:text="${product.id}">...</td> </tr>
Objective: Instantiate and persist a new Product entity.
Phase 1: Form Display (GET)
- Controller: The
showNewFormmethod maps to/products/new. It creates an emptyProductinstance to bind form data.// Web_lab_7/src/main/java/com/example/product_management/controller/ProductController.java @GetMapping("/new") public String showNewForm(Model model) { // Create empty entity for form binding Product product = new Product(); model.addAttribute("product", product); return "product-form"; }
Phase 2: Data Submission (POST)
- Controller: The
saveProductmethod handles thePOSTrequest to/products/save. - Service & Repository:
Since the
idof the new product isnull,JpaRepository.save()identifies this as an INSERT operation.// Web_lab_7/src/main/java/com/example/product_management/service/ProductServiceImpl.java @Override public Product saveProduct(Product product) { // Persist the entity (INSERT if ID is null) return productRepository.save(product); }
Objective: Modify an existing Product entity. This reuses logic from the Create flow but with a populated ID.
Phase 1: Form Pre-filling (GET)
- Controller: The
showEditFormmethod accepts theidvia the URL path/products/edit/{id}. - Service: It attempts to find the existing product.
// Web_lab_7/src/main/java/com/example/product_management/controller/ProductController.java @GetMapping("/edit/{id}") public String showEditForm(@PathVariable Long id, Model model, ...) { return productService.getProductById(id) .map(product -> { // Add existing product to model to pre-fill inputs model.addAttribute("product", product); return "product-form"; }) .orElseGet(() -> { // Handle non-existent ID return "redirect:/products"; }); }
Phase 2: Data Submission (POST)
-
View: The form includes a hidden input for the ID.
<input type="hidden" th:field="*{id}" />
-
Controller & Service: The form submits to the same
/products/saveendpoint. Because theproductobject now contains a non-nullid, thesave()method in the repository executes an UPDATE SQL statement instead of an INSERT.
Objective: Remove a specific product from the database.
-
Request Initiation: The user clicks "Delete," triggering a
GETrequest to/products/delete/{id}. -
Controller: The
deleteProductmethod extracts theidand delegates to the service.// Web_lab_7/src/main/java/com/example/product_management/controller/ProductController.java @GetMapping("/delete/{id}") public String deleteProduct(@PathVariable Long id, RedirectAttributes redirectAttributes) { try { // Initiate delete operation productService.deleteProduct(id); // ... (flash message logic) } catch (Exception e) { // ... (error handling) } return "redirect:/products"; }
-
Service Layer: The service method calls the repository's void delete method.
// Web_lab_7/src/main/java/com/example/product_management/service/ProductServiceImpl.java @Override public void deleteProduct(Long id) { // Execute DELETE FROM products WHERE id = ? productRepository.deleteById(id); }
Objective: Filter products based on multiple criteria (Name, Category, Price Range) combined.
-
Request Initiation: The user submits the filter form, sending a
GETrequest to/products/advanced-searchwith query parameters. -
Controller: The
advancedSearchmethod captures optional parameters and sanitizes empty strings tonullto ensure the repository query logic works correctly.// Web_lab_7/src/main/java/com/example/product_management/controller/ProductController.java @GetMapping("/advanced-search") public String advancedSearch(@RequestParam(required = false) String name, ... ) { // Convert empty strings to null for JPA query compatibility if (name != null && name.trim().isEmpty()) name = null; // ... (call service) List<Product> products = productService.advancedSearch(name, category, minPrice, maxPrice); model.addAttribute("products", products); return "product-list"; }
-
Repository Layer (
ProductRepository.java): A custom JPQL@Queryhandles the multi-criteria logic using check-for-null idioms (:param IS NULL OR field = :param).@Query("SELECT p FROM Product p WHERE " + "(:name IS NULL OR p.name LIKE %:name%) AND " + "(:category IS NULL OR p.category = :category) AND " + "(:minPrice IS NULL OR p.price >= :minPrice) AND " + "(:maxPrice IS NULL OR p.price <= :maxPrice)") List<Product> searchProducts(@Param("name") String name, ...);
Objective: Ensure data integrity by validating user input before saving to the database.
-
Entity Layer (
Product.java): Jakarta Validation annotations are applied to entity fields.@NotBlank(message = "Product name is required") private String name; @DecimalMin(value = "0.01", message = "Price must be greater than 0") private BigDecimal price;
-
Controller: The
saveProductmethod uses@Validto trigger validation andBindingResultto capture errors.@PostMapping("/save") public String saveProduct(@Valid @ModelAttribute("product") Product product, BindingResult result, ...) { // If validation fails, return to form with error details if (result.hasErrors()) { return "product-form"; } // Proceed to save }
-
View Layer (
product-form.html): Thymeleaf displays error messages next to invalid fields.<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error-message">Error</span>
Objective: Allow users to sort the product list by any column (ID, Name, Price, etc.) in Ascending or Descending order.
-
Request Initiation: Clicking a table header sends a request with
sortByandsortDirparameters (e.g.,?sortBy=price&sortDir=desc). -
Controller: The
listProductsmethod converts these strings into a Spring DataSortobject.// Web_lab_7/src/main/java/com/example/product_management/controller/ProductController.java Sort sort = sortDir.equals("asc") ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending(); // Pass sort object to service List<Product> products = productService.getAllProducts(sort);
-
Service Layer: Passes the
Sortobject directly to the repository.@Override public List<Product> getAllProducts(Sort sort) { return productRepository.findAll(sort); }
Objective: Display high-level metrics (Total Value, Average Price) and alerts (Low Stock).
-
Request Initiation: A
GETrequest is made to/dashboard. -
Controller (
DashboardController.java): Calls specialized service methods to aggregate data.@GetMapping public String showDashboard(Model model) { model.addAttribute("totalValue", productService.getTotalValue()); model.addAttribute("averagePrice", productService.getAveragePrice()); model.addAttribute("lowStockProducts", productService.getLowStockProducts(10)); return "dashboard"; }
-
Repository Layer: Uses JPQL Aggregate functions (
SUM,AVG,COUNT).@Query("SELECT COALESCE(SUM(p.price * p.quantity), 0) FROM Product p") BigDecimal calculateTotalValue(); @Query("SELECT p FROM Product p WHERE p.quantity < :threshold") List<Product> findLowStockProducts(@Param("threshold") int threshold);