JavaRush /Blog Java /Random-ES /Guardar archivos en la aplicación y datos sobre ellos en ...

Guardar archivos en la aplicación y datos sobre ellos en la base de datos.

Publicado en el grupo Random-ES
Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 1 Imaginemos que está trabajando en su aplicación web. En mis artículos miro piezas individuales de este mosaico, como: ¿Cómo son útiles estos temas? Y el hecho de que estos ejemplos estén muy cerca de trabajar en proyectos reales y probar estos temas le resultará muy útil. Hoy tomaremos la siguiente pieza de este mosaico: trabajar con archivos, ya que hoy en día ya no se pueden encontrar sitios que no interactúen con ellos (por ejemplo, todo tipo de tiendas web, redes sociales, etc.). La revisión la realizaremos usando como ejemplo los métodos subir/descargar/eliminar , la guardaremos en una carpeta (en recurso) de nuestra aplicación, para no complicarlo. Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 2La idea es que guardemos, además del archivo en sí en nuestro llamado almacenamiento de archivos, una entidad con información sobre nuestro archivo (tamaño, nombre, etc.) en nuestra base de datos: una tabla creada previamente. Es decir, a la hora de cargar un archivo esta entidad nos será de gran utilidad, y a la hora de eliminarlo no debemos olvidarnos de ella de ninguna manera. Echemos un vistazo a esta tabla: Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 3Y configuremos la identificación en AUTO_INCREMENT como adultos, para generar automáticamente un identificador a nivel de base de datos. Primero, echemos un vistazo a nuestra estructura: Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 4La entidad debajo de la tabla que se muestra arriba:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Subir

Veamos el 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);
       }
   }
Cosas interesantes: 9 - Aceptamos el archivo en el formulario MultipartFile. También se puede recibir como una matriz de bytes, pero me gusta más esta opción, ya que podemos MultipartFileextraer varias propiedades del archivo transferido. 10 - 14: ajustamos nuestras acciones de try catchmodo que si ocurre una excepción en un nivel inferior, la reenviamos a un nivel superior y enviamos un error 400 como respuesta. El siguiente es el nivel de servicio:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Veamos la implementació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 - en caso de una IOException, todos nuestros guardados en la base de datos se revertirán. 11 - generamos una clave que será única para el archivo cuando se guarde (incluso si se guardan dos archivos con el mismo nombre, no habrá confusión). 12: construimos una entidad para guardar en la base de datos. 17: conducimos la entidad con la información a la base de datos. 18: guarde el archivo con un nombre cifrado. 20 — devolvemos la entidad creada FileInfo, pero con la identificación generada en la base de datos (esto se discutirá más adelante) y la fecha de creación. Método para generar una clave para un archivo:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Aquí aplicamos un hash al nombre + fecha de creación, lo que garantizará nuestra singularidad. Interfaz de capa Dao:
public interface FileDAO {

   FileInfo create(FileInfo file);
Su implementació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 - crea una fecha que guardaremos. 12 - 21 - guardamos la entidad, pero de una manera más compleja, con la creación explícita del objeto PreparedStatementpara que se pueda extraer la identificación generada (la extrae no con una solicitud separada, sino en forma de metadatos de respuesta ). 22 - 26 - completamos la construcción de nuestra sufrida entidad y se la damos a la cima (de hecho, no la completa, sino que crea un nuevo objeto, completando los campos transferidos y copiando el resto del original) . Veamos cómo se guardarán nuestros archivos en 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 — aceptamos el archivo como una matriz de bytes y por separado el nombre con el que se guardará (nuestra clave generada). 2 - 3 - crea una ruta (y en la ruta escribimos la ruta más nuestra clave) y un archivo a lo largo de ella. 6 - 7 - crea una secuencia y escribe nuestros bytes allí (y envuelve todo esto try-finallypara asegurarte de que la secuencia se cierre definitivamente). Sin embargo, muchos de los métodos pueden generar IOException. En este caso, gracias al reenvío especificado en la cabecera del método, lo pasaremos al controlador y le daremos el estado 400. Probemos todo en Postman: Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 5Como puede ver, todo está bien, la respuesta es 201, la respuesta JSON vino en forma de nuestra entidad persistente en la base de datos, y si miramos en nuestro almacenamiento: DB: vemos Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 6que Tenemos un nuevo valor. (=*;*=)

Descargar

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);
   }
}
También lo envolvemos try-catchy, en caso de IOException, enviamos un estado de respuesta 404 (no encontrado). 4: extraiga la entidad FileInfo adyacente de la base de datos. 5 - usando la clave de la entidad, descargue el archivo 6-8 - devuelva el archivo, agregando el nombre del archivo al encabezado (nuevamente, obtenido de la entidad con información sobre el archivo). Echemos un vistazo más profundo. Interfaz de servicio:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Implementació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);
}
No hay nada particularmente interesante aquí: el método de buscar una entidad por identificación y descargar el archivo, excepto quizás 46: marcamos que tenemos la transacción para leer. Nivel de Dao:
FileInfo findById(Long fileId);
Implementació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 - buscar por identificación usando jdbcTemplatey RowMapper. 8 - 15 - implementación RowMapperpara nuestro caso específico, para comparar datos de la base de datos y los campos del modelo. Vayamos FileManagera ver cómo se carga nuestro archivo:
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();
   }
}
Devolvemos el archivo como un objeto Resourcey buscaremos por clave. 3 - crea Resourcea lo largo del camino + clave. 4 - 8: comprobamos que el archivo en la ruta indicada no esté vacío y lo leemos. Si todo está bien lo devolvemos, y si no, lanzamos una IOException al principio. Comprobemos nuestro método en Postman: Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 7como puede ver, funcionó bien))

Borrar

@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 especial aquí: también devolvemos 404 en caso de falla al usar try-catch. Interfaz de servicio:
void delete(Long fileId) throws IOException;
Implementació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: también revertir los cambios de datos (eliminaciones) cuando ocurre una IOException. 5 - eliminar información sobre el archivo de la base de datos. 6 - elimine el archivo de nuestro "almacenamiento". interfaz dao:
void delete(Long fileId);
Implementació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);
}
Nada de eso, simplemente elimínelo. Eliminar el archivo en sí:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Usamos Postman: Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 8Buscamos en el almacenamiento: Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 9Vacío :) Ahora en la base de datos: Guardar archivos en la aplicación y datos sobre ellos en la base de datos - 10Vemos que todo está bien))

Prueba

Intentemos escribir una prueba para nuestro FileManager. Primero, echemos un vistazo a la estructura de la parte de prueba: mockFile.txt es el archivo con el que probaremos nuestras operaciones con el almacenamiento de archivos. testFileStorage será un reemplazo de nuestro almacenamiento. 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();
   }
Aquí vemos la asignación de datos de prueba. Prueba de guardado de archivos:
@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: utilizando la reflexión de prueba, cambiamos nuestra constante en el servicio para establecer la ruta para guardar el archivo. 5: llame al método que se está probando. 7 - 10 - comprobar la correcta ejecución del guardado. 11 - elimina el archivo guardado (no debemos dejar ningún rastro).
@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();
}
Prueba de carga de archivos: 3 - nuevamente, cambie la ruta de nuestro FileManager. 5 - utilizar el método que se está probando. 7 - 9: comprueba el resultado de la ejecución. Prueba de eliminación de archivos:
@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 - establece la ruta y crea un archivo. 5 - 6 — comprobamos su existencia. 9 - utilizamos un método verificable. 77 - comprobamos que el objeto ya no está allí. Y veamos qué tenemos en términos de dependencias:
<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>
Eso es todo para mí hoy))
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION