JavaRush /จาวาบล็อก /Random-TH /การบันทึกไฟล์ลงในแอปพลิเคชันและข้อมูลเกี่ยวกับไฟล์เหล่านั...
Константин
ระดับ

การบันทึกไฟล์ลงในแอปพลิเคชันและข้อมูลเกี่ยวกับไฟล์เหล่านั้นลงในฐานข้อมูล

เผยแพร่ในกลุ่ม
การบันทึกไฟล์ลงในแอปพลิเคชันและข้อมูลเกี่ยวกับไฟล์เหล่านั้นลงในฐานข้อมูล - 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แต่ด้วยรหัสที่สร้างขึ้นในฐานข้อมูล (เราจะพูดถึงสิ่งนี้ด้านล่าง) และวันที่สร้าง วิธีสร้างคีย์ให้กับไฟล์:
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 มาทดสอบทั้งหมดในบุรุษไปรษณีย์กัน: การบันทึกไฟล์ลงในแอปพลิเคชันและข้อมูลเกี่ยวกับไฟล์เหล่านั้นลงในฐานข้อมูล - 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 — ค้นหาด้วย ID โดยใช้jdbcTemplateและ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 ไว้ด้านบนสุด ลองตรวจสอบวิธีการของเราในบุรุษไปรษณีย์: การบันทึกไฟล์ลงในแอปพลิเคชันและข้อมูลเกี่ยวกับไฟล์เหล่านั้นลงในฐานข้อมูล - 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 - ลบไฟล์ออกจาก "ที่เก็บข้อมูล" ของเรา อินเทอร์เฟซ 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);
}
ไม่มีอะไรเช่นนั้น - เพียงแค่ลบ การลบไฟล์นั้นเอง:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
เราใช้มันในบุรุษไปรษณีย์: การบันทึกไฟล์ลงในแอปพลิเคชันและข้อมูลเกี่ยวกับไฟล์เหล่านั้นลงในฐานข้อมูล - 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