Haskell 我应该使用ReaderT在Servant中传递数据库连接池吗?
我正在构建一个带有服务和持久性的web API。我计划定义一些API端点(大约15个),它们使用连接池来访问DB 例如,端点定义(Haskell 我应该使用ReaderT在Servant中传递数据库连接池吗?,haskell,monad-transformers,servant,Haskell,Monad Transformers,Servant,我正在构建一个带有服务和持久性的web API。我计划定义一些API端点(大约15个),它们使用连接池来访问DB 例如,端点定义(Handlers)之一是: getUser::ConnectionPool->Int->Handler User getUser池uid=do 用户捕获“id”Int:> (获取“[JSON]a :ReqBody'[JSON]a:>Post'[JSON]NoContent ) 类型UserPoint=点“用户”用户 用户服务器::连接池->服务器用户点 用户服务器池u
Handler
s)之一是:
getUser::ConnectionPool->Int->Handler User
getUser池uid=do
用户捕获“id”Int:>
(获取“[JSON]a
:ReqBody'[JSON]a:>Post'[JSON]NoContent
)
类型UserPoint=点“用户”用户
用户服务器::连接池->服务器用户点
用户服务器池uid=
getUser池uid:
姿势池
我将main
定义为:
main=runstdoutlogging。使用PostgreSqlPool连接字符串numConnections$\pool->do
withResource池(runSqlConn$runMigration migrateAll)
liftIO$run appPort(用户服务器池)
但我很快就注意到,我必须将池逐层(在上面的示例中有2层,在我的实际项目中有3层)传递给每个函数(超过20层)。我的直觉告诉我这是一种难闻的气味,但我不太确定
然后我想到了ReaderT
,因为我认为这可能会将池抽象出来。但我担心的是,ReaderT
的引入可能会导致不必要的复杂性:
- 我需要手动举起很多东西李>
- 类型的心智模式将变得更加复杂,因此更难思考李>
- 这意味着我必须放弃
类型,这也使得使用仆人变得更加困难处理程序
ReaderT
。请提供一些建议(如果您能提供一些关于何时使用ReaderT
或其他monad转换器的指导,我将不胜感激)
更新:我发现我可以使用where
-子句来简化这个过程,这基本上解决了我的问题。但我不确定这是否是最佳实践,所以我仍在寻找答案
userServer::Pooled(服务器用户点)
userServer池auth=c:rud其中
c::UserCreation->Handler NoContent
c=未定义
rud uid=r:u:d其中
处理程序用户
r=do
checkAuth池验证
用户处理程序NoContent
u=未定义
d::Handler NoContent
d=未定义
在与服务器一起定义处理程序时,将避免参数传递,因为服务器变得越来越复杂,您可能希望单独定义一些处理程序:
- 也许某些处理程序提供了一些通用功能,在其他服务器中可能很有用
- 共同定义一切意味着一切都意识到其他一切。 将处理程序移动到顶层,甚至移动到另一个模块,将 帮助明确他们真正需要知道的部分。 这可以使处理程序更容易理解
ReaderT
来完成。随着参数数量的增加,ReaderT
(通常与辅助类型类结合使用)变得更具吸引力,因为它使您不必关心参数顺序
我必须一层一层地向下传递池(在示例中)
上面有2层,在我的真实项目中有3层)
每项功能
除了必须传递参数的额外负担(可能是不可避免的),我认为还有一个潜在的更糟糕的问题:您正在通过多层函数来处理低级细节(连接池)。这可能很糟糕,因为:
- 您正在将整个应用程序提交给使用实际数据库的应用程序。如果在测试期间,您希望将其与某种内存存储库切换,会发生什么情况
- 如果需要更改持久化的方式,重构将在应用程序的所有层中产生反响,而不是保持本地化
transferUser::Conn->Handle->IO()
,其中包括对函数readUserFromDb::Conn->IO User
和writeUserToFile::Handle->User->IO()
的硬连线调用,请将其更改为transferUser::IO User->(User->IO)->IO()
注意,N级的辅助函数可以存储在ReaderT
上下文中;级别N+1的函数可以从那里获得它们
这意味着我必须放弃处理程序类型,这使得使用 仆人也更努力了 您可以使用
ReaderT
转换器通过处理程序定义服务器,然后将其传递给函数,该函数将“缩减”到可运行的服务器:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
import Servant
import Servant.API
import Control.Monad.Trans.Reader
type UserAPI1 = "users" :> Capture "foo" Int :> Get '[JSON] Int
data Env = Env
-- also valid type
-- server1 :: Int -> ReaderT Env Handler Int
server1 :: ServerT UserAPI1 (ReaderT Env Handler)
server1 =
\ param ->
do _ <- ask
return param
-- also valid types:
-- server2 :: ServerT UserAPI1 Handler
-- server2 :: Int -> Handler Int
server2 :: Server UserAPI1
server2 = hoistServer (Proxy :: Proxy UserAPI1) (flip runReaderT Env) server1
{-#语言数据类型}
{-#语言类型运算符{-}
进口佣人
导入服务程序API
导入控制.Monad.Trans.Reader
键入UserAPI1=“users”:>捕获“foo”Int:>Get'[JSON]Int
数据环境=环境
--也是有效类型
--server1::Int->ReaderT Env Handler Int
server1::ServerT UserAPI1(ReaderT Env处理程序)
服务器1=
\参数->
do uuhandler Int
服务器2::服务器用户API1
server2=提升服务器(Proxy::Proxy UserAPI1)(flip runReaderT Env)server1
将其封装为一元状态?@bipll您是指状态吗?@XyRen使用where
方法避免必须传递pool参数的一个问题是,它强制您定义用户服务器
中的所有处理程序。在…上