ドメインクラスで型パラメータをサポート

twitter:@backpaper0
さんの発言から。



いいですね!

さっそく実装してSNAPSHOTつくってみました。


ぜひお試しください。ブラッシュアップして次のバージョン(1.32.0)で正式リリースに組み込みたいと考えています。

使用例


使用例を示します。


以下のようにドメインクラスに任意の数の型パラメータを指定できます。ここでは1個だけですが、何個でもOK。

@Domain(valueType = int.class)
public class Weight<T> {

    private final int value;

    public Weight(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public Weight<T> add(Weight<T> other) {
        return new Weight<T>(value + other.value);
    }
}


利用する側では次のように実型引数に具体的な型を指定します(ワイルドカードや型変数の使用はサポートしていません)。

@Entity
public class Person {

    public Weight<kg> weight;
}
@Entity
public class Food {

    public Weight<g> weight;
}


上の例では、単位を表すkgやgという型を作って型引数にしました。

public interface kg {}
public interface g {}


これでいったい何がうれしいかというと、WeightとWeightは別の型なので安全性を高められるということです。
つまり、次のようなコードはコンパイルできません。

Person person = ...
Food food = ...
Weight<kg> weight = person.weight.add(food.weight);

Domainでファクトリメソッドを使うには ?


上の使用例では、Domainクラスをコンストラクタでインスタンス化しています。
ファクトリメソッドが好きな人もいるでしょう。
ファクトリメソッドでは、クラスの型変数宣言と同等の宣言をしてください。
この例では、ofメソッドがファクトリメソッドです。

@Domain(valueType = int.class, factoryMethod = "of")
public class Weight<T> {

    private final int value;

    private Weight(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public Weight<T> add(Weight<T> other) {
        return new Weight<T>(value + other.value);
    }

    public static <T> Weight<T> of(int value) {
        return new Weight<T>(value);
    }
}

ExternalDomainを使うには ?


ValueObjectにアノテーションをつけたくない(つけられない)場合のために、ExternalDomainという機能がありますが、こちらでも型パラメータに対応しています。
注意点は1つです。

  • DomainConverterの実装では、Domainクラスはワイルドカードを使って扱ってください。


これがValueObject。上の例と違ってアノテーションついていません。

public class Weight<T> {

    private final int value;

    public Weight(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public Weight<T> add(Weight<T> other) {
        return new Weight<T>(value + other.value);
    }
}


これが、ValueObjectと基本的な値を相互変換するコンバーター(DomainConverterの実装)。Weightの型引数に?(ワイルドカード)を指定しています。

@ExternalDomain
public class WeightConverter implements DomainConverter<Weight<?>, Integer> {

    @Override
    public Integer fromDomainToValue(Weight<?> domain) {
        ...
    }

    @Override
    public Weight<?> fromValueToDomain(Integer value) {
        ...
    }
}

Doma 1.31.0 補足

DOMA-285について

JDK7 + Gradle 1.6 でビルドして注釈処理が失敗すると、javax.annotation.processing.Messager経由のメッセージ出力がコンソールに表示されないということがわかりました。例外にも同等の内容を含め、コンソールにメッセージが表示されるようにしました。

DOMA-287について

これまで、「アプリでMETA-INF/services/java.sql.Driverを用意してください」という旨をドキュメントに書いていたのですが、これはやめました。多くのケースでJDBCドライバは自動でロードされるからです。自動ロードされない場合は、Class.forNameで明示的にロードしてください、としています。

DOMA-286について

ドキュメントでGradleによるビルド方法を説明するようにしました。

id:t1000leafさんのエントリを参考にさせてもらいました。

ドキュメントで触れているのは、Domaに特化したところだけです。

Doma 1.31.0 をリリース

Doma 1.31.0 をリリースしました。今回は非常に細かい修正だけが入っています。

ダウンロードはこちらからどうぞ。

Mavenをご利用の方はこちらを参照ください。

以前のバージョンから移行するには移行ガイドを参照ください。

リリースノート

Improvement

  • [DOMA-285] - [Core] コンパイルエラーなどが原因で注釈処理が失敗した場合のメッセージを改善しました
  • [DOMA-287] - [Doc] META-INF/services/java.sql.Driverはアプリで用意する必要がないことをドキュメントとエラーメッセージに示しました

Task

  • [DOMA-286] - [Doc] Gradleを使ったビルド方法を示しました

Doma 1.30.0 をリリース

Doma 1.30.0 をリリースしました。

ダウンロードはこちらからどうぞ。

Mavenをご利用の方はこちらを参照ください。

以前のバージョンから移行するには移行ガイドを参照ください。

リリースノート

Bug

  • [DOMA-280] - [Core] フィールドのアクセス修飾子がpublicであるにも拘わらずjava.security.AccessControlExceptioが発生することがある問題に対応しました
  • [DOMA-281] - [Core] H2 Databaseの一意性約違反のエラーコードが23001から23505に変更されている問題に対応しました。
  • [DOMA-282] - [Core] H2 Databaseを使ったページングでSelectOption.limitに0を指定した場合の挙動がH2のバージョン1.2.126と1.3.171で異なる問題に対応しました。

Improvement

  • [DOMA-283] - [Core] SQL Server 2012以降でシーケンスによる識別の自動生成を可能にしました。

H2 Databaseの互換性のない変更

DomaのJIRAに報告もらって気づいたのですが、最近のH2 Databaseには互換性のない変更が加わっているようです。

  • 一意性約違反のエラーコードが23001から23505に変わった
  • SELECTのLIMIT句に0を指定した場合の検索結果が異なる

H2のバージョン1.2.126と1.3.171で比較しています。


Domaの対応バージョンは、今週末にリリース予定です。

DB上のNULLをどう表現するか

TranqというF#専用のDBアクセスライブラリについて話します。

はじめに

Tranqでは、F#らしくDB上のNULLをoption型で扱えます。

たとえば、次のようなテーブル定義を考えます。

create table Person (Id int primary key, Name varchar(50) not null, Age int);

上のテーブルは次のようなF#のレコードにマッピングできます。

type Person = { [<Id>]Id: int; Name: string; Age: int option; }

Ageフィールドに着目してください。NULLに対応させるためにint option型になっています。


ところで、DB上でNULLで扱うからといって、それをそのままアプリケーションに持ち込む必要があるのでしょうか?
判別共用体を使えば、DB上のNULLをもっと別な形で表現できます。
ここでは、それをTranqでどう実現するかを説明します。

判別共用体でNULLを表す

あるアプリケーションでは、年齢で子供と大人とを区別する必要があるとしましょう。そして、年齢不詳の人も扱わないといけないとしましょう。
素直に考えればこれは次のような判別共用体で表せます。

type Age =
  | Child of int
  | Adult of int
  | Unknown

ということは、上記のデータ構造をそのままテーブルのカラムにマッピングできればいいわけですね。
つまり、ケースUnknownをDB上のNULLにマッピングします。


Tranqでは次のように書けます。

module Age = 
  type T = 
    private 
    | Child of int
    | Adult of int
    | Unknown

  (* int option -> T *)
  let compose = function
    | Some age ->
      if age > 20 then Adult age else Child age
    | _ -> Unknown

  (* int T -> int option *)
  let decompose = function
    | Child age | Adult age -> Some age
    | Unknown -> None

  (* T -> T *)
  let incr age =
    decompose age |> Option.map ((+) 1) |> compose

  (* 判別共用体をアクティブパターンへ変換 *)
  let (|Child|Adult|Unknown|) = function
    | Child age -> Child age
    | Adult age -> Adult age
    | Unknown -> Unknown

  (* IDataConv<T, int option> *)
  let conv = { 
    new IDataConv<T, int option> with
      member this.Compose(v) = compose v
      member this.Decompose(t) = decompose t }

簡単に解説します。


利便性のために判別共用体は専用のモジュールに定義します。


判別共用体の生成ルールはcompose関数で表します。判別共用体にprivateをつけることで、このモジュールの外で判別共用体を生成できなくしています。つまり、たとえば、100歳の子供といった矛盾したデータが作られることを防いでいます。


判別共用体をアクティブパターンへ変換していますが、これは、このモジュールの外でパターンマッチを可能にするためです。判別共用体にprivateをつけたためモジュールの外からケースを参照できなくなるのでこの対応が必要です。判別共用体に対するパターンマッチ専用のインタフェースですね。


値convをTranqに登録することで、DBとF#での値変換に使われます。つまり、DBから読み取ったときはcompose関数が呼ばれてプリミティブな値から判別共用体へ変換され、F#からDBへ書き出すときはdecompose関数が呼ばれて判別共用体がプリミティブな値に変換されます。

定義した判別共用体を利用する

こんなにたくさんコード書くのかよー、と思いました?
でも、これ、Tranqに必要なコードはconvの数行だけです。
それ以外は、判別共用体を便利に使う上でアプリケーションに必要だったり、アプリケーションで使うコードです。
カラムに対応させた判別共用体とそのモジュールをリッチにすることでロジックがすっきりするのです。
たとえば次のように書けます。

module Logic =

  (* Person -> Person *)
  let incrAge person = 
    { person with Age = Age.incr person.Age}

  (* Person -> string *)
  let describe person =
    match person.Age with
    | Age.Child _ -> "子供"
    | Age.Adult _ -> "大人"
    | Age.Unknown -> "年齢不詳"

レコードのフィールドに露出したoption型やNullable型を使いながら同等のことをする場合を持ち出すまでもなく、簡潔に書けることがわかります。
Ageフィールドに対応する値がDB上でNULLになりうることをまったく意識する必要がありませんね。

まとめ

  • DB上のNULLは判別共用体で表せる。
  • 判別共用体にすればロジックがすっきり。
  • Tranqを使えばマッピングは簡単。

コード全体はこちらにおいておきます。

TranqでF#の機能を最大限活用したDBアクセス

TranqというF#専用のDBアクセスライブラリを作りました。

NuGetはこちら。

Tranqを作った背景ですが、既存のDBアクセスライブラリだとF#の便利な機能を活かしにくいなと感じたので作ってみました。

Gistに載せたサンプルから抜粋しつつ紹介したいと思います。すべてを見たい場合はこちらをどうぞ。

Tranqの現時点の最新バージョンは、0.1.0.1です。以下、このバージョンに基づいて話します。

コンピュテーション式によるトランザクションの表現

System.Data.Common名前空間のDbConnectionやDbTransactionの値を引数で渡していくのは、コードの見栄えを悪くするので、コンピュテーション式を使って解決しました。コンピュテーション式を使うと引数渡しされる値をコード上から隠すことができます。
こんな感じのコードになります。

module Service = 
  (* 'a -> Tx<Person> *)
  let updateAgeAndHeight id = txRequired {
    let! person = Dao.find id
    let person =
      person 
      |> Logic.incrAge 
      |> Logic.incrHeight
    return! Dao.update person }

updateAgeAndHeight関数では、DBからデータをSELECTして、変更して、DBにUPDATEしています。
DbConnectionやDbTransactionが登場しないのでコードの可読性が上がります。

判別共用体へのマッピング

たとえば、年齢をint型で表してしまうと、他のint型の値と区別がつかなくなってしまいます。さらに20歳未満は子供でそれ以上は成人などと、年齢に意味を持たせたい場合int型では不十分です。そのような場合に役立つのが判別共用体です。

サンプルコードではAge.T型を作成して、PersonテーブルのAgeカラムにマッピングしています。

type Person = { [<Id>]Id: int; Name: string; Age: Age.T; Height: decimal<cm> }

Tranqの設定でマッピングする判別共用体を登録するなどの作業は必要ですが、一度マッピングしてしまえば、後はパターンマッチを使って簡潔にロジックを表現できます。以下のコードでは、子供かどうかで条件分岐していますが、何歳以下を子供とするかというルールは条件分岐には登場しません。すでにマッピング時にルールが適用されているからです。

  let incrHeight person =
    let incr =
      match person.Age with
      | Age.Child age -> 0.1M<cm> * decimal age
      | _ -> 0M<cm>
    { person with Height = person.Height + incr }

DBのカラムを判別共用体にマッピングできるのはTranqの大きな強みです。

依存関係

Tranqの機能ではありませんが、Tranqを使う上で、推奨したいのは、Service(トランザクション)、Logic(業務ロジック)、Dao(DBアクセス)といったレイヤ分けを行ったうえで次のような依存関係にすることです。

  • Service -> Logic
  • Service -> Dao

ポイントはLogicをDaoに依存させないで、Logicを副作用のない関数の集まりとすること。テストしやすくするために必要だと感じています。
ServiceがLogicとDaoをつなぎます。以下のコードが、それをしています。

module Service = 
  (* 'a -> Tx<Person> *)
  let updateAgeAndHeight id = txRequired {
    let! person = Dao.find id
    let person =
      person 
      |> Logic.incrAge 
      |> Logic.incrHeight
    return! Dao.update person }

まとめ

F#のDBアクセスライブラリであるTranqの紹介をしました。
Tranqを使うと、トランザクションを簡潔に記述でき、判別共用体を活用したプログラミングが可能になります。


Tranq、ほかにも、アピールしたい機能があるのですが、また、いずれ紹介したいと思います。
DDDと絡めて話をするなども面白いかもしれないですね。


質問/疑問などあれば、お気軽にしてもらえるとうれしいです。