JavaRush /Blog Java /Random-VI /Lưu tệp vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệ...

Lưu tệp vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu

Xuất bản trong nhóm
Lưu file vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu - 1 Hãy tưởng tượng rằng bạn đang làm việc trên ứng dụng web của mình. Trong các bài viết của mình, tôi xem xét từng phần riêng lẻ của bức tranh khảm này, chẳng hạn như: Những chủ đề này hữu ích như thế nào? Và thực tế là những ví dụ này rất gần với việc thực hiện các dự án thực tế và việc thử nghiệm những chủ đề này sẽ rất hữu ích cho bạn. Hôm nay chúng ta sẽ thực hiện phần tiếp theo của bức tranh khảm này - làm việc với các tệp, vì ngày nay bạn không còn có thể tìm thấy một trang web không tương tác với chúng (ví dụ: tất cả các loại cửa hàng web, mạng xã hội, v.v.). Việc đánh giá sẽ được thực hiện bằng cách sử dụng các phương thức tải lên/tải xuống/xóa làm ví dụ ; chúng tôi sẽ lưu nó vào một thư mục (trong tài nguyên) trong ứng dụng của chúng tôi để không làm phức tạp nó. Lưu file vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu - 2Ý tưởng là chúng tôi sẽ lưu, ngoài chính tệp đó trong cái gọi là bộ lưu trữ tệp của chúng tôi, một thực thể có thông tin về tệp của chúng tôi (kích thước, tên, v.v.) trong cơ sở dữ liệu của chúng tôi - một bảng được tạo trước. Nghĩa là, khi tải một tệp, thực thể này sẽ rất hữu ích cho chúng ta và khi xóa, chúng ta không được quên nó bằng mọi cách. Chúng ta hãy xem bảng này: Lưu file vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu - 3Và hãy đặt id thành AUTO_INCREMENT như người lớn, để tự động tạo mã định danh ở cấp cơ sở dữ liệu. Trước tiên, chúng ta hãy xem cấu trúc của chúng ta: Lưu file vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu - 4Thực thể trong bảng hiển thị ở trên:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Tải lên

Hãy nhìn vào bộ điều khiển:
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {

   private final FileService fileService;

   @PostMapping
   public ResponseEntity<FileInfo> upload(@RequestParam MultipartFile attachment) {
       try {
           return new ResponseEntity<>(fileService.upload(attachment), HttpStatus.CREATED);
       } catch (IOException e) {
           return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
       }
   }
Điều thú vị: 9 - Chúng tôi chấp nhận file có dạng MultipartFile. Nó cũng có thể được nhận dưới dạng một mảng byte, nhưng tôi thích tùy chọn này hơn vì chúng ta có thể MultipartFiletrích xuất các thuộc tính khác nhau của tệp được truyền. 10 - 14 - chúng tôi gói gọn các hành động của mình try catchđể nếu một ngoại lệ xảy ra ở cấp độ thấp hơn, chúng tôi sẽ chuyển tiếp nó lên cấp độ cao hơn và gửi phản hồi lỗi 400. Tiếp theo là mức độ dịch vụ:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Chúng ta hãy xem việc thực hiện:
@Service
@RequiredArgsConstructor
public class FileServiceImpl implements FileService {

   private final FileDAO fileDAO;
   private final FileManager fileManager;

   @Transactional(rollbackFor = {IOException.class})
   @Override
   public FileInfo upload(MultipartFile resource) throws IOException {
       String key = generateKey(resource.getName());
       FileInfo createdFile = FileInfo.builder()
               .name(resource.getOriginalFilename())
               .key(key)
               .size(resource.getSize())
               .build();
       createdFile = fileDAO.create(createdFile);
       fileManager.upload(resource.getBytes(), key);

       return createdFile;
   }
8 - trong trường hợp xảy ra IOException, tất cả các bản lưu trong cơ sở dữ liệu của chúng tôi sẽ bị khôi phục. 11 - chúng tôi tạo một khóa duy nhất cho tệp khi nó được lưu (ngay cả khi hai tệp có cùng tên được lưu, sẽ không có sự nhầm lẫn). 12 — chúng tôi xây dựng một thực thể để lưu vào cơ sở dữ liệu. 17 — chúng tôi đưa thực thể có thông tin vào cơ sở dữ liệu. 18 - lưu tệp với tên băm. 20 - chúng tôi trả về thực thể đã tạo FileInfonhưng có id được tạo trong cơ sở dữ liệu (điều này sẽ được thảo luận bên dưới) và ngày tạo. Phương pháp tạo khóa cho một tập tin:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Ở đây chúng tôi băm tên + ngày tạo, điều này sẽ đảm bảo tính duy nhất của chúng tôi. Giao diện lớp Dao:
public interface FileDAO {

   FileInfo create(FileInfo file);
Việc thực hiện nó:
@Repository
@RequiredArgsConstructor
public class FileDAOImpl implements FileDAO {

   private static final String CREATE_FILE = "INSERT INTO files_info(file_name, file_size, file_key, upload_date) VALUES (?, ?, ?, ?)";

   private final JdbcTemplate jdbcTemplate;

   @Override
   public FileInfo create(final FileInfo file) {
       LocalDate uploadDate = LocalDate.now();
       GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
       jdbcTemplate.update(x -> {
           PreparedStatement preparedStatement = x.prepareStatement(CREATE_FILE, Statement.RETURN_GENERATED_KEYS);
           preparedStatement.setString(1, file.getName());
           preparedStatement.setLong(2, file.getSize());
           preparedStatement.setString(3, file.getKey());
           preparedStatement.setDate(4, Date.valueOf(uploadDate));
           return preparedStatement;
       }, keyHolder);

       return file.toBuilder()
               .id(keyHolder.getKey().longValue())
               .uploadDate(uploadDate)
               .build();
   }
11 - tạo một ngày mà chúng tôi sẽ lưu. 12 - 21 - chúng tôi lưu thực thể, nhưng theo cách phức tạp hơn, với việc tạo đối tượng rõ ràng PreparedStatementđể có thể kéo id được tạo ra (nó kéo nó ra không phải bằng một yêu cầu riêng biệt mà ở dạng siêu dữ liệu phản hồi ). 22 - 26 - chúng tôi hoàn thành việc xây dựng thực thể chịu đựng lâu dài của mình và đưa nó lên hàng đầu (thực tế là anh ấy không hoàn thành nó mà tạo ra một đối tượng mới, điền vào các trường đã chuyển và sao chép phần còn lại từ đối tượng ban đầu) . Hãy xem các tập tin của chúng tôi sẽ được lưu như thế nào FileManager:
public void upload(byte[] resource, String keyName) throws IOException {
   Path path = Paths.get(DIRECTORY_PATH, keyName);
   Path file = Files.createFile(path);
   FileOutputStream stream = null;
   try {
       stream = new FileOutputStream(file.toString());
       stream.write(resource);
   } finally {
       stream.close();
   }
}
1 - chúng tôi chấp nhận tệp dưới dạng một mảng byte và riêng tên mà nó sẽ được lưu (khóa được tạo của chúng tôi). 2 - 3 - tạo một đường dẫn (và trong đường dẫn chúng ta viết đường dẫn cộng với khóa của chúng ta) và một tệp dọc theo nó. 6 - 7 - tạo một luồng và ghi các byte của chúng tôi vào đó (và gói tất cả nội dung này vào try-finallyđể đảm bảo rằng luồng chắc chắn sẽ đóng). Tuy nhiên, nhiều phương thức có thể ném ra IOException. Trong trường hợp này, nhờ chuyển tiếp được chỉ định trong tiêu đề phương thức, chúng tôi sẽ chuyển nó tới bộ điều khiển và đưa ra trạng thái 400. Hãy kiểm tra toàn bộ mọi thứ trong Postman: Lưu file vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu - 5Như chúng ta có thể thấy, mọi thứ đều ổn, phản hồi là 201, JSON phản hồi có dạng thực thể tồn tại lâu dài trong cơ sở dữ liệu và nếu chúng ta nhìn vào bộ lưu trữ của mình: DB: Lưu file vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu - 6chúng ta sẽ thấy rằng chúng ta có một giá trị mới. (=*;*=)

Tải xuống

Bộ điều khiển:
@GetMapping(path = "/{id}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<Resource> download(@PathVariable("id") Long id) {
   try {
       FileInfo foundFile = fileService.findById(id);
       Resource resource = fileService.download(foundFile.getKey());
       return ResponseEntity.ok()
               .header("Content-Disposition", "attachment; filename=" + foundFile.getName())
               .body(resource);
   } catch (IOException e) {
       return new ResponseEntity<>(HttpStatus.NOT_FOUND);
   }
}
Chúng tôi cũng gói nó lại try-catchvà trong trường hợp IOException, chúng tôi sẽ gửi trạng thái phản hồi 404 (không tìm thấy). 4 - kéo thực thể FileInfo liền kề ra khỏi cơ sở dữ liệu. 5 - sử dụng khóa từ thực thể, tải xuống tệp 6-8 - gửi lại tệp, thêm tên tệp vào tiêu đề (một lần nữa, được lấy từ thực thể có thông tin về tệp). Chúng ta hãy nhìn sâu hơn. Giao diện dịch vụ:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Thực hiện:
@Override
public Resource download(String key) throws IOException {
   return fileManager.download(key);
}

@Transactional(readOnly = true)
@Override
public FileInfo findById(Long fileId) {
   return fileDAO.findById(fileId);
}
Không có gì đặc biệt thú vị ở đây: phương pháp tìm kiếm thực thể theo id và tải xuống tệp, có lẽ ngoại trừ 46 - chúng tôi đánh dấu rằng chúng tôi có giao dịch để đọc. Cấp Đạo:
FileInfo findById(Long fileId);
Thực hiện:
private static final String FIND_FILE_BY_ID = "SELECT id, file_name, file_size, file_key, upload_date FROM files_info WHERE id = ?";

@Override
public FileInfo findById(Long fileId) {
   return jdbcTemplate.queryForObject(FIND_FILE_BY_ID, rowMapper(), fileId);
}

private RowMapper<FileInfo> rowMapper() {
   return (rs, rowNum) -> FileInfo.builder()
           .id(rs.getLong("id"))
           .name(rs.getString("file_name"))
           .size(rs.getLong("file_size"))
           .key(rs.getString("file_key"))
           .uploadDate(rs.getObject("upload_date", LocalDate.class))
           .build();
}
4 — tìm kiếm theo id bằng cách sử dụng jdbcTemplateRowMapper. 8 - 15 - triển khai RowMappercho trường hợp cụ thể của chúng tôi, để so sánh dữ liệu từ cơ sở dữ liệu và trường mô hình. Hãy đi FileManagervà xem tập tin của chúng tôi được tải như thế nào:
public Resource download(String key) throws IOException {
   Path path = Paths.get(DIRECTORY_PATH + key);
   Resource resource = new UrlResource(path.toUri());
   if (resource.exists() || resource.isReadable()) {
       return resource;
   } else {
       throw new IOException();
   }
}
Chúng tôi trả lại tệp dưới dạng đối tượng Resourcevà chúng tôi sẽ tìm kiếm theo khóa. 3 - tạo Resourcedọc theo đường dẫn + phím. 4 - 8 — chúng tôi kiểm tra xem tệp tại đường dẫn đã cho có trống không và đọc nó. Nếu mọi thứ đều ổn, chúng tôi sẽ trả lại và nếu không, chúng tôi sẽ ném IOException lên đầu. Hãy kiểm tra phương pháp của chúng tôi trong Postman: Lưu file vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu - 7Như bạn có thể thấy, nó hoạt động tốt))

Xóa bỏ

@DeleteMapping(value = "/{id}")
public ResponseEntity<Void> delete(@PathVariable("id") Long id) {
   try {
       fileService.delete(id);
       return new ResponseEntity<>(HttpStatus.OK);
   } catch (IOException e) {
       return new ResponseEntity<>(HttpStatus.NOT_FOUND);
   }
}
Không có gì đặc biệt ở đây: chúng tôi cũng trả về 404 trong trường hợp sử dụng try-catch. Giao diện dịch vụ:
void delete(Long fileId) throws IOException;
Thực hiện:
@Transactional(rollbackFor = {IOException.class})
@Override
public void delete(Long fileId) throws IOException {
   FileInfo file = fileDAO.findById(fileId);
   fileDAO.delete(fileId);
   fileManager.delete(file.getKey());
}
1 - cũng khôi phục các thay đổi dữ liệu (xóa) khi xảy ra IOException. 5 - xóa thông tin về tệp khỏi cơ sở dữ liệu. 6 - xóa chính tệp đó khỏi “bộ lưu trữ” của chúng tôi. giao diện dao:
void delete(Long fileId);
Thực hiện:
private static final String DELETE_FILE_BY_ID = "DELETE FROM files_info WHERE id = ?";

@Override
public void delete(Long fileId) {
   jdbcTemplate.update(DELETE_FILE_BY_ID, fileId);
}
Không có gì như vậy - chỉ cần xóa. Tự xóa tập tin:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Chúng tôi sử dụng nó trong Người đưa thư: Lưu file vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu - 8Chúng tôi tìm trong bộ lưu trữ: Lưu file vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu - 9Trống :) Bây giờ trong cơ sở dữ liệu: Сохранение файлов в приложение и данных о них на БД - 10Chúng tôi thấy rằng mọi thứ đều tốt))

Bài kiểm tra

Hãy thử viết một bài kiểm tra cho FileManager. Trước tiên, chúng ta hãy xem cấu trúc của phần kiểm tra: mockFile.txt là tệp mà chúng tôi sẽ kiểm tra các hoạt động của mình với bộ lưu trữ tệp. testFileStorage sẽ là sự thay thế cho việc lưu trữ của chúng ta. FileManagerTest:
public class FileManagerTest {

   private static MultipartFile multipartFile;

   private static FileManager manager;

   private static FileInfo file;

   @BeforeClass
   public static void prepareTestData() throws IOException {
       file = FileInfo.builder()
               .id(9L)
               .name("mockFile.txt")
               .key("mockFile.txt")
               .size(38975L)
               .uploadDate(LocalDate.now())
               .build();
       multipartFile = new MockMultipartFile("mockFile", "mockFile.txt", "txt",
               new FileInputStream("src/test/resources/mockFile.txt"));
       manager = new FileManager();
   }
Ở đây chúng ta thấy việc phân công dữ liệu thử nghiệm. Kiểm tra lưu tập tin:
@Test
public void uploadTest() throws IOException {
   ReflectionTestUtils.setField(manager, "DIRECTORY_PATH", "src/test/resources/testFileStorage/");

   manager.upload(multipartFile.getBytes(), "mockFile.txt");

   Path checkFile = Paths.get("src/test/resources/testFileStorage/mockFile.txt");
   assertThat(Files.exists(checkFile)).isTrue();
   assertThat(Files.isRegularFile(checkFile)).isTrue();
   assertThat(Files.size(checkFile)).isEqualTo(multipartFile.getSize());
   Files.delete(checkFile);
}
3 — bằng cách sử dụng phản ánh kiểm tra, chúng tôi thay đổi hằng số trong dịch vụ để đặt đường dẫn lưu tệp. 5 - gọi phương thức đang được thử nghiệm. 7 - 10 — chúng tôi kiểm tra việc thực hiện lưu chính xác. 11 - xóa file đã lưu (chúng ta không nên để lại dấu vết).
@Test
public void downloadTest() throws IOException {
   ReflectionTestUtils.setField(manager, "DIRECTORY_PATH", "src/test/resources/");

   Resource resource = manager.download(file.getKey());

   assertThat(resource.isFile()).isTrue();
   assertThat(resource.getFilename()).isEqualTo(file.getName());
   assertThat(resource.exists()).isTrue();
}
Kiểm tra tải lên tệp: 3 - một lần nữa, thay đổi đường dẫn cho tệp FileManager. 5 - sử dụng phương pháp đang được thử nghiệm. 7 - 9 — kiểm tra kết quả thực hiện. Kiểm tra xóa tập tin:
@Test
public void deleteTest() throws IOException {
   Path checkFile = Paths.get("src/test/resources/testFileStorage/mockFile.txt");
   Files.createFile(checkFile);
   assertThat(Files.exists(checkFile)).isTrue();
   assertThat(Files.isRegularFile(checkFile)).isTrue();
   ReflectionTestUtils.setField(manager, "DIRECTORY_PATH", "src/test/resources/testFileStorage/");

   manager.delete(file.getKey());

   assertThat(Files.notExists(checkFile)).isTrue();
}
9 - 3 - 4 - đặt đường dẫn và tạo tệp. 5 - 6 - chúng tôi kiểm tra sự tồn tại của nó. 9 - chúng tôi sử dụng một phương pháp có thể kiểm chứng được. 77 - chúng tôi kiểm tra xem đối tượng không còn ở đó nữa. Và hãy xem chúng ta có gì về mặt phụ thuộc:
<dependencies>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-jdbc</artifactId>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <scope>test</scope>
       <exclusions>
           <exclusion>
               <groupId>org.junit.vintage</groupId>
               <artifactId>junit-vintage-engine</artifactId>
           </exclusion>
       </exclusions>
   </dependency>
   <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <version>1.18.10</version>
       <scope>provided</scope>
   </dependency>
   <dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
       <version>8.0.18</version>
   </dependency>
   <dependency>
       <groupId>commons-codec</groupId>
       <artifactId>commons-codec</artifactId>
       <version>1.9</version>
   </dependency>
   <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <version>4.13-rc-2</version>
       <scope>test</scope>
   </dependency>
</dependencies>
Đó là tất cả đối với tôi ngày hôm nay))
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION