JavaRush /Blogue Java /Random-PT /Salvando arquivos no aplicativo e dados sobre eles no ban...

Salvando arquivos no aplicativo e dados sobre eles no banco de dados

Publicado no grupo Random-PT
Salvando arquivos no aplicativo e dados sobre eles no banco de dados - 1 Vamos imaginar que você está trabalhando em sua aplicação web. Em meus artigos examino peças individuais desse mosaico, como: Como esses tópicos são úteis? E o fato de esses exemplos estarem muito próximos de trabalhar em projetos reais, e testar esses tópicos será muito útil para você. Hoje vamos pegar a próxima peça desse mosaico - trabalhar com arquivos, já que hoje em dia não é mais possível encontrar um site que não interaja com eles (por exemplo, todo tipo de loja virtual, redes sociais, etc.). A revisão será realizada utilizando como exemplo os métodos upload/download/delete ; salvaremos em uma pasta (no recurso) de nossa aplicação, para não complicar. Salvando arquivos no aplicativo e dados sobre eles no banco de dados - 2A ideia é que salvemos, além do próprio arquivo em nosso chamado armazenamento de arquivos, uma entidade com informações sobre nosso arquivo (tamanho, nome, etc.) em nosso banco de dados - uma tabela pré-criada. Ou seja, ao carregar um arquivo, esta entidade será muito útil para nós, e ao excluí-la não devemos esquecê-la de forma alguma. Vamos dar uma olhada nesta tabela: Salvando arquivos no aplicativo e dados sobre eles no banco de dados - 3E vamos definir o id para AUTO_INCREMENT como adultos, para gerar automaticamente um identificador no nível do banco de dados. Primeiro, vamos dar uma olhada em nossa estrutura: Salvando arquivos no aplicativo e dados sobre eles no banco de dados - 4A entidade abaixo da tabela mostrada acima:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Carregar

Vejamos o controlador:
@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);
       }
   }
Algumas coisas interessantes: 9 - Aceitamos o arquivo no formato MultipartFile. Também pode ser recebido como um array de bytes, mas gosto mais desta opção, pois podemos MultipartFileextrair várias propriedades do arquivo transferido. 10 - 14 - agrupamos nossas ações try catchpara que, se ocorrer uma exceção em um nível inferior, a encaminhemos para um nível mais alto e enviemos um erro 400 como resposta. O próximo é o nível de serviço:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Vejamos a implementação:
@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 - em caso de IOException, todos os nossos saves no banco de dados serão revertidos. 11 - geramos uma chave que será única para o arquivo quando ele for salvo (mesmo que sejam salvos dois arquivos com os mesmos nomes, não haverá confusão). 12 — construímos uma entidade para salvar no banco de dados. 17 — direcionamos a entidade com as informações para o banco de dados. 18 - salve o arquivo com nome com hash. 20 — retornamos a entidade criada FileInfo, mas com o id gerado no banco de dados (falaremos sobre isso a seguir) e a data de criação. Método para gerar uma chave para um arquivo:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Aqui fazemos o hash do nome + data de criação, o que garantirá nossa exclusividade. Interface da camada Dao:
public interface FileDAO {

   FileInfo create(FileInfo file);
Sua implementação:
@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 - crie uma data que iremos salvar. 12 - 21 - salvamos a entidade, mas de forma mais complexa, com a criação explícita do objeto PreparedStatementpara que o id gerado possa ser retirado (retira-o não com uma solicitação separada, mas na forma de metadados de resposta ). 22 - 26 - completamos a construção de nossa entidade sofredora e a entregamos ao topo (na verdade, ele não a completa, mas cria um novo objeto, preenchendo os campos transferidos e copiando o restante do original) . Vamos ver como nossos arquivos serão salvos em 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 — aceitamos o arquivo como um array de bytes e separadamente o nome com o qual ele será salvo (nossa chave gerada). 2 - 3 - crie um caminho (e no caminho escrevemos o caminho mais nossa chave) e um arquivo ao longo dele. 6 - 7 - crie um stream e escreva nossos bytes lá (e envolva tudo isso try-finallypara ter certeza de que o stream será definitivamente fechado). No entanto, muitos dos métodos podem lançar IOException. Neste caso, graças ao encaminhamento especificado no cabeçalho do método, iremos passá-lo ao controlador e daremos o status 400. Vamos testar tudo no Postman: Salvando arquivos no aplicativo e dados sobre eles no banco de dados - 5Como vocês podem ver está tudo bem, a resposta é 201, a resposta JSON veio na forma de nossa entidade persistida no banco de dados, e se olharmos em nosso armazenamento: DB: Salvando arquivos no aplicativo e dados sobre eles no banco de dados - 6vemos que temos um novo valor. (=*;*=)

Download

Controlador:
@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);
   }
}
Também o envolvemos try-catche, em caso de IOException, enviamos um status de resposta 404 (não encontrado). 4 - retire a entidade FileInfo adjacente do banco de dados. 5 - utilizando a chave da entidade, baixe o arquivo 6-8 - envie de volta o arquivo, adicionando o nome do arquivo ao cabeçalho (novamente, obtido da entidade com informações sobre o arquivo). Vamos dar uma olhada mais profunda. Interface de serviço:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Implementação:
@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);
}
Não há nada de particularmente interessante aqui: o método de procurar uma entidade por id e baixar o arquivo, exceto talvez 46 - marcamos que temos a transação para leitura. Nível Tao:
FileInfo findById(Long fileId);
Implementação:
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 — pesquise por id usando jdbcTemplatee RowMapper. 8 - 15 - implementação RowMapperpara nosso caso específico, para comparação de dados do banco de dados e campos do modelo. Vamos FileManagerver como nosso arquivo é carregado:
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();
   }
}
Retornamos o arquivo como um objeto Resourcee pesquisaremos por chave. 3 - crie Resourceao longo do caminho + chave. 4 - 8 — verificamos se o arquivo no caminho fornecido não está vazio e o lemos. Se estiver tudo bem, retornamos e, caso contrário, lançamos uma IOException para o topo. Vamos verificar nosso método no Postman: Salvando arquivos no aplicativo e dados sobre eles no banco de dados - 7Como você pode ver, funcionou bem))

Excluir

@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);
   }
}
Nada de especial aqui: também retornamos 404 em caso de falha ao usar try-catch. Interface de serviço:
void delete(Long fileId) throws IOException;
Implementação:
@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 - também reversão de alterações de dados (exclusões) quando ocorre uma IOException. 5 - exclua informações sobre o arquivo do banco de dados. 6 - exclua o próprio arquivo do nosso “armazenamento”. interface dao:
void delete(Long fileId);
Implementação:
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);
}
Nada disso - apenas exclua. Excluindo o próprio arquivo:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Usamos Postman: Salvando arquivos no aplicativo e dados sobre eles no banco de dados - 8procuramos no armazenamento: Salvando arquivos no aplicativo e dados sobre eles no banco de dados - 9Vazio :) Agora no banco de dados: Сохранение файлов в приложение и данных о них на БД - 10vemos que está tudo bem))

Teste

Vamos tentar escrever um teste para nosso arquivo FileManager. Primeiro, vamos dar uma olhada na estrutura da parte de teste: mockFile.txt é o arquivo com o qual testaremos nossas operações com armazenamento de arquivos. testFileStorage substituirá nosso armazenamento. 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();
   }
Aqui vemos a atribuição de dados de teste. Teste de salvamento de arquivo:
@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 — usando a reflexão de teste, alteramos nossa constante no serviço para definir o caminho para salvar o arquivo. 5 — chame o método que está sendo testado. 7 - 10 — verificamos a correta execução do salvamento. 11 - exclua o arquivo salvo (não devemos deixar rastros).
@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();
}
Teste de upload de arquivo: 3 - novamente, altere o caminho do nosso arquivo FileManager. 5 - utilize o método que está sendo testado. 7 - 9 — verifique o resultado da execução. Teste de exclusão de arquivo:
@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 - defina o caminho e crie um arquivo. 5 - 6 - verificamos sua existência. 9 - usamos um método verificável. 77 - verificamos se o objeto não está mais lá. E vamos ver o que temos em termos de dependências:
<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>
Isso é tudo para mim hoje))
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION