如何让默认值来自数据库?

时间:2016-02-05 19:09:43

标签: haskell yesod

为什么user对象对NothingcreatedAt仍有updatedAt?为什么这些字段没有被数据库分配?

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
  email String
  createdAt UTCTime Maybe default=CURRENT_TIME
  updatedAt UTCTime Maybe default=CURRENT_TIME
  deriving Show
|]

main = runSqlite ":memory:" $ do
  runMigration migrateAll
  userId <- insert $ User "saurabhnanda@gmail.com" Nothing Nothing
  liftIO $ print userId
  user <- get userId
  case user of
    Nothing -> liftIO $ putStrLn ("coulnt find userId=" ++ (show userId))
    Just u -> liftIO $ putStrLn ("user=" ++ (show user))

输出:

UserKey {unUserKey = SqlBackendKey {unSqlBackendKey = 1}}
user=Just (User {userEmail = "saurabhnanda@gmail.com", userCreatedAt = Nothing, userUpdatedAt = Nothing})

2 个答案:

答案 0 :(得分:5)

(编辑:请参阅下面的解决方案,使用触发器)

问题:默认值不会覆盖显式将列设置为NULL

根据SQLite docs

  

如果用户在执行INSERT时没有显式提供任何值,则DEFAULT子句指定要用于列的默认值。

问题是,当Persistent进行插入时,它会将createdAtupdatedAt列指定为NULL

[Debug#SQL] INSERT INTO "user"("email","created_at","updated_at") VALUES(?,?,?); [PersistText "saurabhnanda@gmail.com",PersistNull,PersistNull]

为了得出这个结论,我修改了你的代码片段以添加SQL日志记录(我刚刚复制了runSqlite的源代码并将其更改为log to STDOUT)。我使用了一个文件而不是内存数据库,所以我可以在SQLite编辑器中打开数据库并验证是否设置了默认值。

-- Pragmas and imports are taken from a snippet in the Yesod book. Some of them may be superfluous.
{-# LANGUAGE EmptyDataDecls             #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}
import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Control.Monad.IO.Class (liftIO)
import Data.Time
import Control.Monad.Trans.Resource
import Control.Monad.Logger
import Control.Monad.IO.Class
import Data.Text

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
  email String
  createdAt UTCTime Maybe default=CURRENT_TIME
  updatedAt UTCTime Maybe default=CURRENT_TIME
  deriving Show
|]

runSqlite2 :: (MonadBaseControl IO m, MonadIO m)
          => Text -- ^ connection string
          -> SqlPersistT (LoggingT (ResourceT m)) a -- ^ database action
          -> m a
runSqlite2 connstr = runResourceT
                  . runStdoutLoggingT
                  . withSqliteConn connstr
                  . runSqlConn

main = runSqlite2 "bar.db" $ do
  runMigration migrateAll
  userId <- insert $ User "saurabhnanda@gmail.com" Nothing Nothing
  liftIO $ print userId
  user <- get userId
  case user of
    Nothing -> liftIO $ putStrLn ("coulnt find userId=" ++ (show userId))
    Just u -> liftIO $ putStrLn ("user=" ++ (show user))

这是我得到的输出:

Max@maximilians-mbp /tmp> stack runghc sqlite.hs
Run from outside a project, using implicit global project config
Using resolver: lts-3.10 from implicit global project's config file: /Users/Max/.stack/global/stack.yaml
Migrating: CREATE TABLE "user"("id" INTEGER PRIMARY KEY,"email" VARCHAR NOT NULL,"created_at" TIMESTAMP NULL DEFAULT CURRENT_TIME,"updated_at" TIMESTAMP NULL DEFAULT CURRENT_TIME)
[Debug#SQL] CREATE TABLE "user"("id" INTEGER PRIMARY KEY,"email" VARCHAR NOT NULL,"created_at" TIMESTAMP NULL DEFAULT CURRENT_TIME,"updated_at" TIMESTAMP NULL DEFAULT CURRENT_TIME); []
[Debug#SQL] INSERT INTO "user"("email","created_at","updated_at") VALUES(?,?,?); [PersistText "saurabhnanda@gmail.com",PersistNull,PersistNull]
[Debug#SQL] SELECT "id" FROM "user" WHERE _ROWID_=last_insert_rowid(); []
UserKey {unUserKey = SqlBackendKey {unSqlBackendKey = 1}}
[Debug#SQL] SELECT "email","created_at","updated_at" FROM "user" WHERE "id"=? ; [PersistInt64 1]
user=Just (User {userEmail = "saurabhnanda@gmail.com", userCreatedAt = Nothing, userUpdatedAt = Nothing})

编辑:使用触发器的解决方案:

您可以使用触发器实施created_atupdated_at列。这种方法有一些很好的优点。基本上,updated_at无法通过DEFAULT值强制执行,因此如果您希望数据库(而不是应用程序)管理它,则需要触发器。触发器还解决了在执行原始SQL查询或批量更新时设置updated_at的问题。以下是此解决方案的样子:

CREATE TRIGGER set_created_and_updated_at AFTER INSERT ON user
BEGIN
UPDATE user SET created_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE user.id = NEW.id;
END

CREATE TRIGGER set_updated_at AFTER UPDATE ON user
BEGIN
UPDATE user SET updated_at=CURRENT_TIMESTAMP WHERE user.id = NEW.id;
END

现在输出符合预期:

[Debug#SQL] INSERT INTO "user"("email","created_at","updated_at") VALUES(?,?,?); [PersistText "saurabhnanda@gmail.com",PersistNull,PersistNull]
[Debug#SQL] SELECT "id" FROM "user" WHERE _ROWID_=last_insert_rowid(); []
UserKey {unUserKey = SqlBackendKey {unSqlBackendKey = 1}}
[Debug#SQL] SELECT "email","created_at","updated_at" FROM "user" WHERE "id"=? ; [PersistInt64 1]
user=Just (User {userEmail = "saurabhnanda@gmail.com", userCreatedAt = Just 2016-02-12 16:41:43 UTC, userUpdatedAt = Just 2016-02-12 16:41:43 UTC})

触发解决方案的主要缺点是设置触发器有点麻烦。

编辑2:避免Maybe和Postgres支持

如果您希望避免MaybecreatedAt的{​​{1}}值,您可以在服务器上将它们设置为某个虚拟值,如下所示:

updatedAt

然后让服务器通过触发器设置值。稍微hacky,但在实践中工作得很好。

Postgresql触发器

OP要求SQLite,但我确信人们也在为其他数据库阅读此内容。这是Postgresql版本:

-- | Use 'zeroTime' to get a 'UTCTime' without doing any IO.
-- The main use case of this is providing a dummy-value for createdAt and updatedAt fields on our models. Those values are set by database triggers anyway.
zeroTime :: UTCTime
zeroTime = UTCTime (fromGregorian 1 0 0) (secondsToDiffTime 0)

答案 1 :(得分:1)

根据http://www.yesodweb.com/book/persistent

  

默认属性对Haskell代码完全没有影响   本身;你仍然需要填写所有价值观。这只会影响   数据库架构和自动迁移。

do
  time <- liftIO getCurrentTime
  insert $ User "saurabhnanda@gmail.com" time time