Haskell Persistent Library - 如何从我的数据库中获取数据到我的前端?

Posted

技术标签:

【中文标题】Haskell Persistent Library - 如何从我的数据库中获取数据到我的前端?【英文标题】:Haskell Persistent Library - How do I get data from my database to my frontend? 【发布时间】:2021-06-23 15:40:52 【问题描述】:

您好,感谢您抽出宝贵时间。 我正在尝试创建一个网站,其中包含一个增加计数器的按钮。我希望当前计数器保持不变,如果有人访问我的页面,则应显示当前计数器。 每次单击按钮以增加计数器时,都应该发送一个请求。该请求不包含有关计数器值的任何信息。服务器 - 在我的例子中是一个 warp Web 服务器 - 应该更新数据库中的计数器值,读取更新后的值,然后如果成功则将其发送到前端,如果不成功则发送错误消息。

到目前为止,只有更新有效,因为我没有设法弄清楚如何将数据从数据库获取到前端。 这是我的存储库模块中应该进行更新的代码:

-# LANGUAGE EmptyDataDecls, FlexibleContexts, GADTs, GeneralizedNewtypeDeriving#-
-# LANGUAGE MultiParamTypeClasses, OverloadedStrings, QuasiQuotes #-
-# LANGUAGE TemplateHaskell, TypeFamilies, DataKinds, FlexibleInstances#-
-# LANGUAGE DerivingStrategies, StandaloneDeriving, UndecidableInstances #-

module Repository (increaseCounter) where

import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (runStderrLoggingT)
import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Control.Monad.Reader
import Data.Text
import Data.Maybe

-- setting up the Counter entity with a unique key so I can use the getBy function
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
      Counter
        counterName String
        counterCount Int Maybe
        UniqueCounterName counterName
        deriving Show
    |]
    
increaseCounter :: IO ()
increaseCounter = 
    runStderrLoggingT $ withSqliteConn "//absolute/path/database.db" $ runSqlConn $ do
    runMigration migrateAll -- only for developing

    updateWhere [CounterCounterName ==. "unique name"] [CounterCounterCount +=. Just 1]
    counterEntity <- getBy $ UniqueCounterName name
    liftIO $ print counterEntity

这会编译并实际保存计数器并在每次调用时更新值。但是从类型中可以看出,更新后它只会将计数器值打印到控制台。 我似乎无法理解如何使用从 getBy 函数返回的数据。 文档说:

getBy :: (PersistUniqueRead backend, MonadIO m, PersistRecordBackend record backend) => 
Unique record -> ReaderT backend m (Maybe (Entity record))

'backend m' 基本上是一个嵌套的 monad 吗? 假设我只想发送计数器的值,如果它是Just Int,如果它是Nothing,我想返回-1。 我假设我无法修改 increaseCounter 函数,使其类型为Maybe Int。但是如何将函数传递到 monad/访问内部的值以向前端发送响应?

如果这个问题是肤浅的和/或我目前缺乏太多知识,您能否推荐好的信息来源?像一个好的教程或 youtube 频道之类的东西?

谢谢!

【问题讨论】:

如果您使用 Warp,那么您提供给 Warp 的 run 的 Application 是处理请求并返回响应的内容 - 但您不共享任何此代码。我对您拥有什么以及正在使用什么感到有些困惑。 【参考方案1】:

您可以忽略getBy 类型签名的所有单子部分。如果你得到你的代码进行类型检查,counterEntity 的类型是 Maybe (Entity Counter),这就是这里重要的一切。

如果查询失败(即表中没有该计数器的记录),则counterEntityNothing。否则,它是 Just 一个 Entity Counter 包含检索到的记录:

case counterEntity of
  Just e -> ...

这个e :: Entity Counter 可以通过entityVal 变成Counter。可以使用counterCounterCount 提取该Counter 的所需字段。结果将是 Maybe Int,因为您已将该字段标记为 Maybe,因此您将有另一层 Maybe 进行解包:

case counterEntity of
   Nothing -> -1    -- no record for this counter
   Just e -> case counterCounterCount (entityVal e) of
     Nothing -> -1  -- record, but counter value missing
     Just v -> v

您需要从increaseCounter 返回此值,因此最终版本将如下所示:

increaseCounter :: IO Int
increaseCounter =
    runStderrLoggingT $ withSqliteConn "//absolute/path/database.db" $ runSqlConn $ do
    runMigration migrateAll -- only for developing
    updateWhere [CounterCounterName ==. "unique name"] [CounterCounterCount +=. Just 1]
    counterEntity <- getBy $ UniqueCounterName "unique name"
    return $ case counterEntity of
      Nothing -> -1
      Just e -> case counterCounterCount . entityVal $ e of
        Nothing -> -1
        Just v -> v

无论您以前在哪里成功使用increaseCounter 来增加计数器,您现在都想写:

updatedCounterValue <- increaseCounter

您可以将普通的旧 updatedCounterValue :: Int 传递给前端。

您可能会发现使用upsertBy 更明智,它可以在计数器记录丢失时插入计数器记录,否则更新它。它还返回插入/更新的实体,为您节省了单独的 getBy 调用。我也不明白你为什么用Maybe 标记counterCount。为什么要在表中插入一个没有值的计数器?如果要表示“无计数”,“0”不是更好的起始值吗?

所以,我将架构重写为:

  Counter
    counterName String
    counterCount Int    -- no Maybe
    UniqueCounterName counterName
    deriving Show

increaseCounter 函数为:

increaseCounter :: IO Int
increaseCounter =
    runStderrLoggingT $ withSqliteConn "//absolute/path/database.db" $ runSqlConn $ do
    runMigration migrateAll -- only for developing
    let name = "unique name"
    counterEntity <- upsertBy (UniqueCounterName name)
                              (Counter name 1)
                              [CounterCounterCount +=. 1]
    return $ counterCounterCount (entityVal counterEntity)

插入 1 个计数或增加现有计数。

最后,作为一种通用的设计方法,最好将数据库迁移和连接设置移动到main 函数中,并且可能使用连接池,例如:

#!/usr/bin/env stack
-- stack --resolver lts-18.0 script
--   --package warp
--   --package persistent
--   --package persisent-sqlite

-# LANGUAGE EmptyDataDecls, FlexibleContexts, GADTs, GeneralizedNewtypeDeriving #-
-# LANGUAGE MultiParamTypeClasses, OverloadedStrings, QuasiQuotes #-
-# LANGUAGE TemplateHaskell, TypeFamilies, DataKinds, FlexibleInstances#-
-# LANGUAGE DerivingStrategies, StandaloneDeriving, UndecidableInstances #-

import Control.Monad.Logger (runStderrLoggingT)
import Database.Persist
import Database.Persist.TH
import Database.Persist.Sqlite
import Control.Monad.Reader
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Handler.Warp
import qualified Data.ByteString.Lazy.Char8 as C8

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
      Counter
        counterName String
        counterCount Int
        UniqueCounterName counterName
        deriving Show
    |]

increaseCounter :: ReaderT SqlBackend IO Int
increaseCounter = do
    let name = "unique name"
    counterEntity <- upsertBy (UniqueCounterName name)
                              (Counter name 1)
                              [CounterCounterCount +=. 1]
    return $ counterCounterCount (entityVal counterEntity)

main :: IO ()
main = runStderrLoggingT $ withSqlitePool "some_database.db" 5 $ \pool -> do
  runSqlPool (runMigration migrateAll) pool
  let runDB act = runSqlPool act pool
  liftIO $ run 3000 $ \req res -> do
    count <- runDB $ increaseCounter
    res $ responseLBS
      status200
      [("Content-Type", "text/plain")]
      (C8.pack $ show count ++ "\n")

【讨论】:

非常感谢!这有很大帮助。这不仅直接解决了我的问题,而且还给了我一些关于请求的一般处理的非常有用的提示。

以上是关于Haskell Persistent Library - 如何从我的数据库中获取数据到我的前端?的主要内容,如果未能解决你的问题,请参考以下文章

“类型变量不明确”在 Haskell Yesod 中使用 Persistent

Haskell Persistent 上的 CRUD 模式

Haskell的Persistent sometmes返回500内部服务器错误

Haskell Persistent:可以按包含指定值的字段选择所有行

Haskell Persistent Library - 如何从我的数据库中获取数据到我的前端?

使用 Persistent 输入与数据库的关系