前言
數(shù)據(jù)庫連接是常見的IO操作塞俱,這一節(jié)我們將一些IO與monad transformer結合起來飞涂,看看能否對我們有啟發(fā)恩沛。
這里我們使用sqlite3數(shù)據(jù)庫枣耀,庫用sqlite-simple霉晕。
連接數(shù)據(jù)庫
我們從教程中可以得到一個非常簡單的示例。
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
打開一個連接捞奕,之后就可以用這個連接進行任何數(shù)據(jù)庫操作了牺堰。
繁瑣的代碼
仔細觀察上面的代碼,每次操作都需要打開一個連接颅围,而且每次操作都要帶上conn
連接伟葫,我們有沒有什么辦法做到簡單呢?或者我們觀察其它語言會如何做的院促?
如果用過orm筏养,大家一般都知道需要初始化后才能使用,用javascript可能會這樣寫:
let db = null;
orm.init(config)
.then(instance => {
instance.query("select 1");
// 保存db實例
db = instance;
....
})
;
當然常拓,我們有時會保存instance
這個變量渐溶,以供其它地方使用。像上面的db
變量一樣弄抬。
但在Haskell中無法這樣做掌猛,這是因為它追求純度決定的。那么我們還有什么辦法解決呢?或者我們考慮一下上面的instance
實例荔茬,它并不會憑空產(chǎn)生废膘,它是由orm初始化產(chǎn)生的一個特定實例,在它產(chǎn)生那刻起慕蔚,它的所有一切都已經(jīng)決定好了丐黄。簡而言之,instance
的上下文是由config
決定孔飒。
或許我們可以特例化這些函數(shù)灌闺。
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_
的特例,跟原始函數(shù)比較坏瞄,它們都少了Connection
參數(shù)桂对,我們手動幫它做了。但這種寫法有個問題鸠匀,那就是每執(zhí)行一些這樣的函數(shù)蕉斜,都要年尊連接一次數(shù)據(jù)庫,沒有辦法做到一次連接執(zhí)行多個操作缀棍。
不知道你有沒有這種感覺宅此,db
是我們的一個環(huán)境變量,我們的所有實例連接也都是產(chǎn)生于它爬范,再往下說父腕,每次數(shù)據(jù)庫操作都依然于conn
這個環(huán)境變量。我們是時候來試試ReaderT
了青瀑。
ReaderT封裝
在封裝之前璧亮,我們需要確定要封裝到什么程度,或者說我們期待以什么樣的形式書寫斥难。結合上面我們已遇到的問題枝嘶,我們現(xiàn)在確定需要一種簡便的方法,隱式或自動傳遞conn
蘸炸,還要允許一次打開數(shù)據(jù)庫,能多次操作尖奔。我們可以預想到這樣的代碼:
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
就是一次數(shù)據(jù)庫連接搭儒,do
后面可以跟多次操作。
剛才提到了提茁,一次數(shù)據(jù)庫連接可以當成對配置的依賴淹禾,一次數(shù)據(jù)操作可以當成是對連接的依賴,所以我們可能會有以下兩個類型茴扁。
type App = ReaderT Config IO
type Env = ReaderT Connection IO
一個exec
可以簡單理解成程序自動調用App
這個環(huán)境铃岔,并產(chǎn)生了一個Env
,之后exec
的do
語法都將在這個上下文中進行。
我們給出exec
的實現(xiàn):
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
就是在創(chuàng)建一個App
上下文智嚷,第二個runReaderT
創(chuàng)建了Env
上下文,所以我們把r
放在第二個runReaderT
里纺且。此時它已經(jīng)得到了一個連接實例盏道。
完成了這一步,我們的任務還未完成载碌,sqlite-simple提供的函數(shù)類型并不符合exec
的上下文猜嘱,所以我們需要針對Env
創(chuàng)建特有的函數(shù),為了避免重名嫁艇,我們重新創(chuàng)建函數(shù)朗伶。
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
到這一步,我們就完成了全部的工作步咪,之后只要像樣例那樣使用即可论皆。
多個實例
如果我們需要連接多個數(shù)據(jù)庫,上面這些代碼還能夠使用嗎歧斟?答案是肯定的纯丸,除了exec
,其它函數(shù)并不依賴于Config
環(huán)境變量静袖,它們僅僅依賴于conn
這個上下文觉鼻,所以除了exec
其它函數(shù)都可以照舊。
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對問題的解決方法坠陈。