これは Haskell Advent Calendar 2012 の11日目の記事です。
Haskell でデータ設計を便利に行う発想・方法について書きました。
persistent というライブラリを活用します。
Haskell を知らなくても読めます。
主な対象読者は プログラミングHaskell か すごいHaskellたのしく学ぼう! を読み、Haskell をより使いたい人です。
persistent の概要
いわゆる「ORマッパー」の機能を持つライブラリです。
データ設計を記述するという準備をすれば、
- DBのデータ出し入れをよろしくやってくれます。つまり
- データ型を作ったら insert関数に渡せば、はい、DBレコードが追加されます
- select系関数で取り出したデータには対応する型が付いて安全に使えます
- RDB だけではなく NoSQL (今はMongoDBのみですが) にも使えます
- 高レベル用の関数が用意され、多くの場合SQLを書かずにプログラミングできます
- SQLを書いて、その結果に型を付けてもらうこともできます
データ設計の部分はこんな感じで書きます。
-- 最初の行は一旦おまじない
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
User -- この名前がテーブル名になり、かつデータ型の名前になる
name Text -- カラム名とデータ型、カラム名はアクセサの名前にもなる
age Int
isMale Bool
created UTCDate
|]
この記事ではデータ設計の記述のみ扱います。
ライブラリ関数の使い方は他の記事(TODO:リンクを張る)や haddoc を読んでください。
DBでEnum型を使いたいことはよくあります。
例えば Status というカラムがいくつかの値だけをとる場合です。
方法の候補としては次があります。
- Enum型を扱えるDBを使う
- コード値(1とか2とか)を入れておき、プログラム側で対応付けて変換する
- 文字列で入れておいてプログラム側で対応付けて変換する
persistent を使う場合は次のようにやります。
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Issue
name Text
status Status
|]
data Status = New | Assigned | Reopen | Closed
deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "Status"
定義した Status というデータ型について、derivePersistField することでカラムとして使えるようにできます。
例えば曜日の組合せをレコードに持たせたい場合があります。
「燃えるゴミを捨てる」というTODOを「火曜と金曜」にしたい場合です。
方法の候補としては、IS_SUNDAY...と全ての曜日のやるやらないをBoolで持つことです。
でもpersistを使う場合はこれでOKです。
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Todo
name Text
days (Set Day)
|]
data Day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "Day"
こうできるのは persistentパッケージ で Set a が PersistField のインスタンスにされているからです。
PersistField のインスタンスにすることが、そのデータ型をDBのカラムとして使えるようにするということです。
他にも例えば Map Text v や (a, b) や [a] が PersistField のインスタンスにされていて使えます。
何かライブラリが提供しているデータ型を使うこともできます。
iprouteパッケージ で定義されている IPv4 データ型を使ってみましょう。
import Data.IP (IPv4)
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Instance
name Text
vpcIp IPv4
elasticIp IPv4
|]
derivePersistField "IPv4"
derivePersistField するだけです。
そのデータ型が Read と Show のインスタンスになっていればOKだと考えてよいです。
中身を知りたい人は Database.Persist で PersistField のインスタンス化をしている部分を読みましょう。
derivePersistField を使わずにインスタンス化しても大した労力ではありません。
次のように購入テーブルと、その購入が含む各商品の購入テーブルがあるとしましょう。
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Purchase
date UTCDate
PurchaseDetail
purchase PurchaseId
item ItemId
number Int
|]
1対多の場合は、1側に多をリストとして持たせればOKです。
その際、多側の構造をデータ型として定義し、カラムとして使えるようにします。
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Purchase
date UTCDate
details [PurchaseDetail]
|]
data PurchaseDetail = PurchaseDetail
{ purchaseDetailItem :: ItemId
, purchaseDetailNumber :: Int
} deriving (Show, Read, Eq, Ord)
derivePersistField "PurchaseDetail"
多対多の関連を持つ構造として以下があります。
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
User
name Text
Group
name Text
UserGroup
user UserId
group GroupId
|]
これは User か Group のどちらかに一方のキーをリストで持たせればOKです。
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
User
name Text
groups [GroupId]
Group
name Text
|]
User で検索したいか Group で検索したいかでどちら(or両方)に持たせるか決めます。
関連テーブルがキー以外のカラムを持つ場合は、そのためのデータ型を定義すればよいでしょう。
以上で見てきたことを活用すると、データ設計がコンパクトになります。
モデル(テーブル)数を減らしながら、頭に入りやすいテーブル定義にできます。
今まで使っていて、テーブル数が従来の半分以下くらいになる感覚です。
とても助かっています。
ぜひ使ってみてください。
使用例: これから育てるので12月末くらいに見るとちゃんと書いてあるかも。
persistent本家
技術者を募集しています。
Haskell やら Cloud やらの仕事に興味あればご連絡ください。
もしくは以下のページ内の「3.クラウドエンジニア」にご応募ください。
会社の募集ページ