Faylların proqrama və onlar haqqında məlumatların verilənlər bazasına saxlanması - 1 Təsəvvür edək ki, veb proqramınız üzərində işləyirsiniz. Məqalələrimdə bu mozaikanın ayrı-ayrı hissələrinə baxıram, məsələn: Bu mövzular nə dərəcədə faydalıdır? Və bu nümunələrin real layihələr üzərində işləməyə çox yaxın olması və bu mövzuların sınaqdan keçirilməsi sizin üçün çox faydalı olacaq. Bu gün biz bu mozaikanın növbəti hissəsini götürəcəyik - fayllarla işləmək, çünki bu gün onlarla qarşılıqlı əlaqədə olmayan bir sayt tapa bilməzsən (məsələn, hər cür veb-mağazalar, sosial şəbəkələr və s.). Baxış nümunə olaraq yükləmə/yükləmə/silmə üsullarından istifadə edilməklə həyata keçiriləcək ; onu çətinləşdirməmək üçün onu tətbiqimizdəki qovluqda (resursda) saxlayacağıq. Faylların proqrama və onlar haqqında məlumatların verilənlər bazasına saxlanması - 2İdeya ondan ibarətdir ki, biz sözdə fayl anbarımızda faylın özündən əlavə, verilənlər bazamızda faylımız haqqında məlumatı (ölçüsü, adı və s.) olan bir obyekti - əvvəlcədən yaradılmış cədvəli saxlayacağıq. Yəni faylı yükləyərkən bu varlıq bizim üçün çox faydalı olacaq və silərkən heç bir şəkildə bunu unutmamalıyıq. Gəlin bu cədvələ nəzər salaq: Faylların proqrama və onlar haqqında məlumatların verilənlər bazasına saxlanması - 3Və verilənlər bazası səviyyəsində avtomatik olaraq identifikator yaratmaq üçün id-i böyüklər kimi AUTO_INCREMENT-ə təyin edək. Əvvəlcə quruluşumuza nəzər salaq: Faylların proqrama və onlar haqqında məlumatların verilənlər bazasına saxlanması - 4Yuxarıda göstərilən cədvəlin altındakı obyekt:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Yükləmək

Nəzarətçiyə baxaq:
@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);
       }
   }
Maraqlı şeylər: 9 - Faylı formada qəbul edirik MultipartFile. Onu bayt massivi kimi də qəbul etmək olar, amma bu seçimi daha çox bəyənirəm, çünki biz MultipartFileköçürülmüş faylın müxtəlif xassələrini çıxara bilərik. 10 - 14 - biz hərəkətlərimizi try catchelə bağlayırıq ki, daha aşağı səviyyədə bir istisna baş verərsə, onu yuxarıya yönləndirək və cavab olaraq 400 xəta göndərək. Sonrakı xidmət səviyyəsidir:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Həyata keçirilməsinə baxaq:
@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 halında, verilənlər bazasındakı bütün saxlamalarımız geri qaytarılacaq. 11 - biz saxlandıqda fayl üçün unikal olacaq bir açar yaradırıq (eyni adda iki fayl saxlanılsa belə, qarışıqlıq olmayacaq). 12 — verilənlər bazasında saxlamaq üçün obyekt qururuq. 17 — məlumatı olan qurumu verilənlər bazasına daxil edirik. 18 - faylı hashing adı ilə yadda saxlayın. 20 — yaradılan obyekti qaytarırıq FileInfo, lakin verilənlər bazasında yaradılan id (bu, aşağıda müzakirə olunacaq) və yaradılma tarixi ilə. Fayl üçün açar yaratmaq üsulu:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Burada unikallığımızı təmin edəcək ad + yaradılma tarixini hash edirik. Dao qat interfeysi:
public interface FileDAO {

   FileInfo create(FileInfo file);
Onun həyata keçirilməsi:
@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 - saxlayacağımız bir tarix yaradın. 12 - 21 - biz obyekti saxlayırıq, lakin daha mürəkkəb bir şəkildə, obyektin açıq şəkildə yaradılması ilə PreparedStatementyaradılan id çıxarıla bilər (onu ayrıca bir sorğu ilə deyil, cavab metadata şəklində çıxarır) ). 22 - 26 - biz səbirli varlığımızın tikintisini başa çatdırırıq və zirvəyə veririk (əslində, o, onu tamamlamır, lakin yeni bir obyekt yaradır, köçürülmüş sahələri doldurur və qalanını orijinaldan kopyalayır) . Fayllarımızın necə saxlanacağına baxaq 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 — biz faylı bayt massivi kimi qəbul edirik və ayrı-ayrılıqda onun saxlanacağı adı (yaradılmış açarımız). 2 - 3 - bir yol yaradın (və yolda biz yolu və açarımızı yazırıq) və onun boyunca bir fayl. try-finally6 - 7 - bir axın yaradın və baytlarımızı orada yazın (və axının mütləq bağlanacağına əmin olmaq üçün bütün bunları sarın ). Bununla belə, metodların çoxu IOException-ı ata bilər. Bu halda, metodun başlığında göstərilən yönləndirmə sayəsində biz onu nəzarətçiyə ötürəcəyik və 400 statusunu verəcəyik. Gəlin hər şeyi Postman-da yoxlayaq: Faylların proqrama və onlar haqqında məlumatların verilənlər bazasına saxlanması - 5Gördüyünüz kimi, hər şey yaxşıdır, cavab 201-dir, cavab JSON verilənlər bazasındakı davamlı varlığımız şəklində gəldi və yaddaşımıza baxsaq: DB: görürük Faylların proqrama və onlar haqqında məlumatların verilənlər bazasına saxlanması - 6ki, yeni bir dəyərimiz var. (=*;*=)

Yüklə

Nəzarətçi:
@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);
   }
}
Biz də onu əhatə edirik try-catchvə IOException halında 404 cavab statusu göndəririk (tapılmadı). 4 - bitişik FileInfo obyektini verilənlər bazasından çıxarın. 5 - obyektdən açardan istifadə edərək, faylı endirin 6-8 - faylın adını başlığa əlavə edərək faylı geri göndərin (yenidən fayl haqqında məlumat olan qurumdan əldə edilir). Gəlin daha dərindən nəzər salaq. Xidmət interfeysi:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
İcra:
@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);
}
Burada xüsusilə maraqlı bir şey yoxdur: id-yə görə bir obyekti axtarmaq və faylı yükləmək üsulu, bəlkə də 46 istisna olmaqla - oxumaq üçün əməliyyatımız olduğunu qeyd edirik. Dao səviyyəsi:
FileInfo findById(Long fileId);
İcra:
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();
}
jdbcTemplate4 — və istifadə edərək id ilə axtarın RowMapper. 8 - 15 - RowMapperverilənlər bazası və model sahələrindən məlumatları müqayisə etmək üçün xüsusi işimiz üçün tətbiq. Gedək FileManagervə faylımızın necə yükləndiyinə baxaq:
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();
   }
}
Faylı obyekt kimi qaytarırıq Resourcevə açarla axtarış edəcəyik. 3 - Resourceyol + açarı boyunca yaradın. 4 - 8 — verilən yoldakı faylın boş olmadığını yoxlayırıq və onu oxuyuruq. Hər şey qaydasındadırsa, onu qaytarırıq, yoxsa, yuxarıya IOException atırıq. Postman-da metodumuzu yoxlayaq: Faylların proqrama və onlar haqqında məlumatların verilənlər bazasına saxlanması - 7Gördüyümüz kimi, hər şey qaydasındadır))

Sil

@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);
   }
}
Burada xüsusi bir şey yoxdur: istifadə edilməməsi halında 404-ü də qaytarırıq try-catch. Xidmət interfeysi:
void delete(Long fileId) throws IOException;
İcra:
@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 - həmçinin IOException baş verdikdə məlumat dəyişikliklərinin (silmələrin) geri qaytarılması. 5 - verilənlər bazasından fayl haqqında məlumatı silin. 6 - faylın özünü "yaddaşımızdan" silin. dao interfeysi:
void delete(Long fileId);
İcra:
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);
}
Belə bir şey yoxdur - sadəcə silin. Faylın özünün silinməsi:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Postmandan istifadə edirik: Faylların proqrama və onlar haqqında məlumatların verilənlər bazasına saxlanması - 8Anbara baxırıq: Faylların proqrama və onlar haqqında məlumatların verilənlər bazasına saxlanması - 9Boş :) İndi verilənlər bazasında: Сохранение файлов в приложение и данных о них на БД - 10Hər şeyin yaxşı olduğunu görürük))

Test

Bizim üçün bir test yazmağa çalışaq FileManager. Əvvəlcə test hissəsinin strukturuna nəzər salaq: mockFile.txt fayl saxlama ilə əməliyyatlarımızı sınaqdan keçirəcəyimiz fayldır. testFileStorage yaddaşımızı əvəz edəcək. 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();
   }
Burada test məlumatlarının təyinatını görürük. Fayl saxlama testi:
@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 — test əksindən istifadə edərək, faylın saxlanması yolunu təyin etmək üçün xidmətdəki sabitimizi dəyişdiririk. 5 - sınaqdan keçirilən metodu çağırın. 7 - 10 - qənaətin düzgün icrasını yoxlayın. 11 - saxlanan faylı silin (heç bir iz buraxmamalıyıq).
@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();
}
Fayl yükləmə testi: 3 - yenidən bizim üçün yolu dəyişdirin FileManager. 5 - sınaqdan keçirilən metoddan istifadə edin. 7 - 9 - icra nəticəsini yoxlayın. Fayl silmə testi:
@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 - yolu təyin edin və fayl yaradın. 5 - 6 - varlığını yoxlayırıq. 9 - yoxlanıla bilən bir üsuldan istifadə edirik. 77 - obyektin artıq orada olmadığını yoxlayırıq. Və asılılıqlar baxımından nələrə sahib olduğumuzu görək:
<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>
Bu gün mənim üçün hamısı budur))