Сегодня мы научимся работать с реляционными базами данных из 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