1. 概要

ある種のコンテンツ管理ソリューションを構築するとき、2つの問題を解決する必要があります。 ファイル自体を保存する場所が必要であり、それらにインデックスを付けるための何らかのデータベースが必要です。

ファイルのコンテンツをデータベース自体に保存することも、コンテンツを別の場所に保存してデータベースでインデックスを作成することもできます。

この記事では、基本的なイメージアーカイブアプリケーションを使用して、これらの両方の方法を説明します。 アップロードとダウンロード用のRESTAPIも実装します。

2. 使用事例

私たちの画像アーカイブアプリケーションでは、JPEG画像のアップロードとダウンロードが可能になります。

画像をアップロードすると、アプリケーションはその画像の一意の識別子を作成します。 次に、この識別子を使用してダウンロードできます。

Spring DataJPAおよびHibernateのリレーショナルデータベースを使用します。

3. データベースストレージ

私たちのデータベースから始めましょう。

3.1. 画像エンティティ

まず、Imageエンティティを作成しましょう。

@Entity
class Image {

    @Id
    @GeneratedValue
    Long id;

    @Lob
    byte[] content;

    String name;
    // Getters and Setters
}

idフィールドには@GeneratedValueという注釈が付けられています。 これは、データベースが追加するレコードごとに一意の識別子を作成することを意味します。 これらの値で画像にインデックスを付けることにより、同じ画像の複数のアップロードが互いに競合することを心配する必要がありません。

次に、Hibernate@Lobアノテーションがあります。 これが、JPAに潜在的に大きなバイナリを格納する意図を伝える方法です。

3.2. 画像リポジトリ

次に、データベースに接続するためのリポジトリが必要です。

spring JpaRepositoryを使用します。

@Repository
interface ImageDbRepository extends JpaRepository<Image, Long> {}

これで、画像を保存する準備が整いました。  それらをアプリケーションにアップロードする方法が必要です。

4. RESTコントローラー

MultipartFileを使用して画像をアップロードします。 アップロードすると、後で画像をダウンロードするために使用できるimageIdが返されます。

4.1. 画像のアップロード

アップロードをサポートするImageControllerを作成することから始めましょう。

@RestController
class ImageController {

    @Autowired
    ImageDbRepository imageDbRepository;

    @PostMapping
    Long uploadImage(@RequestParam MultipartFile multipartImage) throws Exception {
        Image dbImage = new Image();
        dbImage.setName(multipartImage.getName());
        dbImage.setContent(multipartImage.getBytes());

        return imageDbRepository.save(dbImage)
            .getId();
    }
}

MultipartFile オブジェクトには、ファイルのコンテンツと元の名前が含まれています。 これを使用して、データベースに保存するためのImageオブジェクトを作成します。

このコントローラーは、生成されたIDを応答の本体として返します。

4.2. 画像のダウンロード

それでは、ダウンロードルートを追加しましょう

@GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
Resource downloadImage(@PathVariable Long imageId) {
    byte[] image = imageRepository.findById(imageId)
      .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND))
      .getContent();

    return new ByteArrayResource(image);
}

imageId パス変数には、アップロード時に生成されたIDが含まれています。 無効なIDが指定された場合、 ResponseStatusException を使用してHTTP応答コード404(見つかりません)を返します。 それ以外の場合は、保存されたファイルのバイトを ByteArrayResource にラップして、ダウンロードできるようにします。

5. データベース画像アーカイブテスト

これで、画像アーカイブをテストする準備が整いました。

まず、アプリケーションを作成しましょう。

mvn package

次に、起動しましょう。

java -jar target/image-archive-0.0.1-SNAPSHOT.jar

5.1. 画像アップロードテスト

アプリケーションの実行後、curlコマンドラインツールを使用して画像をアップロードします

curl -H "Content-Type: multipart/form-data" \
  -F "[email protected]" http://localhost:8080/image

アップロードサービスの応答はimageIdであり、これが最初のリクエストであるため、出力は次のようになります。

1

5.2. 画像ダウンロードテスト

次に、画像をダウンロードできます。

curl -v http://localhost:8080/image/1 -o image.jpeg

-o image.jpeg オプションは、 image.jpeg という名前のファイルを作成し、その中に応答コンテンツを保存します。

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /image/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 
< Accept-Ranges: bytes
< Content-Type: image/jpeg
< Content-Length: 9291

HTTP / 1.1 200を入手しました。これは、ダウンロードが成功したことを意味します。

http:// localhost:8080 / image / 1 を押して、ブラウザに画像をダウンロードしてみることもできます。

6. 個別のコンテンツと場所

これまでのところ、データベース内で画像をアップロードおよびダウンロードすることができます。

もう1つの良いオプションは、ファイルの内容を別の場所にアップロードすることです。 次に、DB内のファイルシステムの場所のみを保存します。

そのためには、Imageエンティティに新しいフィールドを追加する必要があります。

String location;

これには、外部ストレージ内のファイルへの論理パスが含まれます。 私たちの場合には、 これは、サーバーのファイルシステム上のパスになります。 

ただし、このアイデアをさまざまなストアに等しく適用できます。 たとえば、クラウドストレージ– Google CloudStorageまたはAmazonS3を使用できます。 この場所では、 s3:// somebucket / path / to /fileなどのURI形式を使用することもできます。

アップロードサービスは、ファイルのバイトをデータベースに書き込むのではなく、ファイルを適切なサービス(この場合はファイルシステム)に保存してから、ファイルの場所をデータベースに配置します。

7. ファイルシステムストレージ

ファイルシステムに画像を保存する機能をソリューションに追加しましょう。

7.1. ファイルシステムに保存する

まず、画像をファイルシステムに保存する必要があります。

@Repository
class FileSystemRepository {

    String RESOURCES_DIR = FileSystemRepository.class.getResource("/")
        .getPath();

    String save(byte[] content, String imageName) throws Exception {
        Path newFile = Paths.get(RESOURCES_DIR + new Date().getTime() + "-" + imageName);
        Files.createDirectories(newFile.getParent());

        Files.write(newFile, content);

        return newFile.toAbsolutePath()
            .toString();
    }
}

重要な注意事項– 各画像には、アップロード時にサーバー側で定義された一意の場所があることを確認する必要があります。 そうしないと、アップロードが相互に上書きする可能性があります。

同じルールが、一意のキーを作成する必要があるすべてのクラウドストレージに適用されます。 この例では、現在の日付をミリ秒形式で画像名に追加します。

/workspace/archive-achive/target/classes/1602949218879-baeldung.jpeg

7.2. ファイルシステムからの取得

次に、ファイルシステムから画像をフェッチするコードを実装しましょう。

FileSystemResource findInFileSystem(String location) {
    try {
        return new FileSystemResource(Paths.get(location));
    } catch (Exception e) {
        // Handle access or file not found problems.
        throw new RuntimeException();
    }
}

ここでは、その場所を使用して画像を探しています。 次に、FileSystemResourceを返します。

また、ファイルの読み取り中に発生する可能性のある例外をキャッチしています。 特定のHTTPステータスで例外をスローすることもできます。

7.3. データストリーミングとSpringのリソース

findInFileSystem メソッドは、 FileSystemResource、SpringのResourceインターフェイスの実装を返します。

使用した場合にのみファイルの読み取りを開始します。 この場合は、RestControllerを介してクライアントに送信する場合になります。 また、ファイルシステムからユーザーにファイルコンテンツをストリーミングし、すべてのバイトをメモリにロードする手間を省きます

このアプローチは、クライアントにファイルをストリーミングするための優れた一般的なソリューションです。 ファイルシステムの代わりにクラウドストレージを使用している場合は、FileSystemResourceInputStreamResourceByteArrayResourceなどの別のリソースの実装に置き換えることができます。

8. ファイルの内容と場所を接続する

FileSystemRepositoryができたので、これをImageDbRepositoryにリンクする必要があります。

8.1. データベースとファイルシステムへの保存

保存フローから始めて、FileLocationServiceを作成しましょう。

@Service
class FileLocationService {

    @Autowired
    FileSystemRepository fileSystemRepository;
    @Autowired
    ImageDbRepository imageDbRepository;

    Long save(byte[] bytes, String imageName) throws Exception {
        String location = fileSystemRepository.save(bytes, imageName);

        return imageDbRepository.save(new Image(imageName, location))
            .getId();
    }
}

まず、画像をファイルシステム保存します。 次に、その場所を含むレコードをデータベースに保存します

8.2. データベースとファイルシステムからの取得

それでは、idを使用して画像を検索するメソッドを作成しましょう。

FileSystemResource find(Long imageId) {
    Image image = imageDbRepository.findById(imageId)
      .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

    return fileSystemRepository.findInFileSystem(image.getLocation());
}

まず、データベースで画像を探します。 次に、その場所を取得し、ファイルシステムからフェッチします。

データベースにimageIdが見つからない場合は、ResponseStatusExceptionを使用してHTTPNotFound応答を返しています。

9. ファイルシステムのアップロードとダウンロード

最後に、 FileSystemImageController:を作成しましょう。

@RestController
@RequestMapping("file-system")
class FileSystemImageController {

    @Autowired
    FileLocationService fileLocationService;

    @PostMapping("/image")
    Long uploadImage(@RequestParam MultipartFile image) throws Exception {
        return fileLocationService.save(image.getBytes(), image.getOriginalFilename());
    }

    @GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
    FileSystemResource downloadImage(@PathVariable Long imageId) throws Exception {
        return fileLocationService.find(imageId);
    }
}

まず、新しいパスを「/ file-system」で開始しました。

次に、 ImageController と同様のアップロードルートを作成しましたが、dbImageオブジェクトはありません。

最後に、 FileLocationService を使用して画像を検索し、FileSystemResourceをHTTP応答として返すダウンロードルートがあります。

10. ファイルシステムイメージアーカイブテスト

これで、データベースバージョンと同じ方法でファイルシステムバージョンをテストできますが、パスは「file-system」で始まります。

curl -H "Content-Type: multipart/form-data" \
  -F "[email protected]" http://localhost:8080/file-system/image

1

そして、ダウンロードします:

curl -v http://localhost:8080/file-system/image/1 -o image.jpeg

11. 結論

この記事では、ファイルの内容を同じ行または外部の場所に配置して、ファイル情報をデータベースに保存する方法を学習しました。

また、マルチパートアップロードを使用してREST APIを構築およびテストし、Resourceを使用してダウンロード機能を提供してファイルを呼び出し元にストリーミングできるようにしました。

いつものように、コードサンプルはGitHubにあります。