sqlite与monad transformers

前言

数据库连接是常见的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对问题的解决方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • Python 面向对象Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对...
    顺毛阅读 4,210评论 4 16
  • 忽然间,特别期待踏上去云南列车的那一刻的到来,恨不得下一秒就可以到达,那种急切的心情从未如此刻般强烈过。 相对于上...
    猫咪家的小豆豆阅读 811评论 0 1
  • 生活中,无论是收获,还是失去; 朋友中,无论是新朋,还是旧友; 工作中,无论是顺心, 还是压力; 家...
    十里烟花阅读 348评论 1 0
  • 早上,静准备去上班,窗外的2只黑头公小鸟已在机灵地啄食着棕树上的果实。初春的早上8点钟时,暖白色的太阳已经高...
    x若水阅读 263评论 4 0