JavaRush /Java Blog /Random-TW /將文件保存到應用程式並將有關它們的資料保存到資料庫

將文件保存到應用程式並將有關它們的資料保存到資料庫

在 Random-TW 群組發布
將文件保存到應用程式並將有關它們的資料保存到資料庫 - 1 假設您正在開發 Web 應用程式。在我的文章中,我研究了這個馬賽克的各個部分,例如: 這些主題有什麼用?事實上,這些範例非常接近實際項目,測試這些主題將對您非常有用。今天,我們將採取這個馬賽克的下一個部分 - 處理文件,因為現在您再也找不到不與文件互動的網站(例如,各種網上商店、社交網絡等)。審核將以上傳/下載/刪除方法為例進行;我們將其保存到應用程式中的資料夾(在資源中),以免使其複雜化。 將文件保存到應用程式並將有關它們的資料保存到資料庫 - 2我們的想法是,除了文件本身在所謂的文件存儲中之外,我們還將在資料庫中保存一個包含文件資訊(大小、名稱等)的實體 - 一個預先創建的表。也就是說,在載入檔案時,這個實體對我們來說非常有用,而在刪除時,我們無論如何都不能忘記它。我們看一下這張表: 將文件保存到應用程式並將有關它們的資料保存到資料庫 - 3並且我們像大人一樣將id設定為AUTO_INCRMENT,在資料庫層級自動產生一個識別碼。首先,我們來看看我們的結構: 將文件保存到應用程式並將有關它們的資料保存到資料庫 - 4上圖表格下的實體:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

上傳

我們來看看控制器:
@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);
       }
   }
一些有趣的事情: 9 - 我們接受表單中的文件MultipartFile。它也可以作為位元組數組接收,但我更喜歡這個選項,因為我們可以MultipartFile提取傳輸檔案的各種屬性。10 - 14 - 我們將操作包裝起來,try catch以便如果在較低層級發生異常,我們會將其轉發到更高層級並發送 400 錯誤作為回應。其次是服務水準:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
我們看一下實作:
@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 - 如果出現 IOException,我們在資料庫中的所有儲存都會回溯。11 - 我們產生一個密鑰,該密鑰在保存文件時將是唯一的(即使保存兩個同名文件,也不會混淆)。12 — 我們建立一個實體以保存在資料庫中。17 — 我們將帶有資訊的實體驅動到資料庫中。18 - 使用雜湊名稱儲存檔案。20 — 我們返回創建的實體FileInfo,但帶有資料庫中生成的 id(這將在下面討論)和創建日期。產生文件金鑰的方法:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
這裡我們對名稱+創建日期進行哈希處理,這將確保我們的唯一性。Dao層介面:
public interface FileDAO {

   FileInfo create(FileInfo file);
其實現:
@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 - 建立一個我們要儲存的日期。12 - 21 - 我們保存實體,但以更複雜的方式,明確創建對象,PreparedStatement以便可以將生成的 id 拉出(它不是通過單獨的請求,而是以響應元數據的形式將其拉出) ) 。22 - 26 - 我們完成了長期受苦實體的構建並將其交給頂層(事實上,他並沒有完成它,而是創建了一個新對象,填充了傳輸的字段並從原始對象複製了其餘部分) 。讓我們看看我們的文件將如何保存在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 - 我們接受位元組數組形式的文件,並分別接受保存該文件的名稱(我們產生的密鑰)。2 - 3 - 建立一個路徑(在路徑中我們寫入路徑加上我們的金鑰)和一個沿著它的檔案。6 - 7 - 建立一個流並在那裡寫入我們的位元組(並將所有這些內容包裝起來try-finally以確保流肯定會關閉)。但是,許多方法可能會拋出 IOException。在這種情況下,由於方法標頭中指定的轉發,我們將其傳遞給控制器並給出狀態 400。讓我們在 Postman 中測試整個過程: 將文件保存到應用程式並將有關它們的資料保存到資料庫 - 5正如我們所看到的,一切都很好,響應是 201,響應 JSON 以數據庫中持久實體的形式出現,如果我們查看存儲:DB:我們 將文件保存到應用程式並將有關它們的資料保存到資料庫 - 6會看到我們有一個新的價值。(=*;*=)

下載

控制器:
@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);
   }
}
我們也將它包裝起來try-catch,如果出現 IOException,我們會發送 404 回應狀態(找不到)。4 - 從資料庫中提取相鄰的 FileInfo 實體。5 - 使用來自實體的金鑰,下載檔案 6-8 - 發回文件,將檔案名稱新增至標頭(再次從實體取得有關文件的資訊)。讓我們更深入地了解一下。服務介面:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
執行:
@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);
}
這裡沒有什麼特別有趣的:透過 id 搜尋實體並下載檔案的方法,除了 46 之外 - 我們標記我們有讀取事務。道級:
FileInfo findById(Long fileId);
執行:
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();
}
jdbcTemplate4 — 使用和 按 ID 搜尋RowMapper。8 - 15 -RowMapper我們具體案例的實現,用於比較資料庫和模型欄位中的資料。讓我們看看FileManager我們的文件是如何加載的:
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();
   }
}
我們將文件作為物件返回Resource,然後我們將按鍵搜尋。3 -Resource沿著路徑+鍵創建。4 - 8 — 我們檢查給定路徑中的檔案不為空並讀取它。如果一切正常,我們將返回它,如果沒有,我們將向頂部拋出 IOException。讓我們在 Postman 中檢查我們的方法: 將文件保存到應用程式並將有關它們的資料保存到資料庫 - 7如您所見,它工作正常))

刪除

@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);
   }
}
這裡沒什麼特別的:如果使用失敗,我們也會回傳 404 try-catch。服務介面:
void delete(Long fileId) throws IOException;
執行:
@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 - 當發生 IOException 時也會回滾資料變更(刪除)。5 - 從資料庫中刪除有關該檔案的資訊。6 - 從我們的「儲存」中刪除檔案本身。道接口:
void delete(Long fileId);
執行:
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);
}
沒有那樣的事 - 只需刪除即可。刪除檔案本身:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
我們在 Postman 中使用它: 將文件保存到應用程式並將有關它們的資料保存到資料庫 - 8我們在存儲中查找: 將文件保存到應用程式並將有關它們的資料保存到資料庫 - 9空:)現在在資料庫中: Сохранение файлов в приложение и данных о них на БД - 10我們看到一切都很好))

測試

讓我們嘗試為我們的FileManager. 首先,讓我們來看看測試部分的結構:mockFile.txt 是我們將用來測試檔案儲存操作的檔案。testFileStorage 將取代我們的儲存。 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();
   }
在這裡我們看到測試資料分配。文件保存測試:
@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 — 使用測試反射,我們更改服務中的常數以設定保存檔案的路徑。5 — 呼叫被測試的方法。7 - 10 — 我們檢查已儲存的正確執行。11 - 刪除已儲存的檔案(我們不應該留下任何痕跡)。
@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();
}
文件上傳測試: 3 - 再次更改我們的FileManager. 5 - 使用正在測試的方法。7 - 9 — 檢查執行結果。文件刪除測試:
@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-設定路徑並建立檔案。5 - 6 - 我們檢查它的存在。9 - 我們使用可驗證的方法。77 - 我們檢查該物件是否不再存在。讓我們看看我們有什麼依賴關係:
<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>
這就是我今天的全部內容))
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION