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

今回は、DTO(Data Transfer Object)を学びます。DTOは、データの受け渡しを整理し、コードをシンプルにする役割を持つクラスです。ControllerとModelやViewの間だけでなく、次回のデータベースとのやり取りでも役立ちます。DTOを活用することで、より分かりやすく保守しやすいコードを書くことができます。

DTOは、データの受け渡しを整理し、コードをシンプルにする

1. DTO(Data Transfer Object)とは

DTO(Data Transfer Object)は、アプリケーション内でデータをやり取りするためのオブジェクトです。特に、コントローラーとビューの間でデータをやり取りする際に利用され、データ構造を整理し、コードをシンプルにする役割を果たします。

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.io.Serializable; // オブジェクトのバイト列化(シリアライズ)を可能にするインターフェース
import java.time.LocalDateTime; // 日付と時刻を扱うためのクラス

public class CarDto implements Serializable {

	/** 車のID(データベースの主キーを想定) */
	private int carId;

	/** 車の名前またはモデル名 */
	private String name;

	/** 車の価格(整数型で金額を格納) */
	private int price;

	/**
	 * 論理削除された日時を文字列として格納。 DBのカラムが date/datetime であっても、 プログラム側で文字列として管理する場合がある。 null
	 * や空文字の場合は「未削除」を表す想定。
	 */
	private String deletedAt;

	/**
	 * デフォルトコンストラクタ(引数なし) JavaBeans ルールに従い、 new CarsDto() のように引数無しで生成可能にする。
	 */
	public CarDto() {
	}

	/**
	 * フィールドをまとめて初期化するコンストラクタ。
	 * 
	 * @param carId     車のID
	 * @param name      車の名前
	 * @param price     車の価格
	 * @param deletedAt 論理削除された日時(未削除の場合はnullや空文字)
	 */
	public CarDto(int carId, String name, int price, String deletedAt) {
		this.carId = carId;
		this.name = name;
		this.price = price;
		this.deletedAt = deletedAt;
	}

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

	/**
	 * 車のIDを設定するメソッド。
	 * 
	 * @param carId セットしたいID
	 */
	public void setCarId(int carId) {
		this.carId = carId;
	}

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

	/**
	 * 車の名前を設定するメソッド。
	 * 
	 * @param name セットしたい車の名称
	 */
	public void setName(String name) {
		this.name = name;
	}

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

	/**
	 * 車の価格を設定するメソッド。
	 * 
	 * @param price セットしたい価格
	 */
	public void setPrice(int price) {
		this.price = price;
	}

	/**
	 * 論理削除された日時を取得するメソッド。
	 * 
	 * @return deletedAt 物理削除されず、論理削除された場合は日時文字列を格納
	 */
	public String getDeletedAt() {
		return deletedAt;
	}

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

	/**
	 * デバッグやログ出力などでオブジェクト内容を簡単に把握できるよう、 フィールドの値を文字列として整形して返す。
	 * 
	 * @return CarsDtoのフィールド情報を整形した文字列
	 */
	@Override
	public String toString() {
		return "CarsDto [carId=" + carId + ", name=" + name + ", price=" + price + ", deletedAt=" + deletedAt + "]";
	}

	/**
	 * テスト用のメインメソッド。 単体で実行した際、CarsDtoオブジェクトを生成して内容を標準出力に表示する。
	 * 
	 * @param args コマンドライン引数(未使用)
	 */
	public static void main(String[] args) {

		// 日付部分をString化して渡す(例としてLocalDateTimeを利用)
		CarDto car = new CarDto(16, "hustler", 200, LocalDateTime.now().toLocalDate().toString());
		// 生成したCarsDtoをコンソールに表示して確認
		System.out.println(car);
	}
}

このクラスのポイント

  1. JavaBeansのルールに従い、引数なしのコンストラクタを提供しています。フレームワーク(Spring)がオブジェクトを生成する際に利用します。
  2. JavaのSerializableインターフェースを実装しており、オブジェクトのシリアライズ(直列化)が可能です。セッションへの保存やキャッシュ、通信時にオブジェクトをシリアライズして転送可能にします。通常、DTOクラスはSerializableにしておくことが推奨されます。

JavaBeansとシリアライズ

JavaBeansとは、次の規約を満たしたJavaクラスです。

  • 引数なしコンストラクタを持つ。
  • フィールドはprivateで定義され、getterとsetterメソッドを介してアクセス。
  • プロパティ(後述)を操作する際に用います。

一方、Serializableとは、Javaオブジェクトをファイルやネットワークを通じて転送できるようにする仕組み(シリアライズ)を可能にするためのインタフェースです。

Spring Bootでは両者を組み合わせ、データ転送用のDTOとしてよく使用します。


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 のチェック)

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

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

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

4. ThymeleafとJavaBeans(プロパティアクセス)

JavaBeansの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と書くことによってkazuateクラスのインスタンスのgetEmail()メソッドが働きます。

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


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

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

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

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

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

5. DTOを使うメリット

  1. データの整理
    • コントローラーやビューでのデータ管理が明確になる。
  2. コードの簡潔化
    • ビジネスロジックを持たない単純なデータの受け渡しをシンプルにする。
  3. 再利用性の向上
    • DTOを使うことで、異なる部分でのデータ再利用が可能になる。

穴埋め問題

空欄①〜⑨に適切なコードを記入してください。

問題1:DTOクラスの作成

DTOクラス(UserDto)を作成しています。

public class UserDto {

    private String name;
    private int age;

    // getterメソッド
    public String getName() {
        return name;
    }

    public int getAge() {
        return ①(       );
    }

    // setterメソッド
    public void setName(String name) {
        this.name = ②(       );
    }

    public void setAge(int age) {
        this.age = age;
    }
}

問題2(ControllerでDTOを利用する)

@Controller
public class UserController {

    @GetMapping("/user")
    public String userInfo(Model model) {
        UserDto user = new UserDto();
        user.setName("Taro");
        user.③(       )(25);

        model.④(       )("user", user);
        return "userView";
    }
}

問題3(ThymeleafでDTOを使った表示)

ControllerでUserDto型のuserを渡したとき、Thymeleafで以下のように書いて値を表示します。

<p th:text="${user.⑤(       )}">ここに名前</p>
<p th:text="${user.age}">ここに年齢</p>

問題4(DTOのコンストラクタ利用)

以下のDTOクラスにおいて、引数を2つ取るコンストラクタを作成してください。

public class ProductDto {

    private String productName;
    private int price;

    public ProductDto(⑥(            ), int price) {
        this.name = name;
        this.age = ⑦(           );
    }

    // getter, setter省略
}

問題5(ControllerでDTOの活用)

以下のControllerにおいて、受け取ったリクエストパラメータをDTOに設定し、ビューに渡す処理を穴埋めしてください。

@Controller
public class OrderController {

    @GetMapping("/order")
    public String order(
            @RequestParam("item") String item,
            @RequestParam("quantity") int quantity,
            Model model) {

        OrderDto order = new OrderDto();
        order.setItemName(⑧(          ));
        order.setQuantity(quantity);

        model.addAttribute("⑨(        )", order);
        return "orderView";
    }
}

<まとめ:隣の人に正しく説明できたらチェックを付けましょう>

□ DTO(Data Transfer Object)は、アプリケーション内でデータをやり取りするためのオブジェクト

□ 本研修では ControllerとView の間や、 データベースとのやり取りで活用される

□ ビジネスロジックを持たず、データ転送専用のオブジェクト

□ DTOはJavaBeansのルールに従う(引数なしのコンストラクタ、getter/setter を用意)

□ Thymeleafで ${インスタンス名.プロパティ名}と書くと、ゲッターを呼び出し値を表示する。値がnullの場合は何も表示されない

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

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

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