JavaRush /בלוג Java /Random-HE /שמירת קבצים באפליקציה ונתונים עליהם במסד הנתונים

שמירת קבצים באפליקציה ונתונים עליהם במסד הנתונים

פורסם בקבוצה
שמירת קבצים באפליקציה ונתונים עליהם במסד הנתונים - 1 הבה נדמיין שאתה עובד על יישום האינטרנט שלך. במאמרים שלי אני מסתכל על חלקים בודדים של פסיפס זה, כגון: כיצד נושאים אלו מועילים? והעובדה שהדוגמאות הללו קרובות מאוד לעבודה על פרויקטים אמיתיים, ובדיקת הנושאים הללו תהיה מאוד שימושית עבורך. היום ניקח את הקטע הבא בפסיפס הזה - עבודה עם קבצים, שכן כיום כבר לא ניתן למצוא אתר שאינו יוצר איתם אינטראקציה (למשל, כל מיני חנויות אינטרנט, רשתות חברתיות וכדומה). הסקירה תתבצע באמצעות שיטות העלאה/הורדה/מחיקה כדוגמה ; נשמור אותה בתיקייה (במשאב) באפליקציה שלנו, כדי לא לסבך אותה. שמירת קבצים באפליקציה ונתונים עליהם במסד הנתונים - 2הרעיון הוא שנשמור, בנוסף לקובץ עצמו במה שנקרא אחסון הקבצים שלנו, ישות עם מידע על הקובץ שלנו (גודל, שם וכו') במסד הנתונים שלנו - טבלה שנוצרה מראש. כלומר, בעת טעינת קובץ, הישות הזו תהיה שימושית מאוד עבורנו, ובעת המחיקה אסור לשכוח אותה בשום צורה. בואו נסתכל על הטבלה הזו: שמירת קבצים באפליקציה ונתונים עליהם במסד הנתונים - 3ובואו נגדיר את המזהה ל-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 - שמור את הקובץ בשם hashed. 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כך שניתן לשלוף את המזהה שנוצר (הוא שולף אותו לא באמצעות בקשה נפרדת, אלא בצורה של מטא נתונים של תגובה ). 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ו 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 - מחק את הקובץ עצמו מה"אחסון" שלנו. ממשק 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