recordでDTOを書く
このレッスンでは、Java の record がどういう仕組みで、なぜ DTO に向いているのかを説明できるようになります。
クラスで DTO を書くと長くなります
値を運ぶだけのクラスを従来の書き方で作ると、フィールド、コンストラクタ、getter、equals、hashCode、toString をすべて手で書くことになります。項目が3つあるだけで50行を超えることも珍しくありません。しかも本質的な情報は「id と name と email を持つ」という1行だけです。
record は宣言1行で済みます
Java
public record UserResponse(Long id, String name, String email) {}この1行から、コンパイラが次のものを自動で生成します。
| 生成されるもの | 内容 |
|---|---|
| フィールド | private final な id、name、email |
| 正準コンストラクタ | 宣言の順に3つの引数を取るコンストラクタ |
| アクセサ | id()、name()、email() (getId() ではありません) |
| equals と hashCode | 全コンポーネントの値で比較する実装 |
| toString | UserResponse[id=1, name=taro, email=t@example.com] の形 |
アクセサの名前が getXxx ではなくコンポーネント名そのままである点に注意してください。Jackson は record を認識してコンポーネント名をそのまま JSON のキーに使うので、この違いが問題になることはほとんどありません。
不変であることが効いてきます
record のフィールドはすべて final で、setter は生成されません。一度作ったら値が変わらないので、次の利点があります。
- 複数のスレッドから同時に読んでも壊れない
- 「いつの間にか値が書き換わっていた」という種類のバグが起きない
- Map のキーや Set の要素として安心して使える
DTO はまさに「作って渡して読むだけ」の存在なので、この性質と噛み合います。逆に、JPA のエンティティは永続化のために引数なしコンストラクタと可変のフィールドを必要とするため、record にはできません。エンティティは普通のクラス、DTO は record、という住み分けが自然です。
コンパクトコンストラクタで検証も書けます
引数の検証を入れたいときは、コンパクトコンストラクタという書き方が使えます。
Java
public record UserResponse(Long id, String name) {
public UserResponse {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name is required");
}
}
}引数リストを書かない形で、フィールドへ代入される直前に割り込めます。
入れ子にできます
record のコンポーネントに別の record を置けます。住所を持つ利用者のように、構造のあるレスポンスを型で素直に表現できます。
Java
public record Address(String city, String zip) {}
public record UserResponse(Long id, String name, Address address) {}演習で書くもの
演習では入れ子になった record を自分で定義し、それを組み立ててから整形した文字列を返します。Spring では、この record がそのまま JSON へ直列化されてレスポンス本文になります。ここでは直列化の代わりに、アクセサを使って値を読み出し、決まった形の文字列を組み立ててください。record を宣言するだけでアクセサとコンストラクタが手に入ること、そして入れ子でも自然に扱えることを手で確かめます。
要件
- Main の中に record Address(String city, String zip) を定義すること
- Main の中に record UserResponse(long id, String name, Address address) を定義すること
- describe は UserResponse を組み立て、id、name、address.city、address.zip をこの順に縦棒でつないだ文字列を返すこと
入出力例
describe(1, "taro", "tokyo", "1000001") → "1|taro|tokyo|1000001"
describe(42, "hanako", "osaka", "5300001") → "42|hanako|osaka|5300001"
describe(7, "guest", "kyoto", "6000001") → "7|guest|kyoto|6000001"