[Thực hành] Ứng dụng Quản lý khách hàng: Quản lý tỉnh
NỘI DUNG BÀI VIẾT
Mục tiêu
Luyện tập sử dụng Spring Data Repositories.
Mô tả
Trong phần này, chúng ta sẽ thay thế việc tự triển khai tầng repository bằng cách sử dụng Spring Data Repositories.
Cùng với đó, chúng ta sẽ bổ sung các tính năng mới sau:
- Quản lý các tỉnh thành của khách hàng
- Hiển thị khách hàng theo từng tỉnh thành
Hướng dẫn
Bước 1: Thay thế tầng repository bằng repository của Spring Data.
- Xoá bỏ lớp CustomerRepositoryImpl.
- Xoá bỏ interface Repository
- Chỉnh sửa interface CustomerRepository, kế thừa PagingAndSortingRepository
- Chỉnh sửa interface CustomerService và lớp CustomerServiceImpl để chuyển sang sử dụng Repository mới. Lưu ý chuyển đổi kiểu List<Customer> sang Iterable<Customer>
- Chỉnh sửa CustomerController để phù hợp với CustomerService mới. Lưu ý chuyển sang sử dụng Iterable thay cho List.
Kết quả Interface CustomerRepository:
public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long> { } Kết quả lớp CustomerServiceImpl: package com.codegym.cms.service.impl; import com.codegym.cms.model.Customer; import com.codegym.cms.repository.CustomerRepository; import com.codegym.cms.service.CustomerService; import org.springframework.beans.factory.annotation.Autowired; public class CustomerServiecImpl implements CustomerService{ @Autowired private CustomerRepository customerRepository; @Override public Iterable<Customer> findAll() { return customerRepository.findAll(); } @Override public Customer findById(Long id) { return customerRepository.findOne(id); } @Override public void save(Customer customer) { customerRepository.save(customer); } @Override public void remove(Long id) { customerRepository.delete(id); } }
Thực hiện các thao tác cấu hình trong ApplicationConfig:
- Loại bỏ Bean customerRepository
- Thêm annotation @EnableJpaRepositories(“com.codegym.cms.repository”)
Chạy thử ứng dụng để đảm bảo các thay đổi đã hoạt động tốt.
Bước 2: Quản lý tỉnh
- Tạo model Province
- Tạo ProvinceRepository kế thừa PagingAndSortingRepository
- Tạo ProvinceService. Cập nhật file cấu hình ApplicationConfig để tạo bean mới: provinceService.
- Tạo ProvinceController (nên thực hiện lần lượt từng chức năng: Tạo, xem danh sách, sửa, xoá)
- Tạo các view để quản lý tỉnh: list, create, delete, edit
Lưu ý: Khi viết mã nguồn, cần thực hiện lần lượt các chức năng và chạy thử lần lượt, tránh trường hợp viết tất cả các chức năng cùng một lúc.
Kết quả lớp Province:
package com.codegym.cms.model; import javax.persistence.*; import java.util.List; @Entity @Table(name = "provinces") public class Province { @Id @GeneratedValue(strategy= GenerationType.AUTO) private Long id; private String name; @OneToMany(targetEntity = Customer.class) private List<Customer> customers; public Province() { } public Province(String name) { this.name = name; } 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 List<Customer> getCustomers() { return customers; } public void setCustomers(List<Customer> customers) { this.customers = customers; } } Interface ProvinceRepository: package com.codegym.cms.repository; import com.codegym.cms.model.Province; import org.springframework.data.repository.PagingAndSortingRepository; public interface ProvinceRepository extends PagingAndSortingRepository<Province, Long> { } Interface ProvinceService: package com.codegym.cms.service; import com.codegym.cms.model.Province; public interface ProvinceService { Iterable<Province> findAll(); Province findById(Long id); void save(Province province); void remove(Long id); } Lớp ProvinceServiceImpl: package com.codegym.cms.service.impl; import com.codegym.cms.model.Province; import com.codegym.cms.repository.ProvinceRepository; import com.codegym.cms.service.ProvinceService; import org.springframework.beans.factory.annotation.Autowired; public class ProvinceServiceImpl implements ProvinceService { @Autowired private ProvinceRepository provinceRepository; @Override public Iterable<Province> findAll() { return provinceRepository.findAll(); } @Override public Province findById(Long id) { return provinceRepository.findOne(id); } @Override public void save(Province province) { provinceRepository.save(province); } @Override public void remove(Long id) { provinceRepository.delete(id); } }
Lớp ProvinceController:
package com.codegym.cms.controller; import com.codegym.cms.model.Province; import com.codegym.cms.service.ProvinceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.servlet.ModelAndView; @Controller public class ProvinceController { @Autowired private ProvinceService provinceService; @GetMapping("/provinces") public ModelAndView listProvinces(){ Iterable<Province> provinces = provinceService.findAll(); ModelAndView modelAndView = new ModelAndView("/province/list"); modelAndView.addObject("provinces", provinces); return modelAndView; } @GetMapping("/create-province") public ModelAndView showCreateForm(){ ModelAndView modelAndView = new ModelAndView("/province/create"); modelAndView.addObject("province", new Province()); return modelAndView; } @PostMapping("/create-province") public ModelAndView saveProvince(@ModelAttribute("province") Province province){ provinceService.save(province); ModelAndView modelAndView = new ModelAndView("/province/create"); modelAndView.addObject("province", new Province()); modelAndView.addObject("message", "New province created successfully"); return modelAndView; } @GetMapping("/edit-province/{id}") public ModelAndView showEditForm(@PathVariable Long id){ Province province = provinceService.findById(id); if(province != null) { ModelAndView modelAndView = new ModelAndView("/province/edit"); modelAndView.addObject("province", province); return modelAndView; }else { ModelAndView modelAndView = new ModelAndView("/error.404"); return modelAndView; } } @PostMapping("/edit-province") public ModelAndView updateProvince(@ModelAttribute("province") Province province){ provinceService.save(province); ModelAndView modelAndView = new ModelAndView("/province/edit"); modelAndView.addObject("province", province); modelAndView.addObject("message", "Province updated successfully"); return modelAndView; } @GetMapping("/delete-province/{id}") public ModelAndView showDeleteForm(@PathVariable Long id){ Province province = provinceService.findById(id); if(province != null) { ModelAndView modelAndView = new ModelAndView("/province/delete"); modelAndView.addObject("province", province); return modelAndView; }else { ModelAndView modelAndView = new ModelAndView("/error.404"); return modelAndView; } } @PostMapping("/delete-province") public String deleteProvince(@ModelAttribute("province") Province province){ provinceService.remove(province.getId()); return "redirect:provinces"; } } View /province/list.html <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Provinces</title> </head> <body> <a href="/create-province">Create new province</a> <h1>Provinces</h1> <table border="1"> <tr> <th>Name</th> <th>Edit</th> <th>Delete</th> </tr> <th:block th:each="province : ${provinces}"> <tr> <td th:text="${province.name}"></td> <td><a th:href="@{/edit-province/__${province.id}__ }">Edit</a></td> <td><a th:href="@{/delete-province/__${province.id}__ }">Delete</a></td> </tr> </th:block> </table> </body> </html> View /province/create.html <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Create province</title> </head> <body> <h1>Create province</h1> <p> <a href="/provinces">Province list</a> </p> <th:block th:if="${message}"> <p th:text="${message}"></p> </th:block> <form th:action="@{/create-province}" th:object="${province}" method="post"> <table> <tr> <td>Name:</td> <td><input type="text" th:field="*{name}"/></td> </tr> <tr> <td></td> <td><input type="submit" value="Create province"></td> </tr> </table> </form> </body> </html> View /province/edit.html <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Edit province</title> </head> <body> <h1>Edit province</h1> <p> <a href="/provinces">Province list</a> </p> <th:block th:if="${message}"> <p th:text="${message}"></p> </th:block> <form th:action="@{/edit-province}" th:object="${province}" method="post"> <input th:type="hidden" name="id" th:field="*{id}"> <table> <tr> <td>First name:</td> <td><input type="text" th:field="*{name}"/></td> </tr> <tr> <td></td> <td><input type="submit" value="Update province"></td> </tr> </table> </form> </body> </html> View /province/delete.html <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Delete province</title> </head> <body> <h1>Delete province</h1> <h2>Are you sure?</h2> <p> <a href="/provinces">Province list</a> </p> <form th:action="@{/delete-province}" th:object="${province}" method="post"> <input th:type="hidden" name="id" th:field="*{id}"> <table> <tr> <td>Name:</td> <td th:text="${province.name}"></td> </tr> <tr> <td></td> <td><input type="submit" value="Delete province"></td> </tr> </table> </form> </body> </html>
Chạy ứng dụng và đi đến đường dẫn /provinces để quan sát và dùng thử các chức năng mới.
Bước 3: Cập nhật lớp khách hàng để sử dụng thêm thuộc tính mới khi tạo khách hàng: Tỉnh
Cập nhật lớp Customer, thêm thuộc tính provice và các getter/setter tương ứng:
@Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy= GenerationType.AUTO) private Long id; private String firstName; private String lastName; @ManyToOne @JoinColumn(name = "province_id") private Province province; ... ... }
Cập nhật form tạo Customer để hiển thị thêm lựa chọn tỉnh.
Lớp CustomerController bổ sung thêm phương thức để lấy về danh sách các tỉnh. Khi tạo một customer mới thì sẽ cho phép người dùng lựa chọn province:
@Controller public class CustomerController { @Autowired private CustomerService customerService; @Autowired private ProvinceService provinceService; @ModelAttribute("provinces") public Iterable<Province> provinces(){ return provinceService.findAll(); } ... ... }
@ModelAttribyte(“provinces”) là cách để gắn danh sách provinces vào tất cả các model của view, có thể sử dụng ở phần view.
Bổ sung thêm dropdown để lựa chọn tỉnh trong file /customer/create.html
<tr> <td>Province:</td> <td> <select th:field="*{province}"> <option th:each="p : ${provinces}" th:value="${p.id}" th:text="${p.name}"></option> </select> </td> </tr>
Trong trường hợp này, giá trị của trường <select> sẽ là id của province, do đó, chúng ta cần chuyển đổi từ id của province sang object của province. Để thực hiện việc này, chúng ta có thể sử dụng converter hoặc formatter (xem lại bài Formatter và Converter).
Tạo lớp com.codegym.cms.formatter.ProvinceFormatter:
package com.codegym.cms.formatter; import com.codegym.cms.model.Province; import com.codegym.cms.service.ProvinceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.Formatter; import org.springframework.stereotype.Component; import java.text.ParseException; import java.util.Locale; @Component public class ProvinceFormatter implements Formatter<Province> { private ProvinceService provinceService; @Autowired public ProvinceFormatter(ProvinceService provinceService) { this.provinceService = provinceService; } @Override public Province parse(String text, Locale locale) throws ParseException { return provinceService.findById(Long.parseLong(text)); } @Override public String print(Province object, Locale locale) { return "[" + object.getId() + ", " +object.getName() + "]"; } }
Lớp ProvinceFormatter sử dụng provinceService để chuyển đổi từ id của province sang object của province.
Đăng ký formatter bằng cách override phương thức addFormatter() trong lớp ApplicationConfig:
... public class ApplicationConfig extends WebMvcConfigurerAdapter implements ApplicationContextAware { ...... @Override public void addFormatters(FormatterRegistry registry) { registry.addFormatter(new ProvinceFormatter(applicationContext.getBean(ProvinceService.class))); } ...... }
Chạy thử ứng dụng để quan sát kết quả:

Bước 4: Chỉnh sửa file /customer/list.html để hiển thị tên tỉnh
<table border="1"> <tr> <th>First name</th> <th>Last name</th> <th>Province</th> <th>Edit</th> <th>Delete</th> </tr> <th:block th:each="customer : ${customers}"> <tr> <td th:text="${customer.firstName}"></td> <td th:text="${customer.lastName}"></td> <td th:text="${customer.province.name}"></td> <td><a th:href="@{/edit-customer/__${customer.id}__ }">Edit</a></td> <td><a th:href="@{/delete-customer/__${customer.id}__ }">Delete</a></td> </tr> </th:block> </table>
Để hiển thị tên của province, đơn giản chỉ cần sử dụng biểu thức ${customer.province.name}, JPA sẽ tự động thực hiện câu lệnh truy vấn và trả về tên tỉnh.
Chạy ứng dụng và quan sát kết quả.

Bước 5: Cập nhật file /customer/edit.html để cho phép thay đổi province của customer:
<form th:action="@{/edit-customer}" th:object="${customer}" method="post"> <input th:type="hidden" name="id" th:field="*{id}"> <table> <tr> <td>First name:</td> <td><input type="text" th:field="*{firstName}"/></td> </tr> <tr> <td>Last name:</td> <td><input type="text" th:field="*{lastName}"/></td> </tr> <tr> <td>Province:</td> <td> <select name="province"> <option th:each="p : ${provinces}" th:value="${p.id}" th:text="${p.name}" th:selected="(${p.id} == *{province.id})"></option> </select> </td> </tr> <tr> <td></td> <td><input type="submit" value="Update customer"></td> </tr> </table> </form>
Chạy ứng dụng để quan sát kết quả.
Bước 6: Hiển thị danh sách khách hàng của một tỉnh
Trong bước này, chúng ta sẽ tạo một trang xem chi tiết của tỉnh, trong trang này sẽ hiển thị danh sách các khách hàng của tỉnh đó.
Cập nhật CustomerRepository để trả về danh sách customer của một province:
public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long> { Iterable<Customer> findAllByProvince(Province province); }
Cập nhật CustomerService và CustomerServiceImpl để trả về danh sách customer của một province:
public interface CustomerService { ... Iterable<Customer> findAllByProvince(Province province); } public class CustomerServiecImpl implements CustomerService { ... @Override public Iterable<Customer> findAllByProvince(Province province) { return customerRepository.findAllByProvince(province); } }
Cập nhật ProvinceController để thêm phương thức xem chi tiết một province:
@Controller public class ProvinceController { @Autowired private ProvinceService provinceService; @Autowired private CustomerService customerService; ... @GetMapping("/view-province/{id}") public ModelAndView viewProvince(@PathVariable("id") Long id){ Province province = provinceService.findById(id); if(province == null){ return new ModelAndView("/error.404"); } Iterable<Customer> customers = customerService.findAllByProvince(province); ModelAndView modelAndView = new ModelAndView("/province/view"); modelAndView.addObject("province", province); modelAndView.addObject("customers", customers); return modelAndView; } }
Tạo file /province/view.html để hiển thị tên của province và danh sách khách hàng tương ứng:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>View province</title> </head> <body> <h1>View province: <span th:text="${province.name}"></span></h1> <a href="/provinces">Province list</a> <table border="1"> <tr> <th>First name</th> <th>Last name</th> </tr> <th:block th:each="customer : ${customers}"> <tr> <td th:text="${customer.firstName}"></td> <td th:text="${customer.lastName}"></td> </tr> </th:block> </table> </body> </html>
Chạy ứng dụng và quan sát kết quả.
Mã nguồn tham khảo (nhánh jpa-repository): https://github.com/codegym-vn/spring-jpa-customer-management/tree/jpa-repository
Leave a Reply