Doma2 Domainのすゝめ

2021/12/14に公開

https://adventar.org/calendars/6972


実務でDoma2を利用させていただいており、積極的にDomainクラスを導入しています。
この記事では、Domainクラスの使い方や使いたい理由についてご紹介します。

この記事を書くにあたり、公式のgetting-startedをDomainクラスに置き換えてみました。
ぜひ見てみてくださいね。
https://github.com/mahaker/getting-started/tree/master/java-15

Doma2とは

Java,Kotlinで利用可能なDBアクセスフレームワークです。
詳しい説明は公式ドキュメントに譲らせてください :bow:

https://doma.readthedocs.io/en/2.50.0/

Domainクラスという素晴らしい機能

そんなDoma2には、Domainクラスという機能があります。
一言で言うと プリミティブ型やそのラッパークラスを、独自の型でラップできる機能 です。
作ったDomainクラスは、Entityクラスのフィールドとして利用することができます。

Domainクラスの作り方

ユーザを表すテーブルusersの中に、人名を表すカラムuser_nameがあったとしましょう。
user_nameをDomainクラスとして表現すると↓のようになります。

// @Domainアノテーションを付与します。
// valueType属性には、Domainクラスが保持する値の型(この場合はString)を指定します
@Domain(valueType = String.class, factoryMethod = "of")
public class UserName {

    private final String value;

    // factoryMethodを定義すると、コンストラクタをprivateにできます(publicでも動きます)
    private UserName(String value) {
        this.value = value;
    }

    String getValue() {
        return value;
    }

    public static UserName of(String value) {
        return new UserName(value);
    }
    
    // equals と hashCode も実装しましょう
}

そしてusersテーブルに対応するエンティティは↓のようになります。

@Entity(immutable = true)
@Table(name = "users")
public class User {
    private final UserName userName;
    
    // Domainクラスを使わなかったらこうなります
    // private final String userName;

    public User(UserName userName) {
        this.userName = userName;
    }
    
    public UserName getUserName() {
      return userName;
    }
}

Domainクラスを使いたい理由

いよいよ本題です!
実際にDomainクラスを導入して良かったなーと思う点を紹介します。

引数に型が付く

エンティティのコンストラクタはもちろん、業務ロジックのコードにも型が付くようになります。
たとえばこんな感じです。

// 記事のURLをメールで送信する
public void sendMail(UserEmailAddress mailAddress, ArticleUrl articleUrl) {
   ...
}

// Domainクラスを使わなかったらこんな感じ
public void sendMail(String mailAddress, String articleUrl) {
   ...
}

こうすることで「引数を逆にしちゃった」といった間違いがなくなります。
引数が多くなるほど順番に気を使いますし、コードレビューも大変ですよね。

ジェネリクスが分かりやすくなる

ジェネリクスがDomainクラスになることで、ListやMapの中身を推測できるようになります。
また、CollectorやFunctionインターフェースも厳密に定義できるようになります。

実装を見ないとListやMapの中身が分からないのはツライですよね。

collect検索がもっと便利になるのも嬉しいポイントです。(コード例はこちら)

final Map<EmployeeId, Name> employeeNames
      = dao.selectAll(toMap(employee -> employee.id, employee -> employee.name));
      
// コンパイルエラーになる
final Map<Age, Name> employeeNames
      = dao.selectAll(toMap(employee -> employee.id, employee -> employee.name));

// Domainクラスを使わなかったらこんな感じ
final Map<Integer, String> employeeNames
      = dao.selectAll(toMap(employee -> employee.id, employee -> employee.name));

// Employee#idをキーにしたいが、コンパイルエラーにならない
final Map<Integer, String> employeeNames
      = dao.selectAll(toMap(employee -> employee.age, employee -> employee.name));

Domainクラスにロジックが書ける

Domainクラスは「ただのクラス」なのでロジックを書けますし、単体テストも書くことができます。
文字列の変換処理やバリデーションなどがあちこちに書かれている場合は、Dmainクラスや独自型にロジックを移すと良いかもしれません。
ロジック=メソッドなので、名前がつけられるのも嬉しいポイントですね。

すべてに型をつけたくなる

Domainクラスを導入してから、プリミティブやそのラッパークラスが登場することに違和感が出てきました。

例えば、ユーザの「誕生日」から「年齢」を計算する処理があったとしましょう。
「誕生日」のカラムは存在しますが、「年齢」のカラムは存在しないものとします。

この処理を実装する前に「年齢」型を実装し、戻り値を「年齢」型にします。
ただの数字や文字が「現実世界の何に対応するか」を表現するように心がけています。

// 「年齢」を表す型
class Age {
  private final int value;
  
  public static Age of(int value) {
    this.value = value;
  }
}

@Entity(immutable = true)
public class User {
    private final BirthDate birthDate;

    // コンストラクタ
    
    // 年齢を取得する
    public Age getAge() {
      return birthDate.calcAge(); // 年齢を計算するロジックはDomainクラスに
    }
}

少しずつ導入できる

Domainクラスの導入は1カラムから始めることができます。

まずはDomainクラスを1つ実装しましょう。
既存のエンティティのフィールドを置き換えてもいいですし、新規で作るエンティティにDomainクラスを使うようにしても良いです。

影響の少なそうなカラムから始めるのが良いかもしれませんね。

IntelliJ IDEAの.var補完がより便利に

これは意外な副産物でした。
IntelliJ IDEAのPostfix completionという(これまた)素晴らしい機能があります。

Domainクラスや独自型で.varを使うと、変数名を型の名前にしてくれます。
タイプ数が減ってくれるのは嬉しいですね!

型名が変数名の候補に!

プリミティブだとこうはならない

まとめ

Doma2のDomainクラスについてご紹介しました。

見返してみると「独自型を使いたい理由」が多かったような気がしますが、それをEntityに簡単に持ち込めるのがDomainクラスの良いところだと思います。

どんどん型を作っていきましょう!

Discussion