1. SpringBoot / Hibernate là gì
SpringBoot: xem bài viết bắt đầu với SpringBoot trong Java
Hibernate: là một công cụ ORM sử dụng JDBC API để tương tác với cơ sở dữ liệu, nhờ nó mà những truy vấn tới cơ sở dữ liệu từ máy chủ sẽ được viết một cách đơn giản và trực quan hơn, ngoài ra tăng hiệu suất đáng kể so với cách làm thủ công thông thường nhờ Hibernate Cache Storage
2. Tạo base, thêm dependencies vào file pom.xml
✨Bước 1: Xem lại bài viết bắt đầu với SpringBoot trong Java để tạo base project
✨Bước 2: Ngoài các thư viện chính như bạn đã biết, chúng ta cần thêm những thư viện sau để đảm bảo chức năng CRUD cho website
<!--Thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<exclusions>
<exclusion>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</exclusion>
</exclusions>
</dependency>
Thymeleaf - Thay thế cho JSP và JSTL - Hiệu suất được cải thiện đáng kể
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Một module nhằm đơn giản hóa việc implement JPA trong project
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
Thư viện kết nối với cơ sở dữ liệu MySQL
3. Tạo Database, Generate Persitance Mapping (Tự động tạo các Entity Classes)
✨Bước 1: Tạo một cơ sở dữ liệu và cấu hình connection string tại application.properties
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://103.97.125.251/satellit_Satellite?autoReconnect=true&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=satellit_satellite1012
spring.datasource.password=<yourpasshere>
✨Bước 2: Điều chỉnh một số settings khác (kèm hướng dẫn chi tiết) trong file application.properties
#Tùy chỉnh in hoặc không in câu lệnh SQL mà Hibernate thực hiện lên Console (Để tiện debug)
spring.jpa.show-sql=true
#Tùy chỉnh tạo mới (create) / cập nhật (update) hoặc không làm gì (none) với cơ sở dữ liệu theo các Entity Classes đã được Mapping
spring.jpa.hibernate.ddl-auto=none
#Tùy chọn Dialect: ở đây mình dùng MySQL
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
#Tùy chỉnh phương pháp đặt tên bảng, tên cột trong các câu lệnh được tự động tạo: nếu không tùy chỉnh chỗ này, rất có thể Hibernate sẽ hiểu nhầm tên mà bạn đặt dẫn đến các câu lệnh SQL bị gerenate sai
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
#Tùy chỉnh này để bỏ đoạn ?JSESSIONID=XXXXX ở các đường link mà SpringBoot mỗi lần chạy hay tự tạo ra
server.servlet.session.cookie.http-only=true
server.servlet.session.tracking-modes=cookie
✨Bước 3: Tự động tạo các Entities ứng với mỗi bảng trong database
- Ở bên dưới trái giao diện của IDEA, chọn Persistance (In dọc)
- Chuột phải vào Data Source hiện tại chọn Generate Persistance Mapping -> By Database Schema
- Tại phần Choose Data Source, thêm mới một MySQL Data Source và nhập thông tin database của bạn vào
- Tại phần Package, chọn Package bạn muốn chưa các Entity Classes trong project
- Cuối cùng, chọn tất cả các bảng bạn muốn tự động Generate Classes và ấn OK
4. Tạo các Repository Interfaces để truy vấn dữ liệu từ database
- Trong project của mình, các bạn tạo thêm một Package tên là Repository để lưu các repo cho từng entities.
- Trong ví dụ này mình sử dụng cơ sở dữ liệu sau, vì vậy chúng ta sẽ có 3 Interfaces tương ứng với 3 Entities Class được Mapping ở bước III
public interface ClassRepository extends JpaRepository<ClassEntity, Integer> {
}
- Cuối cùng, để truy vấn từ database, các bạn đơn giản chỉ cần khai báo một interface và sử dụng hàm dựng sẵn của nó, ví dụ như
@Autowired
ClassRepository classRepository;
@GetMapping(value = {"/", "/index"})
public String index(Model model) {
model.addAttribute("ClassList",classRepository.findAll());
}
- Tuy nhiên, nếu câu lệnh sql của bạn có độ phức tạp cao và hàm dựng sẵn không hỗ trợ cho lệnh này, bạn có thể tự custom cho mình bằng cách thêm các định nghĩa hàm và anonation trong interface và sử dụng nó tương tự như sau:
public interface ClassRepository extends JpaRepository<ClassEntity, Integer> {
@Query(value = "SELECT * FROM DEMO_1_Class WHERE name=?1", nativeQuery = true)
List<ClassEntity> findByClassName(String name);
}
@Autowired
ClassRepository classRepository;
@GetMapping(value = {"/", "/index"})
public String index(Model model,HttpSevletRequest request) {
String className=request.getParameter("className");
model.addAttribute("ClassList",classRepository.findByClassName(className));
}
5. Controller và Views
Tiếp theo, các bạn sẽ tạo các Controller và View, chi tiết các bạn có thể tải project bên dưới về xem, có một số điểm quan trọng như sau
- Bắt đầu: tạo một Class và gắn @Controller anonation lên đầu, bạn sẽ có một Controller, sau đó với mỗi Controller bạn có thể sẽ có 1 hoặc nhiều hàm @Get hoặc @Post Mapping, có rất nhiều cách để khai báo và mục đích của nó được nêu rõ tại đây. Bên dưới là một ví dụ điển hình cho Controller chuyển hướng trang
@Controller
public class HomeController implements ErrorController {
@GetMapping(value = {"/", "/index"})
public String index(Model model) {
return "home";
}
@GetMapping(value = {"/about"})
public String index(Model model) {
return "about";
}
}
* Các file views của bạn nên được đặt ở thư mục mặt định /resources/templates/*.html
- Tạo đường dẫn cho các thành phần một cách chính xác:
🎇 Root Path mặc định để Spring nhận các file .html của bạn là ở thư mục templates, nếu các bạn muốn truy cập tiếp vào các subfolder, tại Controller chúng ta đơn giản chỉ thêm tên folder vào giá trị trả về của hàm như sau
return "home"; //File home.html nằm ở templates/home.html
return "Common/home"; //File home.html nằm ở templates/Common/home.html
🎇 Để link các file tĩnh như CSS/JS hay Image trong website, bạn cần đặt những file này tại thư mục static/Resource và tương tự, Spring nhận Root Path mặc định ở thư mục static . Để sử dụng thì chúng ta nên thực hiện liên kết từ thư mục root như sau
<link rel="stylesheet" th:href="@{/Resource/css/bootstrap.css}">
<link rel="shortcut icon" th:href="@{/Resource/favicon.ico}" />
*thẻ th:href="@{/}" đóng vai trò như một liên kết đến Root Path của Project, nhờ vậy khi bạn bỏ đoạn code này ở bất kỳ thư mục views nào, nó vẫn nhận được static file mà chúng ta đã gán như trên
- Truy vấn Database: Controller truy vấn vào database sẽ có một số khác biệt, đầu tiên bạn cần phải khai báo (các) repo interfaces mình sẽ dùng trong Controller này, tiếp theo thì chúng ta sẽ sử dụng các hàm có sẵn của nó để truy vấn, hoặc những hàm mà bạn đã tự custom ở bước IV
@Controller
public class HomeController implements ErrorController {
@Autowired //Sử dụng DI(Dependency Injection) cho object này
StudentRepository studentRepository;
@GetMapping(value = {"/", "/index"}) //Mapping tới những đường dẫn http://domain.com/ hoặc http://domain.com/index
public String index(Model model) {
try {
//Sử dụng Interface để query toàn bộ dữ liệu của bảng Student và mapping vào trường StudentList trong model
model.addAttribute("StudentList",studentRepository.findAll());
} catch (Exception e) {
e.printStackTrace();
}
return "home";
}
- Lấy dữ liệu từ view: dưới đây là ví dụ về một API thêm mới một Record trong Database với dữ liệu được nhập từ form và trả về trạng thái xử lí. Để lấy dữ liệu từ form chúng ta sẽ sử dụng biến request khai báo ở hàm
@PostMapping(value = {"/api/class/add"})
public String add(HttpServletRequest request, Model model) {
try {
//Get parameter from form
String name=request.getParameter("name");
String major=request.getParameter("major");
//Create new class
ClassEntity classEntity=new ClassEntity();
classEntity.setName(name);
classEntity.setMajor(major);
//Save to database
classRepository.save(classEntity);
} catch (Exception e) {
e.printStackTrace();
}
return "AJAX/success";
}
*Form của chúng ta sẽ như sau
<form action="/api/class/add" method="post">
<input type="text" name="name" required placeholder="Class name"><br><br>
<input type="text" name="major" required placeholder="Class major"><br><br>
<input class="btn btn-outline-primary" type="submit" value="Add"><br>
</form>
- Trả dữ liệu về view: ví dụ bên dưới sẽ lấy dữ liệu được truyền vào là một id của sinh viên, và trả về view trang thông tin chi tiết của sinh viên đó (Ex: http://localhost:8080/student?id=3 sẽ trả về trang thông tin chi tiết của sinh viên có id=3
@GetMapping(value = "/student")
public String getClassesStudentJoined(HttpServletRequest request, Model model) {
try {
//Lấy id
int id=Integer.parseInt(request.getParameter("id"));
//Tìm kiếm sinh viên bằng id
StudentEntity student=studentRepository.findById(id).orElse(null);
//Trả về view thông tin của sinh viên tìm được
model.addAttribute("StudentSelected",student);
} catch (Exception e) {
e.printStackTrace();
return "Common/error_404";
}
return "studentdetail";
}
- Điều hướng động: cũng là ví dụ trên, nhưng để đường link của bạn đẹp hơn, không hiển thị id mà thay vào đó là tên của sinh viên ví dụ như http://localhost:8080/studen/ductin sẽ trả về trang thông tin chi tiết của sinh viên có tên là ductin, chúng ta sẽ sử dụng @PathVariable, cụ thể như sau:
@GetMapping(value = "/student/{student_name}")
public String getClassesStudentJoined(@PathVariable("student_name") String student_name,HttpServletRequest request, Model model) {
try {
//Tìm kiếm sinh viên bằng tên
StudentEntity student=studentRepository.findByStudentName(student_name);
//Trả về view thông tin của sinh viên tìm được
model.addAttribute("StudentSelected",student);
} catch (Exception e) {
e.printStackTrace();
return "Common/error_404";
}
return "studentdetail";
* Như đã nói ở phần IV, hàm findByStudentName() không hỗ trợ sẵn cho các bạn mà chúng ta phải tự tạo bên trong repository:
public interface StudentRepository extends JpaRepository<StudentEntity, Integer> {
@Query(value = "SELECT * FROM DEMO_1_Student WHERE name=?1", nativeQuery = true)
StudentEntity findByStudentName(String name);
}
- Bắt lỗi:
🧨Bắt lỗi 404: trường hợp người dùng truy cập vào một đường link không tồn tại trên website, chúng ta sẽ sử dụng Controller sau để tự redirect về trang error_404.html
@RequestMapping("/error")
public String handleError() {
return "Common/error_404";
}
* để đường dẫn /error được Spring nhận là một Error Path, chúng ta thêm setting sau vào application.properties
server.error.path=/error
🧨Bắt lỗi truy vấn Database: để tránh trường hợp Database bị sập hoặc có những sai sót trong xử lí logic của Controller, hoặc Database trả về những dữ liệu không mong muốn gây lỗi server, chúng ta nên bọc tất cả các đoạn code xử lí logic hoặc có thực hiện truy vấn trong try..catch block, và trả về một trang thông báo lỗi, nhằm tránh trường hợp ảnh hưởng tới các thành phần khác của website
@GetMapping(value = {"/classes"})
public String classes(Model model) {
try {
//Lấy dữ liệu ClassesList từ Database
model.addAttribute("ClassesList",classRepository.findAll());
} catch (Exception e) {
e.printStackTrace();
//Bắt lỗi nếu lấy dữ liệu bị gặp sự cố
model.addAttribute("InternalLog", e.toString());
return "Common/error_internal";
}
return "classes";
}
- Một số thao tác xuất dữ liệu sử dụng ThymLeaf trong view:
✨Khai báo: đặt ở đầu mỗi trang .html của bạn
<html lang="vi" xmlns:th="http://www.thymeleaf.org">
✨Xuất dữ liệu của một List:
<th:block th:each="i: ${StudentList}">
<tr>
<td>[(${i.id})]</td>
<td>[(${i.name})]</td>
<td>[(${i.phone})]</td>
<td>[(${i.email})]</td>
<td>[(${i.game})]</td>
</tr>
</th:block>
✨Chèn một đường link động hoặc truyền biến vào một hàm JavaScript: sử dụng các thẻ được thymleft hỗ trợ th:<arrtibutename>
<!--Một đường link động -->
<a th:href="'/student/'+${i.getName()}" class="btn btn-outline-success">Classes</a>
<!--Gọi một hàm JS với một biến-->
<a th:onclick="deleteStudentAPI('[(${i.id})]')" class="btn btn-outline-danger">Delete</a>
<!--Gọi một hàm JS với nhiều biến-->
<a th:onclick="editStudentForm('[(${i.id})]','[(${i.name})]','[(${i.phone})]','[(${i.email})]','[(${i.game})]')" class="btn btn-outline-warning">Edit</a>
✨Sử dụng Conditional Statements (IF/ELSE/...)
<th:block th:if="${ClassList.isEmpty()}">
<h2>No class found !</h2>
</th:block>
✨Chia bố cục, phân trang, chèn một hoặc nhiều nội dung từ file html khác: tạo một file bố cục footer.html, sau đó sử dụng thẻ th:insert để chèn nội dung đó vào trang hiện tại, chú ý Root Path vẫn nằm ở thư mục /templates
<!--Common/footer.html-->
<html lang="vi" xmlns:th="http://www.thymeleaf.org">
<!-- =========== FOOTER ============ -->
<div th:fragment="content">
<footer class="bg-light text-center text-lg-start">
<div class="text-center p-3" style="background-color: rgba(0, 0, 0, 0.2)">
© 2021 Copyright:
<a class="text-dark" href="https://satdevelop.com">satdevelop.com</a>
</div>
</footer>
</div>
<!--Sử dụng nội dung của site footer.html cho trang hiện tại-->
<th:block th:insert="Common/footer :: content"/>
6. Thêm thao tác truy vấn vào website sử dụng AJAX
- Tạo API: Tạo một @PostMapping như ở phần 5 - Lấy dữ liệu từ view
- Ví dụ: Hàm thêm một sinh viên mới vào Database
function addStudentAPI(){
//Lấy dữ liệu của form để chạy AJAX
var formData = new FormData(document.getElementById("form-add-student"));
$.ajax({
url: '/api/student/add', //Gửi một request post tới link này
type: 'POST',
data: formData, //Dữ liệu của request là form được gán ở bước 1
cache: false,
contentType: false,
processData: false
}).done(function (result) {
//Xử lí thông tin, thông báo thành công / thất bại sau khi api xử lí xong
});
}
7. Tổng kết
Vì để tránh trường hợp người đọc vào xóa và thay đổi dữ liệu trong Database này nên phần mật khẩu mình để trống, để chạy được Project bên dưới, các bạn có thể tự thay đổi lại connection string trong file application.properties như hướng dẫn ở phần III
Website mặc định sẽ chạy ở http://localhost:8080 (chrome tab không tự động bật khi project chạy)
🎇 Template mẫu: https://github.com/satellite1012/SpringBoot-Functional-Template
🎇 REF: None