Haskell и хождение в базы данных с помощью HDBC
31 июля 2013
Сегодня мы научимся работать с реляционными базами данных из Haskell. Будет написана небольшая «телефонная книга» с CLI, которая будет хранить наши контакты в PostgreSQL. В мире Haskell есть много библиотек для работы с базами данных. Мы воспользуемся HDBC.
Как написано в самой документации по HDBC, в данном пакете многое было скопировано с Perl‘ового DBI. Пакет HDBC предоставляет единый интерфейс для работы со всеми СУБД. Детали работы с конкретными СУБД описаны в отдельных пакетах, называемых драйверами. Такой подход позволяет легко переходить с одной базы данных на другую, конечно, если только в приложении не используются возможности, предоставляемые только одной СУБД. Для работы с PostgreSQL через HDBC понадобится драйвер HDBC-postgresql. На Hackage также доступны драйверы для SQLite и MySQL. Кроме того, предусмотрен драйвер для работы по ODBC. Это позволяет работать из Haskell с Oracle, IBM DB2, Microsoft SQL Server и другими СУБД, поддерживающими данный API.
В Ubuntu перед сборкой HDBC-postgresql нужно сказать:
Из пакета HDBC-postgresql понадобится единственная функция:
Как вы уже догадались, она устанавливает соединение с PostgreSQL. Интересно, что функция является ленивой. Если по каким-то причинам приложение ничего не будет делать с базой данных, соединение не будет установлено.
Первый аргумент функции connectPostgreSQL генерируется примерно так:
getConnectString = do
conf <- loadConfig
let dbconf = subconfig "database" conf
[name, user, pass] <- mapM (\n -> require dbconf n :: IO String)
["name", "user", "pass"]
host <- lookupDefault "localhost" dbconf "host" :: IO String
port <- lookupDefault 5432 dbconf "port" :: IO Int
return $ "host=" ++ host ++ " port=" ++ show port ++ " dbname=" ++
name ++ " user=" ++ user ++ " password=" ++ pass
where
fname = ".phonebookrc"
loadConfig =
catch (load [Required $ "$(HOME)/" ++ fname])
(\e ->
do mapM_ putStrLn $ ["Failed to open ~/" ++ fname, ""] ++
usage fname
throw (e :: IOException))
Параметры подключения к СУБД хранятся в файле ~/.phonebookrc. Для парсинга конфига используется уже знакомый нам пакет configurator. Перед началом работы с базой данных нужно создать в ней таблицу phonebook:
Основные функции в пакете HDBC следующие.
Выполнение запросов внутри транзакции.
Выполнить запрос и вернуть количество затронутых строк. Отлично подходит для выполнения простых INSERT-, UPDATE- и DELETE-запросов.
:: IConnection conn =>
conn -> String -> [SqlValue] -> IO [[SqlValue]]
Выполнить запрос и вернуть полученные в результате его выполнения строки. Специально для выполнения SELECT-запросов. Эта функция является строгой. Есть аналогичная ленивая функция quickQuery, без штриха в конце имени.
Приготовиться к выполнению запроса.
Выполнить приготовленный с помощью функции prepare запрос с заданными параметрами. Для SELECT-запросов всегда возвращает 0, в остальных случаях — число затронутых строк.
Многократно выполнить приготовленный запрос для множества параметров. Для некоторых СУБД это может работать существенно быстрее, чем многократный вызов execute.
Возвращает все строки, полученные в результате выполнения запроса. Также для аналогичных целей пакет HDBC предоставляет функции fetchAllRows’, fetchRow, fetchRowAL, fetchRowMap, fetchAllRowsAL, fetchAllRowsAL’, fetchAllRowsMap, fetchAllRowsMap’. Функции, в чьем имени есть штрих, являются строгими. Наличие в имени функции «AL» означает, что функция возвращает ассоциативный список. Если в имени есть «Map», значит вместо списка значений или списка пар из имени столбца и значения функция возвращает Map.
Отсоединиться от базы данных.
Устаревшая функция, сохраненная для обратной совместимости. Раньше она использовалась для перехвата динамических исключений, которые в наше время остались только в легаси коде. Не используйте ее. Исключения, бросаемые в HDBC, прекрасно ловятся традиционными функциями из Control.Exception.
Все это выглядит не слишком сложно, правда? Теперь рассмотрим код модуля Phonebook.Storage, предоставляющего основные функции нашей с вами «телефонной книги».
create, read, update, delete,
ContactId, Name, Phone
) where
import Prelude hiding (read)
import Database.HDBC
import qualified Data.ByteString.Char8 as BS
type ContactId = Integer
type Name = String
type Phone = String
Здесь просто перечисляются экспортируемые функции и объявляются синонимы типов. Функция read модуля Prelude скрывается, потому что в модуле Phonebook.Storage содержится функция с таким же именем. Если этого не сделать, становится непонятно, какая из двух функций экспортируется модулем.
create name phone conn =
withTransaction conn (create' name phone)
create' name phone conn = do
changed <- run conn query [SqlString name, SqlString phone]
return $ changed == 1
where
query = "insert into phonebook (name, phone, last_changed)" ++
" values (?, ?, now())"
read :: IConnection a => a -> IO [(ContactId, Name, Phone)]
read conn = do
rslt <- quickQuery' conn query []
return $ map unpack rslt
where
query = "select id, name, phone from phonebook order by id"
unpack [SqlInteger cid, SqlByteString name, SqlByteString phone] =
(cid, BS.unpack name, BS.unpack phone)
unpack x = error $ "Unexpected result: " ++ show x
update :: IConnection a => ContactId -> Name -> Phone -> a -> IO Bool
update cid name phone conn =
withTransaction conn (update' cid name phone)
update' cid name phone conn = do
changed <- run conn query
[SqlString name, SqlString phone, SqlInteger cid]
return $ changed == 1
where
query = "update phonebook set name = ?, phone = ?," ++
" last_changed = now() where id = ?"
delete :: IConnection a => ContactId -> a -> IO Bool
delete cid conn =
withTransaction conn (delete' cid)
delete' cid conn = do
changed <- run conn "delete from phonebook where id = ?"
[SqlInteger cid]
return $ changed == 1
Довольно скучная реализация функций create, read, update и delete (CRUD). Собственно, это и есть весь модуль. В модуле Phonebook.Utils находится уже рассмотренная функция чтения конфига, а также не представляющее никакого интереса usage-сообщение, которое появляется в случае, если конфиг отсутствует. Еще строк 60 кода находятся в модуле Phonebook.Interface.CLI.
В заключение хотелось бы отметить следующее. Во-первых, не все драйверы баз данных одинаково полезны. Например, если вы решите использовать HDBC-mysql в многопоточном приложении, вас может ждать неприятный сюрприз. Во-вторых, обратите внимание, что для хранения имен и телефонов мы использовали varchar, а не char. Если использовать char, HDBC будет отдавать имена и телефоны с кучей пробелов на конце, которые придется выпиливать самостоятельно. Наконец, если попытаться создать контакт со слишком длинным именем или телефоном, программа бросит исключение. В реальных приложениях нужно либо ловить такие исключения, либо как следует проверять параметры запросов.
Ссылки по теме:
- Глава Using Databases в книге Real World Haskell;
- HDBI — форк HDBC с несколькими существенными отличиями;
- Судя по англоязычным блогам, для работы с PostgreSQL многие используют пакет postgresql-simple;
- Если для работы с базами данных вы предпочитаете ORM-подобные решения, обратите внимание на пакет persistent;
Все исходники к этой заметке вы найдете в этом архиве. Инструкции по его сборке вы найдете здесь.
Дополнение: Пишем простой RESTful сервис с использованием Scotty
Метки: Haskell, СУБД, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.