JavaRush /Java Blog /Random-KO /파일을 애플리케이션에 저장하고 그에 관한 데이터를 데이터베이스에 저장

파일을 애플리케이션에 저장하고 그에 관한 데이터를 데이터베이스에 저장

Random-KO 그룹에 게시되었습니다
파일을 애플리케이션에 저장하고 그에 관한 데이터를 데이터베이스에 저장 - 1 당신이 웹 애플리케이션 작업을 하고 있다고 상상해 보자. 내 기사에서는 다음과 같은 모자이크의 개별 조각을 살펴봅니다. 이 주제는 어떻게 유용합니까? 그리고 이러한 예제가 실제 프로젝트 작업과 매우 유사하다는 사실과 이러한 주제를 테스트하는 것은 여러분에게 매우 유용할 것입니다. 오늘 우리는 이 모자이크의 다음 부분을 다룰 것입니다. 즉, 파일 작업입니다. 요즘에는 파일과 상호 작용하지 않는 사이트(예: 모든 종류의 웹 상점, 소셜 네트워크 등)를 더 이상 찾을 수 없기 때문입니다. 검토는 업로드/다운로드/삭제 방법을 예로 사용하여 수행되며 복잡하지 않도록 애플리케이션의 폴더(리소스)에 저장합니다. 파일을 애플리케이션에 저장하고 그에 관한 데이터를 데이터베이스에 저장 - 2아이디어는 소위 파일 저장소에 있는 파일 자체 외에도 파일에 대한 정보(크기, 이름 등)가 포함된 엔터티를 데이터베이스(미리 생성된 테이블)에 저장한다는 것입니다. 즉, 파일을 로드할 때 이 엔터티는 우리에게 매우 유용할 것이며, 삭제할 때 어떤 식으로든 잊어서는 안 됩니다. 이 테이블을 살펴보겠습니다. 파일을 애플리케이션에 저장하고 해당 데이터를 데이터베이스에 저장 - 3그리고 성인처럼 ID를 AUTO_INCREMENT로 설정하여 데이터베이스 수준에서 자동으로 식별자를 생성해 보겠습니다. 먼저 구조를 살펴보겠습니다. 파일을 애플리케이션에 저장하고 해당 데이터를 데이터베이스에 저장 - 4위에 표시된 테이블 아래의 엔터티는 다음과 같습니다.
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

업로드

컨트롤러를 살펴보겠습니다:
@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);
       }
   }
흥미로운 점: 9 - 우리는 형식으로 파일을 받아들입니다 MultipartFile. 바이트 배열로 수신할 수도 있지만 MultipartFile전송된 파일의 다양한 속성을 추출할 수 있기 때문에 이 옵션이 더 마음에 듭니다. 10 - 14 - try catch낮은 수준에서 예외가 발생하면 더 높은 수준으로 전달하고 응답으로 400 오류를 보낼 수 있도록 작업을 래핑합니다. 다음은 서비스 수준입니다.
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
구현을 살펴보겠습니다.
@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 - IOException이 발생하면 데이터베이스에 저장된 모든 내용이 롤백됩니다. 11 - 파일이 저장될 때 해당 파일에 대해 고유한 키를 생성합니다(동일한 이름을 가진 두 개의 파일이 저장되더라도 혼동이 발생하지 않습니다). 12 — 데이터베이스에 저장할 엔터티를 만듭니다. 17 — 정보가 포함된 엔터티를 데이터베이스로 이동합니다. 18 - 해시된 이름으로 파일을 저장합니다. 20 — 생성된 엔터티를 반환 FileInfo하지만 데이터베이스에 생성된 ID(이 내용은 아래에서 설명함)와 생성 날짜를 반환합니다. 파일에 대한 키를 생성하는 방법:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
여기서는 이름 + 생성 날짜를 해시하여 고유성을 보장합니다. Dao 레이어 인터페이스:
public interface FileDAO {

   FileInfo create(FileInfo file);
구현:
@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 - 저장할 날짜를 만듭니다. 12 - 21 - 엔터티를 저장하지만 PreparedStatement생성된 ID를 가져올 수 있도록 개체를 명시적으로 생성하여 더 복잡한 방법으로 저장합니다(별도의 요청이 아닌 응답 메타데이터 형식으로 가져옵니다). ). 22 - 26 - 우리는 오래 참음 엔터티의 구성을 완료하고 이를 맨 위에 제공합니다(실제로 그는 그것을 완료하지 않고 새 객체를 생성하여 전송된 필드를 채우고 나머지는 원본 필드에서 복사합니다) . 파일이 다음 위치에 어떻게 저장되는지 살펴보겠습니다 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 - 파일을 바이트 배열로 받아들이고 파일이 저장될 이름(생성된 키)을 별도로 받아들입니다. 2 - 3 - 경로를 만들고(경로에 경로와 키를 함께 작성) 파일을 만듭니다. 6 - 7 - 스트림을 생성하고 거기에 바이트를 씁니다(그리고 try-finally스트림이 확실히 닫힐 수 있도록 이 모든 항목을 래핑합니다). 그러나 많은 메소드에서 IOException이 발생할 수 있습니다. 이 경우 메소드 헤더에 지정된 전달 덕분에 이를 컨트롤러에 전달하고 상태 400을 제공합니다. Postman에서 모든 것을 테스트해 보겠습니다. 파일을 애플리케이션에 저장하고 그에 관한 데이터를 데이터베이스에 저장 - 5보시다시피 모든 것이 정상입니다. 응답은 201이고 응답 JSON은 데이터베이스에 지속된 엔터티의 형태로 왔습니다. 스토리지를 살펴보면 DB 파일을 애플리케이션에 저장하고 해당 데이터를 데이터베이스에 저장 - 6: 우리에게는 새로운 가치가 있습니다. (=*;*=)

다운로드

제어 장치:
@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 - 파일을 다시 보내고 헤더에 파일 이름을 추가합니다(다시 파일에 대한 정보가 있는 엔터티에서 가져옴). 좀 더 자세히 살펴보겠습니다. 서비스 인터페이스:
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로 엔터티를 검색하고 파일을 다운로드하는 방법(아마도 46을 제외하고) - 읽을 트랜잭션이 있음을 표시합니다. 다오 레벨:
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 — jdbcTemplate및 를 사용하여 ID로 검색합니다 RowMapper. 8 - 15 - RowMapper데이터베이스 및 모델 필드의 데이터를 비교하기 위한 특정 사례에 대한 구현입니다. FileManager파일이 어떻게 로드되는지 살펴보겠습니다 .
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();
   }
}
파일을 객체로 반환 Resource하고 키로 검색합니다. 3 - Resource경로 + 키를 따라 생성합니다. 4 - 8 — 주어진 경로의 파일이 비어 있지 않은지 확인하고 읽습니다. 모든 것이 정상이면 반환하고, 그렇지 않으면 맨 위에 IOException을 발생시킵니다. Postman에서 방법을 확인해 보겠습니다. 파일을 애플리케이션에 저장하고 해당 데이터를 데이터베이스에 저장 - 7보시다시피 제대로 작동했습니다.))

삭제

@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;
구현:
@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 - "저장소"에서 파일 자체를 삭제합니다. 다오 인터페이스:
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);
}
그런 건 없어요. 그냥 삭제하세요. 파일 자체 삭제:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Postman을 사용합니다. 파일을 애플리케이션에 저장하고 그에 관한 데이터를 데이터베이스에 저장 - 8저장소를 살펴봅니다. 파일을 애플리케이션에 저장하고 해당 데이터를 데이터베이스에 저장 - 9비어 있습니다. :) 이제 데이터베이스에서 Сохранение файлов в приложение и данных о них на БД - 10모든 것이 정상임을 확인합니다.))

시험

에 대한 테스트를 작성해 보겠습니다 FileManager. 먼저 테스트 부분의 구조를 살펴보겠습니다. mockFile.txt는 파일 저장소를 사용하여 작업을 테스트할 파일입니다. testFileStorage는 우리의 저장소를 대체할 것입니다. 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();
   }
여기에서 테스트 데이터 할당을 볼 수 있습니다. 파일 저장 테스트:
@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 - 테스트 반영을 사용하여 서비스의 상수를 변경하여 파일을 저장할 경로를 설정합니다. 5 — 테스트 중인 메서드를 호출합니다. 7 - 10 - 저장이 올바르게 실행되었는지 확인합니다. 11 - 저장된 파일을 삭제합니다(어떤 흔적도 남기지 않아야 합니다).
@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();
}
파일 업로드 테스트: 3 - 다시 FileManager. 5 - 테스트 중인 방법을 사용합니다. 7 - 9 — 실행 결과를 확인합니다. 파일 삭제 테스트:
@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>
오늘은 그게 전부입니다))
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION