JavaRush /Blog Java /Random-MS /Menyimpan fail ke aplikasi dan data tentangnya ke pangkal...

Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data

Diterbitkan dalam kumpulan
Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data - 1 Mari bayangkan bahawa anda sedang mengusahakan aplikasi web anda. Dalam artikel saya, saya melihat kepingan individu mozek ini, seperti: Bagaimanakah topik ini berguna? Dan hakikat bahawa contoh-contoh ini sangat hampir dengan bekerja pada projek sebenar, dan menguji topik ini akan sangat berguna untuk anda. Hari ini kita akan mengambil bahagian seterusnya mozek ini - bekerja dengan fail, kerana pada masa kini anda tidak lagi dapat mencari tapak yang tidak berinteraksi dengan mereka (contohnya, semua jenis kedai web, rangkaian sosial, dan sebagainya). Semakan akan dijalankan menggunakan kaedah muat naik/muat turun/padam sebagai contoh ; kami akan menyimpannya ke folder (dalam sumber) dalam aplikasi kami, supaya tidak merumitkannya. Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data - 2Ideanya ialah kami akan menyimpan, sebagai tambahan kepada fail itu sendiri dalam storan fail yang dipanggil kami, entiti dengan maklumat tentang fail kami (saiz, nama, dll.) dalam pangkalan data kami - jadual yang telah dibuat sebelumnya. Iaitu, apabila memuatkan fail, entiti ini akan sangat berguna kepada kita, dan apabila memadam, kita tidak boleh melupakannya dalam apa cara sekalipun. Mari kita lihat jadual ini: Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data - 3Dan mari kita tetapkan id kepada AUTO_INCREMENT seperti orang dewasa, untuk menjana pengecam secara automatik pada peringkat pangkalan data. Mula-mula, mari kita lihat struktur kami: Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data - 4Entiti di bawah jadual yang ditunjukkan di atas:
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {

   private Long id;

   private String name;

   private Long size;

   private String key;

   private LocalDate uploadDate;
}

Muat naik

Mari lihat pengawal:
@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);
       }
   }
Perkara yang menarik: 9 - Kami menerima fail dalam borang MultipartFile. Ia juga boleh diterima sebagai tatasusunan bait, tetapi saya lebih suka pilihan ini, kerana kita boleh MultipartFilemengekstrak pelbagai sifat fail yang dipindahkan. 10 - 14 - kami membungkus tindakan kami try catchsupaya jika pengecualian berlaku pada tahap yang lebih rendah, kami memajukannya lebih tinggi dan menghantar ralat 400 sebagai respons. Seterusnya ialah tahap perkhidmatan:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Mari kita lihat pelaksanaannya:
@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 - dalam kes IOException, semua simpanan kami dalam pangkalan data akan ditarik balik. 11 - kami menjana kunci yang akan menjadi unik untuk fail apabila ia disimpan (walaupun dua fail dengan nama yang sama disimpan, tidak akan ada kekeliruan). 12 — kami membina entiti untuk disimpan dalam pangkalan data. 17 — kami memacu entiti dengan maklumat ke dalam pangkalan data. 18 - simpan fail dengan nama cincang. 20 — kami mengembalikan entiti yang dibuat FileInfo, tetapi dengan id yang dihasilkan dalam pangkalan data (ini akan dibincangkan di bawah) dan tarikh penciptaan. Kaedah untuk menjana kunci kepada fail:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Di sini kami mencincang nama + tarikh penciptaan, yang akan memastikan keunikan kami. Antara muka lapisan Dao:
public interface FileDAO {

   FileInfo create(FileInfo file);
Pelaksanaannya:
@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 - buat tarikh yang akan kami simpan. 12 - 21 - kami menyimpan entiti, tetapi dengan cara yang lebih kompleks, dengan penciptaan objek yang jelas PreparedStatementsupaya id yang dihasilkan boleh ditarik keluar (ia mengeluarkannya bukan dengan permintaan yang berasingan, tetapi dalam bentuk metadata tindak balas ). 22 - 26 - kami menyelesaikan pembinaan entiti kami yang telah lama menderita dan memberikannya kepada bahagian atas (sebenarnya, dia tidak menyelesaikannya, tetapi mencipta objek baru, mengisi medan yang dipindahkan dan menyalin yang lain dari yang asal) . Mari lihat bagaimana fail kami akan disimpan dalam 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 — kami menerima fail sebagai tatasusunan bait dan secara berasingan nama di bawahnya ia akan disimpan (kunci hasil kami). 2 - 3 - buat laluan (dan dalam laluan kita tulis laluan ditambah kunci kita) dan fail di sepanjangnya. 6 - 7 - buat strim dan tulis bait kami di sana (dan bungkus semua bahan ini try-finallyuntuk memastikan strim itu pasti akan ditutup). Walau bagaimanapun, banyak kaedah boleh membuang IOException. Dalam kes ini, terima kasih kepada pemajuan yang dinyatakan dalam pengepala kaedah, kami akan menyerahkannya kepada pengawal dan memberikan status 400. Mari kita uji semuanya dalam Posman: Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data - 5Seperti yang anda lihat, semuanya baik-baik saja, responsnya ialah 201, respons JSON datang dalam bentuk entiti berterusan kami dalam pangkalan data, dan jika kami melihat ke dalam storan kami: DB: Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data - 6kami melihat bahawa kita ada nilai baru. (=*;*=)

Muat turun

Pengawal:
@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);
   }
}
Kami juga membungkusnya try-catchdan dalam kes IOException kami menghantar status respons 404 (tidak ditemui). 4 - tarik keluar entiti FileInfo bersebelahan daripada pangkalan data. 5 - menggunakan kunci dari entiti, muat turun fail 6-8 - hantar semula fail, menambah nama fail pada pengepala (sekali lagi, diperoleh daripada entiti dengan maklumat tentang fail). Mari kita lihat lebih mendalam. Antara muka perkhidmatan:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Pelaksanaan:
@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);
}
Tiada apa-apa yang menarik di sini: kaedah mencari entiti dengan id dan memuat turun fail, kecuali mungkin 46 - kami menandakan bahawa kami mempunyai transaksi untuk membaca. Tahap Dao:
FileInfo findById(Long fileId);
Pelaksanaan:
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 — cari mengikut id menggunakan jdbcTemplatedan RowMapper. 8 - 15 - pelaksanaan RowMapperuntuk kes khusus kami, untuk membandingkan data daripada pangkalan data dan medan model. Mari pergi FileManagerdan lihat bagaimana fail kami dimuatkan:
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();
   }
}
Kami mengembalikan fail sebagai objek Resource, dan kami akan mencari dengan kunci. 3 - buat Resourcesepanjang laluan + kunci. 4 - 8 — kami menyemak bahawa fail di laluan yang diberikan tidak kosong dan membacanya. Jika semuanya OK, kami mengembalikannya, dan jika tidak, kami membuang IOException ke atas. Mari semak kaedah kami dalam Posman: Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data - 7Seperti yang anda lihat, ia berfungsi OK))

Padam

@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);
   }
}
Tiada yang istimewa di sini: kami juga mengembalikan 404 sekiranya berlaku kegagalan menggunakan try-catch. Antara muka perkhidmatan:
void delete(Long fileId) throws IOException;
Pelaksanaan:
@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 - juga rollback perubahan data (pemadaman) apabila IOException berlaku. 5 - padam maklumat tentang fail daripada pangkalan data. 6 - padamkan fail itu sendiri daripada "storan" kami. antara muka dao:
void delete(Long fileId);
Pelaksanaan:
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);
}
Tiada apa-apa seperti itu - hanya padam. Memadam fail itu sendiri:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Kami menggunakan Posmen: Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data - 8Kami melihat dalam storan: Menyimpan fail ke aplikasi dan data tentangnya ke pangkalan data - 9Kosong :) Sekarang dalam pangkalan data: Сохранение файлов в приложение и данных о них на БД - 10Kami melihat bahawa semuanya baik))

Ujian

Mari cuba menulis ujian untuk kami FileManager. Mula-mula, mari kita lihat struktur bahagian ujian: mockFile.txt ialah fail yang akan digunakan untuk menguji operasi kami dengan storan fail. testFileStorage akan menjadi pengganti storan kami. 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();
   }
Di sini kita melihat tugasan data ujian. Ujian simpanan fail:
@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 — menggunakan refleksi ujian, kami menukar pemalar kami dalam perkhidmatan untuk menetapkan laluan untuk menyimpan fail. 5 — panggil kaedah yang diuji. 7 - 10 — kami menyemak pelaksanaan simpan yang betul. 11 - padam fail yang disimpan (kami tidak sepatutnya meninggalkan sebarang kesan).
@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();
}
Ujian muat naik fail: 3 - sekali lagi, tukar laluan untuk FileManager. 5 - gunakan kaedah yang diuji. 7 - 9 — semak keputusan pelaksanaan. Ujian pemadaman fail:
@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 - tetapkan laluan dan buat fail. 5 - 6 - kita semak kewujudannya. 9 - kami menggunakan kaedah yang boleh disahkan. 77 - kami menyemak sama ada objek itu sudah tiada. Dan mari lihat apa yang kita ada dari segi kebergantungan:
<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>
Itu sahaja untuk saya hari ini))
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION