sqlite - Haskell、Sqlite、池和仆人
问题描述
背景
我编写了一个简单的 Servant 应用程序,它将一些信息存储在 SQLite db 文件中。另外,我创建了一个运行数据库查询的通用函数:
{-# LANGUAGE OverloadedStrings #-}
type Db m a = ReaderT SqlBackend (NoLoggingT (ResourceT m)) a
dbFileName :: Text
dbFileName = "./myDb.db"
runAction :: MoinadUnliftIO m => Db m a -> m a
runAction = runSqlite dbFileName
还有一个辅助函数将 DB 操作转换为仆人处理程序:
convert :: IO a -> Handler a
convert = Handler . ExceptT . try
withDb :: Db IO a -> Handler a
withDb = convert . runAction
这有效,我的处理程序按预期响应 HTTP 调用,但有时它们相互冲突,并且其中一个调用尝试访问 SQLite 文件,而另一个调用正忙,这会导致异常。
如果我的理解是正确的,Pool
在我的runAction
函数中添加一个应该可以解决这个问题。但是,似乎我对 Haskell 的经验不够,因为我无法让所有类型都很好地匹配。
问题:如何将 Pool 添加到我的应用程序中?
我在网上找到了一些片段(例如https://github.com/haskell-servant/example-servant-persistent/blob/master/src/App.hs)。经过几次尝试和一些我不一定理解的更改后,我想出了以下代码:
-- slightly different now
type Db m a = ReaderT SqlBackend m a
dbPool :: Pool SqlBackend
dbPool = do
pool <- createSqlitePool dbFileName 5
runSqlPool (runMigration migrateAll) pool
pool
runAction :: MoinadUnliftIO m => Db m a -> m a
runAction action = runSqlPool action dbPool
这个没有编译,我收到以下错误消息:
No instance for (Monad Pool) arising from a do statement
In a stmt of a 'do' block: pool <- createSqlitPool dbFileName 5
有人可以帮我解决这个问题 - 如果可能的话 - 推荐有价值的阅读材料以更好地理解 Haskell?我真的很喜欢这门语言,掌握了基础知识,但我对它的更多现实生活方面相当不知所措。
编辑
按照答案中提供的提示,我找到了解决方案。这一点都不简单,所以我不能把整个代码放在这里,但关键思想在这里:
1.)事实证明,我不能只是“创建一个Pool
然后在这里和那里使用它。APool
包含在一个单子中,所以我需要以单子的方式使用它。2.)最终的解决方案看起来像此处显示的片段:
- 使用类型别名
type DbPool = Pool SqlBacked
而不是ConnectionPool
在片段中使用。 - 将所有
Servant
处理程序声明为函数DbPool -> Handler a
。 DbPool
在 Main 中创建一个。- 将创建的传递
pool
给需要它的每个函数。
它有效,但我感觉它过于复杂了。随着时间的推移,我会看看是否有办法简化它。
解决方案
我相信您的问题是由于对单子缺乏了解造成的。网上有很多教程,所以请允许我简化问题并在您的代码上下文中进行解释。
当我们在 Haskell 中写x :: Int
时,我们的意思是这x
是一个整数值。在定义中x
我们可以写
x :: Int
x = 1+2
但我们不能写
x :: Int
x = do
putStrLn "choose an integer!"
v <- readLn
v -- return v would also be wrong
因为x
上面不是整数,而是最终会产生整数的动作。Haskell 在类型上清楚地区分了这两件事,并迫使我们写x :: IO Int
出来表明这一点。
在你写的代码中
dbPool :: Pool SqlBackend
现在,Pool SqlBackend
是一个值,例如Int
. 该声明声称您将定义dbPool
为将评估为池的表达式。这不是一个可以查询数据库并最终生成池的操作,这是池本身。
通过声明那个类型,你把自己画到了一个角落,因为现在不可能调用createSqlitePool dbFileName 5
来获取池:这将是一个动作,但你只承诺了一个值。
如何解决这个问题?我不知道persistent
图书馆,所以我不能绝对肯定地建议修复。不过,我可以建议一些尝试。
好吧,让我们看一下createSqlitePool
自身的类型:
createSqlitePool :: (MonadLoggerIO m, MonadUnliftIO m)
=> Text -> Int -> m (Pool SqlBackend)
请注意,最终结果不是Pool SqlBackend
,而是m (Pool SqlBackend)
。换句话说,不是类型的值Pool SqlBackend
,而是将产生的动作Pool SqlBackend
。
首先,您可以尝试为您的操作 使用类似的类型dbPool
。
dbPool :: (MonadLoggerIO m, MonadUnliftIO m) => m (Pool SqlBackend)
也许您也可以选择Db IO
作为您的 monad m
,并将所有内容简化为
dbPool :: Db IO (Pool SqlBackend)
然后,您的代码可能应该修改如下:
dbPool :: Pool SqlBackend
dbPool = do
pool <- createSqlitePool dbFileName 5
runSqlPool (runMigration migrateAll) pool
return pool -- note the "return"
runAction :: MonadUnliftIO m => Db m a -> m a
runAction action = do
pool <- dbPool -- run the action, get the value
runSqlPool action pool
推荐阅读
- excel - 电话号码的多种布局需要自定义格式
- php - 从 Google App Engine 中的 app.yaml 文件中检索环境变量
- apache-spark - 如何在 AWS 胶水中的多个工作人员上运行 pySpark 代码
- c - 没有任何内容被写入 outfile
- javascript - 为什么文本块等于图像的大小?
- node.js - Heroku 应用程序在部署节点 js 应用程序时崩溃了 h10
- uuid - 处理 UUID 冲突概率的最合理方法是什么(两个用户被分配相同的 UUID)
- node.js - 为多个关联续集相同的别名
- r - 我应该使用什么替代 `spread` 函数和 tidyr 包?
- java - Java线程内存计算