JavaRush /Java Blog /Random-ID /Menyimpan file ke aplikasi dan data tentangnya ke databas...

Menyimpan file ke aplikasi dan data tentangnya ke database

Dipublikasikan di grup Random-ID
Menyimpan file ke aplikasi dan data tentangnya ke database - 1 Bayangkan Anda sedang mengerjakan aplikasi web Anda. Dalam artikel saya, saya melihat masing-masing bagian dari mosaik ini, seperti: Apa manfaat topik-topik ini? Dan fakta bahwa contoh-contoh ini sangat mirip dengan pengerjaan proyek nyata, dan menguji topik-topik ini akan sangat berguna bagi Anda. Hari ini kita akan mengambil bagian berikutnya dari mosaik ini - bekerja dengan file, karena saat ini Anda tidak dapat lagi menemukan situs yang tidak berinteraksi dengannya (misalnya, semua jenis toko web, jejaring sosial, dan sebagainya). Review akan dilakukan dengan menggunakan metode upload/download/delete sebagai contoh , kita akan menyimpannya ke folder (dalam resource) di aplikasi kita, agar tidak mempersulitnya. Menyimpan file ke aplikasi dan data tentangnya ke database - 2Idenya adalah bahwa kita akan menyimpan, selain file itu sendiri dalam apa yang disebut penyimpanan file, sebuah entitas dengan informasi tentang file kita (ukuran, nama, dll.) di database kita - tabel yang telah dibuat sebelumnya. Artinya, saat memuat file, entitas ini akan sangat berguna bagi kita, dan saat menghapus, kita tidak boleh melupakannya dalam hal apa pun. Mari kita lihat tabel ini: Menyimpan file ke aplikasi dan data tentangnya ke database - 3Dan mari kita atur id menjadi AUTO_INCREMENT seperti orang dewasa, untuk secara otomatis menghasilkan pengenal di tingkat database. Pertama, mari kita lihat struktur kita: Menyimpan file ke aplikasi dan data tentangnya ke database - 4Entitas di bawah tabel 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;
}

Mengunggah

Mari kita lihat pengontrolnya:
@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);
       }
   }
Beberapa hal menarik: 9 - Kami menerima file dalam bentuk MultipartFile. Itu juga dapat diterima sebagai array byte, tapi saya lebih menyukai opsi ini, karena kita dapat MultipartFilemengekstrak berbagai properti dari file yang ditransfer. 10 - 14 - kita menggabungkan tindakan kita try catchsehingga jika pengecualian terjadi pada tingkat yang lebih rendah, kita meneruskannya lebih tinggi dan mengirimkan kesalahan 400 sebagai respons. Berikutnya adalah tingkat layanan:
public interface FileService {

   FileInfo upload(MultipartFile resource) throws IOException;
Mari kita lihat implementasinya:
@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 - jika terjadi IOException, semua simpanan kami di database akan dibatalkan. 11 - kami membuat kunci yang unik untuk file tersebut saat disimpan (bahkan jika dua file dengan nama yang sama disimpan, tidak akan ada kebingungan). 12 - kami membangun entitas untuk disimpan dalam database. 17 - kami memasukkan entitas dengan informasi ke dalam database. 18 - simpan file dengan nama hash. 20 — kami mengembalikan entitas yang dibuat FileInfo, tetapi dengan id yang dihasilkan dalam database (ini akan dibahas di bawah) dan tanggal pembuatan. Metode untuk menghasilkan kunci ke suatu file:
private String generateKey(String name) {
   return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Di sini kita hash nama + tanggal pembuatan, yang akan memastikan keunikan kita. Antarmuka lapisan Dao:
public interface FileDAO {

   FileInfo create(FileInfo file);
Implementasinya:
@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 tanggal yang akan kita simpan. 12 - 21 - kami menyimpan entitas, tetapi dengan cara yang lebih kompleks, dengan pembuatan objek secara eksplisit PreparedStatementsehingga id yang dihasilkan dapat ditarik keluar (menariknya bukan dengan permintaan terpisah, tetapi dalam bentuk metadata respons ). 22 - 26 - kami menyelesaikan pembangunan entitas kami yang telah lama menderita dan memberikannya ke atas (pada kenyataannya, dia tidak menyelesaikannya, tetapi membuat objek baru, mengisi bidang yang ditransfer dan menyalin sisanya dari yang asli) . Mari kita lihat bagaimana file kita akan disimpan di 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 file sebagai array byte dan secara terpisah nama penyimpanannya (kunci yang kami buat). 2 - 3 - buat jalur (dan di jalur tersebut kami menulis jalur ditambah kunci kami) dan file di sepanjang jalur tersebut. 6 - 7 - buat aliran dan tulis byte kami di sana (dan bungkus semua ini try-finallyuntuk memastikan aliran pasti akan ditutup). Namun, banyak metode yang dapat menampilkan IOException. Dalam hal ini, berkat penerusan yang ditentukan dalam header metode, kami akan meneruskannya ke pengontrol dan memberikan status 400. Mari kita uji semuanya di Tukang Pos: Menyimpan file ke aplikasi dan data tentangnya ke database - 5Seperti yang bisa kita lihat, semuanya baik-baik saja, responsnya adalah 201, respons JSON datang dalam bentuk entitas yang bertahan di database, dan jika kita melihat ke dalam penyimpanan kita: DB: Menyimpan file ke aplikasi dan data tentangnya ke database - 6kita akan melihat bahwa kita mempunyai nilai baru. (=*;*=)

Unduh

Pengendali:
@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 kasus IOException kami mengirimkan status respons 404 (tidak ditemukan). 4 - mengeluarkan entitas FileInfo yang berdekatan dari database. 5 - menggunakan kunci dari entitas, unduh file 6-8 - kirim kembali file, tambahkan nama file ke header (sekali lagi, diperoleh dari entitas dengan informasi tentang file). Mari kita lihat lebih dalam. Antarmuka layanan:
Resource download(String key) throws IOException;

FileInfo findById(Long fileId);
Penerapan:
@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);
}
Tidak ada yang menarik di sini: metode mencari entitas berdasarkan id dan mengunduh file, kecuali mungkin 46 - kami menandai bahwa kami memiliki transaksi untuk dibaca. tingkat Dao:
FileInfo findById(Long fileId);
Penerapan:
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 — mencari berdasarkan id menggunakan jdbcTemplatedan RowMapper. 8 - 15 - implementasi RowMapperuntuk kasus khusus kami, untuk membandingkan data dari database dan bidang model. Ayo pergi FileManagerdan lihat bagaimana file kita dimuat:
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 file sebagai objek Resource, dan kami akan mencari berdasarkan kunci. 3 - buat Resourcedi sepanjang jalur + kunci. 4 - 8 — kami memeriksa apakah file pada jalur yang diberikan tidak kosong dan membacanya. Jika semuanya baik-baik saja, kami mengembalikannya, dan jika tidak, kami melemparkan IOException ke atas. Mari kita periksa metode kita di Tukang Pos: Menyimpan file ke aplikasi dan data tentangnya ke database - 7Seperti yang Anda lihat, ini berfungsi dengan baik))

Menghapus

@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);
   }
}
Tidak ada yang istimewa di sini: kami juga mengembalikan 404 jika terjadi kegagalan penggunaan try-catch. Antarmuka layanan:
void delete(Long fileId) throws IOException;
Penerapan:
@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 mengembalikan perubahan data (penghapusan) ketika IOException terjadi. 5 - hapus informasi tentang file dari database. 6 - hapus file itu sendiri dari "penyimpanan" kami. antarmuka dao:
void delete(Long fileId);
Penerapan:
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);
}
Tidak ada yang seperti itu - hapus saja. Menghapus file itu sendiri:
public void delete(String key) throws IOException {
       Path path = Paths.get(DIRECTORY_PATH + key);
       Files.delete(path);
   }
}
Kami menggunakan Tukang Pos: Menyimpan file ke aplikasi dan data tentangnya ke database - 8Kami mencari di penyimpanan: Menyimpan file ke aplikasi dan data tentangnya ke database - 9Kosong :) Sekarang di database: Menyimpan file ke aplikasi dan data tentangnya ke database - 10Kami melihat semuanya baik-baik saja))

Tes

Mari kita coba menulis tes untuk kita FileManager. Pertama, mari kita lihat struktur bagian pengujian: mockFile.txt adalah file yang akan kita gunakan untuk menguji operasi penyimpanan file kita. testFileStorage akan menjadi pengganti penyimpanan 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 penugasan data pengujian. Tes penyimpanan file:
@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 pengujian, kami mengubah konstanta kami di layanan untuk mengatur jalur penyimpanan file. 5 — memanggil metode yang sedang diuji. 7 - 10 - periksa kebenaran pelaksanaan penyimpanan. 11 - hapus file yang disimpan (kita tidak boleh meninggalkan jejak apa pun).
@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();
}
Tes unggah file: 3 - sekali lagi, ubah jalur untuk file FileManager. 5 - gunakan metode yang sedang diuji. 7 - 9 — periksa hasil eksekusi. Tes penghapusan file:
@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 - atur jalur dan buat file. 5 - 6 - kami memeriksa keberadaannya. 9 - kami menggunakan metode yang dapat diverifikasi. 77 - kami memeriksa bahwa objek tersebut sudah tidak ada lagi. Dan mari kita lihat apa yang kita miliki dalam hal ketergantungan:
<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 saja untuk saya hari ini))
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION