JavaRush /Java-Blog /Random-DE /Speichern von Dateien in der Anwendung und Daten darüber ...

Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank

Veröffentlicht in der Gruppe Random-DE
Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank – 1 Stellen wir uns vor, Sie arbeiten an Ihrer Webanwendung. In meinen Artikeln betrachte ich einzelne Teile dieses Mosaiks, wie zum Beispiel: Wie nützlich sind diese Themen? Und die Tatsache, dass diese Beispiele der Arbeit an realen Projekten sehr nahe kommen, und das Testen dieser Themen wird für Sie sehr nützlich sein. Heute widmen wir uns dem nächsten Mosaiksteinchen – der Arbeit mit Dateien, da es heutzutage keine Website mehr gibt, die nicht mit ihnen interagiert (z. B. alle Arten von Webshops, sozialen Netzwerken usw.). Die Überprüfung wird am Beispiel der Methoden zum Hochladen/Herunterladen/Löschen durchgeführt ; wir werden sie in einem Ordner (in der Ressource) unserer Anwendung speichern, um sie nicht zu verkomplizieren. Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank – 2Die Idee ist, dass wir zusätzlich zur Datei selbst in unserem sogenannten Dateispeicher eine Entität mit Informationen zu unserer Datei (Größe, Name usw.) in unserer Datenbank speichern – eine vorab erstellte Tabelle. Das heißt, beim Laden einer Datei ist diese Entität für uns sehr nützlich, und beim Löschen dürfen wir sie auf keinen Fall vergessen. Werfen wir einen Blick auf diese Tabelle: Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank – 3Und setzen wir die ID wie Erwachsene auf AUTO_INCREMENT, um automatisch eine Kennung auf Datenbankebene zu generieren. Werfen wir zunächst einen Blick auf unsere Struktur: Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank – 4Die Entität unter der oben gezeigten Tabelle:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Hochladen

Schauen wir uns den Controller an:
@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);
       }
   }
Einige interessante Dinge: 9 - Wir akzeptieren die Datei im Formular MultipartFile. Es kann auch als Array von Bytes empfangen werden, aber diese Option gefällt mir besser, da wir MultipartFileverschiedene Eigenschaften der übertragenen Datei extrahieren können. 10 – 14 – Wir packen unsere Aktionen try catchso ein, dass wir, wenn eine Ausnahme auf einer niedrigeren Ebene auftritt, sie nach oben weiterleiten und als Antwort einen 400-Fehler senden. Als nächstes kommt der Servicelevel:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Schauen wir uns die Implementierung an:
@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 – Im Falle einer IOException werden alle unsere Speicherungen in der Datenbank zurückgesetzt. 11 – Wir generieren einen Schlüssel, der für die Datei beim Speichern eindeutig ist (auch wenn zwei Dateien mit demselben Namen gespeichert werden, kommt es nicht zu Verwechslungen). 12 – Wir erstellen eine Entität, um sie in der Datenbank zu speichern. 17 – Wir übertragen die Entität mit den Informationen in die Datenbank. 18 – Speichern Sie die Datei unter einem gehashten Namen. 20 – Wir geben die erstellte Entität zurück FileInfo, jedoch mit der generierten ID in der Datenbank (dies wird weiter unten besprochen) und dem Erstellungsdatum. Methode zum Generieren eines Schlüssels zu einer Datei:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Hier hashen wir den Namen und das Erstellungsdatum, um unsere Einzigartigkeit sicherzustellen. Dao-Layer-Schnittstelle:
public interface FileDAO {

   FileInfo create(FileInfo file);
Seine Umsetzung:
@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 - Erstellen Sie ein Datum, das wir speichern. 12 - 21 - Wir speichern die Entität, jedoch auf komplexere Weise, mit der expliziten Erstellung des Objekts, PreparedStatementdamit die generierte ID abgerufen werden kann (sie wird nicht mit einer separaten Anfrage, sondern in Form von Antwortmetadaten abgerufen). ). 22 - 26 - Wir schließen den Aufbau unseres leidgeprüften Wesens ab und geben es an die Spitze (tatsächlich vollendet er es nicht, sondern erstellt ein neues Objekt, füllt die übertragenen Felder aus und kopiert den Rest vom Original) . Mal sehen, wie unsere Dateien gespeichert werden 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 – Wir akzeptieren die Datei als Array von Bytes und separat den Namen, unter dem sie gespeichert wird (unseren generierten Schlüssel). 2 - 3 - Erstellen Sie einen Pfad (und in den Pfad schreiben wir den Pfad plus unseren Schlüssel) und eine Datei darauf. 6 - 7 - Erstellen Sie einen Stream und schreiben Sie unsere Bytes dort (und packen Sie all diese Dinge ein, try-finallyum sicherzustellen, dass der Stream definitiv geschlossen wird). Viele der Methoden können jedoch eine IOException auslösen. In diesem Fall übergeben wir es dank der im Methodenheader angegebenen Weiterleitung an den Controller und geben den Status 400 an. Testen wir das Ganze in Postman: Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank – 5Wie Sie sehen, ist alles in Ordnung, die Antwort ist 201, die Antwort JSON kam in Form unserer persistenten Entität in der Datenbank, und wenn wir in unseren Speicher schauen: DB: Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank – 6sehen wir das Wir haben einen neuen Wert. (=*;*=)

Herunterladen

Regler:
@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);
   }
}
Wir packen es auch ein try-catchund senden im Falle einer IOException einen 404-Antwortstatus (nicht gefunden). 4 – Ziehen Sie die angrenzende FileInfo-Entität aus der Datenbank heraus. 5 – Laden Sie die Datei mithilfe des Schlüssels von der Entität herunter. 6-8 – Senden Sie die Datei zurück und fügen Sie den Dateinamen zum Header hinzu (wiederum von der Entität mit Informationen über die Datei erhalten). Werfen wir einen genaueren Blick. Serviceschnittstelle:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Implementierung:
@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);
}
Hier gibt es nichts besonders Interessantes: die Methode, eine Entität anhand der ID zu suchen und die Datei herunterzuladen, außer vielleicht 46 – wir markieren, dass wir die Transaktion zum Lesen haben. Dao-Level:
FileInfo findById(Long fileId);
Implementierung:
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 – Suche nach ID mit jdbcTemplateund RowMapper. 8 - 15 - Implementierung RowMapperfür unseren speziellen Fall zum Vergleich von Daten aus den Datenbank- und Modellfeldern. Schauen wir mal FileManager, wie unsere Datei geladen wird:
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();
   }
}
Wir geben die Datei als Objekt zurück Resourceund suchen nach Schlüssel. 3 - ResourceEntlang des Pfades + Schlüssel erstellen. 4 - 8 – Wir prüfen, ob die Datei im angegebenen Pfad nicht leer ist, und lesen sie. Wenn alles in Ordnung ist, geben wir es zurück, und wenn nicht, werfen wir eine IOException nach oben. Schauen wir uns unsere Methode in Postman an: Dateien in der Anwendung und Daten darüber in der Datenbank speichern – 7Wie Sie sehen, hat es gut funktioniert))

Löschen

@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);
   }
}
Hier gibt es nichts Besonderes: Wir geben auch 404 zurück, wenn ein Fehler auftritt try-catch. Serviceschnittstelle:
void delete(Long fileId) throws IOException;
Implementierung:
@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 – auch Rollback von Datenänderungen (Löschungen), wenn eine IOException auftritt. 5 – Informationen über die Datei aus der Datenbank löschen. 6 - Löschen Sie die Datei selbst aus unserem „Speicher“. Dao-Schnittstelle:
void delete(Long fileId);
Implementierung:
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);
}
Nichts dergleichen – einfach löschen. Die Datei selbst löschen:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Wir verwenden Postman: Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank – 8Wir schauen in den Speicher: Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank – 9Leer :) Jetzt in der Datenbank: Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank – 10Wir sehen, dass alles in Ordnung ist))

Prüfen

Versuchen wir, einen Test für unsere zu schreiben FileManager. Werfen wir zunächst einen Blick auf die Struktur des Testteils: „mockFile.txt“ ist die Datei, mit der wir unsere Vorgänge mit der Dateispeicherung testen werden. testFileStorage wird ein Ersatz für unseren Speicher sein. 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();
   }
Hier sehen wir die Testdatenzuordnung. Dateispeichertest:
@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 – Mithilfe der Testreflexion ändern wir unsere Konstante im Dienst, um den Pfad zum Speichern der Datei festzulegen. 5 – Rufen Sie die zu testende Methode auf. 7 - 10 - Überprüfen Sie die korrekte Ausführung des Speichervorgangs. 11 - Löschen Sie die gespeicherte Datei (wir sollten keine Spuren hinterlassen).
@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();
}
Datei-Upload-Test: 3 – Ändern Sie erneut den Pfad für unsere FileManager. 5 – Verwenden Sie die getestete Methode. 7 - 9 – Überprüfen Sie das Ausführungsergebnis. Test zum Löschen von Dateien:
@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 - Legen Sie den Pfad fest und erstellen Sie eine Datei. 5 - 6 – wir prüfen seine Existenz. 9 - Wir verwenden eine überprüfbare Methode. 77 - Wir prüfen, ob das Objekt nicht mehr vorhanden ist. Und sehen wir uns an, welche Abhängigkeiten wir haben:
<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>
Das ist alles für mich heute))
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION