Skip to content

Instantly share code, notes, and snippets.

@seizans
Created December 10, 2012 15:21
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