Monday, September 9, 2019

Hướng dẫn xử lý file: Upload / Download sử dụng Spring Boot, JPA, Hibernate, và MySQL database.

Trong bài viết này, tôi sẽ hướng dẫn bạn làm như thế nào để upload và download files bằng RESTful spring boot web service. Files sẽ được lưu trữ trong database MySQL.

Bài viết này là phần tiếp theo của bài viết trước, tại đó tôi đã hướng dẫn cho bạn làm như thế nào để upload files và lưu trữ chúng tại filesystem nội bộ.

Chúng ta sẽ sử dùng lại hầu hết các mã và khái niệm được mô tả trong bài viết trước. Vì vậy, tôi khuyên bạn nên tìm hiểu bài viết trước khi đọc cái này.

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


1. JPA và MySQL dependencies

Vì chúng ta sẽ lưu trữ các tệp trong cơ sở dữ liệu MySQL, nên chúng ta sẽ cần các dependencies JPAMySQL cùng với dependency Web

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

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

 <dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
 </dependency>

 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
 </dependency>
</dependencies>
Nếu bạn đang xây dựng dự án từ đầu, bạn có thể tạo bộ khung project từ Spring Initialzr website.

2. Cấu hình Database và Multipart File properties

Tiếp theo, chúng ta cần configure các thông tin để kết nối đến database MySQL như url, username, và password. Chúng ta sẽ configure nó tại file src/main/resources/application.properties

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url= jdbc:mysql://localhost:3306/file_demo?useSSL=false
spring.datasource.username= root
spring.datasource.password= 123456

## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto = update

## Hibernate Logging
logging.level.org.hibernate.SQL= DEBUG


## MULTIPART (MultipartProperties)
# Enable multipart uploads
spring.servlet.multipart.enabled=true
# Threshold after which files are written to disk.
spring.servlet.multipart.file-size-threshold=2KB
# Max file size.
spring.servlet.multipart.max-file-size=200MB
# Max Request Size
spring.servlet.multipart.max-request-size=215MB
File properties trên có chứa các thông tin thuộc tính của file Multipart. Bạn có thể thay đổi các thuộc tính đó sao cho đúng với requirements của bạn.

3. DBFile model

Hãy tạo một file entity để mô hình hóa các thuộc tính sẽ được lưu trữ vào database

package com.mpt.filedemo.model;

import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;

@Entity
@Table(name = "files")
public class DBFile {
    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;

    private String fileName;

    private String fileType;

    @Lob
    private byte[] data;

    public DBFile() {

    }

    public DBFile(String fileName, String fileType, byte[] data) {
        this.fileName = fileName;
        this.fileType = fileType;
        this.data = data;
    }

    // Getters and Setters (Omitted for brevity)
}
Lưu ý, nội dung của file sẽ được lưu trữ dưới dạng một mảng byte trong cơ sở dữ liệu.

4. DBFileRepository

Kế tiếp, chúng ta cần tạo một file repository để thực hiện lưu files xuống database và lấy chúng ra.

package com.mpt.filedemo.repository;

import com.mpt.filedemo.model.DBFile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface DBFileRepository extends JpaRepository<DBFile, String> {

}

5. DBFileStorageService

Dưới đây là thông tin class DBFileStorageService chứa các methods để lưu trữ và truy xuất files to/from database

package com.mpt.filedemo.service;

import com.mpt.filedemo.exception.FileStorageException;
import com.mpt.filedemo.exception.MyFileNotFoundException;
import com.mpt.filedemo.model.DBFile;
import com.mpt.filedemo.repository.DBFileRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;

@Service
public class DBFileStorageService {

    @Autowired
    private DBFileRepository dbFileRepository;

    public DBFile storeFile(MultipartFile file) {
        // Normalize file name
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());

        try {
            // Check if the file's name contains invalid characters
            if(fileName.contains("..")) {
                throw new FileStorageException("Sorry! Filename contains invalid path sequence " + fileName);
            }

            DBFile dbFile = new DBFile(fileName, file.getContentType(), file.getBytes());

            return dbFileRepository.save(dbFile);
        } catch (IOException ex) {
            throw new FileStorageException("Could not store file " + fileName + ". Please try again!", ex);
        }
    }

    public DBFile getFile(String fileId) {
        return dbFileRepository.findById(fileId)
                .orElseThrow(() -> new MyFileNotFoundException("File not found with id " + fileId));
    }
}

6. FileController (File upload/download REST APIs)

Cuối cùng, dưới đây là thông tin các Rest APIs để upload và download files

package com.mpt.filedemo.controller;

import com.mpt.filedemo.model.DBFile;
import com.mpt.filedemo.payload.UploadFileResponse;
import com.mpt.filedemo.service.DBFileStorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RestController
public class FileController {

    private static final Logger logger = LoggerFactory.getLogger(FileController.class);

    @Autowired
    private DBFileStorageService DBFileStorageService;

    @PostMapping("/uploadFile")
    public UploadFileResponse uploadFile(@RequestParam("file") MultipartFile file) {
        DBFile dbFile = DBFileStorageService.storeFile(file);

        String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/downloadFile/")
                .path(dbFile.getId())
                .toUriString();

        return new UploadFileResponse(dbFile.getFileName(), fileDownloadUri,
                file.getContentType(), file.getSize());
    }

    @PostMapping("/uploadMultipleFiles")
    public List<UploadFileResponse> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
        return Arrays.asList(files)
                .stream()
                .map(file -> uploadFile(file))
                .collect(Collectors.toList());
    }

    @GetMapping("/downloadFile/{fileId}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileId) {
        // Load file from database
        DBFile dbFile = DBFileStorageService.getFile(fileId);

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(dbFile.getFileType()))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + dbFile.getFileName() + "\"")
                .body(new ByteArrayResource(dbFile.getData()));
    }

}

7. Kết quả

Tôi sẽ sử dụng lại code cho phần front-end đã hoàn chỉnh mà tôi đã trình bày trong bài viết trước. Bạn nên xem bài viết đó để tìm hiểu thêm về code front-end.

Khi bạn đã có code giao diện người dùng, hạy chạy ứng dụng và bắt đầu test.

OK, đó là tất cả những gì tôi muốn trình bày. Cảm ơn vì đã đọc!