Java Spring Boot Bài 8: CRUD Sản phẩm – Phân Trang + Validate + Upload File + JOIN categories

Giới thiệu khóa học: Xây dựng Web Bán Hàng Online với Java Spring Boot, Oracle DB và Thymeleaf

Mục tiêu bài học:

  • CRUD bảng Product có quan hệ Category.
  • Phân trang danh sách sản phẩm.
  • Validate dữ liệu khi thêm/sửa.
  • Upload hình ảnh sản phẩm.
  • Tìm kiếm sản phẩm theo tên, sắp xếp theo giá.

🗄️ 1️⃣ SQL Cơ sở dữ liệu

CREATE DATABASE WebBanHangDB;
GO

USE WebBanHangDB;
GO
-- Nếu bảng Category có rồi thì không cần tạo lại
CREATE TABLE Category (
    id INT IDENTITY(1,1) PRIMARY KEY,
    name NVARCHAR(255) NOT NULL
);

CREATE TABLE Product (
    id INT IDENTITY(1,1) PRIMARY KEY,
    name NVARCHAR(255) NOT NULL,
    price FLOAT NOT NULL,
    image NVARCHAR(255),
    category_id INT FOREIGN KEY REFERENCES Category(id)
);

INSERT INTO Category (name) VALUES
(N'Điện thoại'), (N'Máy tính'), (N'Thiết bị văn phòng');

INSERT INTO Product (name, price, image, category_id) VALUES
(N'iPhone 15', 29999, 'iphone15.jpg', 1),
(N'Laptop Dell', 22000, 'dell.jpg', 2),
(N'Máy in Canon', 4500, 'canon.jpg', 3);

📦 2️⃣ Cấu trúc thư mục

src/
├─ main/
│  ├─ java/com/lungcode/
│  │  ├─ entity/Category.java
│  │  ├─ entity/Product.java
│  │  ├─ repository/CategoryRepository.java
│  │  ├─ repository/ProductRepository.java
│  │  ├─ service/ProductService.java
│  │  ├─ service/impl/ProductServiceImpl.java
│  │  └─ controller/ProductController.java
│  └─ resources/
│     ├─ templates/product/list.html
│     ├─ templates/product/form.html
│     ├─ static/uploads/
│     └─ application.properties

⚙️ 3️⃣ Cấu hình kết nối SQL Server

📄 File: src/main/resources/application.properties

spring.datasource.url=jdbc:sqlserver://localhost:1433;databaseName=WebBanHangDB
spring.datasource.username=sa
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=5MB

🧑‍💻 4️⃣ Entity

📄 File: src/main/java/com/lungcode/entity/Category.java

package com.lungcode.entity;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Data
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
}

📄 File: src/main/java/com/lungcode/entity/Product.java

package com.lungcode.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Entity
@Data
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @NotBlank(message = "Tên sản phẩm không được để trống!")
    private String name;

    @Min(value = 1, message = "Giá phải lớn hơn 0!")
    private double price;

    private String image;

    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;
}

📂 5️⃣ Repository

📄 File: src/main/java/com/lungcode/repository/CategoryRepository.java

package com.lungcode.repository;

import com.lungcode.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CategoryRepository extends JpaRepository<Category, Integer> {}

📄 File: src/main/java/com/lungcode/repository/ProductRepository.java

package com.lungcode.repository;

import com.lungcode.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Integer> {
    Page<Product> findByNameContainingIgnoreCase(String name, Pageable pageable);
}

🔧 6️⃣ Service

📄 File: src/main/java/com/lungcode/service/ProductService.java

package com.lungcode.service;

import com.lungcode.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface ProductService {
    Page<Product> getAll(String keyword, Pageable pageable);
    Product save(Product product);
    Product getById(Integer id);
    void deleteById(Integer id);
}

📄 File: src/main/java/com/lungcode/service/impl/ProductServiceImpl.java

package com.lungcode.service.impl;

import com.lungcode.entity.Product;
import com.lungcode.repository.ProductRepository;
import com.lungcode.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductRepository repository;

    @Override
    public Page<Product> getAll(String keyword, Pageable pageable) {
        if (keyword != null && !keyword.isEmpty()) {
            return repository.findByNameContainingIgnoreCase(keyword, pageable);
        }
        return repository.findAll(pageable);
    }

    @Override
    public Product save(Product product) {
        return repository.save(product);
    }

    @Override
    public Product getById(Integer id) {
        return repository.findById(id).orElse(null);
    }

    @Override
    public void deleteById(Integer id) {
        repository.deleteById(id);
    }
}

🌐 7️⃣ Controller

📄 File: src/main/java/com/lungcode/controller/ProductController.java

package com.lungcode.controller;

import com.lungcode.entity.Category;
import com.lungcode.entity.Product;
import com.lungcode.repository.CategoryRepository;
import com.lungcode.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

@Controller
@RequestMapping("/products")
public class ProductController {

    @Autowired private ProductService service;
    @Autowired private CategoryRepository categoryRepo;

    @GetMapping
    public String list(Model model,
                       @RequestParam(defaultValue = "0") int page,
                       @RequestParam(defaultValue = "") String keyword) {
        Pageable pageable = PageRequest.of(page, 5);
        model.addAttribute("products", service.getAll(keyword, pageable));
        model.addAttribute("keyword", keyword);
        return "product/list";
    }

    @GetMapping("/create")
    public String createForm(Model model) {
        model.addAttribute("product", new Product());
        model.addAttribute("categories", categoryRepo.findAll());
        return "product/form";
    }

    @PostMapping("/save")
    public String save(@Valid @ModelAttribute Product product,
                       BindingResult result,
                       @RequestParam("file") MultipartFile file) throws IOException {
        if (result.hasErrors()) {
            return "product/form";
        }

        if (!file.isEmpty()) {
            String path = "src/main/resources/static/uploads/" + file.getOriginalFilename();
            file.transferTo(new File(path));
            product.setImage(file.getOriginalFilename());
        }

        service.save(product);
        return "redirect:/products";
    }

    @GetMapping("/edit/{id}")
    public String edit(@PathVariable Integer id, Model model) {
        Product product = service.getById(id);
        model.addAttribute("product", product);
        model.addAttribute("categories", categoryRepo.findAll());
        return "product/form";
    }

    @GetMapping("/delete/{id}")
    public String delete(@PathVariable Integer id) {
        service.deleteById(id);
        return "redirect:/products";
    }
}

🎨 8️⃣ Giao diện Thymeleaf

📄 File: src/main/resources/templates/product/list.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Danh sách sản phẩm</title>
</head>
<body>
<h2>Danh sách sản phẩm</h2>
<form method="get" th:action="@{/products}">
    Tìm tên: <input type="text" name="keyword" th:value="${keyword}">
    <button type="submit">Tìm kiếm</button>
</form>
<a th:href="@{/products/create}">Thêm mới</a>
<table border="1">
    <tr>
        <th>ID</th><th>Tên</th><th>Giá</th><th>Hình</th><th>Danh mục</th><th>Hành động</th>
    </tr>
    <tr th:each="p : ${products.content}">
        <td th:text="${p.id}"></td>
        <td th:text="${p.name}"></td>
        <td th:text="${p.price}"></td>
        <td><img th:src="@{'/uploads/' + ${p.image}}" width="50"/></td>
        <td th:text="${p.category.name}"></td>
        <td>
            <a th:href="@{'/products/edit/' + ${p.id}}">Sửa</a>
            <a th:href="@{'/products/delete/' + ${p.id}}"
               onclick="return confirm('Bạn chắc chắn muốn xoá?')">Xoá</a>
        </td>
    </tr>
</table>

<div>
    <span th:if="${products.hasPrevious()}">
        <a th:href="@{|/products?page=${products.number - 1}&keyword=${keyword}|}">Trang trước</a>
    </span>
    <span th:text="${products.number + 1}"></span>/<span th:text="${products.totalPages}"></span>
    <span th:if="${products.hasNext()}">
        <a th:href="@{|/products?page=${products.number + 1}&keyword=${keyword}|}">Trang tiếp</a>
    </span>
</div>
</body>
</html>

📄 File: src/main/resources/templates/product/form.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Thêm / Sửa sản phẩm</title>
</head>
<body>
<h2 th:text="${product.id == null} ? 'Thêm sản phẩm' : 'Sửa sản phẩm'"></h2>

<form th:action="@{/products/save}" th:object="${product}" method="post" enctype="multipart/form-data">
    <input type="hidden" th:field="*{id}"/>
    Tên sản phẩm: <input type="text" th:field="*{name}"/> <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span><br/>
    Giá: <input type="number" th:field="*{price}"/> <span th:if="${#fields.hasErrors('price')}" th:errors="*{price}"></span><br/>
    Ảnh: <input type="file" name="file"/><br/>
    Danh mục:
    <select th:field="*{category}">
        <option th:each="c : ${categories}" th:value="${c}" th:text="${c.name}"></option>
    </select><br/>
    <button type="submit">Lưu</button>
</form>

<a th:href="@{/products}">Quay lại danh sách</a>
</body>
</html>

Total
0
Shares
Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Previous Post
Giới thiệu khóa học: Xây dựng Web Bán Hàng Online với Java Spring Boot, Oracle DB và Thymeleaf

Java Spring Boot Bài 7: CRUD cho danh mục (categories) – Spring Boot + Thymeleaf + JPA + Tìm kiếm + Sắp xếp

Related Posts