2014年8月31日日曜日

[haskell][persistent][sqlite] Haskellでデータベースを利用するには

HaskellでDBの読み書きをするにはどうすればいいか?
yesodのpersistentパッケージが利用できます。本家のサイトを参考にpersistentパッケージの特徴と、簡単な処理を行うサンプルをまとめました。

persistentパッケージの特徴

  • 「型安全、簡潔、宣言的」という原則に従う
  • DB(PostgreSQL, SQLite, MySQL, MongoDB, etc.)に依存しないAPI
  • 安全で生産的なクエリーインターフェース
  • 他のプログラミング言語からのDBアクセスも考慮
  • yesodに含まれているが、独立したライブラリとして利用可能

主要な型の説明

  • PersistValue
    • persistentの基本構成要素。DBから出し入れする値を表すデータ型。
    • 
      data PersistValue = PersistText Text
                        | PersistByteString ByteString
                        | PersistInt64 Int64
                        | PersistDouble Double
                        | PersistRational Rational
                        | PersistBool Bool
                        | PersistDay Day
                        | PersistTimeOfDay TimeOfDay
                        | PersistUTCTime UTCTime
                        | PersistZonedTime ZT
                        | PersistNull
                        | PersistList [PersistValue]
                        | PersistMap [(Text, PersistValue)]
                        | PersistObjectId ByteString -- ^ intended especially for MongoDB backend
      
      
      
  • PersistField
    • リレーショナルデータベースのカラムに対応。Haskellの任意のデータ型(datatype)とPersistValueを相互にどうマーシャリングする手順を定義する。
  • PersistEntity
    • PersistentEntityのインスタンスはリレーショナルデータベースのテーブルに対応する。
  • PersistentStore
    • 各データストア(PostgreSQL, SQLite, MongoDB, etc.)はPersistentStoreのインスタンスを持つ。PersistentValueからDB固有の値への変換が行われる。
    • runSqliteは引数で渡されたconnection stringを用いて、DBへのコネクションを一つ生成する。一回のrunSqliteの呼び出しは、一つのトランザクションの中で実行される。
  • 対応関係
    • persistentの型と、データベースの対応関係を整理すると以下のようになる。
    • persistentSQL
      PersistValueデータの型(VARCHAR, INTEGER, etc)
      PersistFieldカラム
      PersistEntityテーブル
      PersistStoreデータストア(PostgreSQL, SQLite, MySQL, MongoDB)

データのINSERT/SELECTサンプル

以下の処理を行うサンプルを掲載しておきます。個々の処理が何を意味しているのかはコメントを参考にしてください。
  • personテーブルを生成
  • レコードを一つINSERT
  • SELECTで登録したレコードを取得
  • 取得したレコードを表示


{-# LANGUAGE EmptyDataDecls    #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE GADTs             #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
import Control.Monad.IO.Class  (liftIO)
import Database.Persist         -- persistentパッケージ
import Database.Persist.Sqlite  -- persistent-sqliteパッケージ
import Database.Persist.TH      -- persistent-templateパッケージ

-- mkPersist
--   mkPersist :: MkPersistSettings -> [EntityDef SqlType] -> Q [Dec]
--   データ型、PersistentEntityインスタンスを生成
-- sqlSettings
--   mkPersistの挙動を変える設定値
-- mkMigrate "migrateAll"
--   マイグレーション処理を定義
-- QuansiQuotes [xx|..|] でスキーマ定義。
--   persistentLowaCaseはテーブル名・フィールド名に、"_"区切り小文字を
--   利用することを指示している。
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person             -- personテーブル。プライマリキーとして"id"カラム自動生成。
    name String    -- nameカラム(VARCHAR, NOT NULL制約)
    age Int Maybe  -- ageカラム(INTEGER, MaybeはNULLを許容することを意味する)
    deriving Show
|]

main :: IO ()
main = runSqlite ":memory:" $ do -- DBオープン時の引数。メモリDB利用。"test.db"等の
                                 -- ファイル名を渡すとファイルDBがオープンされる。
    -- personテーブル生成
    runMigration migrateAll

    -- name: "Michael", age: 26 のレコードを INSERT
    -- id(プライマリキー)が返値として返される。
    michaelId <- insert $ Person "Michael" $ Just 26
    -- id でレコードを SELECT。該当レコードが返される。
    michael <- get michaelId
    -- 返されたレコード(Personインスタンス)を表示。
    liftIO $ print michael



Uniqueness

大文字から始まるデータ型の宣言を追加することで、指定のカラムにUNIQUE制約を付与することができます。
以下にINSERT/SELECTサンプルをベースに、addressおよび、firstName+lastNameにUNIQUE制約を付与したサンプルコードを掲載しておきます。UNIQUE制約に関連する処理のみコメントで説明を加えています。


{-# LANGUAGE EmptyDataDecls    #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE GADTs             #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
import Control.Monad.IO.Class  (liftIO)
import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
    firstName String -- nameをfistNameと
    lastName String  -- lastNameに分解
    age Int Maybe
    address String  -- addressカラム(VARCHAR)を新たに追加
    Address address -- addressカラムをUNIQUE制約に(先頭大文字でAddressを宣言)
    PersonName firstName lastName -- firstName, lastNameの組合せをUNIQUE制約に
    deriving Show
|]

main :: IO ()
main = runSqlite ":memory:" $ do
    runMigration migrateAll

    -- addressも引数に追加。bobのデータも登録してみる。
    let michaelAddress = "1-2 xx Tokyo"
    michaelId <- insert $ Person "Michael" "Snoyman" (Just 26) michaelAddress
    bobId <- insert $ Person "Bob" "Marley" (Just 29) "2-20 yyy Hokkaido"

    -- UNIQUE制約フィールドは getBy で SELECTがかけられる
    bob <- getBy $ Address "2-20 yyy Hokkaido"
    -- bob の情報を表示。
    liftIO $ print bob

    -- PersonNameでもSELECTが可能。fistName, lastNameを渡してインスタンスを生成
    michael <- getBy $ PersonName "Michael" "Snoyman"
    liftIO $ print michael

    -- 登録されていないAddressでSELECTするとNothingが返される
    fail <- getBy $ Address "123-456 Fukuoka"
    liftIO $ print fail

    -- UNIQUE制約に違反するレコード(Michaelと重複)を登録しようとするとエラーになる
    markId <- insert $ Person "Mark" "Twain" (Just 29) michaelAddress
    -- エラー出力:
    -- uniqueness: user error (SQLite3 returned ErrorConstraint while attempting to perform step.)
    mark <- get markId
    liftIO $ print mark



参考にしたサイト



0 件のコメント:

コメントを投稿