LoginSignup
0
0

SpringBoot + MySQL 画像・動画登録の簡易メモ

Last updated at Posted at 2024-06-26

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 の追加を以下に記載

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) 設定

application.properties
spring.servlet.multipart.max-file-size=1073741824
spring.servlet.multipart.max-request-size=1073741824

画像・動画のみ判定するバリデーション作成

MultipartFile に対してコンテンツタイプを判定するカスタムバリデーションを作成
コンテンツタイプは念のため渡せるようにしておく
バリデーションを作るときは以下の4つをやる

  • アノテーションクラス
  • バリデーションクラス
  • メッセージプロパティ(デフォルト)
  • メッセージプロパティ(日本語用)

アノテーションクラス

prefixs にチェックするコンテンツタイプの接頭語(image, video, etc...)を渡せるようにする

FileContents.java
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 で始まるやつ以外ならエラーにする

FileContentsValidator.java
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 と同じ場所に作る

ValidationMessages.properties
file.contents.message = MediaType is {prefixs}

メッセージプロパティ(日本語用)

application.properties と同じ場所に作る

ValidationMessages_ja.properties
file.contents.message = コンテンツタイプが {prefixs} を選んでください

テーブルのレコードの値を持つDTO作成(フォームもこれ使う)

テーブルのレコード、フォームの入力に合わせてクラス変数を設定
バイナリデータを img タグの src で使えるよにするエンコード処理も作っておく

Contentwk.java
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 使ってバリデーション結果を設定してる

ContentsController.java
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 使って適当に設定
画像は丸枠、動画は四角枠で表示するようにする

contentwk.html
<!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 で以下みたいになる
画像、動画アップロードできて表示する感じです

image.png

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