yesodで全てのモデルにcreatedAt,updatedAtを作りたかった話
- Yesod Advent Calendar 2016 - Qiitaの5日目の記事です
- 私の
createdAt
,updatedAt
との戦いの記録
背景
User
email Text
name Text
createdAt UTCTime
updatedAt UTCTime
のように,それぞれのモデルに生成時間と更新時間を付けたい.
- 役に立つことがあるかもしれない
- 全てのモデルにつけることで一貫性を保ちたい
問題
単純にFormを
userForm :: Form User
userForm = renderBootstrap3 BootstrapBasicForm $ User <$>
areq emailField "email" Nothing <*>
areq textField "name" Nothing <*>
lift (liftIO getCurrentTime) <*>
lift (liftIO getCurrentTime) <*
bootstrapSubmit ("submit" :: BootstrapSubmit Text)
のように作ってしまうと,当然getCurrentTime
アクションが2回実行されるので,生成時間と更新時間がズレてしまいます.
誤った解決法
formの型をForm User
を,Form (UTCTime -> UTCTime -> User)
とします.
これはformの型の長さが際限なく増えていき,FileInfo
などが絡むと可読性が極めて悪くなるので,やめるべきであると結論づけました.
簡単な解決法
単純なコンストラクタであるUserを使うのではなく,ラムダ式を書いて1つのUTCTime
を2つのフィールドに格納してUser
を合成します.
しかし,これも場当たり的な対処であり,モデルが増えてフォームが増えていくと書くのが段々面倒くさくなっていくという問題があります.
統一的な解決法
現在私はこのような関数を記述して使用することにしています.
-- | Formで`getCurrentTime`を2回実行しないで済むためのコンストラクタ生成装置
withCurrentTime :: (Applicative (t m), MonadTrans t, MonadIO m) =>
t m (UTCTime -> UTCTime -> a) -> t m a
withCurrentTime ctor = (\c t -> c t t) <$> ctor <*> lift (liftIO getCurrentTime)
この関数を使えば,単純なコンストラクタからcreatedAt
, updatedAt
を必要としないコンストラクタが解決できます.
真の問題
そもそも,全てのモデルにcreatedAt
, updatedAt
は必要でしょうか?生成日は必要なことが多いでしょうが,更新日時は必要性が疑問なモデルも多いです.追記型であるpostgresqlとの相性も悪い.
railsやdjangoのように,勝手にフィールドを補完してくれるオプションがあるならば,プログラムが一貫性を保ちますが,yesodはそういったことは行いません.
それには,haskellの型がrubyやpythonの型とは大きく性質が異なることも影響していると考えています.デフォルトでnullではないことや,要素を動的に増やすことが出来ないことですね.
いくら一貫性を保ちたいからと言って,合理性無くフィールドを増やすのは,haskellプログラミングにおいては避けるべきであると考えるようになりました.haskellでプログラミングを行うならば,データ型の要素に関しては「それは本当に必要なの?」と考えるべきです.
更新時間が必要なモデルのほうが特殊なのであり,この件に関しては一貫性よりも合理性を重視するべきでした.
私のデータベース設計は稚拙であり,きちんとRDBと型について学習し直すべきでした.
ていうか、すべてのテーブルにcreated_atとupdated_atをつけるのSQLアンチパターン27章として執筆されるべきだと思うんだけど。
— 人格が優れているエンジニア (@a_suenami) 2016年10月10日
早くこのツイートを読みたかった.