Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Haskellで便利にデータ設計

Haskellで便利にデータ設計

概要

これは 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 を読んでください。

Enum型(列挙型)を使う

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 することでカラムとして使えるようにできます。

Enumの組合せ型を使う

例えば曜日の組合せをレコードに持たせたい場合があります。
「燃えるゴミを捨てる」という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 のインスタンスにされていて使えます。

他のデータ型を 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 を使わずにインスタンス化しても大した労力ではありません。

1対多の関連テーブルを消す

次のように購入テーブルと、その購入が含む各商品の購入テーブルがあるとしましょう。

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.クラウドエンジニア」にご応募ください。
会社の募集ページ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment