JavaのDTOとは?複数のデータをまとめて受け渡すクラスの使い方

最終更新日

Schoo Java入門 中級 第6回 Webアプリケーション構築演習(基礎編)

計算した「小計・送料・合計」を、まとめて画面へ渡したい。このように、ひとまとまりのデータを別の処理へ受け渡すときに使うクラスが、DTOです。DTOを使えば、受け取った側で「何番目の値だろう」と考えずに、getSubtotal()getTotal() のような名前で必要な値を取り出せます。

DTOは、特別な命令ではありません。これまで学んだフィールド・コンストラクタ・getterを使ったJavaのクラスです。この記事では、見積もりを計算するModelから、Servletを経由してJSPへ、一つの計算結果を受け渡す例で使い方を確認します。

Schoo「Java入門 中級」第6回「Webアプリケーション構築演習(基礎編)」に対応する補足記事です。授業を受けていなくても読めるよう、この記事だけで例の仕様とコードを示します。WebのコードはJava 17・Tomcat 10.1の jakarta.servlet を前提にしています。

合計だけ返すと、画面に小計と送料を出せない

次のような見積もり画面を作るとします。DBへの保存や注文の確定は行わず、数量から金額を計算して表示するだけです。

  • 商品の単価は500円。数量は1〜20の整数を受け付ける。
  • 小計は「500円 × 数量」。
  • 小計が3000円未満なら送料300円、3000円以上なら送料0円。
  • 合計は「小計 + 送料」。画面には小計・送料・合計の3つを表示する。
数量小計送料合計
52500円300円2800円
63000円0円3000円

メソッドから int の合計2800だけを返すと、呼び出したServletには小計と送料が届きません。だからといって、JSPでもう一度送料を計算すると、送料のルールを変更したときに複数箇所の修正が必要になります。計算は一か所で行い、その結果をまとめて渡す方が、役割を分けやすくなります。

// この書き方では3つの値を返せません(コンパイルエラー)
return subtotal, shippingFee, total;

Javaのメソッドが一度のreturnで返す値は一つです。ただし、複数のフィールドを持つオブジェクトの参照を、一つの戻り値として返すことはできます。そこで、小計・送料・合計を持つ QuoteResult クラスを用意します。

return new QuoteResult(subtotal, shippingFee, total);

returnするものは一つですが、そのオブジェクトに3つの金額が入っています。受け取った側は、必要な金額をgetterで取り出します。

DTOとは「受け渡すデータをまとめる」という役割

DTOは Data Transfer Object の略です。本記事では、Modelの処理結果などを別の役割へ受け渡すためのデータクラスとして扱います。DTO という予約語や、必須のアノテーションがあるわけではありません。

クラス名も QuoteDTO でなければならないわけではありません。今回は「見積もり結果」であることが分かる QuoteResult と名付けます。クラスの名前だけではなく、何をまとめて、どこへ渡すのかがDTOを理解するポイントです。

担当今回のクラス・ファイルすること
Modelの計算処理QuoteModel数量を検証し、小計・送料・合計を計算する
結果のデータQuoteResult(DTO)計算済みの3つの金額を持ち、getterで返す
ControllerQuoteServlet入力を受け取り、Modelを呼び、結果をJSPへ渡す
Viewquote-result.jspDTOの値を取り出してHTMLに表示する

MVCのModelは、データや業務ルールを扱う部分を広く指します。DTOはModelと対立する「第4の役割」ではありません。ここではModel側の中でも、計算するクラスと、結果をまとめるクラスを分けていると考えてください。

イラスト:3つの金額を、一つの結果として画面へ渡す

数量5の例。QuoteModelで小計2500円・送料300円・合計2800円を計算し、QuoteResultにまとめて返す。Servletはquoteという名前でリクエスト属性に入れ、forward先のJSPがgetterで金額を取得してHTMLへ出力する。
数量5の場合を示しています。上から下の矢印は計算結果の受け渡し、最後の右向きの矢印はHTMLへの出力です。画像を選択すると拡大できます。

ServletがModelを呼び出した後、計算結果がこの順に渡ります。ブラウザへQuoteResultオブジェクトを直接送るわけではありません。JSPがサーバー側で金額をHTMLに埋め込み、ブラウザはそのHTMLを受け取ります。

1.結果をまとめるQuoteResultクラスを作る

src/main/java/model/QuoteResult.java

package model;

public class QuoteResult {
    private final int subtotal;
    private final int shippingFee;
    private final int total;

    public QuoteResult(int subtotal, int shippingFee, int total) {
        this.subtotal = subtotal;
        this.shippingFee = shippingFee;
        this.total = total;
    }

    public int getSubtotal() {
        return subtotal;
    }

    public int getShippingFee() {
        return shippingFee;
    }

    public int getTotal() {
        return total;
    }
}

このクラスが、今回のDTOです。コンストラクタで受け取った値をフィールドへ入れ、getterでそのまま取り出します。「3000円以上なら送料無料」といった条件分岐は、ここには書きません。今回は計算済みの結果を持つ役割に絞っています。

コード意味
private final int subtotal;このオブジェクトが持つ小計。privateなので外から直接書き換えず、finalなので初期化後に再代入しない。
this.subtotal = subtotal;引数subtotalの値を、このオブジェクトのフィールドsubtotalへ入れる。
getSubtotal()保存してある小計を返す。もう一度小計を計算する処理ではない。

この例では、計算後に値がばらばらに書き換わらないよう、finalのフィールドとgetterだけにしています。DTOに必ずfinalが必要、あるいは必ずsetterが必要という決まりはありません。また、DTOという名前が付いていれば値の正しさまで保証されるわけではありません。このサンプルではQuoteModelが整合する金額を計算してから渡します。

2.Modelで計算し、DTOを戻り値にする

src/main/java/model/QuoteModel.java

package model;

public class QuoteModel {
    public QuoteResult calculate(int quantity) {
        if (quantity < 1 || quantity > 20) {
            throw new IllegalArgumentException("数量は1〜20で指定してください。");
        }

        int subtotal = 500 * quantity;
        int shippingFee = 300;
        if (subtotal >= 3000) {
            shippingFee = 0;
        }
        int total = subtotal + shippingFee;

        return new QuoteResult(subtotal, shippingFee, total);
    }
}

メソッドの戻り値の型が int ではなく、QuoteResult になっています。数量5で呼ぶと、最後の行は new QuoteResult(2500, 300, 2800) と同じ値をコンストラクタに渡します。

QuoteModel model = new QuoteModel();
QuoteResult result = model.calculate(5);

System.out.println(result.getSubtotal());    // 2500
System.out.println(result.getShippingFee()); // 300
System.out.println(result.getTotal());       // 2800

これは呼び出す側の処理の抜粋です。result は、戻されたQuoteResultオブジェクトを指す変数です。「計算するためのmodel」と「計算結果を持つresult」は、別のオブジェクトです。

DTOに入れる値は、同じ型だけでなくても構いません。たとえば見積もり番号をString、合計をintで持つこともできます。ただし、今回必要なのは3つの金額なので、関係のない画面情報やDB接続情報まで一緒に入れません。

3.Servletで結果を受け取り、リクエスト属性に入れる

入力画面をアプリ直下に置き、POSTで数量を送ります。ここからはファイル単位のコードです。アプリのコンテキストパスは /lesson06 として説明します。環境が別名なら、その部分を読み替えてください。

src/main/webapp/quote-form.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>見積もり入力</title>
</head>
<body>
    <h1>見積もり入力</h1>
    <p>単価500円。小計3000円未満は送料300円です。</p>
    <form action="quote" method="post">
        <label for="quantity">数量(1〜20)</label>
        <input id="quantity" name="quantity" type="number"
               min="1" max="20" required>
        <button type="submit">見積もりを表示</button>
    </form>
</body>
</html>

src/main/java/controller/QuoteServlet.java

package controller;

import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import model.QuoteModel;
import model.QuoteResult;

@WebServlet("/quote")
public class QuoteServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        String text = request.getParameter("quantity");

        if (text == null || text.isBlank()) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            request.getRequestDispatcher("/quote-error.jsp")
                    .forward(request, response);
            return;
        }

        QuoteResult result;
        try {
            int quantity = Integer.parseInt(text.trim());
            QuoteModel model = new QuoteModel();
            result = model.calculate(quantity);
        } catch (IllegalArgumentException e) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            request.getRequestDispatcher("/quote-error.jsp")
                    .forward(request, response);
            return;
        }

        request.setAttribute("quote", result);
        request.getRequestDispatcher("/quote-result.jsp")
                .forward(request, response);
    }
}

DTOに関係するのは、次の3か所です。

  1. result = model.calculate(quantity); で計算結果を受け取る。
  2. request.setAttribute("quote", result); で、その結果をリクエストに預ける。
  3. forward() で、同じリクエストを使ってJSPへ処理を進める。

DTOとリクエストスコープは別のものです。DTOは「受け渡すデータの形」、リクエストスコープは「そのデータを置いておく場所」です。setAttributeでDTOの各フィールドを文字列へ変換するわけでも、別のDTOを自動で作るわけでもありません。このサンプルでは同じオブジェクトの参照を渡しています。

一方、getParameter("quantity") はブラウザから送られた入力をStringとして取得する処理です。入力パラメータのquantityと、サーバー内で設定した属性のquoteは区別してください。

入力が空、整数ではない、範囲外の場合は400とエラー画面を返します。NumberFormatExceptionはIllegalArgumentExceptionの子クラスなので、ここでは数値変換とModelの検証を同じcatchで扱えます。HTMLのmin・max・requiredだけに頼らず、サーバー側でも確認しています。

4.JSPはDTOから値を取り出して表示する

src/main/webapp/quote-result.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="model.QuoteResult" %>
<%
    QuoteResult result =
            (QuoteResult) request.getAttribute("quote");
%>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>見積もり結果</title>
</head>
<body>
    <h1>見積もり結果</h1>
    <% if (result != null) { %>
        <p>小計: <%= result.getSubtotal() %>円</p>
        <p>送料: <%= result.getShippingFee() %>円</p>
        <p>合計: <%= result.getTotal() %>円</p>
    <% } else { %>
        <p>入力画面から数量を送信してください。</p>
    <% } %>
    <a href="quote-form.html">入力画面に戻る</a>
</body>
</html>

getAttribute("quote") の戻り値はObject型です。この例ではServletがQuoteResultを入れているので、(QuoteResult) とキャストして受け取ります。キャストで新しいDTOを作ったり、計算したりするわけではありません。

JSPに小計や送料の計算式はありません。getSubtotal()getShippingFee()getTotal() で、Modelが計算した値を表示するだけです。送料のルールを変える場合はQuoteModelの計算を直し、JSPの表示はそのまま使えます。

quote-result.jspを直接開くと、Servletを通らないのでquote属性はありません。取得結果はnullになります。この例ではnullを確認し、入力画面からの送信を案内しています。

入力エラー用:src/main/webapp/quote-error.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>入力内容の確認</title>
</head>
<body>
    <h1>入力内容の確認</h1>
    <p>数量は1〜20の整数で入力してください。</p>
    <a href="quote-form.html">入力画面に戻る</a>
</body>
</html>

この例がHTMLへ出しているのは、固定文言と計算済みのint値です。DTOに商品名などの文字列を追加する場合、DTOに入れたこと自体では安全になりません。外部から来た文字列をHTMLへ表示するときは、出力に応じたエスケープが必要です。

動作確認:DTOの値と画面の値がつながっているか

http://localhost:8080/lesson06/quote-form.html を開きます。Servletの /quote を直接開くのではなく、フォームから送信してください。このServletはPOSTを受け付ける設計です。

確認する操作期待する結果見るポイント
数量5を送信小計2500円・送料300円・合計2800円3つのgetterで、同じ見積もりの内訳を表示できる
数量6を送信小計3000円・送料0円・合計3000円送料無料の境界がModelの計算に反映される
数量20を送信小計10000円・送料0円・合計10000円許容範囲の上限でも表示できる
数量0・21・abcを直接POST400・入力エラー画面画面側の制限を回避した入力も検証される
quote-result.jspを直接開く入力画面から数量を送信するよう案内DTOがない経路でnullのままgetterを呼ばない

通常のブラウザ操作では、numberやmin・maxの制約で不正な送信が止まる場合があります。サーバー側の検証は、HTTPクライアントなどで不正値を直接送るテストと分けて確認します。

配列やMapで返す方法とは何が違うのか

今回の3つの値は、配列で返すこともできます。ただし、受け取る側に「0番は小計、1番は送料、2番は合計」という約束が必要になります。

// 配列で返す場合の取り出し方
int total = amounts[2];

// DTOで返す場合の取り出し方
int total = result.getTotal();

配列は同じ種類の要素の並びを扱うのが得意です。小計・送料・合計のようにそれぞれ意味が違う値には、名前の付いたフィールドとgetterがあると、使い間違いに気付きやすくなります。

Mapならキーに名前を付けられますが、amounts.get("totla") のようなキーの誤字はコンパイル時には分かりません。DTOで存在しない getTotla() を呼べばコンパイルエラーになります。必要な項目と型をクラスで明示できるのが、DTOを用意するメリットです。

ただし、合計一つしか使わないメソッドなら、intで返すだけで十分な場合もあります。どんな戻り値にもDTOを作るのではなく、関連する複数の値を一つの結果として渡したいかを基準に考えます。

DTOを使うときに混同しやすいこと

  • DTOを作るだけではDBには保存されない。保存が必要なら、別途DAOなどでDB処理を書く。今回の例にDBは登場しない。
  • DTOはリクエスト専用ではない。通常のJavaのメソッド間でも使える。今回はJSPへ渡すためにリクエスト属性を使っている。
  • getterを呼んでも再計算はしない。今回のgetterは保存済みの値を返すだけ。送料のルールはQuoteModelにある。
  • DTOに入れればすべて正しい値になるわけではない。入力の検証や計算の責任は、設計した処理側で持つ。
  • DTOは「使う目的」の呼び名。この例では特別な継承やimplements、Serializableの指定は必要ない。

確認問題

この記事の単価500円、数量1〜20、小計3000円以上で送料無料という仕様を使って考えてください。

  1. model.calculate(5) が返したDTOについて、getSubtotal()・getShippingFee()・getTotal()が返す値をそれぞれ答えてください。
  2. 数量6ではDTOの3つの値はどうなりますか。また、送料無料になる基準を5000円へ変更するとき、DTO・Model・JSPのどこを修正しますか。
  3. 次の処理を担当するファイルを答えてください。「送料を決める」「計算結果の3つの値を持つ」「結果をrequest属性へ入れる」「結果の金額をHTMLへ出す」。
  4. Servletでは setAttribute("quote", result)、JSPでは getAttribute("result") と書きました。何が起き、どう直しますか。JSPでnew QuoteResult(…)を書いて作り直すべきでしょうか。

演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。

まとめ

  • DTOは、関連するデータを一つにまとめて受け渡すためのクラス。
  • Modelで計算した結果をDTOで返せば、呼び出し側は名前付きのgetterで取り出せる。
  • DTO・リクエストスコープ・JSPの役割は別。データの形、置き場所、表示を分けて考える。
  • 計算する処理と表示する処理に同じ計算式を重複させず、一つの結果を受け渡す。

関連記事

シェアする