Truy xuất database sử dụng Spring Data JPA Phần 3
Đăng lúc: 03:29 PM - 24/03/2025 bởi Charles Chung - 260Trong bài này chúng ta sẽ tìm hiểu về các mối quan hệ giữa các thực thể trong JPA

1. Giới thiệu
Trong JPA (Java Persistence API), relationship (quan hệ) là cách mà các entity (thực thể) liên kết với nhau trong cơ sở dữ liệu. JPA hỗ trợ nhiều loại quan hệ khác nhau để thể hiện sự kết nối giữa các bảng. Các loại quan hệ chính trong JPA bao gồm:
One-to-One (1:1)
Quan hệ "Một-Một" nghĩa là mỗi thực thể trong bảng này chỉ liên kết với một thực thể trong bảng kia.
Ví dụ: Một User
có một Profile
duy nhất.
Cách triển khai trong JPA:
- @OneToOne: Dùng để đánh dấu quan hệ một-một giữa hai thực thể.
- @JoinColumn: Dùng để xác định cột khóa ngoại (foreign key) trong bảng liên kết.
One-to-Many (1:N)
Quan hệ "Một-Nhiều" nghĩa là một thực thể trong bảng này có thể liên kết với nhiều thực thể trong bảng kia.
Ví dụ: Một Author
có thể viết nhiều Book
.
Cách triển khai trong JPA:
- @OneToMany: Được sử dụng trong thực thể phía "một" của quan hệ.
- @ManyToOne: Được sử dụng trong thực thể phía "nhiều" của quan hệ.
- @JoinColumn: Dùng để xác định khóa ngoại.
Many-to-One (N:1)
Quan hệ "Nhiều-Một" nghĩa là nhiều thực thể trong bảng này có thể liên kết với một thực thể trong bảng kia.
Ví dụ: Nhiều Order
có thể liên kết với một Customer
.
Cách triển khai trong JPA:
- @ManyToOne: Được sử dụng trong thực thể phía "nhiều" của quan hệ.
- @OneToMany: Được sử dụng trong thực thể phía "một" của quan hệ.
- @JoinColumn: Dùng để xác định khóa ngoại.
Many-to-Many (N:M)
Quan hệ "Nhiều-Nhiều" nghĩa là nhiều thực thể trong bảng này có thể liên kết với nhiều thực thể trong bảng kia.
Ví dụ: Một Student
có thể đăng ký nhiều Course
và ngược lại, mỗi Course
có thể có nhiều Student
.
Cách triển khai trong JPA:
- @ManyToMany: Được sử dụng trong cả hai thực thể để đánh dấu quan hệ nhiều-nhiều.
- @JoinTable: Dùng để chỉ định bảng liên kết trung gian (join table) khi không sử dụng khóa ngoại trực tiếp.
2. Ví dụ demo
Trong ví dụ này chúng ta sẽ đi tìm hiểu mối quan hệ OneToMany và ManyToOne, hình mô tả như dưới
Bước 1: Tạo project springboot session6example3 (chi tiết xem bài 1) nhớ chọn các dependency như Phần 1, cấu trúc như hình dưới
Bước 2: Copy ckeditor vào thư mục static như hình trên (tải tại đây)
Bước 3: Cấu hình chuỗi kết nối tới database trong tệp application.properties
spring.application.name=session6example3 spring.datasource.url= jdbc:sqlserver://localhost:1433;encrypt=true;trustServerCertificate=true;databaseName=springboot02 spring.datasource.username= sa spring.datasource.password= 123465 spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.SQLServerDialect spring.jpa.hibernate.ddl-auto= update spring.jpa.properties.hibernate.use_nationalized_character_data =true
Bước 4: Code lớp Topic
package com.bkap.entities; import java.util.Date; import java.util.HashSet; import java.util.Set; import org.springframework.format.annotation.DateTimeFormat; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; @Entity @Table(name="topics") public class Topic { @Id @GeneratedValue(strategy =GenerationType.IDENTITY) @Column(name="topicid") private Long topicId; @Column(name="topicname",columnDefinition = "nvarchar(100)",unique = true) @NotBlank(message = "Hãy nhập tên chủ đề") private String topicName; @Column(name="brief",columnDefinition = "nvarchar(1000)") private String brief; @Column(name="createdate") @DateTimeFormat(pattern = "dd/MM/yyyy hh:mm a") private Date createDate; @Column(name="status") private boolean status; @OneToMany(mappedBy = "topic") private Set<Blog> blogs=new HashSet<Blog>(); public Set<Blog> getBlogs(){ return blogs; } public Long getTopicId() { return topicId; } public void setTopicId(Long topicId) { this.topicId = topicId; } public String getTopicName() { return topicName; } public void setTopicName(String topicName) { this.topicName = topicName; } public String getBrief() { return brief; } public void setBrief(String brief) { this.brief = brief; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public boolean isStatus() { return status; } public void setStatus(boolean status) { this.status = status; } }
Bước 5: Code lớp Blog
package com.bkap.entities; import java.util.Date; import org.springframework.format.annotation.DateTimeFormat; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; @Entity @Table(name="blogs") public class Blog { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name="blogid") private Long blogId; @NotBlank(message = "Hãy nhập tiêu đề") @Column(name="title",columnDefinition = "nvarchar(1000)") private String title; @NotBlank(message = "Hãy nhập mô tả") @Column(name="brief",columnDefinition = "nvarchar(1000)") private String brief; @Column(name="content",columnDefinition = "nvarchar(max)") private String content; @Column(name="picture",columnDefinition = "nvarchar(1000)") private String picture; @DateTimeFormat(pattern = "dd/MM/yyyy hh:mm a") @Column(name="createdate") private Date createDate; @Column(name="status") private int status; @ManyToOne @JoinColumn(name = "topicid",nullable = false) private Topic topic=new Topic(); public Topic getTopic() { return topic; } public Long getBlogId() { return blogId; } public void setBlogId(Long blogId) { this.blogId = blogId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getBrief() { return brief; } public void setBrief(String brief) { this.brief = brief; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getPicture() { return picture; } public void setPicture(String picture) { this.picture = picture; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } }
Bước 6: Code lớp TopicRepository và BlogRepository
package com.bkap.repositories; import org.springframework.data.jpa.repository.JpaRepository; import com.bkap.entities.Topic; public interface TopicRepository extends JpaRepository<Topic,Integer> { } package com.bkap.repositories; import org.springframework.data.jpa.repository.JpaRepository; import com.bkap.entities.Blog; public interface BlogRepository extends JpaRepository<Blog, Integer> { }
Bước 6: Code lớp TopicService
package com.bkap.services; import java.util.List; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.bkap.entities.Blog; import com.bkap.entities.Topic; import com.bkap.repositories.TopicRepository; @Service public class TopicService { @Autowired private TopicRepository repository; public List<Topic> getAll(){ return repository.findAll(); } public Set<Blog> getBlogs(int topicId){ return repository.getReferenceById(topicId).getBlogs(); } public void insert(Topic topic) { repository.save(topic); } public void update(Topic topic) { repository.save(topic); } public Topic getById(int topicId) { return repository.findById(topicId).get(); } public void delete(int topicId) { repository.deleteById(topicId); } }
Bước 7: Code lớp BlogService
package com.bkap.services; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.bkap.entities.Blog; import com.bkap.entities.Topic; import com.bkap.repositories.BlogRepository; @Service public class BlogService { @Autowired BlogRepository repository; public List<Blog> getAll(){ return repository.findAll(); } public Topic getTopic(int blogId) { return repository.getReferenceById(blogId).getTopic(); } public void insert(Blog blog) { repository.save(blog); } public void delete(int blogId) { repository.deleteById(blogId); } public void update(Blog blog) { repository.save(blog); } public Blog getById(int blogId) { return repository.findById(blogId).get(); } }
Bước 8: Code lớp HomeController
package com.bkap.controllers; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Date; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import com.bkap.entities.Blog; import com.bkap.entities.Topic; import com.bkap.services.BlogService; import com.bkap.services.TopicService; import jakarta.validation.Valid; @Controller public class HomeController { @Autowired TopicService topicService; @Autowired BlogService blogService; @GetMapping("/") public String home(Model model) { return "index"; } //Topics logic @GetMapping("/topics") public String topic(Model model) { model.addAttribute("topics",topicService.getAll()); return "topic/index"; } @GetMapping("/topics/create") public String createTopic(Model model) { model.addAttribute("topic",new Topic()); return "topic/create"; } @PostMapping("/topics/create") public String createTopic(@Valid Topic topic, BindingResult result, Model model) { if(result.hasErrors()) return "topic/create"; topic.setCreateDate(new Date()); topicService.insert(topic); return "redirect:/topics"; } @GetMapping("/topics/edit/{id}") public String editTopic(@PathVariable int id, Model model) { var topic=topicService.getById(id); model.addAttribute("topic",topic); return "topic/edit"; } @PostMapping("/topics/edit") public String editTopic(@Valid Topic topic, BindingResult result, Model model) { if(result.hasErrors()) return "topic/edit"; topicService.update(topic); return "redirect:/topics"; } @GetMapping("/topics/delete/{id}") public String deleteTopic(@PathVariable int id, Model model) { topicService.delete(id); return "redirect:/topics"; } //Blogs logic @GetMapping("/blogs") public String blogs(Integer topicid, Model model) { model.addAttribute("topicid",topicid); model.addAttribute("topics", topicService.getAll()); if(topicid==null || topicid==0) model.addAttribute("blogs",blogService.getAll()); else model.addAttribute("blogs",topicService.getBlogs(topicid)); return "blog/index"; } @GetMapping("/blogs/create") public String createBlog(Model model) { model.addAttribute("topics", topicService.getAll()); model.addAttribute("blog",new Blog()); return "blog/create"; } @PostMapping("/blogs/create") public String createBlog(@Valid Blog blog, BindingResult result, @RequestParam MultipartFile file, Model model) { blog.setCreateDate(new Date()); if(result.hasErrors()) { model.addAttribute("topics", topicService.getAll()); model.addAttribute("blog",blog); return "blog/create"; } try { // upload ảnh if (!file.isEmpty()) { File imageFolder=new File( new ClassPathResource(".").getFile()+"/static/images"); if(!imageFolder.exists()) imageFolder.mkdir(); Path path=Paths.get(imageFolder.getAbsolutePath(),file.getOriginalFilename()); System.out.println(path); file.transferTo(path); blog.setPicture("/images/"+file.getOriginalFilename()); } } catch (IllegalStateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } blogService.insert(blog); return "redirect:/blogs"; } @GetMapping("/blogs/delete/{id}") public String deleteBlog(@PathVariable int id, Model model) { blogService.delete(id); return "redirect:/blogs"; } @GetMapping("/blogs/edit/{id}") public String editBlog(@PathVariable int id, Model model) { model.addAttribute("topics", topicService.getAll()); model.addAttribute("blog",blogService.getById(id)); return "blog/edit"; } @PostMapping("/blogs/edit") public String editBlog(@Valid Blog blog, BindingResult result,String pictureOld, @RequestParam MultipartFile file, Model model) { blog.setCreateDate(new Date()); if(result.hasErrors()) { model.addAttribute("topics", topicService.getAll()); model.addAttribute("blog",blog); return "blog/create"; } try { // upload ảnh if (!file.isEmpty()) { File imageFolder=new File( new ClassPathResource(".").getFile()+"/static/images"); if(!imageFolder.exists()) imageFolder.mkdir(); Path path=Paths.get(imageFolder.getAbsolutePath(),file.getOriginalFilename()); System.out.println(path); file.transferTo(path); blog.setPicture("/images/"+file.getOriginalFilename()); }else { blog.setPicture(pictureOld); } } catch (IllegalStateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } blogService.insert(blog); return "redirect:/blogs"; } @GetMapping("/blogs/detail/{id}") public String details(@PathVariable int id, Model model) { model.addAttribute("blog",blogService.getById(id)); return "blog/detail"; } }
Bước 9: Code các view: index.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Quản lý blog</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" /> </head> <body> <div class="container"> <h1 class="mt-3">QUẢN LÝ BLOG</h1> <hr/> <a class="btn btn-primary" href="/topics">Chủ đề</a> <a class="btn btn-info" href="/blogs">Blogs</a> </div> </body> </html>
view topic/index.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Danh sách chủ đề</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" /> </head> <body> <div class="container"> <h1 class="mt-3">Danh sách chủ đề</h1> <a th:href="@{/topics/create}" class="btn btn-primary">Thêm mới</a> <hr/> <table class="table table-bordered"> <tr> <th>#</th> <th>Tên chủ đề</th> <th>Mô tả</th> <th>Ngày tạo</th> <th>Tình trạng</th> <th></th> </tr> <tr th:each="t:${topics}" th:object="${t}"> <td th:text="*{topicId}"></td> <td th:text="*{topicName}"></td> <td th:text="*{brief}"></td> <td th:text="*{#dates.format(createDate, 'dd-MM-yyyy hh:mm a')}"></td> <td th:if="*{status}">Hiển thị</td> <td th:unless="*{status}">Ẩn</td> <td> <a th:href="@{/topics/delete/{id}(id=*{topicId})}" class="btn btn-danger" onclick="return confirm('Bạn có muốn xóa không?')" >Xóa</a> <a th:href="@{/topics/edit/{id}(id=*{topicId})}" class="btn btn-info">Sửa</a> <a th:href="@{/blogs?topicid={topicid}(topicid=*{topicId})}" class="btn btn-success">Bài viết</a> </td> </tr> </table> <hr/> <a href="/" class="btn btn-danger">Quay lại</a> </div> </body> </body> </html>
view topic/create.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Thêm mới chủ đề</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" /> </head> <body> <div class="container"> <h1 class="mt-3">Thêm mới chủ đề</h1> <hr /> <form th:action="@{create}" method="post" th:object="${topic}"> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="topicName">Tên chủ đề</label> <div class="col-sm-10"> <input type="text" th:field="*{topicName}" class="form-control" /> <span th:if="${#fields.hasErrors('topicName')}" th:errors="*{topicName}" class="text-danger">Topic name error</span> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="brief">Mô tả</label> <div class="col-sm-10"> <input type="text" th:field="*{brief}" class="form-control" /> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="status">Tình trạng</label> <div class="col-sm-10"> <input type="checkbox" th:field="*{status}" /> Hiển thị </div> </div> <hr /> <button class="btn btn-primary">Lưu</button> <a href="/topics" class="btn btn-danger">Quay lại</a> </form> </div> </body> </body> </html>
view topic/edit.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Sửa chủ đề</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" /> </head> <body> <div class="container"> <h1 class="mt-3">Sửa chủ đề</h1> <hr /> <form th:action="@{/topics/edit}" method="post" th:object="${topic}"> <input type="hidden" th:field="*{topicId}"/> <input type="hidden" name="createDate" th:value="*{#dates.format(createDate,'dd/MM/yyyy hh:mm a')}"/> <span th:errors="*{createDate}"></span> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="topicName">Tên chủ đề</label> <div class="col-sm-10"> <input type="text" th:field="*{topicName}" class="form-control" /> <span th:if="${#fields.hasErrors('topicName')}" th:errors="*{topicName}" class="text-danger">Topic name error</span> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="brief">Mô tả</label> <div class="col-sm-10"> <input type="text" th:field="*{brief}" class="form-control" /> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="status">Tình trạng</label> <div class="col-sm-10"> <input type="checkbox" th:field="*{status}" /> Hiển thị </div> </div> <hr /> <button class="btn btn-primary">Lưu</button> <a href="/topics" class="btn btn-danger">Quay lại</a> </form> </div> </body> </body> </html>
view blog/index.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Danh bài viết</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css"/> </head> <body> <div class="container"> <h1 class="mt-3">Danh sách bài viết</h1> <p><a th:href="@{/blogs/create}" class="btn btn-primary">Thêm mới</a> </p> <form th:action="@{/blogs}" method="get"> <select class="form-control" name="topicid" onchange="forms[0].submit()"> <option value="0">------Chọn chủ đề-------</option> <option th:each="t:${topics}" th:value="${t.topicId}" th:text="${t.topicName}" th:selected="${t.topicId==topicid}" /> </select> </form> <hr/> <table class="table table-bordered"> <tr> <th>#</th> <th>Tiêu đề</th> <th>Mô tả ngắn</th> <th>Ngày tạo</th> <th>Chủ đề</th> <th>Ảnh</th> <th>Tình trạng</th> <th></th> </tr> <tr th:each="b:${blogs}" th:object="${b}"> <td th:text="${b.blogId}"></td> <td th:text="${b.title}"></td> <td th:text="${b.brief}"></td> <td th:text="${b.createDate}"></td> <td th:text="${b.topic.topicName}"></td> <td> <img th:src="${b.picture}" width="100"/> </td> <td th:text="${b.status}"></td> <td> <a th:href="@{/blogs/delete/{id}(id=*{blogId})}" onclick="return confirm('Bạn có muốn xóa không?')" ><span class="fas fa-trash"></span></a> <a th:href="@{/blogs/edit/{id}(id=*{blogId})}" ><span class="fas fa-edit"></span></a> <a th:href="@{/blogs/detail/{id}(id=*{blogId})}" ><span class="fas fa-list"></span></a></td> </tr> </table> <hr/> <a href="/" class="btn btn-danger">Quay lại</a> </div> </body> </body> </html>
view blog/create.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Thêm mới blog</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" /> </head> <body> <div class="container"> <h1 class="mt-3">Thêm mới blog</h1> <hr /> <form th:action="@{create}" method="post" th:object="${blog}" enctype="multipart/form-data" > <div class="form-group row"> <label class="col-sm-2 col-form-label" for="title">Tiêu đề</label> <div class="col-sm-10"> <input type="text" th:field="*{title}" class="form-control" /> <span th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="text-danger">tiêu đề error</span> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="brief">Mô tả</label> <div class="col-sm-10"> <input type="text" th:field="*{brief}" class="form-control" /> <span th:if="${#fields.hasErrors('brief')}" th:errors="*{brief}" class="text-danger">Mô tả error</span> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="content">Nội dung</label> <div class="col-sm-10"> <textarea rows="5" cols="25" th:field="*{content}" class="form-control"></textarea> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="file">Ảnh:</label> <div class="col-sm-10"> <input type="file" name="file" class="form-control" accept="image/*" /> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="topicid">Chủ đề</label> <div class="col-sm-10"> <select class="form-control" th:field="*{topic.topicId}"> <option th:each="t:${topics}" th:value="${t.topicId}" th:text="${t.topicName}"/> </select> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="status">Tình trạng</label> <div class="col-sm-10"> <select class="form-control" th:field="*{status}"> <option value="1">Mới tạo</option> <option value="2">Đã duyệt</option> <option value="3">Hủy</option> </select> </div> </div> <hr /> <button class="btn btn-primary">Lưu</button> <a href="/blogs" class="btn btn-danger">Quay lại</a> </form> </div> <script src="/ckeditor/ckeditor.js"></script> <script> CKEDITOR.replace('content') </script> </body> </html>
view blog/edit.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Thêm mới blog</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" /> </head> <body> <div class="container"> <h1 class="mt-3">Sửa blog</h1> <hr /> <form th:action="@{/blogs/edit}" method="post" th:object="${blog}" enctype="multipart/form-data" > <input type="hidden" th:field="*{blogId}"/> <input type="hidden" name="createDate" th:value="*{#dates.format(createDate,'dd/MM/yyyy hh:mm a')}"/> <input type="hidden" name="pictureOld" th:value="*{picture}"/> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="title">Tiêu đề</label> <div class="col-sm-10"> <input type="text" th:field="*{title}" class="form-control" /> <span th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="text-danger">tiêu đề error</span> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="brief">Mô tả</label> <div class="col-sm-10"> <input type="text" th:field="*{brief}" class="form-control" /> <span th:if="${#fields.hasErrors('brief')}" th:errors="*{brief}" class="text-danger">Mô tả error</span> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="content">Nội dung</label> <div class="col-sm-10"> <textarea rows="5" cols="25" th:field="*{content}" class="form-control"></textarea> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="file">Ảnh:</label> <div class="col-sm-10"> <input type="file" name="file" class="form-control" accept="image/*" /> <img th:src="*{picture}" width="100"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="topicid">Chủ đề</label> <div class="col-sm-10"> <select class="form-control" th:field="*{topic.topicId}"> <option th:each="t:${topics}" th:value="${t.topicId}" th:text="${t.topicName}"/> </select> </div> </div> <div class="form-group row"> <label class="col-sm-2 col-form-label" for="status">Tình trạng</label> <div class="col-sm-10"> <select class="form-control" th:field="*{status}"> <option value="1">Mới tạo</option> <option value="2">Đã duyệt</option> <option value="3">Hủy</option> </select> </div> </div> <hr /> <button class="btn btn-primary">Lưu</button> <a href="/blogs" class="btn btn-danger">Quay lại</a> </form> </div> <script src="/ckeditor/ckeditor.js"></script> <script> CKEDITOR.replace('content') </script> </body> </html>
view blog/detail.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Danh bài viết</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" /> </head> <body> <div class="container" th:object="${blog}"> <h1 class="mt-3" th:text="*{title}"></h1> <i th:text="*{brief}"></i> <i>(ngày tạo</i> <i th:text="*{#dates.format(createDate,'dd/MM/yyyy hh:mm a')}"></i><i>)</i> <div><img th:src="*{picture}" width="200"/></div> <div th:utext="*{content}"> </div> <hr/> <a href="/" class="btn btn-danger">Quay lại</a> </div> </body> </body> </html>
Bước 10: Chạy và kiểm tra kết quả
Source code tải tại đây
3. Video demo
thay lời cảm ơn!
Các bài cũ hơn
- Truy xuất database sử dụng Spring Data JPA Phần 2 (02:14 PM - 23/03/2025)
- Truy xuất database sử dụng Spring Data JPA Phần 1 (02:14 PM - 20/03/2025)
- Validation Form-Upload File Image-Nhúng CKEditor trong SpringBoot (10:28 AM - 19/03/2025)
- Tìm hiểu về Form trong Thymeleaf (10:30 PM - 18/03/2025)
- Giới thiệu và cài đặt Thymeleaf trong Eclipse (09:58 PM - 16/03/2025)