ラベル persistent の投稿を表示しています。 すべての投稿を表示
ラベル persistent の投稿を表示しています。 すべての投稿を表示

2015年11月29日日曜日

[haskell][persistent][sqlite] Persistentパッケージ利用時にテーブルにインデックスを生成する方法

PersistentパッケージにはMigration機能が備わっており、自動的にテーブルを生成してくれます。スキーマ変更を行った際にも、変換が可能な限りテーブル内のレコードを保持したまま新しいスキーマに変換してくれます(Migration機能については過去のエントリでまとめています)。

自分が利用する上で、インデックスやトリガーを生成する手順が紹介されておらず困っていたのですが、rawExecuteという関数を用いることで自由にDDLを発行できることがわかりました。以下その手順とサンプルを紹介しておきます。

サンプルコード:

以下は、personテーブルのnameカラムにインデックスをs生成するサンプルです。runMigration実行直後に、runExecuteを実行することでインデックスを生成しています。このサンプルではインデックスを生成しているだけですが、同じ手順でトリガーの生成(CREATE TRIGGER)も可能です。
{-# LANGUAGE EmptyDataDecls             #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# 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パッケージ

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
    name String
    age Int Maybe
    deriving Show
|]

main :: IO ()
main = runSqlite "test.db" $ do
    -- personテーブル生成
    runMigration migrateAll
    -- nameカラムにindexを生成する。placeholderがないため第2引数は[]でよい。
    -- 2回目以降、すでにインデックスが存在している場合のためにIF NOE EXISTSが必要。
    rawExecute "CREATE INDEX IF NOT EXISTS idx_name_on_person ON person(name);" [] -- here!

    michaelId <- insert $ Person "Michael" $ Just 26
    michael <- get michaelId
    liftIO $ print michael
runExecuteの第一引数には実行するSQLを記載します。既にインデックスが生成済みの場合には何も処理を行わないよう"IF NOT EXISTS"を指定しています。第二引数にはplaceholderの値を指定しますがこの例ではplaceholderは利用していないため"[]"を渡しています。
"IF NOT EXISTS"を指定しないと既に存在しているインデックスを再度生成しようとして、以下のようなエラーが報告されます。
*** Exception: SQLite3 returned ErrorError while attempting to perform prepare "CREATE INDEX idx_name_on_person ON person(name);": index idx_name_on_person already exists

補足:

Yesod BookのPersistentの解説には、rawQueryの利用手順が紹介されています。rawQueryとrawExecuteの型はそれぞれ以下のようになっています。
rawQueryが実行結果を配列で返すのに対し、rawExecuteの結果はunit(空)となっています。このためDDL (Data Definition Language)実行時にはrawExecuteを利用するのがよいと思われます。
さらにrawSqlという関数もあるようですが、こちらの使い分けはよく分からず…。ご存じの方是非ご教授ください。m(_ _)m
(2015/12/16:追記)rawQueryはData.Conduit.Source型で結果を返します。これは結果が巨大であってもストリーム処理できることを意味しています。これに対しrawSqlは単純にリストで結果を返すという違いがあります。

参考:

2015年8月29日土曜日

[haskell][persistent][sqlite] Persistentパッケージのmigration機能のまとめ

HaskellでDB操作ができるPersistentパッケージの紹介をしましたが、このエントリではPersistentパッケージが提供しているmigration機能をまとめておきます。
DBを作って運用していると、機能追加や仕様変更に伴いスキーマ変更が必要になるケースが多々あります。このようなケースにおいてPersistentのmigration機能がどれくらい使えるのかを調べた結果です。

基本:

Persistetのmigration機構は(保守的なルールに沿って)スキーマ変更をある程度まで自動で処理してくれます。
ロードしたDB内のテーブル情報と、コードで定義されたEntity Definition(テーブル定義)を比較し、以下のケースにおいてスキーマの変更を行います。
  • カラムの型を変更した場合:
    • ただし、値の変換ができない場合には、DBによって拒否されることになります。
  • カラムを追加した場合:
    • ただし、追加したカラムにNOT NULL制約がある場合は、デフォルト値は設定されず、エラーとなります。
  • カラムのNOT NULL制約を外してNULL許容にした場合:
    • 変換を実行します。
    • 逆にNULL許容カラムにNON NULL制約に変更するケースの結果は、DBの状態に依存します(NULL値が存在しなければ成功、存在していると失敗)。
  • 新規Entity(テーブル)が追加された場合
Persistentは以下のケースには対応していません。
  • フィールド、Entith(テーブル)のリネーム
    • 変更前の名前と変更後の名前の対応関係がわからないので…。
  • フィールドの削除
    • データ喪失につながるので、デフォルトではフィールド削除はエラーとなります。ですが、runMigration()の代わりにrunMigrationUnsafe()を呼ぶことで強制的に削除することは可能です(もちろん非推奨)。

実験:

Migration対象のDBを以下のコードでold.dbという名前で生成します。このコードで生成されたold.dbを、新しいスキーマにmigrateすると、結果がどのようになるかをまとめておきます。
{-# LANGUAGE EmptyDataDecls             #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}
import Database.Persist
import Database.Persist.TH
import Database.Persist.Sqlite
import Control.Monad.IO.Class (liftIO)

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
    name String
    age Int
    deriving Show
|]

main :: IO ()
main = runSqlite "old.db" $ do
    -- this line added: that's it!
    runMigration migrateAll
    michaelId <- insert $ Person "Michael" 26
    michael <- get michaelId
    liftIO $ print michael


以下、いくつかのスキーマ変更を実際に試してみた結果です。
  • NULL許容カラムの追加:OK
    • コードの変更点
    • Person
          name String
          age Int
          address String Maybe  -- add new NULL-able column
          deriving Show
      |]
      
          michaelId <- insert $ Person "Michael" 26 (Just "Tokyo")
      
    • 実行結果ログ
    • Migrating: CREATE TEMP TABLE "person_backup"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" INTEGER NOT NULL,"address" VARCHAR NULL)
      Migrating: INSERT INTO "person_backup"("id","name","age") SELECT "id","name","age" FROM "person"
      Migrating: DROP TABLE "person"
      Migrating: CREATE TABLE "person"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" INTEGER NOT NULL,"address" VARCHAR NULL)
      Migrating: INSERT INTO "person" SELECT "id","name","age","address" FROM "person_backup"
      Migrating: DROP TABLE "person_backup"
      Just (Person {personName = "Michael", personAge = 26, personAddress = Just "Tokyo"})
      
  • NON NULL制約のカラム追加:NG
    • コード変更点
    • share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
      Person
          name String
          age Int
          address String  -- add new NON NULL column
          deriving Show
      |]
      
          michaelId <- insert $ Person "Michael" 26 "Tokyo"
      
    • 実行結果ログ
    • Migrating: CREATE TEMP TABLE "person_backup"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" INTEGER NOT NULL,"address" VARCHAR NOT NULL)
      Migrating: INSERT INTO "person_backup"("id","name","age") SELECT "id","name","age" FROM "person"
      add_column: user error (SQLite3 returned ErrorConstraint while attempting to perform step.)
      
  • カラムの型の変更:OK
    • コード変更点
    • Person
          name String
          age String  -- change type from Int to String
          deriving Show
      |]
      
          michaelId <- insert $ Person "Michael" "26"
      
    • 実行結果ログ
    • Migrating: CREATE TEMP TABLE "person_backup"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" VARCHAR NOT NULL)
      Migrating: INSERT INTO "person_backup"("id","name","age") SELECT "id","name","age" FROM "person"
      Migrating: DROP TABLE "person"
      Migrating: CREATE TABLE "person"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" VARCHAR NOT NULL)
      Migrating: INSERT INTO "person" SELECT "id","name","age" FROM "person_backup"
      Migrating: DROP TABLE "person_backup"
      Just (Person {personName = "Michael", personAge = "26"})
      
ログを見る限り以下の手順でmigrationを行っています。参照したYesodサイトに記載がありましたがSQLiteではALTER TABLEの機能が足りないためだと思われます。
  1. 古いスキーマでperson_backupテーブルを生成
  2. person_backupテーブルにオリジナルのpersonテーブルの内容をコピー
  3. personテーブルを破棄
  4. 新しいスキーマでpersonテーブルを生成
  5. person_backupの内容をpersonにコピー
  6. person_backupテーブルを破棄

宿題:

自分の用途では、NON NULL制約のカラムを追加し、既存レコードの値にはデフォルト値を埋めた状態にしたかったのですがPersistent備え付けの機能ではできないようです。
migration処理はTemplateHaskellによって生成されたコードで実行されているらしいので、そのあたりを弄ればなんとかなるのかも。
カラム名の変更も新スキーマのカラムと旧スキーマのカラム名の対応をmigration処理にうまく伝えることができれば、なんとかなりそうな気がします。このあたりは今後の課題として、方法がわかったらまたblogにまとめます。

参考:

2014年9月27日土曜日

[haskell][persistent][sqlite] Haskellでデータベースを扱うには(Query編)

前回のエントリでpersistentパッケージを用い、簡単なDBアクセスの手順(スキーマ定義、挿入、削除、単純なレコード取得)を紹介しました。今回は、Queryに関する情報をまとめておきます。

このエントリで紹介するQuery:
  • IDを用いた単純な値(レコード)の取得:get
  • UNIQUE制約のキーを用いた値(レコード)の取得:getBy
  • 検索・ソート条件を自由に設定できる検索:selectX
  • SQL直書きによる検索:rawQuery

準備:リストを用いてDBへレコードを一括挿入

  • 様々なqueryの動作を確認するために、予め10件程度のデータをデータベースに格納しておきます。以下、コードになります。
  • gender.hs
  • 
    {-# LANGUAGE TemplateHaskell   #-}
    module Gender where
    
    import Database.Persist.TH
    
    data Gender = Male | Female
        deriving (Show, Read, Eq)
    derivePersistField "Gender"
    
    
    • TemplateHaskellが生成したコードは、生成したのと同じmodule内で使用することはできないため、別ファイルで定義。
  • person.hs
  • 
    {-# 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
    import Data.Conduit
    import qualified Data.Conduit.List as CL
    import Gender
    
    share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
    Person
        name String
        age Int
        gender Gender -- Genderカラム(VARCHAR: Male or Female)
        Name name     -- nameカラムにUnique制約を付与
        deriving Show
    |]
    
    person_list = [(Person "Michael" 26 Male),
                   (Person "Mark" 27 Male),
                   (Person "Jhon" 55 Male),
                   (Person "Cyndy" 25 Female),
                   (Person "Amy" 30 Female),
                   (Person "Linda" 19 Female),
                   (Person "Steve" 42 Male),
                   (Person "Dorothy" 37 Female),
                   (Person "Robert" 40 Male),
                   (Person "George" 15 Male)]
    
    main :: IO ()
    main = runSqlite "person.db" $ do
        runMigration migrateAll
        ids <- mapM insert person_list
        liftIO $ print ids
        return ()
    
    
    • このプログラムを実行するとカレントディレクトリに10人分のデータが登録されたperson.dbというデータベースファイルが生成されます。PersonのリストをmapMで一括insertしています。

IDを用いた単純な値(レコード)の取得:get

  • IDに対応する値がMaybeでラップされて返される。
  • 値が存在しない場合はNothing。
  • person.hsのmainに以下のコードを追加すれば、getの動作を確認できます。
  • 
      -- success to refer Michael
      michael <- get ((Key (PersistInt64 1)) :: Key (PersonGeneric SqlBackend))
      liftIO $ print michael
      -- fail to get with id. Nothing is returned
      noone <- get ((Key (PersistInt64 100)) :: Key (PersonGeneric SqlBackend))
      liftIO $ print noone
    
    
    • printでMaybe Personの内容を表示しています。idに1を指定すると"Michael"のデータが返されます。idに100を指定した場合は該当するレコードが存在しないためNothingが返されます。

UNIQUE制約のキーを用いた値(レコード)の取得:getBy

  • getとほぼ同じ。異なるのは以下の2点のみ。
    1. IDの代わりにUNIQUE制約フィールドを表すUnique値を用いる
    2. Valueの代わりにIDとValueが格納されたEntityが返される
  • person.hsのmainを以下のように書き換えると、getByの動作を確認できます。
  • 
    print_entity Nothing = do
      liftIO $ print "failed to obtain value..."
    print_entity (Just (Entity key person)) = do
      liftIO $ print key
      liftIO $ print person
    
    main :: IO ()
    main = runSqlite "person.db" $ do
      runMigration migrateAll
      -- success to refer Cyndy
      cyndy <- getBy $ Name "Cyndy"
      liftIO $ print cyndy
      print_entity cyndy
      -- fail to get with Name. Nothing is returned
      noone <- getBy $ Name "NotFoundName"
      liftIO $ print noone
      print_entity noone
    
    
    • print_entityでEntityの内容を表示しています。"Cyndy"という名前でgetByした場合には"Cyndyl"のデータが返されます。DBに存在しない"NotFoundName"で検索した場合には、Nothingが返され"failed to obtain value..."という文字列が出力されます。

検索・ソート条件を自由に設定できる検索:selectX

    • select関連関数
      • selectSource
        • 引数:FilterのリストとSelectOptのリスト
        • 返値:IDとValueが格納されたEntityリスト(conduitによるストリームデータ)
      • selectList
        • 引数:FilterのリストとSelectOptのリスト(selectSourceと同じ)
        • 返値:IDとValueが格納されたEntityのリスト
      • selectFirst
        • 引数:FilterのリストとSelectOptのリスト(selectSourceと同じ)
        • 返値:先頭のIDとValueを格納したEntity(空の場合はNothing)
      • selectKeys
        • 引数:Filterのリスト
        • 返値:IDと指定カラム値(レコード全体は返されない)
    • 引数の詳細
      • Filter
        • 結果を絞り込む条件をリストで表現
          • equal(==.)
          • not equal(!=.)
          • more than or equal(>=.)
          • more than(>.)
          • less than or equal(<=.)
          • less than(<.)
          • is member(<-.)
          • is not member(/<-.)
        • AND/ORの表現
          • AND:一つのリストの中にならべた条件はAND
          • OR:リストとリストを(||.)で結ぶ
      • SelectOpt
        • ソート条件(Asc/Desc)
        • 取得開始位置(OffsetBy)
        • 取得結果数(LimitTo)
    • selectListのサンプルです。
    • 
        -- 厄年(男性:25 or 52 or 62, 女性:19 or 33, 37)の人を検索し名前順で取得
        found <- selectList
                 ( [PersonGender ==. Male, PersonAge <-. [25, 42, 61]]
                   ||. [PersonGender ==. Female, PersonAge <-. [19, 33, 37]] )
                 [ Asc PersonName ]
        -- 結果として得た Entity のリストを順に表示
        liftIO $ mapM print found
      
      
      • 厄年に該当する人を名前順に並べて取得して表示するコードです。GenderにMail/Femail以外を指定したり、PersonAgeに数値以外の型を指定するとコンパイル時の型チェックでエラーを検出してくれます。

    SQL直書きによる検索:rawQuery

    • SQLを直に指定して検索することも可能です。
    • raqQueryの型:
    • mainの中に以下のコードを追加するとSQLを直に与えて、結果を得ることができます。
    • 
        -- 名前が"y"で終端するPersonを検索
        let sql = "SELECT name FROM Person WHERE name LIKE '%y'"
        rawQuery sql [] $$ CL.mapM_ (liftIO . print)
      
      


    参考にしたサイト

    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
    
    


    参考にしたサイト