【问题标题】:How to let default values come from the database?如何让默认值来自数据库?
【发布时间】:2016-05-15 21:01:03
【问题描述】:

为什么user 对象对于createdAtupdatedAt 仍然有Nothing?为什么这些字段没有被数据库分配?

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})

【问题讨论】:

  • 我猜你希望在插入过程中使用default=CURRENT_TIME 来填写Nothings,也许吧?但我敢打赌default=CURRENT_TIME 实际上只在迁移期间使用。再说一次,我从来没有使用过这些来自的库(似乎是persistent-template——在你的问题中可能值得一提)。
  • 严重依赖 Template Haskell 的一个大问题是,当您生成的代码没有达到您的预期时,您要么必须手动分析它(而且它只是 hideous i>) 或者您必须简单地知道发生了什么,可能是编写代码以生成损坏代码的人。即使您确实发现了问题,如果它是一次性生成的,那么提取一小部分代码并对其进行更改也是不可能的。你最好的选择可能是不使用这个 default=.. 东西并自己编写这个功能。
  • 您使用的是哪个 SQL 后端?我建议从命令行访问您的 SQL 数据库并检查表定义是什么(您需要类似 SHOW CREATE TABLE foo 用于 MySQL 和 \d+ email 用于 Postgres。
  • @SaurabhNanda 请参阅下面使用 SQLite 触发器的解决方案。
  • 在我看来,persistent 的行为看起来是对的。可能是我太习惯生态系统了...如果你想要creationupdate的时间,为什么不通过适当的时间而不是Nothing?还有为什么它被建模为Maybe

标签: haskell yesod


【解决方案1】:

(编辑:使用触发器查看下面的解决方案)

问题:默认值不会覆盖将列显式设置为 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]

为了得出这个结论,我修改了你的 sn-p 以添加 SQL 日志记录(我只是复制了runSqlite 的源并将其更改为日志到 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 支持

如果您想避免为createdAtupdatedAt 设置Maybe 值,您可以在服务器上将它们设置为一些虚拟值,如下所示:

-- | 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)

然后让服务器通过触发器设置值。有点hacky,但在实践中效果很好。

Postgresql 触发器

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

CREATE OR REPLACE FUNCTION create_timestamps()   
        RETURNS TRIGGER AS $$
        BEGIN
            NEW.created_at = now();
            NEW.updated_at = now();
            RETURN NEW;   
        END;
        $$ language 'plpgsql';

CREATE OR REPLACE FUNCTION update_timestamps()   
        RETURNS TRIGGER AS $$
        BEGIN
            NEW.updated_at = now();
            RETURN NEW;   
        END;
        $$ language 'plpgsql';

CREATE TRIGGER users_insert BEFORE INSERT ON users FOR EACH ROW EXECUTE PROCEDURE create_timestamps();
CREATE TRIGGER users_update BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_timestamps();

【讨论】:

  • 好侦探!你赢得了一笔小额奖金。
  • 谢谢@dfeuer,这真的让我很开心:)
  • @MaxGabriel 我相信以 Yesod 的编写方式,您的解决方案是最实用的。但是,问题仍然存在,Yesod 无法表示“未指定”值。在这种情况下,您有一个存在的值、一个 NULL 值和一个 unspecified 值。从行插入的角度来看,unspecified 值不应被视为 NULL,因为这会阻止 DB 填充默认值。
  • @SaurabhNanda 提出问题?
【解决方案2】:

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

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

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

【讨论】:

  • 但是@SaurabhNanda 在打印之前正在从数据库中检索新记录。
  • 在系统设计中拥有单一的“真相”来源不是一个好主意吗?在这种情况下,数据库不是这个“真相”的正确位置——即 createdAt 和 updatedAt 的默认值吗?
猜你喜欢
  • 2015-03-18
  • 2018-09-25
  • 2020-05-24
  • 2021-09-27
  • 2015-02-20
  • 2015-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多