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=…
で十分。 - 画像加工は「その場でやる」オンザフライ変換で軽量に対応。