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を使えばマッピングは簡単。

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