Spring Boot File Management sử dụng JPA, Hibernate, MySQL và ReactJs (1)

06 tháng 03, 2023 - 5330 lượt xem

Upload và download các file là những tác vụ rất phổ biến mà các lập trình viên thường xuyên phải triển khai trong quá trình phát triển ứng dụng.

Trong bài viết này, chúng ta sẽ tìm hiểu về cách quản lý file cơ bản trong ứng dụng SpringBoot REST API. Những file này sau khi được upload sẽ được lưu trữ trong hệ quản trị cơ sở dữ liệu MySQL.

Trước tiên, chúng ta sẽ xây dựng API REST để có thể upload và download file, sau đó kiểm tra các API đã viết bằng Postman. Chúng ta cũng có thể làm một giao diện frontend cơ bản, áp dụng javascript để có thể có thể upload file.

OK, bắt đầu nào!!! 😀😁

Tạo ứng dụng

Chúng ta có thể sử dụng theo các cách sau :

  • Sử dụng trang web https://start.spring.io/ (đây là trang web cho phép tạo ứng dụng SpringBoot), sau khi điền các thông tin cơ bản, chọn các dependence cần thiết, chúng ta có thể download về và sử dụng.
  • Sử dụng các IDE có hỗ trợ tạo ứng dụng SpringBoot : VSCode, Intellij, …

Sau đây là cấu trúc ứng dụng hoàn chỉnh mà các bạn có thể tham khảo.

Cấu trúc ứng dụng

Danh sách các dependence được sử dụng

Trong ứng dụng này chúng ta sẽ lưu trữ các file trong cơ sở dữ liệu MySQL, nên sẽ cần các dependence JPA, MySQL, Spring Web, Lombok

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
   <groupId>com.mysql</groupId>
   <artifactId>mysql-connector-j</artifactId>
   <scope>runtime</scope>
</dependency>

<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>

Cấu hình Database và Multipart File

Tiếp theo, chúng ta cần định cấu hình url, usernamepassword của cơ sở dữ liệu MySQL. Bạn có thể cấu hình nó trong src/main/resources/application.properties

## Cấu hình DATASOURCE
spring.datasource.url=jdbc:mysql://localhost:3306/db-image?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=123

## Cấu hình Hibernate
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

## Cấu hình MULTIPART
# Max file size
spring.servlet.multipart.max-file-size=100MB

# Max Request Size
spring.servlet.multipart.max-request-size=100MB

Các bạn có thể thay đổi các thông tin trong tệp cấu hình trên dựa trên các thông số trong ứng dụng của các bạn

Image Entity

Đầu tiên chúng ta sẽ tạo Image Entity, class này sẽ ánh xạ với bảng image trong database

package vn.techmaster.image.entity;

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

import java.time.LocalDateTime;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "image")
public class Image {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    private Integer id;

    @Column(name = "name")
    private String name;

    @Column(name = "type")
    private String type;

    @Lob // Large Object
    @Column(name = "data", columnDefinition = "LONGBLOB")
    private byte[] data;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @PrePersist
    public void prePersist() {
        createdAt = LocalDateTime.now();
    }

    public Image(String name, String type, byte[] data) {
        this.name = name;
        this.type = type;
        this.data = data;
    }
}
Image.java

Trong Entity Image, chúng ta có 1 số thuộc tính sau

  • id : sử dụng @GeneratedValue(strategy = GenerationType.AUTO) để generate id khi lưu vào database
  • name : tên của file
  • type : loại file (ví dụ : image/png, image/jpg, …)
  • data : Thông tin của file, lưu dưới dạng byte[]
  • createdAt : thời gian upload

Anotation @Lob chỉ định cơ sở dữ liệu sẽ lưu trữ thuộc tính này dưới dạng Large Object.

BLOBBinary Large Object thường dùng để lưu trữ dữ liệu ở dạng binary như image, audio hoặc video

Anotation @PrePersist là một trong các life cycle của entity, được thực hiện trước khi entity được lưu vào database. Trong trường này chúng ta sử dụng @PrePersist để lưu giá trị createdAt (thời gian upload) của image

Image Repository

Tiếp theo chúng ta tạo repository để có thể tương tác với database

package vn.techmaster.image.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import vn.techmaster.image.entity.Image;

import java.util.List;

public interface ImageRepository extends JpaRepository<Image, Integer> {
   // Lấy danh sách tất cả image theo thời gian tạo giảm dần
    List<Image> findByOrderByCreatedAtDesc();
}
ImageRepository

Image Controller

Bây giờ chúng ta viết các API REST để upload và download các file. Trong file ImageController chúng ta định nghĩa các API thực hiện các công việc sau:

  • Lấy danh sách tất cả các image
  • Upload image
  • Xem image
  • Download image
  • Xóa image

Chú ý : Các API Xem image, Download image chúng ta cần set giá trị Content-Type trên header của response để có thể xem được ảnh ở phía fontend

Các bạn có thể xem nội dung file ImageController dưới đây để biết thêm chi tiết

package vn.techmaster.image.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import vn.techmaster.image.entity.Image;
import vn.techmaster.image.service.ImageService;

@RestController
@RequestMapping("api/v1/images")
@RequiredArgsConstructor
public class ImageController {
    private final ImageService imageService;

    // Lấy danh sách ảnh
    @GetMapping("")
    public ResponseEntity<?> getAllImage() {
        return ResponseEntity.ok(imageService.getAllImage());
    }

    // Xem ảnh
    @GetMapping("{id}")
    public ResponseEntity<?> readImage(@PathVariable Integer id) {
        Image image = imageService.getImage(id);
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(image.getType()))
                .body(image.getData());
    }

    // Upload ảnh
    @PostMapping("")
    public ResponseEntity<?> uploadImage(@ModelAttribute("file") MultipartFile file) {
        return new ResponseEntity<>(imageService.uploadImage(file), HttpStatus.CREATED);
    }

    // Download ảnh
    @GetMapping("/download/{id}")
    public ResponseEntity<?> downloadImage(@PathVariable Integer id) {
        Image image = imageService.getImage(id);
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(image.getType()))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + image.getName() + "\"")
                .body(new ByteArrayResource(image.getData()));
    }

    // Xóa ảnh
    @DeleteMapping("{id}")
    public ResponseEntity<?> deleteImage(@PathVariable Integer id) {
        imageService.deleteImage(id);
        return ResponseEntity.noContent().build(); // 204
    }
}
ImageController

Image Service

Mỗi API được định nghĩa trong ImageController chúng ta sẽ định nghĩa các method tương ứng trong ImageService để xử lý. Trong này chúng ta định nghĩa các method xử lý các công việc sau

  • Lấy danh sách tất cả file
  • Lấy chi tiết file
  • Upload file
  • Xóa file

Trong ImageService cần tương tác với database vì vậy cần inject bean imageRepository vào để có thể sử dụng

Ngoài ra trong này có inject thêm bean FileUtils, tác dụng của bean này chứa các method hữu ích để thao tác với file

Các bạn có thể xem nội dung file ImageService dưới đây để biết thêm chi tiết

package vn.techmaster.image.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import vn.techmaster.image.entity.Image;
import vn.techmaster.image.exception.NotFoundException;
import vn.techmaster.image.repository.ImageRepository;
import vn.techmaster.image.response.ImageResponse;
import vn.techmaster.image.utils.FileUtils;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ImageService {
    private final ImageRepository imageRepository;
    private final FileUtils fileUtils;

    public List<Image> getAllImage() {
        return imageRepository.findByOrderByCreatedAtDesc();
    }

    public Image getImage(Integer id) {
        return imageRepository.findById(id).orElseThrow(() -> {
            throw new NotFoundException("Not found image with id = " + id);
        });
    }

    public Image uploadImage(MultipartFile file) {
        fileUtils.validateFile(file);

        try {
            String fileName = StringUtils.cleanPath(file.getOriginalFilename());
            Image image = new Image(fileName, file.getContentType(), file.getBytes());
            return imageRepository.save(image);
        } catch (Exception e) {
            throw new RuntimeException("Upload image error");
        }
    }

    public void deleteImage(Integer id) {
        Image image = imageRepository.findById(id).orElseThrow(() -> {
            throw new NotFoundException("Not found image with id = " + id);
        });

        imageRepository.delete(image);
    }
}
ImageService

Phần định nghĩa và xử lý exception, các bạn có thể tham khảo code trong repo mình đặt ở phía dưới

FileUtils

FileUtils chứa các method hữu ích khi thao tác với file, chủ yếu trong phần này chúng ta sẽ có các method để validate file trước khi lưu vào trong database

Ví dụ một số trường hợp chúng ta cần validate:

  • Kiểm tra tên file?
  • Kiểm tra type file có hợp lệ hay không?
  • Kiểm tra size file có trong khoảng cho phép hay không?
package vn.techmaster.image.utils;

import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import vn.techmaster.image.exception.BadRequestException;

import java.util.ArrayList;
import java.util.List;

@Component
public class FileUtils {
   // Validate file
    public void validateFile( MultipartFile file) {
        // Kiểm tra tên file
        String fileName = file.getOriginalFilename();
        if(fileName == null || fileName.isEmpty()) {
            throw new BadRequestException("tên file không được để trống");
        }

        // image.png -> png
        // avatar.jpg -> jpg
        // Kiểm tra đuôi file (jpg, png, jpeg)
        String fileExtension = getFileExtension(fileName);
        if(!checkFileExtension(fileExtension)) {
            throw new BadRequestException("file không đúng định dạng");
        }

        // Kiểm tra dung lượng file (<= 2MB)
        double fileSize =  (double) (file.getSize() / 1_048_576);
        if( fileSize > 2) {
            throw new BadRequestException("file không được vượt quá 2MB");
        }
    }

   // Lấy extension của file (ví dụ png, jpg, ...)
    public String getFileExtension(String fileName) {
        int lastIndexOf = fileName.lastIndexOf(".");
        return fileName.substring(lastIndexOf + 1);
    }

   // Kiểm tra extension của file có được phép hay không
    public boolean checkFileExtension(String fileExtension) {
        List<String> extensions = new ArrayList<>(List.of("png", "jpg", "jpeg", "pdf"));
        return extensions.contains(fileExtension.toLowerCase());
    }
}
FileUtils

Test API

Phần này, chúng ta sẽ test API dựa trên công cụ PostMan

1. Lấy danh sách ảnh

Lấy danh sách ảnh

2. Upload ảnh

Upload ảnh

3. Xem ảnh

xem ảnh

4. Download ảnh

download ảnh

5. Xóa ảnh

Xóa ảnh

Vậy là chúng ta đã hoàn thành ứng dụng “File Management” với các chức năng cơ bản. Hi vọng các bạn thấy bài viết này hữu ích và thú vị 😁😁😁

Trong phần tiếp theo, chúng ta sẽ làm ứng dụng FrontEnd cơ bản kết nối với ứng dụng SpringBoot để có thể thao tác với file ở trên UI

Phần sources code của bài viết này, các bạn có thể tham khảo tại đây: https://github.com/buihien0109/image-management/tree/main/image-management-backend

Các bạn có thể tham khảo thêm khóa học này nhé:

Bình luận

avatar
Trịnh Minh Cường 2023-03-06 09:23:26.408344 +0000 UTC

Chúc mừng thầy lên 1 bài đầu tiên.

Hãy là một con rùa, mỗi ngày tiến lên một bước cũng tốt.

Avatar
* Vui lòng trước khi bình luận.
Ảnh đại diện
  +1 Thích
+1