SpringBoot で MySQL 使って画像・動画登録したときの簡易メモ
テーブルにBLOB型のカラム作って、アプリ・DBともに 1GB まで送れるように設定変更してやってく感じです
JPA 使わずに JDBCTemplate 使ってたりエンティティとフォームで同じクラス使ったりとごまかしてます...
大雑把な流れ
- 接続DB, ユーザ, テーブル作成
- プロジェクト作成
- DB・アプリともにデータ送信の上限設定
- 画像・動画のみ判定するバリデーション作成
- テーブルのレコードの値を持つDTO作成(フォームもこれ使う)
- コントローラ作成
- HTML作成
実行環境
- Windows11
- Pleiades 2024 Full Edition
- Maven v3.9.6
- pringFramework Boot v3.3.0
- Java v21
- spring-boot-starter-thymeleaf
- spring-boot-starter-web
- spring-boot-starter-data-jdbc
- spring-boot-starter-validation
- MySQL v8.0.28
接続DB, ユーザ, テーブル作成
接続するDB, ユーザ, テーブルを作成する
dbname, dbuser, dbpass は適当に好きな値に変えてOKです(後続はこの値を使用)
# DB作成
CREATE DATABASE dbname;
# ユーザ作成・権限追加
CREATE USER 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';
GRANT ALL PRIVILEGES ON dbname.* TO 'dbuser'@'localhost';
# 使用DB設定
use dbname
# テーブル作成 content が画像・動画のバイナリ入るカラム
CREATE TABLE IF NOT EXISTS contentwk (
id INT AUTO_INCREMENT,
title VARCHAR(20),
content_type VARCHAR(20),
content MEDIUMBLOB,
PRIMARY KEY(id)
);
プロジェクト作成
Springスタータ・プロジェクト でプロジェクト作成する
依存関係は以下を設定する
- Spring Boot DevTools
- Spring Web
- Lombok
- Thymeleaf
- MySQL Driver
- Spring Data JDBC # 先のこと考えると Spring Data JPA で良いかも...
- Validation
あと DB 設定など application.properties
の追加を以下に記載
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/dbname
spring.datasource.username=dbuser
spring.datasource.password=dbpass
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# sql log
logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
logging.level.org.springframework.jdbc.core.StatementCreatorUtils=TRACE
DB・アプリともにデータ送信の上限設定
両方とも設定を変更して1GBまで送れるようにしてます
DBは総計なのでファイルだけだと1GB無理かも...
他にもなんかしら設定変更が必要な可能性ありそうだな...
DB (MySQL) 設定
SET GLOBAL max_allowed_packet=1073741824;
アプリ (SpringBoot) 設定
spring.servlet.multipart.max-file-size=1073741824
spring.servlet.multipart.max-request-size=1073741824
画像・動画のみ判定するバリデーション作成
MultipartFile に対してコンテンツタイプを判定するカスタムバリデーションを作成
コンテンツタイプは念のため渡せるようにしておく
バリデーションを作るときは以下の4つをやる
- アノテーションクラス
- バリデーションクラス
- メッセージプロパティ(デフォルト)
- メッセージプロパティ(日本語用)
アノテーションクラス
prefixs にチェックするコンテンツタイプの接頭語(image, video, etc...)を渡せるようにする
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Constraint(validatedBy = FileContentsValidator.class)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FileContents {
String message() default "{file.contents.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] prefixs();
}
バリデーションクラス
コンテンツタイプが prefixs で始まるやつ以外ならエラーにする
import java.util.Arrays;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class FileContentsValidator implements ConstraintValidator<FileContents, MultipartFile> {
private String[] prefixs;
@Override
public void initialize(FileContents annotation) {
this.prefixs = annotation.prefixs();
}
@Override
public boolean isValid(MultipartFile value, ConstraintValidatorContext context) {
return value != null
&& Arrays.asList(prefixs).stream().anyMatch(prefix -> value.getContentType().startsWith(prefix));
}
}
メッセージプロパティ(デフォルト)
application.properties と同じ場所に作る
file.contents.message = MediaType is {prefixs}
メッセージプロパティ(日本語用)
application.properties と同じ場所に作る
file.contents.message = コンテンツタイプが {prefixs} を選んでください
テーブルのレコードの値を持つDTO作成(フォームもこれ使う)
テーブルのレコード、フォームの入力に合わせてクラス変数を設定
バイナリデータを img タグの src で使えるよにするエンコード処理も作っておく
import java.util.Base64;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Contentwk {
// テーブルのみ
private Integer id;
private String contentType;
private byte[] content;
// テーブル, 入力フォーム
@NotBlank
private String title;
// 入力フォームのみ
@FileContents(prefixs = { "image", "video" })
private MultipartFile contentFile;
// バイナリをエンコードする
public String getContentEncoding() {
return Base64.getEncoder().encodeToString(content);
};
// src で使えるよに文字列生成
public String getSrc() {
return "data:" + contentType + ";base64," + Base64.getEncoder().encodeToString(content);
};
}
コントローラ作成
登録・参照のアクションをつくる
RedirectAttributes
使ってバリデーション結果を設定してる
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class ContentController6 {
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("/contentwk")
public String select(Model model) {
String sql = "SELECT id, title, content_type, content FROM contentwk";
RowMapper<Contentwk> rowMapperss = new BeanPropertyRowMapper<>(Contentwk.class);
List<Contentwk> list = jdbcTemplate.query(sql, rowMapperss);
model.addAttribute("list", list);
model.addAttribute("errResult", model.asMap().get("errResult"));
return "contentwk";
}
@PostMapping(value = "/contentwk/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(Model model, @Validated @ModelAttribute Contentwk contentwk, BindingResult result,
RedirectAttributes attributes) throws IOException {
if (result.hasErrors()) {
attributes.addFlashAttribute("errResult", result);
return "redirect:/contentwk";
}
String sql = "INSERT INTO contentwk (title, content_type, content) VALUES (?, ?, ?)";
Object[] args = { contentwk.getTitle(), contentwk.getContentFile().getContentType(),
contentwk.getContentFile().getBytes() };
jdbcTemplate.update(sql, args);
return "redirect:/contentwk";
}
}
HTML作成
CSS は BootStrap5 使って適当に設定
画像は丸枠、動画は四角枠で表示するようにする
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Content Image Video Upload</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<div class="container text-center">
<h2 class="display-4 m-3">Contents Upload</h2>
<form th:action="@{/contentwk/upload}" method="post" enctype="multipart/form-data">
<div class="row m-3">
<div class="col-md-1">
<label for="title" class="col-form-label">タイトル</label>
</div>
<div class="col">
<input type="text" name="title" class="form-control" placeholder="title">
</div>
</div>
<div class="row m-3">
<div class="col-md-1">
<label for="content" class="col-form-label">コンテンツ</label>
</div>
<div class="col">
<input class="form-control" type="file" name="contentFile" accept="image/*, video/*">
</div>
</div>
<button type="submit" class="btn btn-primary">Upload</button></td>
</form>
<div th:if="${errResult}" class="col-auto text-danger">
<li th:each="err : ${errResult.fieldErrors}" th:text="${err.field + ':' + err.defaultMessage}"></li>
</div>
<hr />
<h2 class="display-4 m-3">Contents View</h2>
<div class="row">
<div class="col-lg-4 col-md-6 mb-3" th:each="content : ${list}">
<th:block th:if="${#strings.startsWith(content.contentType, 'image')}">
<img class="mx-auto rounded-circle" width="300" height="300" th:src="${content.src}" loading="lazy" />
</th:block>
<th:block th:if="${#strings.startsWith(content.contentType, 'video')}">
<video controls class="mx-auto" width="300" height="300" th:src="${content.src}" loading="lazy"></video>
</th:block>
<h4 class="fw-normal" th:text="${content.title}"></h4>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>
実行確認
http://localhost:8080/contentwk で以下みたいになる
画像、動画アップロードできて表示する感じです