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

sudo apt-get install postgresql-server-dev- 9.1

Из пакета HDBC-postgresql понадобится единственная функция:

connectPostgreSQL :: String -> IO Connection

Как вы уже догадались, она устанавливает соединение с PostgreSQL. Интересно, что функция является ленивой . Если по каким-то причинам приложение ничего не будет делать с базой данных, соединение не будет установлено.

Первый аргумент функции connectPostgreSQL генерируется примерно так:

getConnectString :: IO String
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:

CREATE TABLE phonebook ( id SERIAL PRIMARY KEY , name VARCHAR ( 64 ) , phone VARCHAR ( 64 ) , last_changed TIMESTAMP ) ;

Основные функции в пакете HDBC следующие.

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

Выполнение запросов внутри транзакции.

run :: IConnection conn => conn -> String -> [ SqlValue ] -> IO Integer

Выполнить запрос и вернуть количество затронутых строк. Отлично подходит для выполнения простых INSERT-, UPDATE- и DELETE-запросов.

quickQuery’
:: IConnection conn =>
conn -> String -> [ SqlValue ] -> IO [ [ SqlValue ] ]

Выполнить запрос и вернуть полученные в результате его выполнения строки. Специально для выполнения SELECT-запросов. Эта функция является строгой. Есть аналогичная ленивая функция quickQuery, без штриха в конце имени.

prepare :: IConnection conn => conn -> String -> IO Statement

Приготовиться к выполнению запроса.

execute :: Statement -> [ SqlValue ] -> IO Integer

Выполнить приготовленный с помощью функции prepare запрос с заданными параметрами. Для SELECT-запросов всегда возвращает 0, в остальных случаях — число затронутых строк.

executeMany :: Statement -> [ [ SqlValue ] ] -> IO ( )

Многократно выполнить приготовленный запрос для множества параметров. Для некоторых СУБД это может работать существенно быстрее, чем многократный вызов execute.

fetchAllRows :: Statement -> IO [ [ SqlValue ] ]

Возвращает все строки, полученные в результате выполнения запроса. Также для аналогичных целей пакет HDBC предоставляет функции fetchAllRows’, fetchRow, fetchRowAL, fetchRowMap, fetchAllRowsAL, fetchAllRowsAL’, fetchAllRowsMap, fetchAllRowsMap’. Функции, в чьем имени есть штрих, являются строгими. Наличие в имени функции «AL» означает, что функция возвращает ассоциативный список. Если в имени есть «Map», значит вместо списка значений или списка пар из имени столбца и значения функция возвращает Map.

disconnect :: IConnection conn => conn -> IO ( )

Отсоединиться от базы данных.

handleSqlError :: IO a -> IO a

Устаревшая функция, сохраненная для обратной совместимости . Раньше она использовалась для перехвата динамических исключений , которые в наше время остались только в легаси коде. Не используйте ее. Исключения, бросаемые в HDBC, прекрасно ловятся традиционными функциями из Control.Exception .

Все это выглядит не слишком сложно, правда? Теперь рассмотрим код модуля Phonebook.Storage, предоставляющего основные функции нашей с вами «телефонной книги».

module 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 :: IConnection a => Name -> Phone -> a -> IO Bool
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 ;

Все исходники к этой заметке вы найдете в этом архиве . Инструкции по его сборке вы найдете здесь .

Дополнение: Работа с PostgreSQL в Haskell при помощи замечательных пакетов postgresql-simple и postgresql-simple-migration

Дополнение: Пишем простой RESTful сервис с использованием Scotty

admin

Share
Published by
admin

Recent Posts

Консоль удаленного рабочего стола(rdp console)

Клиент удаленного рабочего стола (rdp) предоставляет нам возможность войти на сервер терминалов через консоль. Что…

1 месяц ago

Настройка сети в VMware Workstation

В VMware Workstation есть несколько способов настройки сети гостевой машины: 1) Bridged networking 2) Network…

1 месяц ago

Логи брандмауэра Windows

Встроенный брандмауэр Windows может не только остановить нежелательный трафик на вашем пороге, но и может…

1 месяц ago

Правильный способ отключения IPv6

Вопреки распространенному мнению, отключить IPv6 в Windows Vista и Server 2008 это не просто снять…

1 месяц ago

Ключи реестра Windows, отвечающие за параметры экранной заставки

Параметры экранной заставки для текущего пользователя можно править из системного реестра, для чего: Запустите редактор…

1 месяц ago

Как управлять журналами событий из командной строки

В этой статье расскажу про возможность просмотра журналов событий из командной строки. Эти возможности можно…

1 месяц ago