読者です 読者をやめる 読者になる 読者になる

anti scroll

ブラウザと小説の新しい関係を模索する

OCamlでObject-relational mapping(ORM)みたいなやつ

プログラミング ocaml

ocorm表題の件をなんとなく実現するライブラリです。

https://github.com/tategakibunko/ocorm

ocormが提供するMakeRelationMapファンクターの定義はこのようなものです。

module MakeRelationMap (F:FieldMap)(S:Schema):RelationMap with type t = F.t

このファンクターは、スキーマの定義モジュール(Schema)と、データ型の変換モジュール(FieldMap)を受け取り、次のことをするモジュールを生成します。

  • (string, string) listを、FieldMapが定義するオブジェクトに変換
  • (string, string) listを、Schemaが定義する制約に基づいてバリデーション

Schemaモジュールの定義

こんな風に定義します。

module User = struct
  let table_name = "user"
  let props = [
    ("id", PrimaryKeyProperty);
    ("name", StringProperty [NotNull; MinLength 3; MaxLength 16]);
    ("point", IntProperty [NotNull; Default (`Int 0)]);
  ]
end

フィールド名と属性のセットをpropsにセット。属性定義には、制約を付けることができます。

FieldMapモジュールの定義

例としてjson-wheelライブラリの定義するJson_type.tに変換する処理を定義してみます。

module JsonFieldMap = struct
  type t = Json_type.t
  let null = Json_type.Null
  let map_value prop value =
    match prop with
      | PrimaryKeyProperty -> Json_type.Int (int_of_string value)
      | StringProperty _ -> Json_type.String value
      | TextProperty _ -> Json_type.String value
      | IntProperty _ -> Json_type.Int (int_of_string value)
      | FloatProperty _ -> Json_type.Float (float_of_string value)
      | BooleanProperty _ -> Json_type.Bool (value = "t")
  let map_alist alist = Json_type.Object alist
  let map_list lst =  Json_type.Array lst
end

null/int/string/float/list/alistといった属性を、自分が変換したいオブジェクトにマッピングする処理を記述します(alistは属性リスト)。

ちなみに上の変換処理ですが、本当はもう少し厳密に変換したほうが良いと思います(空文字だったらどうする?とか)。

MakeRelationMapファンクター

これら二つのモジュールをMakeRelationMapに渡すと、UserテーブルをJSONに変換したり、バリデーションしたりできるモジュールが手に入ります。

module UserMap = MakeRelationMap(JsonFieldMap)(User)

オブジェクト変換

JSONに変換するには、UserMap.objectifyを使います。

let table_row = [("id", "1"); ("name", "taro"); ("point", "100")] in
let json = UserMap.objectify table_row in (** Json_type.t が取得できる *)
Json_io.string_of_json json

=> {"id":1, "name":"taro", "point":100}

バリデーション

データをバリデーションするには、UserMap.validateを使います。

(** nameには3文字以上、16文字以下の制約が付いている *)
UserMap.validate [("id", "1"); ("name", "a"); ("point", "100")]
=> exception: name is too short(min 3, max 16)

jingooオブジェクトを出力させる

schemaの定義と変換のモジュールが別々に定義できるので、例えばjingooのテンプレートに渡せるオブジェクトを作りたい場合は、JsonFieldMapモジュールを、こんな感じのモジュールに差し替えるだけで対応できます。

open Jg_types

module JingooFieldMap = struct
  type t = tvalue
  let null = Tnull
  let map_value property value =
    match property with
      | PrimaryKeyProperty -> Tint (int_of_string value)
      | StringProperty _ -> Tstr value
      | TextProperty _ -> Tstr value
      | IntProperty _ -> Tint (int_of_string value)
      | FloatProperty _ -> Tfloat (float_of_string value)
      | BooleanProperty _ -> Tbool (value="t")
  let map_alist alist =
    let hash = Hashtbl.create (List.length alist) in
    Thash (List.fold_left (fun h (n,v) -> Hashtbl.add h n v; h) hash alist)
  let map_list rows = Tlist rows
end

(** jingooのオブジェクトに変換するモジュール *)
module UserMap2 = MakeRelationMap(JingooFieldMap)(User)

ORM……なのか?

……ご覧の通り、ORMといってもActiveRecordみたいにSQLに関わる部分まで抽象化しているわけではなく、あくまでSQL呼び出しの結果が(string, string) list で取得できたとして、それを特定のオブジェクトに変換したり、ついでにバリデーションしたりっていう仕事をするためのライブラリです。