[database-devel] (Postgresql-simple) transparently using Transactions/Savepoints
tsuraan
tsuraan at gmail.com
Thu Oct 25 20:07:15 CEST 2012
One problem that I've noticed in the postgresql-simple API (and
probably the other -simple APIs) is that the programmer needs to be
careful when using "withTransaction". If you have some function that
uses it, then that function must not be called from within some other
withTransaction context. This is sort of a SQL issue; calling BEGIN
from within a BEGIN doesn't work well (postgres just gives a warning
when you do this, which is downright scary). However, from within a
transaction, one can use the SAVEPOINT calls; these create
mini-transactions that may be rolled back or released without rolling
back or committing the surrounding transaction. Moreover, if an error
occurs from within a savepoint (such as attempting to violate a
uniqueness constraint), one can simply undo the savepoint instead of
having to roll back the entire transaction.
I've written a small wrapper around postgresql-simple that
transparently uses transactions and savepoints where appropriate. The
code lives at https://github.com/tsuraan/postgresql-transactions .
I'm not thrilled with the API; I think that having transactions be a
StateT wrapping IO would be let me use Connections again instead of
making my own crappy data type wrapping them, but whenever I try to do
that I get confused :( This approach was easy to write, despite the
unfortunate API that is exposes. If anybody could show me how to make
this API nice, I would be very appreciative. The github README has an
example of why I think this transaction/savepoint abstraction is
worthwhile; for those who don't like visiting links, I'll paste my
example below so that everybody can easily see and critique my
motivation. All input will be appreciated!
{-# LANGUAGE OverloadedStrings #-}
{-|
This is a simple motivating example for why postgresql-transactions may be
a useful thing to have. Prepare the database with the following:
BEGIN;
CREATE TABLE Users( userid SERIAL NOT NULL PRIMARY KEY
, username TEXT NOT NULL UNIQUE);
CREATE TABLE Messages( msgid SERIAL NOT NULL PRIMARY KEY
, msg TEXT NOT NULL UNIQUE);
CREATE TABLE Logs( logid SERIAL NOT NULL PRIMARY KEY
, logged TIMESTAMP NOT NULL DEFAULT now()
, userid INT NOT NULL REFERENCES Users
, msgid INT NOT NULL REFERENCES Messages
);
COMMIT;
-}
import Database.PostgreSQL.Simple.Transactions
import Data.ByteString ( ByteString )
import Control.Exception
getOrCreate :: PgTx -> Query -> Query -> ByteString -> IO Int
getOrCreate pg get create value = readRow
where
readRow :: IO Int
readRow = do
rows <- query pg get (Only value)
case rows of
[] -> withTransaction pg mkRow `onException` readRow
[Only rowid] -> return rowid
mkRow :: PgTx -> IO Int
mkRow pg' = do
[Only r] <- query pg' create (Only value)
return r
getUserId :: PgTx -> ByteString -> IO Int
getUserId pg =
getOrCreate pg
"SELECT userid FROM Users WHERE username=?"
"INSERT INTO Users(username) VALUES(?) RETURNING userid"
getMsgId :: PgTx -> ByteString -> IO Int
getMsgId pg =
getOrCreate pg
"SELECT msgid FROM Messages where msg=?"
"INSERT INTO Messages(msg) VALUES(?) RETURNING msgid"
logMsg :: PgTx -> ByteString -> ByteString -> IO Int
logMsg pg username msg = withTransaction pg go
where
go :: PgTx -> IO Int
go pg' = do
userid <- getUserId pg' username
msgid <- getMsgId pg' msg
[Only logid] <- query pg' "INSERT INTO Logs(userid, msgid) VALUES(?, ?) \
\RETURNING logid"
(userid, msgid)
return logid
main = do
c <- connect "host=localhost"
getUserId c "jim" >>= \jimid -> putStrLn ("jim id is " ++ (show jimid))
logMsg c "al" "login" >>= print
logMsg c "bob" "login" >>= print
logMsg c "al" "read email" >>= print
logMsg c "al" "logout" >>= print
logMsg c "bob" "logout" >>= print
More information about the database-devel
mailing list