JavaRush /Java Blog /Random-TL /Pag-save ng mga file sa application at data tungkol sa mg...

Pag-save ng mga file sa application at data tungkol sa mga ito sa database

Nai-publish sa grupo
Сохранение файлов в приложение и данных о них на БД - 1 Isipin natin na nagtatrabaho ka sa iyong web application. Sa aking mga artikulo tinitingnan ko ang mga indibidwal na piraso ng mosaic na ito, tulad ng: Paano kapaki-pakinabang ang mga paksang ito? At ang katotohanan na ang mga halimbawang ito ay napakalapit sa paggawa sa mga tunay na proyekto, at ang pagsubok sa mga paksang ito ay magiging lubhang kapaki-pakinabang para sa iyo. Ngayon ay kukunin natin ang susunod na piraso ng mosaic na ito - nagtatrabaho sa mga file, dahil sa ngayon ay hindi ka na makakahanap ng isang site na hindi nakikipag-ugnayan sa kanila (halimbawa, lahat ng uri ng mga web shop, mga social network, at iba pa). Ang pagsusuri ay isasagawa gamit ang mga paraan ng pag-upload/pag-download/pagtanggal bilang isang halimbawa ; ise-save namin ito sa isang folder (sa mapagkukunan) sa aming aplikasyon, upang hindi ito kumplikado. Сохранение файлов в приложение и данных о них на БД - 2Ang ideya ay ise-save namin, bilang karagdagan sa file mismo sa aming tinatawag na file storage, isang entity na may impormasyon tungkol sa aming file (laki, pangalan, atbp.) sa aming database - isang pre-created na talahanayan. Iyon ay, kapag naglo-load ng isang file, ang entity na ito ay magiging kapaki-pakinabang sa amin, at kapag nagtanggal, hindi namin dapat kalimutan ang tungkol dito sa anumang paraan. Tingnan natin ang talahanayang ito: Сохранение файлов в приложение и данных о них на БД - 3At itakda natin ang id sa AUTO_INCREMENT tulad ng mga nasa hustong gulang, upang awtomatikong bumuo ng isang identifier sa antas ng database. Una, tingnan natin ang aming istraktura: Сохранение файлов в приложение и данных о них на БД - 4Ang entity sa ilalim ng talahanayan na ipinapakita sa itaas:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Mag-upload

Tingnan natin ang 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);
       }
   }
Mga kawili-wiling bagay: 9 - Tinatanggap namin ang file sa form MultipartFile. Maaari din itong matanggap bilang isang hanay ng mga byte, ngunit mas gusto ko ang opsyong ito, dahil maaari nating MultipartFilekunin ang iba't ibang katangian ng inilipat na file. 10 - 14 - binabalot namin ang aming mga aksyon try catchupang kung may maganap na pagbubukod sa mas mababang antas, ipinapasa namin ito nang mas mataas at nagpapadala ng 400 na error bilang tugon. Susunod ay ang antas ng serbisyo:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Tingnan natin ang pagpapatupad:
@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 - sa kaso ng isang IOException, ang lahat ng aming mga pag-save sa database ay ibabalik. 11 - bumubuo kami ng isang susi na magiging kakaiba para sa file kapag na-save ito (kahit na naka-save ang dalawang file na may parehong mga pangalan, hindi magkakaroon ng kalituhan). 12 — bumuo kami ng isang entity upang i-save sa database. 17 — hinihimok namin ang entity na may impormasyon sa database. 18 - i-save ang file na may hashed na pangalan. 20 — ibinabalik namin ang nilikhang entity FileInfo, ngunit kasama ang nabuong id sa database (pag-uusapan natin ito sa ibaba) at ang petsa ng paglikha. Paraan para sa pagbuo ng isang susi sa isang file:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Dito namin hash ang pangalan + petsa ng paglikha, na magtitiyak sa aming pagiging natatangi. Dao layer interface:
public interface FileDAO {

   FileInfo create(FileInfo file);
Pagpapatupad nito:
@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 — создаём date которую и сохраним. 12 - 21 — сохраняем сущность, но более сложным путем, с явным созданием an object PreparedStatement, чтобы можно было вытащить сгенерированный id (он его вытягивает не отдельным requestом, а в виде ответных метаданных). 22 - 26 — достраиваем нашу многострадальную сущность и отдаем наверх (на самом деле он его не достраивает, а создаёт новый an object, заполняя переданные поля и копируя остальные с изначального). Давайте посмотрим, How будут сохраняться наши файлы в 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 — принимаем файл в виде массива byteов и отдельно Name, под которым он будет сохранен (наш сгенерированный ключ). 2 - 3 — создаем путь (а в пути прописываем путь плюс наш ключ) и файл по нему. 6 - 7 — создаем поток и пишем туда наши byteы (и оборачиваем это все добро в try-finally чтобы быть уверенными, что поток точно закроется). Тем не менее, многие из методов могут нам выкинуть IOException. В таком случае, благодаря прописанной в шапке метода проброске, мы прокинем его в контроллер и отдадим 400 статус. Давайте протестируем все это дело в Postman: Сохранение файлов в приложение и данных о них на БД - 5Как видим, все отлично, ответ 201, ответный JSON пришел в виде нашей сохраняемой сущности в БД, и если заглянем в наше хранorще: БД: Сохранение файлов в приложение и данных о них на БД - 6увидим, что у нас появилось новое meaning. (=*;*=)

Download

Controller:
@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);
   }
}
Так же оборачиваем в try-catch и в случае IOException отправляем 404 статус ответа (не найдено). 4 — вытягиваем из БД прилежащую сущность FileInfo. 5 — по ключу из сущности скачиваем файл 6-8 — отправляем назад файл, при этом добавив в хедер file name (опять же, полученное из сущности с информацией о файле). Давайте посмотрим глубже. Интерфейс сервиса:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Реализация:
@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);
}
Тут особо интересного ничего нет: метод поиска сущности по id и загрузка file, разве что 46 — помечаем, что транзакция у нас для чтения. Уровень dao:
FileInfo findById(Long fileId);
Имплементация:
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 — поиск по id c использованием jdbcTemplate и RowMapper. 8 - 15 — реализация RowMapper для нашего конкретного случая, для сопоставления данных из БД и полей модели. Идем в FileManager и смотрим, How загружается наш файл:
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();
   }
}
Возвращаем файл в виде an object Resource, а искать будем по ключу. 3 — создаем Resource по пути + ключ. 4 - 8 — проверяем, что файл по заданному пути не пуст и читаем. Если всё ОК, возвращаем его, а если нет, прокидываем IOException наверх. Проверяем наш метод в Postman: Сохранение файлов в приложение и данных о них на БД - 7Как видим, он отработал на ОК))

Delete

@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);
   }
}
Тут ничего особенного: также возвращаем 404 в случае неудачи с помощью try-catch. Интерфейс сервиса:
void delete(Long fileId) throws IOException;
Implementation:
@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 — также откат изменения данных (удаления) при падении IOException. 5 — удаляем информацию о файле из БД. 6 — удаляем сам файл из нашего “хранorща”. Интерфейс dao:
void delete(Long fileId);
Реализация:
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);
}
Ничего такого — просто delete. Удаление самого file:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Юзаем в Postman: Сохранение файлов в приложение и данных о них на БД - 8Смотрим в хранorще: Сохранение файлов в приложение и данных о них на БД - 9Пусто :) Теперь в БД: Сохранение файлов в приложение и данных о них на БД - 10Видим, что все good))

Test

Давайте попробуем написать тест под наш FileManager. Для начала взглянем на структуру тестовой части: mockFile.txt — это файл, с помощью которого мы будем тестить наши операции с file storage. testFileStorage будет заменой нашего хранorща. 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();
   }
Здесь мы видим задание тестовых данных. Тест сохранения 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 — с помощью тестовой рефлексии меняем нашу константу в сервисе для задания пути сохранения file. 5 — вызываем проверяемый метод. 7 - 10 — проверяем правильность исполнения сохранения. 11 — удаляем сохраненный файл (мы не должны оставить ниHowих следов).
@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();
}
Тест загрузки file: 3 — опять же, меняем путь для нашего FileManager. 5 — юзаем проверяемый метод. 7 - 9 — проверяем результат исполнения. Тест удаления 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 — задаем путь и создаем файл. 5 - 6 — проверяем его существование. 9 — используем проверяемый метод. 77 — проверяем, что обьекта уже нет. И смотрим, что там у нас по зависимостям:
<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>
На этом у меня сегодня всё))
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION