前回は「Thymeleaf」を学び、Controllerから渡されたデータを画面に動的に表示する方法を学習しました。Thymeleafを使うことで、Webページの表現が多彩になります。

今回は、DTO(Data Transfer Object)を学びます。DTOは、複数の関連するデータをひとつのオブジェクトにまとめることでデータの受け渡しを整理し、コードをシンプルにする役割を持つクラスです。次章で学ぶDAOとDTOで名前が似ていますが、TransferのTで覚えて下さい。どちらもMVCのMにあたります。

1. DTO(Data Transfer Object)とは

DTO(Data Transfer Object)は、アプリケーション内でデータをやり取りするためのオブジェクトです。データ構造を整理し、コードをシンプルにする役割を持ち、本研修では主に以下の場面で利用します。

  • ブラウザからのリクエストをコントローラーで受け取るとき
  • コントローラーからビジネスロジックにデータを引き渡すとき
  • Daoクラス(後述するデータアクセス用のクラス)との間でデータをやり取りするとき
DTOはお弁当箱

例えるなら、DTOクラスはおかずやご飯を詰めて持ち運ぶ「お弁当箱」です。いろいろな食材(データ)を整理して、相手に届けるための「入れ物」です。味付けや調理(処理)は済んでおり、DTOはそれを詰めて持ち運ぶだけです。DTOは、ビジネスロジック(計算や判断)、データベース処理などは行いません。

DTOは用途に応じてModelまたはHttpSessionに入れて利用します。

Modelに入れるのは画面表示用です。Modelの特性として1つのリクエスト・レスポンスで消えるため一時的なデータに適しています。

一方、HttpSessionオブジェクトに入れるのはログイン情報や買い物かごなどの状態保持のためです。複数のリクエストとレスポンスでデータを保持できます。

2. Spring BootにおけるDTOの利用

本記事では、DTOを用いて自動車の情報を管理し、ビューに表示する方法を紹介します。

3. サンプルプロジェクトの構成

com.example.demo
├── model
│   ├── CarDto.java
├── controller
│   ├── CarController.java
├── templates
│   ├── cars.html

3.1 CarDto.java(DTOクラス)

解説はコメントを見て下さい。

package com.example.demo.model;

import java.time.LocalDateTime; // 日付と時刻を扱うためのクラス

/**
 * 車情報を保持するDTOクラス。
 */
public class CarDto {

	/** 車のID */
	private int carId;

	/** 車の名前 */
	private String name;

	/** 車の価格 */
	private int price;

	/** 論理削除された日時 */
	private String deletedAt;

	/**
	 * デフォルトコンストラクタ。
	 */
	public CarDto() {
	}

	/**
	 * 車情報を初期化するコンストラクタ。
	 *
	 * @param carId     車のID
	 * @param name      車の名前
	 * @param price     車の価格
	 * @param deletedAt 論理削除された日時
	 */
	public CarDto(int carId, String name, int price, String deletedAt) {
		this.carId = carId;
		this.name = name;
		this.price = price;
		this.deletedAt = deletedAt;
	}

	/**
	 * 車のIDを取得する。
	 *
	 * @return 車のID
	 */
	public int getCarId() {
		return carId;
	}

	/**
	 * 車のIDを設定する。
	 *
	 * @param carId 車のID
	 */
	public void setCarId(int carId) {
		this.carId = carId;
	}

	/**
	 * 車の名前を取得する。
	 *
	 * @return 車の名前
	 */
	public String getName() {
		return name;
	}

	/**
	 * 車の名前を設定する。
	 *
	 * @param name 車の名前
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * 車の価格を取得する。
	 *
	 * @return 車の価格
	 */
	public int getPrice() {
		return price;
	}

	/**
	 * 車の価格を設定する。
	 *
	 * @param price 車の価格
	 */
	public void setPrice(int price) {
		this.price = price;
	}

	/**
	 * 論理削除された日時を取得する。
	 *
	 * @return 論理削除された日時
	 */
	public String getDeletedAt() {
		return deletedAt;
	}

	/**
	 * 論理削除された日時を設定する。
	 *
	 * @param deletedAt 論理削除された日時
	 */
	public void setDeletedAt(String deletedAt) {
		this.deletedAt = deletedAt;
	}

	/**
	 * オブジェクトの内容を文字列として返す。
	 *
	 * @return CarDtoのフィールド情報
	 */
	@Override
	public String toString() {
		return "CarDto [carId=" + carId + ", name=" + name + ", price="
				+ price + ", deletedAt=" + deletedAt + "]";
	}

	/**
	 * 動作確認用のメインメソッド。
	 *
	 * @param args コマンドライン引数
	 */
	public static void main(String[] args) {

		// 現在日時を文字列に変換して、CarDtoオブジェクトを生成する
		CarDto car = new CarDto(16, "hustler", 200, LocalDateTime.now().toString());

		// 生成したCarDtoオブジェクトの内容をコンソールに表示する
		System.out.println(car);
	}
}

調べてみましょう

名前の由来はコーヒー豆

JavaBeansとは、Javaで部品のように再利用しやすいクラスを作るための決まりです。

主に「引数なしコンストラクタを持つ」「フィールドはprivateにする」「getter/setterで値を取得・設定する」などの特徴があります。DTOは多くの場合JavaBeansの形式で作られます。

ただし、この研修ではJavaBeans特有の高度な機能を使うわけではありませんので、JavaBeansを使用していません。

3.2 CarController.java(コントローラー)

package com.example.demo.controller;

import com.example.demo.model.CarDto;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.ArrayList;
import java.util.List;

@Controller
public class CarController {

    @GetMapping("/cars")
    public String showCars(Model model) {
        List<CarDto> carsList = new ArrayList<>();
        carsList.add(new CarDto(1, "セダン", 2590000, "2024-04-01"));
        carsList.add(new CarDto(2, "クーペ", 4990000, null));
        carsList.add(new CarDto(3, "SUV", 2990000, null));
        
        model.addAttribute("carsList", carsList);
        return "cars";
    }
}

なお、このようにコントローラー内で直接リストやDTOを生成しているのは、実務の観点からは推奨されません。コードが肥大化し、読みづらくなってしまいます。今回は説明のために簡略化しています。

3.3 cars.html(Thymeleafテンプレート)

<!doctype html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8" />
    <title>自動車の販売</title>
</head>
<body>
    <h2>取扱商品</h2>
    <div th:each="c : ${carsList}" th:if="${c.deletedAt == null}">
        <p>車名: <span th:text="${c.name}"></span></p>
        <p>価格: <span th:text="${c.price} + '円'"></span></p>
        <hr>
    </div>
</body>
</html>

このコードのポイント

th:each による繰り返し処理

<div th:each="c : ${carsList}">
  1. ${carsList} という名前でコントローラーから渡されたDTOのリストを順番に繰り返し処理します。
  2. Thymeleafで繰り返しを行う場合、この書式を使います。
  3. th:each は 「変数名 : コレクション」 の形式で記述するのでしたね。

今回は販売中の車のみ表示したいため、deletedAt がnullの車だけを表示しています。

<div th:each="c : ${carsList}" th:if="${c.deletedAt == null}">

そして、以下のようにc.nameとだけ書いてCarDtoクラスのgetName()メソッドを呼び出すのが次に学ぶプロパティアクセスです。

<p>車名: <span th:text="${c.name}"></span></p>

4. ThymeleafとDTO

getter/setterを使う

Thymeleafで ${bean.field} と書くと、getField() メソッドを呼び出します。プロパティアクセスといいます。

プロパティとは、getter/setterメソッドのXxxxの先頭文字を小文字にしたものです。ちなみに、英語の【property】には所有物や特性という意味があります。
public class CustomerBean implements Serializable {
    private int customerId;
    private String name;

    // getter/setter ...
}

<p th:text="${customer.customerId}"></p>
<p th:text="${customer.name}"></p>

これで getCustomerId() や getName() が呼ばれます。nullの場合は何も表示されません(th:textは空文字となる)。

例えば、customer.emailと書くことによってCustomerクラスのインスタンスのgetEmail()メソッドが働きます。

  1. ①${customer.telephone}という記述によって働くのはどのクラスの何というメソッドですか?
あなたの答え:
  1. ②CustomerクラスのgetAddress()というメソッドをプロパティアクセスするにはどう書けばいいですか?
あなたの答え:


プロパティ化したいメソッド名の先頭にはget(is)/setを付けるというルールがあります。

なお、getter/setterメソッドはアクセサメソッド【accessor methods】と総称されることもあります。アクセサメソッドはIDEを使っていれば簡単に挿入できます。皆さんはもうマスターしていますか?

フィールドを private で修飾するとクラス外からはアクセスできなくなるということを我々はカプセル化と情報隠蔽のところで学びました。そこでアクセサメソッドを使ってフィールドにアクセスします。

ここである程度勉強している人は「情報隠蔽をしてもアクセサメソッドがあれば同じではないか?」と考えるかも知れません。しかし、思い出していただきたいのですが、int型のフィールドにはint型の値であれば何でも入ったのに対して、メソッドを経由すれば、特定の値(例えば、マイナスの値)は入らないようにすることもできましたね。さらに、フィールドに読み込みだけ可能で、一切書き込みができないクラスを作りたいのであれば、setterメソッドを作らなければよいのです。

なお、このプロパティの考え方は他のオブジェクト指向言語にも発展的に採用されています。例えばC#、Python、Kotlin等のプロパティはより便利に使えるようになっています。

データバインディングの仕組みと実装例

Webアプリケーションの構築において、画面のフォームに入力されたデータをサーバー側で受け取り、適切に処理する工程は不可欠です。Spring Bootでは、送信されたデータをJavaのオブジェクトへ自動的に割り当てる機能が提供されています。

データバインディングとは?

データバインディングとは、画面の入力フォームで指定された名前と、DTOの中に用意された変数の名前を結びつけ、送信された値を自動的に代入する仕組みのことです。

ソースコードの例

以下は、画面からの入力を受け取るためのUserDtoクラスの定義です。

package com.example.demo.model;

public class UserDto {

    private String loginId;
    private String password;

    public UserDto() {
    }

	public String getLoginId() {
		return loginId;
	}

	public void setLoginId(String loginId) {
		this.loginId = loginId;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

}

以下は、送信されたデータを受け取ってログイン判定を行うLoginControllerクラスの処理です。

	@PostMapping("/login")
	public String login(UserDto userDto, HttpSession session, Model model) {

		String loginId = userDto.getLoginId();
		String password = userDto.getPassword();

		if (loginId == null || loginId.isEmpty() || password == null || password.isEmpty()) {
			model.addAttribute("error", "ユーザーIDまたはパスワードを入力してください");
			return "login-error";
		}

		if ("imai".equals(loginId) && "p".equals(password)) {
			session.setAttribute("loginId", loginId);
			return "redirect:/home";
		}

		model.addAttribute("error", "ユーザーIDまたはパスワードが間違っています");
		return "login-error";
	}

データバインディングが実行される流れ

上記のコードではLoginControllerのloginメソッドの第一引数には、UserDto型の引数が定義されています。

画面のHTML側で、入力項目のname属性にloginIdおよびpasswordという値を指定してデータを送信すると、Spring Bootは以下の手順でデータバインディングを実行します。

  1. 送信されたパラメータ名であるloginIdとpasswordの名称を識別します。
  2. loginメソッドの引数に指定されているUserDtoクラスの内部を探します。
  3. 命名規則に適合するセッターメソッドであるsetLoginIdおよびsetPasswordを自動的に呼び出します。
  4. メソッドの引数として渡された入力文字列を、オブジェクト内のフィールド変数へと格納します。

バインディングが完了した状態でloginメソッドの内部処理が始まるため、メソッドの冒頭にある「userDto.getLoginId()」や「userDto.getPassword()」を呼び出すことで、開発者が個別に抽出処理を記述することなく、画面の入力値を取り出すことが可能となります。

データバインディングを使用するメリット

  • 画面の入力項目が多数存在する場合でも、コントローラーの引数を増やす必要がないため、プログラムの見通しがシンプルになります。
  • 画面から送信された文字列を、Java内部で扱うオブジェクト型や数値型へ自動的に変換する処理が内部で行われるため、型変換(Integer.parseInt()メソッドを使った文字列から整数値への変換など)の記述を削減できます。
  • データのまとまりを1つのインスタンスとして扱えるため、データを一括して転送することが容易になります。

データバインディングの注意点

  • HTMLのname属性に指定した文字列と、DTO側に定義したセッターメソッドの名称が1文字でも異なっている場合、紐付けが行われず、対象のフィールド値はnullに設定されます。

DTOを使うことで、Spring Bootのアプリケーションにおけるデータの受け渡しを整理し、可読性の高いコードを実現できます。本記事のサンプルを参考に、DTOを活用したクリーンな設計を意識してみましょう。

第7章の今回はDTOによってデータの受け渡しを整理する方法について学びました。

第8章は「Spring BootとMySQLの連携 〜 データを保存・取得しよう」です。