JavaRush /Java 博客 /Random-ZH /将文件保存到应用程序并将有关它们的数据保存到数据库

将文件保存到应用程序并将有关它们的数据保存到数据库

已在 Random-ZH 群组中发布
将文件保存到应用程序并将有关它们的数据保存到数据库 - 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);
   }
}
我们使用邮递员: 将文件保存到应用程序并将有关它们的数据保存到数据库 - 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