6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Haskell・Servant+Persistent/Esqueletoで作る実用WebAPI (8) モデル(テーブル定義)

Last updated at Posted at 2018-01-03

はじめに

今回は、WebフレームワークでのMVCでいうM:モデルがテーマです。他のWebフレームワークのご多分に漏れず、Persistentでも「設定より規約」をベースにしつつ、それなりのカスタマイズも可能です。本記事では、標準の規約を紹介しつつ、WebAPIを作る上で使いがちなカスタマイズのポイントにも触れていきます。

まずはここを見ましょう

Persistent Entity Syntax

↑これです。モデル定義の上で大事なポイントのほとんどが書いてあります。英語ですが、困ったらここを見ましょう。

モデル定義の基本

モデル定義例

以前の記事で簡単に紹介しましたが、改めてモデル定義とその結果出来るものについて説明します。例として、下記のテーブルを定義したとします。この結果、RDBのテーブルとしては「person」テーブルが生成され、HaskellとしてはPerson型が生成されます。

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  Person
    name Text
    age Int Maybe
    prsnType PersonType

    deriving Show

  BlogPost
    title Text
    authorId PersonId
    timestamp UTCTime

    deriving Show
]

1行目のshareのところは、基本的におまじないと思っておいていいかと思いますが、簡単にコメントするとしたらこんなところでしょうか。

  • mkPersist sqlSettings :次行以降の型定義をPersistentで使える形に変換します(多分)。sqlSettingsはデフォルト設定(気にしない)
  • mkMigrate :"migrateAll"というマイグレーションの対象を作成します。アプリ開始時(main関数)でのマイングレーションで指定します
  • persistLowerCase:テーブル名等をスネークケース(大文字→小文字+アンダースコア追加)にする

生成されるテーブルカラムとHaskellの関数

テーブルのカラムとPerson型のメンバにアクセスするゲッター関数は下記のようになります。カラムの型はMySQLの前提です(他のRDBでは違う型になります)。

テーブルカラム名 同カラムの型 NULL可 ゲッター関数名 同関数の型
id BIGINT No なし PersonId
name TEXT No personName Text
age BIGINT Yes personAge Maybe Int
person_type TEXT No personPrsnType PersonType

カラム「id」は自動的に生成されます(Primary指定がない限り)。このidはPerson型のメンバとしては含まれませんが、idの型は「PersonId」として定義されます。RDBからの戻り値でidの値が必要なシーンでは「Entity Person型」として、PersonId型とPerson型がセットにして返されます。モデル定義で「Maybe」があるカラムはNULL可となります(なければNULL不可)。モデル定義では「型名 Maybe」であり、これはHaskellの型定義(Maybe 型名)と逆ですので注意が必要です。ゲッター関数は自動的にプレフィックスとしてテーブル名がつきます。ゲッター関数はキャメルケース、テーブルカラムはスネークケースとなります。

列挙型の扱いについて

各データの属性値を管理したい場合、例えば「personテーブルのレコードで、user用レコードならtype=1, admin用レコードならtype=2」としたいようなケースがあるかと思います。RDB的には「属性定義のマスターテーブルを作成して、結合させながら使う」というのもあるかと思いますが、Persistentでは、列挙型の値を取り扱うのがオススメです。テーブルを消費せずにすみますし、値そのものもHaskellの型管理の対象になるからです。

「personテーブル」の例でいうと、下記のように「PersonType」という型を定義する際にderivePersistField関数を使用するだけで、その型がモデルに使用できるようになります。

-- 列挙型定義
data PersonType = PersonTypeUser | PersonTypeAdmin
    deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "PersonType" -- Persistentで使用するために必要

外部キー制約

モデル定義で、メンバの型を他のテーブルのID型にすると、RDBでは該当のカラムに外部キー制約がつきます。例では、BlogPost型のauthorIdがPersonId型となっていますので、RDBではblog_postテーブルのカラムauthor_idは、personテーブルのidへの外部キー制約が自動的につきます。外部キー制約の対象のテーブルは、他のテーブルだけでなく自分のテーブルに対しても指定できます(こういう場合も「外部キー」というんでしょうか?)。

  Person
    name Text
    age Int Maybe
    parentPersonId PersonId -- 自テーブルに対するキー制約

    deriving Show

外部キー制約に違反する更新系操作(insert, update, delete)をしようとした場合は、Persistentから例外が投げられます。また、外部キー制約の相互のカラム(先の例で言うと、blog_postテーブルのauthor_idとpersonテーブルのid)がRDBとして同じ型でない場合には、マイグレーションをしようとした時にエラーとなります。これは通常の開発では起こらない問題ですが、例えば既存のテーブルに対して、Haskellのプログラムを後から作った場合など(=マイグレーションを使わずにテーブルを作成した場合など)で起こります。

時刻、日付について

時刻の型は、Perisistentでは「UTCTime型」となります。Haskellにはタイムゾーンを扱うことができるZoneTime型やLocalTime型もあるのですが、Persistentで使用できるのはUTCTime型のみ、となります。タイムゾーンについての考え方については後で詳しく書きます。

例にはないですが、日付はPersistentではDay型になります。ここは特に悩むポイントはありません。

カスタマイズ

RDBカラムの型の指定

RDBでのカラムの型はPersistentでのモデル定義での型で自動的に決まるのですが、型をちょっと変えたい場合があります。例えば「Personのageは年齢なので、標準のBIGINTでは大きすぎる。TINYINTにしたい」などです。こういった場合には、モデル定義のところに「sqltype」を使います。

-- ageのRDB側の型をTINYINTにしたい場合の変更例
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  Person
    name Text
    age Int Maybe sqltype=tinyint -- TINYINT指定
    prsnType PersonType

    deriving Show
]

また、RDBでいうデフォルト値の指定などもできますが、普通にPersitentを使う分には特に必要となるシーンはないと思います(Persistentでinsertをする際には、全てのカラムの値を与えることになるため)。

プライマリキー(単一カラム)

プライマリキーを「id」ではなく、別のカラムや型で持ちたい、というケースがあります。Persistentでは、「Primary」を指定すると、そのカラムをプライマリキーにすることができます。Primaryが指定された時は、「id」キーは作成されません。

注意点としては、プライマリキーにできる型の制約があります。そのため、場合によってはRDBのカラムの型を指定する必要があります。

BlogPostの例でいうと、例えばPersonId型のようなID型はそのままプライマリキーに指定できます。

  BlogPost
    title Text
    authorId PersonId
    timestamp UTCTime

    Primary authorId
    deriving Show

titleもプライマリキーにしたい場合は、RDBのTEXT型プライマリキーにはできないので、varchar(255)などの型にします。

  BlogPost
    title Text sqltype=varchar(255) -- プライマリーキーにするために必要
    authorId PersonId
    timestamp UTCTime

    Primary title
    deriving Show

プライマリキー(複数カラム)

RDBでは「2以上のカラムの組み合わせでプライマリキーとみなす」という使い方があります。Persistentでは、Primary指定で複数のメンバを指定するだけでOKです。

  BlogPost
    title Text sqltype=varchar(255) -- プライマリキーにするために必要
    authorId PersonId
    timestamp UTCTime

    Primary title authorId -- titleとauthorIdのペアでプライマリキーにする
    deriving Show

ユニークキー制約

ユニークキー制約をつけることも可能です。TEXT型のままではユニークキー制約がつけられないのは、プライマリーキーの時と同じ事情です。下記の例ではtitleにユニークキー制約をつけています。ユニークキー制約のところの「UniqueTitle」という名前は、大文字で始まっていれば何でも構わないと思います。

  BlogPost
    title Text sqltype=varchar(255) -- ユニークキー制約のために必要
    authorId PersonId
    timestamp UTCTime

    UniqueTitle title -- ユニークキー制約設定
    deriving Show

プリミティブではない型の保持

RDBによっては、「配列型」なるものが使えたりしますが、Persistentではそういった型に会わせたSQLを生成することはできません。JSON型等も同じ状況です。

しかし、Haskellでのリスト型のデータをテキストに変換して、RDBでのTEXT型として保存/読み出しをするだけなら可能です。効率の良い検索等は期待できないですが、データの保持だけできればよい、というシーンは結構あるでしょう。

使い方は簡単で、例のBlogPostでauthorIdのかわりに「authorIdList」というメンバをPersonIdのリスト型で定義してあげれば、データのテキスト化やテキストからのデータ型への復旧等はPersistent側で全てやってくれます。

  BlogPost
    title Text
    authorIdList [PersonId] -- リスト型を指定(RDBではTEXT型になる)
    timestamp UTCTime

    Primary title
    deriving Show

自分で作成した型などで同様のことをしようとすると、別の対応が必要になる場合があります。これについては後の記事で紹介します。

型の扱いについての指針

ここでは、Haskell側で使用する型についての指針について書きます。

文字列型

Persistentでは文字列用の型はString型、Text型、ByteString型の全てがそのまま使えます。Text型を使用しても面倒はないので、特別な事情がない限りはText型を使いましょう。日本語対応も問題ありません。Persistentに限らず、WebAPIの開発では基本的にString型は使う必要はありません。

pwstore-fastパッケージを用いてパスワードの取り回しをする場合には、パスワードのメンバをByteString型にしておくといいです。パスワード用の関数を扱う際の変換の手間が減ります。

タイムゾーンについて

上述したように、Persistentでは時刻用の型としてはUTCTimeのみ利用できます。日本で開発をしていると、JSTを想定することも多いかと思います。JSTをどのように実装するかは悩むところです。Persistentを使うという前提であれば、以下の2つの選択肢になるかと思います。

  • (1) 時刻をUTCとして扱う
    スタンダードな使い方ですが、日付の切り替わりが「日本時間の0時ではなく午前9時」になります。そのため、日付をRDBで扱うアプリケーションでは困ることが起こる場合があります。RDBのタイムゾーンはUTCにする必要があります。

  • (2) 時刻をJSTとして扱う
    Haskellでの型はUTCTimeとして扱うが、実態としてはJSTの時刻を扱う、というやり方です。RDBのタイムゾーンをJSTにする必要があります。日付の切り替わりが0時になるため、「日本向け」かつ「日付の扱いが必要」であれば、こちらの選択肢がいいかと思います。
    ただし、「APIのインタフェースとして、そのまま出力すると9時間ずれた時刻として認識されてしまう」「現在時刻が9時間ずれとなる」という問題点があります。このあたりの対応のやり方については、次の記事で記載します。

複数DB定義

WebAPIとして実装する場合でも、複数のRDBに接続することはよくあるかと思います。私が実装した例でも、MySQLx2+SQLitex1、というパターンがありました。ここでは、モデル定義についてだけ記述します。DBへの接続に関する話は別の記事で書きます。

複数DBを使用する場合は、それぞれのDBに対するモデルを並列で書くだけでOKです。

-- RDB1向けのモデル定義
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  Person
    name Text
    age Int Maybe
    prsnType PersonType

    deriving Show

  BlogPost
    title Text
    authorId PersonId
    timestamp UTCTime

    deriving Show
]

-- RDB2向けのモデル定義
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  Person2
    name Text

    deriving Show
]

まとめ

Persistentで扱える型は、一般的なRDBで扱える型を全て利用できるわけではないですが、ここに書いたポイントを利用するだけでも、かなりのWebAPIが実装できるのではないかと思います。今回はRDB(最もバックエンド側)の型定義についての話題でしたので、次回はAPIインタフェース(WebAPIにとって、最もフロントエンド側)での型定義を取り扱います。

参考にしたサイト

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?