JavaRush /مدونة جافا /Random-AR /حفظ الملفات في التطبيق والبيانات الخاصة بها في قاعدة البي...

حفظ الملفات في التطبيق والبيانات الخاصة بها في قاعدة البيانات

نشرت في المجموعة
حفظ الملفات في التطبيق والبيانات المتعلقة بها في قاعدة البيانات - 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 - احفظ الملف باسم مجزأ. 20 — نعيد الكيان الذي تم إنشاؤه FileInfo، ولكن مع المعرف الذي تم إنشاؤه في قاعدة البيانات (سيتم مناقشة ذلك أدناه) وتاريخ الإنشاء. طريقة توليد مفتاح للملف:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
هنا نقوم بتجزئة الاسم + تاريخ الإنشاء، مما سيضمن تفردنا. واجهة طبقة داو:
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);
}
لا يوجد شيء مثير للاهتمام بشكل خاص هنا: طريقة البحث عن كيان عن طريق المعرف وتنزيل الملف، باستثناء 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 - احذف الملف نفسه من "مخزننا". واجهة الداو:
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