sqlite与monad transformers

2018-03-21  本文已影响0人  XGLey

前言

数据库连接是常见的IO操作,这一节我们将一些IO与monad transformer结合起来,看看能否对我们有启发。

这里我们使用sqlite3数据库,库用sqlite-simple。

连接数据库

我们从教程中可以得到一个非常简单的示例。

main :: IO ()
main = do
  conn <- open db
  execute_ conn "create table if not exists users (id integer primary key, age integer not null, name text not null)"
  execute conn "insert into users (name, age) values (?, ?)" ("haoren" :: T.Text, 10 :: Int)
  id' <- lastInsertRowId conn
  user' <- query conn "select * from users where id = ?" (Only id') :: IO [User]
  close conn
  print user'
  putStrLn "Finished"
# output
[User {userId = 1, userName = "haoren", userAge = 10}]
Finished

open打开一个连接,之后就可以用这个连接进行任何数据库操作了。

繁琐的代码

仔细观察上面的代码,每次操作都需要打开一个连接,而且每次操作都要带上conn连接,我们有没有什么办法做到简单呢?或者我们观察其它语言会如何做的?

如果用过orm,大家一般都知道需要初始化后才能使用,用javascript可能会这样写:

let db = null;

orm.init(config)
    .then(instance => {
        instance.query("select 1");
        // 保存db实例
        db = instance;
        ....
    })
;

当然,我们有时会保存instance这个变量,以供其它地方使用。像上面的db变量一样。

但在Haskell中无法这样做,这是因为它追求纯度决定的。那么我们还有什么办法解决呢?或者我们考虑一下上面的instance实例,它并不会凭空产生,它是由orm初始化产生的一个特定实例,在它产生那刻起,它的所有一切都已经决定好了。简而言之,instance的上下文是由config决定。

或许我们可以特例化这些函数。

exe' :: Query -> IO ()
exe' sql = do
  conn <- open db
  execute_ conn sql
  close conn

qq :: (FromRow r, ToRow t) => Query -> t -> IO [r]
qq sql arg = do
  conn <- open db
  result <- query conn sql arg
  close conn
  return result

exe'execute_的特例,跟原始函数比较,它们都少了Connection参数,我们手动帮它做了。但这种写法有个问题,那就是每执行一些这样的函数,都要年尊连接一次数据库,没有办法做到一次连接执行多个操作。

不知道你有没有这种感觉,db是我们的一个环境变量,我们的所有实例连接也都是产生于它,再往下说,每次数据库操作都依然于conn这个环境变量。我们是时候来试试ReaderT了。

ReaderT封装

在封装之前,我们需要确定要封装到什么程度,或者说我们期待以什么样的形式书写。结合上面我们已遇到的问题,我们现在确定需要一种简便的方法,隐式或自动传递conn,还要允许一次打开数据库,能多次操作。我们可以预想到这样的代码:

main :: IO ()
main = do
  exec $ do
    run' "create table if not exists users (id integer primary key, age integer not null, name text not null)"

  user' <- exec $ do
     run "insert into users (name, age) values (?, ?)" ("haoren" :: T.Text, 10 :: Int)
     id' <- lastId
     find "select * from users where id = ?" (Only id') :: Env [User]

  print user'
  putStrLn "Finished"

一次exec就是一次数据库连接,do后面可以跟多次操作。

刚才提到了,一次数据库连接可以当成对配置的依赖,一次数据操作可以当成是对连接的依赖,所以我们可能会有以下两个类型。

type App = ReaderT Config IO

type Env = ReaderT Connection IO

一个exec可以简单理解成程序自动调用App这个环境,并产生了一个Env,之后execdo语法都将在这个上下文中进行。

我们给出exec的实现:

exec :: Env a -> IO a
exec r = flip runReaderT "user.db" $ do
  db <- ask
  conn <- liftIO $ open db
  result <- liftIO $ runReaderT r conn
  liftIO $ close conn
  return result

user.db就是我们的db啦。第一个runReaderT就是在创建一个App上下文,第二个runReaderT创建了Env上下文,所以我们把r放在第二个runReaderT里。此时它已经得到了一个连接实例。

完成了这一步,我们的任务还未完成,sqlite-simple提供的函数类型并不符合exec的上下文,所以我们需要针对Env创建特有的函数,为了避免重名,我们重新创建函数。

run :: ToRow t => Query -> t -> Env ()
run sql args = do
  conn <- ask
  liftIO $ execute conn sql args

run' :: Query -> Env ()
run' sql = do
  conn <- ask
  liftIO $ execute_ conn sql

find :: (ToRow t, FromRow r) => Query -> t -> Env [r]
find sql args = do
  conn <- ask
  liftIO $ query conn sql args

find' :: FromRow r => Query -> Env [r]
find' sql = do
  conn <- ask
  liftIO $ query_ conn sql

lastId :: Env Int64
lastId = do
  conn <- ask
  liftIO $ lastInsertRowId conn

到这一步,我们就完成了全部的工作,之后只要像样例那样使用即可。

多个实例

如果我们需要连接多个数据库,上面这些代码还能够使用吗?答案是肯定的,除了exec,其它函数并不依赖于Config环境变量,它们仅仅依赖于conn这个上下文,所以除了exec其它函数都可以照旧。

genExec :: Config -> Env a -> IO a
genExec db r = flip runReaderT db $ do
  db <- ask
  conn <- liftIO $ open db
  result <- liftIO $ runReaderT r conn
  liftIO $ close conn
  return result

execA :: Env a -> IO a
execA = genExec "user.db"

execB :: Env a -> IO a
execB = genExec "myuser.db"

main :: IO ()
main = do
  execB $ do
    run' "create table if not exists users (id integer primary key, age integer not null, name text not null)"

  user' <- execA $ do
     run "insert into users (name, age) values (?, ?)" ("haoren" :: T.Text, 10 :: Int)
     id' <- lastId
     find "select * from users where id = ?" (Only id') :: Env [User]

  print user'
  putStrLn "Finished"

小结

我们从实际一个例子,看到了transformer对问题的解决方法。

上一篇下一篇

猜你喜欢

热点阅读