JavaRush /Blog Java /Random-FR /Enregistrement des fichiers dans l'application et des don...

Enregistrement des fichiers dans l'application et des données les concernant dans la base de données

Publié dans le groupe Random-FR
Enregistrement des fichiers dans l'application et des données les concernant dans la base de données - 1 Imaginons que vous travaillez sur votre application Web. Dans mes articles, j'examine des pièces individuelles de cette mosaïque, telles que : En quoi ces sujets sont-ils utiles ? Et le fait que ces exemples soient très proches du travail sur des projets réels, et tester ces sujets vous sera très utile. Aujourd'hui, nous allons aborder la prochaine étape de cette mosaïque : travailler avec des fichiers, car de nos jours, vous ne pouvez plus trouver de sites qui n'interagissent pas avec eux (par exemple, toutes sortes de boutiques en ligne, de réseaux sociaux, etc.). La revue sera réalisée en utilisant comme exemple les méthodes upload/download/delete ; nous la sauvegarderons dans un dossier (en ressource) de notre application, afin de ne pas la compliquer. Sauvegarde des fichiers dans l'application et des données les concernant dans la base de données - 2L'idée est que nous allons enregistrer, en plus du fichier lui-même dans notre soi-disant stockage de fichiers, une entité contenant des informations sur notre fichier (taille, nom, etc.) dans notre base de données - une table pré-créée. C'est-à-dire que lors du chargement d'un fichier, cette entité nous sera très utile, et lors de la suppression, nous ne devons en aucun cas l'oublier. Jetons un coup d'œil à ce tableau : Sauvegarde des fichiers dans l'application et des données les concernant dans la base de données - 3Et définissons l'identifiant sur AUTO_INCREMENT comme les adultes, pour générer automatiquement un identifiant au niveau de la base de données. Tout d'abord, jetons un coup d'œil à notre structure : Sauvegarde des fichiers dans l'application et des données les concernant dans la base de données - 4l'entité sous le tableau ci-dessus :
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Télécharger

Regardons le contrôleur :
@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);
       }
   }
Choses intéressantes : 9 - Nous acceptons le dossier sous la forme MultipartFile. Il peut également être reçu sous forme de tableau d'octets, mais j'aime mieux cette option, car nous pouvons MultipartFileextraire diverses propriétés du fichier transféré. 10 - 14 - nous englobons nos actions de try catchsorte que si une exception se produit à un niveau inférieur, nous la transmettons plus haut et envoyons une erreur 400 en réponse. Vient ensuite le niveau de service :
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Regardons la mise en œuvre :
@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 cas d'IOException, toutes nos sauvegardes dans la base de données seront annulées. 11 - nous générons une clé qui sera unique pour le fichier lors de sa sauvegarde (même si deux fichiers portant les mêmes noms sont enregistrés, il n'y aura pas de confusion). 12 — nous construisons une entité à enregistrer dans la base de données. 17 — nous conduisons l'entité avec les informations dans la base de données. 18 - enregistrez le fichier avec un nom haché. 20 — nous renvoyons l'entité créée FileInfo, mais avec l'identifiant généré dans la base de données (cela sera discuté ci-dessous) et la date de création. Méthode de génération d'une clé d'un fichier :
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Ici, nous hachons le nom + la date de création, ce qui garantira notre unicité. Interface de la couche Dao :
public interface FileDAO {

   FileInfo create(FileInfo file);
Sa mise en œuvre :
@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 - créez une date que nous enregistrerons. 12 - 21 - nous sauvegardons l'entité, mais de manière plus complexe, avec la création explicite de l'objet PreparedStatementafin que l'identifiant généré puisse être extrait (il l'extrait non pas avec une requête séparée, mais sous forme de métadonnées de réponse ). 22 - 26 - nous terminons la construction de notre entité qui souffre depuis longtemps et la remettons au sommet (en fait, il ne la termine pas, mais crée un nouvel objet, en remplissant les champs transférés et en copiant le reste de l'original) . Voyons comment nos fichiers seront enregistrés dans 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 — nous acceptons le fichier comme un tableau d'octets et séparément le nom sous lequel il sera enregistré (notre clé générée). 2 - 3 - créez un chemin (et dans le chemin nous écrivons le chemin plus notre clé) et un fichier le long de celui-ci. 6 - 7 - créez un flux et écrivez-y nos octets (et enveloppez tout cela try-finallypour être sûr que le flux se fermera définitivement). Cependant, de nombreuses méthodes peuvent lancer IOException. Dans ce cas, grâce au transfert spécifié dans l'en-tête de la méthode, nous le transmettrons au contrôleur et lui donnerons le statut 400. Testons le tout dans Postman : Sauvegarde des fichiers dans l'application et des données les concernant dans la base de données - 5Comme on peut le voir, tout va bien, la réponse est 201, la réponse JSON est venue sous la forme de notre entité persistante dans la base de données, et si on regarde dans notre stockage : DB : Sauvegarde des fichiers dans l'application et des données les concernant dans la base de données - 6on verra que nous avons une nouvelle valeur. (=*;*=)

Télécharger

Manette:
@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);
   }
}
Nous l'enveloppons également try-catchet en cas d'IOException, nous envoyons un statut de réponse 404 (introuvable). 4 - extrayez l'entité FileInfo adjacente de la base de données. 5 - à l'aide de la clé de l'entité, téléchargez le fichier 6-8 - renvoyez le fichier en ajoutant le nom du fichier à l'en-tête (encore une fois, obtenu de l'entité avec des informations sur le fichier). Examinons plus en profondeur. Interface des services :
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Mise en œuvre:
@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);
}
Il n'y a rien de particulièrement intéressant ici : la méthode de recherche d'une entité par identifiant et de téléchargement du fichier, sauf peut-être 46 - on marque que l'on a la transaction à lire. Niveau Dao :
FileInfo findById(Long fileId);
Mise en œuvre:
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 — recherche par identifiant en utilisant jdbcTemplateet RowMapper. 8 - 15 - implémentation RowMapperpour notre cas spécifique, pour comparer les données de la base de données et les champs du modèle. Allons FileManagervoir comment notre fichier est chargé :
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();
   }
}
Nous renvoyons le fichier en tant qu'objet Resourceet nous rechercherons par clé. 3 - créer Resourcele long du chemin + clé. 4 - 8 — nous vérifions que le fichier au chemin donné n'est pas vide et le lisons. Si tout va bien, nous le renvoyons, et sinon, nous lançons une IOException en haut. Vérifions notre méthode dans Postman : Sauvegarde des fichiers dans l'application et des données les concernant dans la base de données - 7comme vous pouvez le voir, cela a fonctionné correctement))

Supprimer

@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);
   }
}
Rien de spécial ici : on renvoie également 404 en cas d'échec en utilisant try-catch. Interface des services :
void delete(Long fileId) throws IOException;
Mise en œuvre:
@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 - également la restauration des modifications de données (suppressions) lorsqu'une IOException se produit. 5 - supprimez les informations sur le fichier de la base de données. 6 - supprimez le fichier lui-même de notre « stockage ». Interface Dao :
void delete(Long fileId);
Mise en œuvre:
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);
}
Rien de tel - supprimez simplement. Suppression du fichier lui-même :
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
On utilise Postman : Sauvegarde des fichiers dans l'application et des données les concernant dans la base de données - 8On regarde dans le stockage : Sauvegarde des fichiers dans l'application et des données les concernant dans la base de données - 9Vide :) Maintenant dans la base de données : Sauvegarde des fichiers dans l'application et des données les concernant dans la base de données - 10On voit que tout va bien))

Test

Essayons d'écrire un test pour notre FileManager. Voyons d'abord la structure de la partie test : mockFile.txt est le fichier avec lequel nous allons tester nos opérations avec le stockage de fichiers. testFileStorage remplacera notre stockage. 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();
   }
Ici, nous voyons l'affectation des données de test. Test de sauvegarde de fichiers :
@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 — en utilisant la réflexion de test, nous modifions notre constante dans le service pour définir le chemin d'enregistrement du fichier. 5 — appelle la méthode testée. 7 - 10 - vérifier la bonne exécution de la sauvegarde. 11 - supprimez le fichier enregistré (nous ne devons laisser aucune trace).
@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();
}
Test de téléchargement de fichiers : 3 - encore une fois, modifiez le chemin de notre fichier FileManager. 5 - utiliser la méthode testée. 7 - 9 — vérifiez le résultat de l'exécution. Test de suppression de fichier :
@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 - définissez le chemin et créez un fichier. 5 - 6 — nous vérifions son existence. 9 - nous utilisons une méthode vérifiable. 77 - on vérifie que l'objet n'est plus là. Et voyons ce que nous avons en termes de dépendances :
<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>
C'est tout pour moi aujourd'hui))
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION