JavaRush /Blog Java /Random-PL /Zapisywanie plików do aplikacji oraz danych o nich do baz...

Zapisywanie plików do aplikacji oraz danych o nich do bazy danych

Opublikowano w grupie Random-PL
Zapisywanie plików do aplikacji i danych o nich do bazy danych - 1 Wyobraźmy sobie, że pracujesz nad aplikacją internetową. W swoich artykułach przyglądam się poszczególnym fragmentom tej mozaiki, takim jak: W jaki sposób te tematy są przydatne? A fakt, że te przykłady są bardzo zbliżone do pracy nad prawdziwymi projektami i testowanie tych tematów będzie dla Ciebie bardzo przydatny. Dzisiaj zajmiemy się kolejnym elementem tej mozaiki - pracą z plikami, ponieważ w dzisiejszych czasach nie można już znaleźć witryny, która nie wchodzi z nimi w interakcję (na przykład wszelkiego rodzaju sklepy internetowe, sieci społecznościowe itp.). Recenzja zostanie przeprowadzona przykładowo metodą upload/download/delete ; zapiszemy ją w folderze (w zasobie) w naszej aplikacji, żeby nie komplikować. Zapisywanie plików do aplikacji i danych o nich do bazy danych - 2Pomysł jest taki, że oprócz samego pliku w naszym tzw. magazynie plików zapiszemy w naszej bazie danych także encję zawierającą informacje o naszym pliku (rozmiar, nazwę itp.) – wcześniej utworzoną tabelę. Oznacza to, że podczas ładowania pliku podmiot ten będzie nam bardzo przydatny, a podczas usuwania nie możemy w żaden sposób o nim zapomnieć. Spójrzmy na tę tabelę: Zapisywanie plików do aplikacji i danych o nich do bazy danych - 3I ustawmy identyfikator na AUTO_INCREMENT jak dorośli, aby automatycznie wygenerować identyfikator na poziomie bazy danych. Na początek przyjrzyjmy się naszej strukturze: Zapisywanie plików do aplikacji i danych o nich do bazy danych - 4Podmiot pod tabelą pokazaną powyżej:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Wgrywać

Spójrzmy na kontroler:
@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);
       }
   }
Kilka ciekawostek: 9 - Akceptujemy plik w formacie MultipartFile. Można go również otrzymać jako tablicę bajtów, ale ta opcja bardziej mi się podoba, ponieważ możemy MultipartFilewyodrębnić różne właściwości przesyłanego pliku. 10 - 14 - zawijamy nasze działania try catchtak, aby w przypadku wystąpienia wyjątku na niższym poziomie, przekazaliśmy go wyżej i w odpowiedzi wysłaliśmy błąd 400. Następny jest poziom usług:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Przyjrzyjmy się implementacji:
@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 - w przypadku wyjątku IOException wszystkie nasze zapisy w bazie zostaną wycofane. 11 - generujemy klucz, który będzie unikalny dla pliku podczas jego zapisywania (nawet jeśli zostaną zapisane dwa pliki o takich samych nazwach, nie będzie żadnych nieporozumień). 12 — budujemy encję do zapisania w bazie danych. 17 — wprowadzamy podmiot z informacją do bazy danych. 18 - zapisz plik z zahaszowaną nazwą. 20 — zwracamy utworzoną jednostkę FileInfo, ale z wygenerowanym identyfikatorem w bazie (o tym będzie mowa poniżej) i datą utworzenia. Metoda generowania klucza do pliku:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Tutaj haszujemy nazwę + datę powstania, co zapewni nam niepowtarzalność. Interfejs warstwy Dao:
public interface FileDAO {

   FileInfo create(FileInfo file);
Jego wdrożenie:
@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 - stwórz datę, którą zapiszemy. 12 - 21 - zapisujemy encję, ale w bardziej złożony sposób, poprzez jawne utworzenie obiektu, PreparedStatementaby można było wyciągnąć wygenerowany identyfikator (wyciąga go nie osobnym żądaniem, ale w postaci metadanych odpowiedzi ). 22 - 26 - kończymy budowę naszej cierpliwej istoty i oddajemy ją na górę (w rzeczywistości on jej nie kończy, ale tworzy nowy obiekt, wypełniając przeniesione pola i kopiując resztę z pierwotnego) . Zobaczmy jak nasze pliki zostaną zapisane w 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 — akceptujemy plik jako tablicę bajtów i osobno nazwę pod jaką zostanie zapisany (nasz wygenerowany klucz). 2 - 3 - utwórz ścieżkę (w ścieżce zapisujemy ścieżkę i nasz klucz) i wzdłuż niej plik. 6 - 7 - utwórz strumień i zapisz tam nasze bajty (i zawiń to wszystko, try-finallyaby mieć pewność, że strumień na pewno się zamknie). Jednak wiele metod może zgłosić wyjątek IOException. W tym wypadku dzięki przekazaniu określonemu w nagłówku metody przekażemy je do kontrolera i nadamy status 400. Przetestujmy całość w Postmanie: Zapisywanie plików do aplikacji i danych o nich do bazy danych - 5Jak widać, wszystko jest w porządku, odpowiedź to 201, odpowiedź JSON przyszła w postaci naszej utrwalonej jednostki w bazie danych i jeśli zajrzymy do naszej pamięci: DB: Zapisywanie plików do aplikacji i danych o nich do bazy danych - 6widzimy to mamy nową wartość. (=*;*=)

Pobierać

Kontroler:
@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);
   }
}
Zawijamy go również try-catchi w przypadku wyjątku IOException wysyłamy status odpowiedzi 404 (nie znaleziono). 4 - wyciągnij sąsiednią jednostkę FileInfo z bazy danych. 5 - za pomocą klucza od podmiotu pobierz plik 6-8 - odeślij plik, dodając w nagłówku nazwę pliku (ponownie uzyskaną od podmiotu z informacją o pliku). Przyjrzyjmy się głębiej. Interfejs usługi:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Realizacja:
@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);
}
Nie ma tu nic szczególnie ciekawego: sposób wyszukania podmiotu po id i pobrania pliku, może poza 46 - zaznaczamy, że mamy transakcję do odczytu. Poziom Tao:
FileInfo findById(Long fileId);
Realizacja:
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 — szukaj po identyfikatorze za pomocą jdbcTemplatei RowMapper. 8 - 15 - implementacja RowMapperdla naszego konkretnego przypadku, do porównywania danych z bazy danych i pól modelu. Chodźmy FileManageri zobaczmy jak ładuje się nasz plik:
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();
   }
}
Zwracamy plik jako obiekt Resourcei będziemy szukać według klucza. 3 - utwórz Resourcewzdłuż ścieżki + klawisz. 4 - 8 — sprawdzamy, czy plik w podanej ścieżce nie jest pusty i czytamy go. Jeśli wszystko jest w porządku, zwracamy to, a jeśli nie, rzucamy na górę wyjątek IOException. Sprawdźmy naszą metodę w Postmanie: Zapisywanie plików do aplikacji i danych o nich do bazy danych - 7Jak widać zadziałała OK))

Usuwać

@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);
   }
}
Nie ma tu nic specjalnego: zwracamy również 404 w przypadku niepowodzenia przy użyciu try-catch. Interfejs usługi:
void delete(Long fileId) throws IOException;
Realizacja:
@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 - także wycofywanie zmian danych (usunięć) w przypadku wystąpienia wyjątku IOException. 5 - usuń informację o pliku z bazy danych. 6 - usuń sam plik z naszego „magazynu”. interfejs dao:
void delete(Long fileId);
Realizacja:
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);
}
Nic takiego - po prostu usuń. Usuwanie samego pliku:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Używamy tego w Postmanie: Zapisywanie plików do aplikacji i danych o nich do bazy danych - 8Zaglądamy do magazynu: Zapisywanie plików do aplikacji i danych o nich do bazy danych - 9Pusty :) Teraz w bazie danych: Сохранение файлов в приложение и данных о них на БД - 10Widzimy, że wszystko jest w porządku))

Test

Spróbujmy napisać test dla naszego FileManager. Na początek przyjrzyjmy się strukturze części testowej: mockFile.txt to plik, za pomocą którego przetestujemy nasze działania z przechowywaniem plików. testFileStorage zastąpi naszą pamięć masową. 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();
   }
Tutaj widzimy przypisanie danych testowych. Test zapisywania pliku:
@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 — korzystając z refleksji testowej, zmieniamy naszą stałą w serwisie, aby ustawić ścieżkę zapisu pliku. 5 — wywołaj testowaną metodę. 7 - 10 — sprawdzamy poprawność wykonania zapisu. 11 - usuń zapisany plik (nie powinniśmy zostawiać żadnych śladów).
@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 przesyłania pliku: 3 - ponownie zmień ścieżkę do naszego pliku FileManager. 5 - użyj testowanej metody. 7 - 9 — sprawdź wynik wykonania. Test usuwania plików:
@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 - ustaw ścieżkę i utwórz plik. 5 - 6 - sprawdzamy jego istnienie. 9 - stosujemy metodę weryfikowalną. 77 - sprawdzamy, czy obiektu już nie ma. Zobaczmy, co mamy pod względem zależności:
<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>
To wszystko dla mnie dzisiaj))
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION