0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像の出し方メモ

Posted at

DBに格納したBLOB画像を 最小構成で返すAPI を作るなら、
「DAOでbyte[]を取得 → Controllerでそのまま返す」だけで十分。
最低限 Cache-Controlオンザフライのリサイズ を足せば、現場で使いやすい実装になる。


設計の要点

  • DBは専用テーブルに格納
    例: IMAGES(id, content_type, bytes BLOB, updated_at)
  • DAOはシンプルに byte[] を返却
    DomaならSQLも書かずにOK。
  • Controllerは最低限のHTTPレスポンス
    Content-Type, Cache-Control, body だけ。
  • 拡張するならリサイズ程度
    ?w=200&h=200&fit=cover を受けてオンザフライ変換。
  • 余計な機能は省く
    ETag, AbortController対応, 署名付きURL などは不要。

実装例

1. DAO(Doma)

@Dao
@ConfigAutowireable
public interface ImageDao {
  @Select
  byte[] findBytesById(Long id);

  @Select
  String findContentTypeById(Long id);
}

2. サービス

@Service
public class ImageService {
  private final ImageDao dao;
  public ImageService(ImageDao dao) { this.dao = dao; }

  public Optional<ImageData> findImage(Long id, Integer w, Integer h) {
    byte[] bytes = dao.findBytesById(id);
    if (bytes == null) return Optional.empty();
    String contentType = dao.findContentTypeById(id);

    if (w != null && h != null) {
      try (var in = new ByteArrayInputStream(bytes)) {
        BufferedImage original = ImageIO.read(in);
        Image scaled = original.getScaledInstance(w, h, Image.SCALE_SMOOTH);
        BufferedImage output = new BufferedImage(w, h, original.getType());
        Graphics2D g2 = output.createGraphics();
        g2.drawImage(scaled, 0, 0, null);
        g2.dispose();
        try (var baos = new ByteArrayOutputStream()) {
          ImageIO.write(output, contentType.replace("image/", ""), baos);
          bytes = baos.toByteArray();
        }
      } catch (IOException ignore) {
        // リサイズ失敗時はオリジナルを返す
      }
    }
    return Optional.of(new ImageData(contentType, bytes));
  }

  public record ImageData(String contentType, byte[] bytes) {}
}

3. Controller

@RestController
@RequestMapping("/api/images")
public class ImageController {
  private final ImageService service;
  public ImageController(ImageService service) { this.service = service; }

  @GetMapping("/{id}")
  public ResponseEntity<byte[]> getImage(
      @PathVariable Long id,
      @RequestParam(required = false) Integer w,
      @RequestParam(required = false) Integer h
  ) {
    return service.findImage(id, w, h)
      .map(img -> ResponseEntity.ok()
        .contentType(MediaType.parseMediaType(img.contentType()))
        .cacheControl(CacheControl.maxAge(Duration.ofDays(7)))
        .body(img.bytes()))
      .orElse(ResponseEntity.notFound().build());
  }
}

フロント側からの利用例

<!-- オリジナル -->
<img src="/api/images/1" />

<!-- サムネイル(200x200にリサイズ) -->
<img src="/api/images/1?w=200&h=200" />

ポイントまとめ

  • 本当に必要な処理だけを残すと見通しが良い。
  • キャッシュは Cache-Control: max-age=… で十分。
  • 画像加工は「その場でやる」オンザフライ変換で軽量に対応。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?