JavaRush /Java Blog /Random-IT /Salvataggio di file nell'applicazione e dati su di essi n...

Salvataggio di file nell'applicazione e dati su di essi nel database

Pubblicato nel gruppo Random-IT
Salvataggio di file nell'applicazione e relativi dati nel database - 1 Immaginiamo che tu stia lavorando sulla tua applicazione web. Nei miei articoli guardo i singoli tasselli di questo mosaico, come ad esempio: In che modo sono utili questi argomenti? E il fatto che questi esempi siano molto vicini al lavoro su progetti reali e testare questi argomenti ti sarà molto utile. Oggi prenderemo il prossimo pezzo di questo mosaico: lavorare con i file, poiché oggigiorno non è più possibile trovare un sito che non interagisca con essi (ad esempio, tutti i tipi di negozi web, social network e così via). La revisione verrà effettuata utilizzando come esempio le modalità upload/download/cancella ; la salveremo in una cartella (in risorsa) nella nostra applicazione, per non complicarla. Salvataggio di file nell'applicazione e relativi dati nel database - 2L'idea è che salveremo, oltre al file stesso nel nostro cosiddetto file storage, un'entità con le informazioni sul nostro file (dimensione, nome, ecc.) nel nostro database: una tabella precreata. Cioè, quando carichiamo un file, questa entità ci sarà molto utile e, durante l'eliminazione, non dobbiamo dimenticarcene in alcun modo. Diamo un'occhiata a questa tabella: Salvataggio di file nell'applicazione e relativi dati nel database - 3E impostiamo l'id su AUTO_INCREMENT come gli adulti, per generare automaticamente un identificatore a livello di database. Per prima cosa, diamo un'occhiata alla nostra struttura: Salvataggio di file nell'applicazione e relativi dati nel database - 4L'entità sotto la tabella mostrata sopra:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Caricamento

Diamo un'occhiata al controller:
@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);
       }
   }
Alcune cose interessanti: 9 - Accettiamo il file in formato MultipartFile. Può anche essere ricevuto come un array di byte, ma preferisco questa opzione, poiché possiamo MultipartFileestrarre varie proprietà del file trasferito. 10 - 14 - racchiudiamo le nostre azioni in try catchmodo che se si verifica un'eccezione a un livello inferiore, la inoltreremo a un livello superiore e invieremo un errore 400 come risposta. Il prossimo è il livello di servizio:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Diamo un'occhiata all'implementazione:
@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 - in caso di IOException, tutti i nostri salvataggi nel database verranno ripristinati. 11 - generiamo una chiave che sarà univoca per il file al momento del salvataggio (anche se vengono salvati due file con lo stesso nome non ci sarà confusione). 12 — costruiamo un'entità da salvare nel database. 17: inseriamo l'entità con le informazioni nel database. 18 - salva il file con un nome con hash. 20 — restituiamo l'entità creata FileInfo, ma con l'ID generato nel database (questo sarà discusso di seguito) e la data di creazione. Metodo per generare una chiave per un file:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Qui cancelliamo il nome + la data di creazione, che garantirà la nostra unicità. Interfaccia del livello Dao:
public interface FileDAO {

   FileInfo create(FileInfo file);
La sua implementazione:
@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 - creare una data che salveremo. 12 - 21 - salviamo l'entità, ma in modo più complesso, con la creazione esplicita dell'oggetto PreparedStatementin modo che l'id generato possa essere estratto (lo estrae non con una richiesta separata, ma sotto forma di metadati di risposta ). 22 - 26 - completiamo la costruzione della nostra entità longanime e la riportiamo in alto (infatti non la completa, ma crea un nuovo oggetto, compilando i campi trasferiti e copiando il resto da quello originale) . Vediamo come verranno salvati i nostri file in 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 — accettiamo il file come un array di byte e separatamente il nome con cui verrà salvato (la nostra chiave generata). 2 - 3 - creiamo un percorso (e nel percorso scriviamo il percorso più la nostra chiave) e un file lungo di esso. 6 - 7 - crea uno stream e scrivi lì i nostri byte (e aggiungi tutto questo try-finallyper essere sicuro che lo stream si chiuda definitivamente). Tuttavia, molti metodi possono generare IOException. In questo caso, grazie all'inoltro specificato nell'intestazione del metodo, lo passeremo al controller e gli daremo lo stato 400. Testiamo il tutto in Postman: Salvataggio di file nell'applicazione e relativi dati nel database - 5come possiamo vedere, va tutto bene, la risposta è 201, la risposta JSON è arrivata sotto forma della nostra entità persistente nel database e se esaminiamo il nostro archivio: DB: Salvataggio di file nell'applicazione e relativi dati nel database - 6vedremo che abbiamo un nuovo valore. (=*;*=)

Scaricamento

Controllore:
@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);
   }
}
Lo avvolgiamo anche try-catche in caso di IOException inviamo uno stato di risposta 404 (non trovato). 4 - estrarre l'entità FileInfo adiacente dal database. 5 - utilizzando la chiave dell'entità, scaricare il file 6-8 - rispedire il file, aggiungendo nell'intestazione il nome del file (sempre ottenuto dall'entità con le informazioni sul file). Diamo uno sguardo più approfondito. Interfaccia di servizio:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Implementazione:
@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);
}
Non c'è nulla di particolarmente interessante qui: il metodo per cercare un'entità tramite ID e scaricare il file, tranne forse 46: segniamo che abbiamo una transazione per la lettura. Livello Dao:
FileInfo findById(Long fileId);
Implementazione:
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: cerca per ID utilizzando jdbcTemplatee RowMapper. 8 - 15 - implementazione RowMapperper il nostro caso specifico, per il confronto dei dati del database e dei campi del modello. Andiamo FileManagera vedere come viene caricato il nostro file:
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();
   }
}
Restituiamo il file come oggetto Resourcee cercheremo per chiave. 3 - crea Resourcelungo il percorso + chiave. 4 - 8 — controlliamo che il file nel percorso indicato non sia vuoto e lo leggiamo. Se tutto è a posto, lo restituiamo e, in caso contrario, lanciamo una IOException all'inizio. Controlliamo il nostro metodo in Postman: Salvataggio di file nell'applicazione e relativi dati nel database - 7come puoi vedere, ha funzionato bene))

Eliminare

@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);
   }
}
Niente di speciale qui: restituiamo anche 404 in caso di errore utilizzando try-catch. Interfaccia di servizio:
void delete(Long fileId) throws IOException;
Implementazione:
@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 - anche rollback delle modifiche ai dati (eliminazioni) quando si verifica una IOException. 5 - eliminare le informazioni sul file dal database. 6 - eliminare il file stesso dalla nostra “memoria”. interfaccia dao:
void delete(Long fileId);
Implementazione:
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);
}
Niente del genere: basta eliminare. Eliminazione del file stesso:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Usiamo Postino: Salvataggio di file nell'applicazione e dati su di essi nel database - 8guardiamo nell'archivio: Salvataggio di file nell'applicazione e relativi dati nel database - 9vuoto :) Ora nel database: Salvataggio di file nell'applicazione e relativi dati nel database - 10vediamo che va tutto bene))

Test

Proviamo a scrivere un test per il nostro file FileManager. Per prima cosa diamo un'occhiata alla struttura della parte di test: mockFile.txt è il file con cui testeremo le nostre operazioni con l'archiviazione dei file. testFileStorage sostituirà il nostro spazio di archiviazione. 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();
   }
Qui vediamo l'assegnazione dei dati del test. Prova di salvataggio del file:
@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 — utilizzando la riflessione del test, modifichiamo la nostra costante nel servizio per impostare il percorso per salvare il file. 5: chiama il metodo da testare. 7 - 10 - controlla la corretta esecuzione del salvataggio. 11 - cancella il file salvato (non dobbiamo lasciare tracce).
@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 di caricamento file: 3 - ancora una volta, cambia il percorso per il nostro file FileManager. 5 - utilizzare il metodo in fase di sperimentazione. 7 - 9: controlla il risultato dell'esecuzione. Prova di eliminazione file:
@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 - imposta il percorso e crea un file. 5 - 6 — ne controlliamo l'esistenza. 9 - utilizziamo un metodo verificabile. 77 - controlliamo che l'oggetto non ci sia più. E vediamo cosa abbiamo in termini di dipendenze:
<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>
Per me oggi è tutto))
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION