HDBCを使う

さてようやくDB操作を実践出来ます。
せっかく導入に成功したHDBC-postgresqlですが、本エントリには出て来ません。DB操作はHDBCというパッケージで抽象化されているからです。
JavaでいうところのJDBCがHDBCで、JDBCドライバがHDBC-postgresqlに当たるわけですね。

というわけで今回はHDBCの範囲でのDB操作をやってみます。

テーブルのCREATE

SELECT文以外はDatabase.HDBC.run関数を使うのがお手軽な感じです。

createTable :: IConnection c => c -> IO ()
createTable = \c -> run c "create table COUNTER (count bigint)" []
            >>  return ()

INSERT/UPDATE

ここでもDatabase.HDBC.run関数を使います。

insert :: IConnection c => c -> IO (Integer)
insert = \conn -> run conn "insert into COUNTER values (1)" []

update :: IConnection c => c -> IO (Integer)
update = \conn -> run conn "update COUNTER set count = count + 1" []

SELECT

Database.HDBC.quickQuery関数を使うのがお手軽なようです。

select :: IConnection c => c -> IO ([[SqlValue]])
select = \conn -> quickQuery conn "select COUNT from COUNTER" []

トランザクション

Database.HDBCにcommit関数やrollback関数が提供されていますが、よりHaskellらしいのはwithTransaction関数です。
型は

IConnection conn => conn -> (conn -> IO a) -> IO a

DB接続と関数を渡すと、1つのトランザクションの中で関数を実行してくれるようです。関数型言語らしい関数だと思います。
例えば以下のように使います。

resetCount :: IConnection c => c -> IO Integer
resetCount = \c -> withTransaction c core
  where
    core = \c -> run c "delete from COUNTER" []

応用編・カウンター

これらの関数を組み合わせて、簡単なカウンターを作ってみました。
元ネタはこちらのページです。
ちょっと長いけど、全文を掲載。

module Counter (
  createTable
  ,getNowCount
  ,resetCount
) where

import Database.HDBC

getCountFromRow :: [[SqlValue]] -> Integer
getCountFromRow [] = 0
getCountFromRow ([count]:_) = (fromSql count) :: Integer

createTable :: IConnection c => c -> IO ()
createTable = \c -> run c "create table COUNTER (count bigint)" []
            >>  return ()

getNowCount :: IConnection c => c -> IO Integer
getNowCount conn = withTransaction conn core
  where
    core conn = quickQuery conn sql []
         >>= return . getCountFromRow
         >>= \cnt  -> insertOrUpdate cnt
         >>  return (cnt + 1)
    sql = case hdbcDriverName conn of
                 "sqlite3" -> "select count from COUNTER"
                 _         -> "select count from COUNTER for update"
    insert = run conn "insert into COUNTER values (1)" []
    update = run conn "update COUNTER set count = count + 1" []
    select = quickQuery conn "select COUNT from COUNTER" []
    insertOrUpdate 0 = insert
    insertOrUpdate _ = update

resetCount :: IConnection c => c -> IO Integer
resetCount conn = withTransaction conn core
  where
    core conn = run conn "delete from COUNTER" []

大事なのはgetNowCount関数です。

COUNTテーブルにレコードがなければINSERTして1を返します。
レコードがあれば値を取得して、UPDATEでカウントアップしてその値を返します。

コーディングスタイルに迷う

今回はinsertやupdateという関数はgetNowCount関数からしか使わないため、getNowCount関数の中のローカル関数として定義しました。このためgetNowCount関数の引数をローカル関数から参照し、関数宣言を簡略化しています。
しかしこれ、やり過ぎると良くない気がします。理由はgetNowCount関数の引数のスコープが広くなるからです。

しかし一方で、where以下の関数は他に使い回すことなんてまずないほど用途が限定されているので、getNowCount関数の引数に依存しても構わない気もします(だからこそローカル関数にしているのですし)。

記述性・汎用性・可読性のバランスはどの言語にも共通の問題のようです。