CÔNG NGHỆ THÔNG TIN >> SINH VIÊN BKAP

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 - 260

Trong 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

alt text

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!

QUẢNG CÁO - TIẾP THỊ