Java Spring Boot Bài 7: 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 6: CRUD cho danh mục (categories) – Spring Boot + Thymeleaf + JPA + Tìm kiếm + Sắp xếp

Related Posts
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

Khóa học lập trình ứng dụng web bán hàng online với Java Spring Boot và Oraclr DB

👉 Xem chi tiết tại đây