вторник, 26 июля 2011 г.

MongoDB и Haskell. Упрощаем себе жизнь


Многие методы, примененные в статье устарели и неактуальны. Обновленная версия.

Наверное уже все слышали модное нынче слово "nosql", кто-то возможно уже использовал на деле эти замечательные базы данных.

Haskell также имеет биндинги к различным nosql-бд, а именно:

  • Redis
  • Cassandra
  • CouchDB
  • Riak
  • MongoDB


В данной статье я расскажу об использовании MongoDB и Haskell.



Прежде чем совершать какие-либо операции с БД, мы должны установить подключение. Делается это так:
pool <- newConnPool 1 $ host "127.0.0.1"
-- или так
pool <- newConnPool 1 $ Host "127.0.0.1" (PortNumber 3000)
Функция newConnPool создает пул из TCP соединений. Но только при первом обращении, поэтому возникновение ошибки в этом месте невозможно.

Установив подключение, мы можем обращаться к базе данных при помощи Access-монады:
access safe Master pool action

Последним аргументом идет функция с возвращаемым типом Action m a. Пример такой функции:
action :: Action IO Cursor
action = use (Database $ u "database_name") $ find (select [] "collection_name") {sort = ["_id" =: (1 :: Int)]}

Тип Cursor можно преобразовать в Document с помощью функции rest. Это можно делать сразу:
action :: Action IO [Document]
action = use (Database $ u "database_name") $ rest =<< find (select [] "collection_name") {sort = ["_id" =: (1 :: Int)]}

Тогда на выходе мы получим список из Document, а далее будем извлекать из него значения с помощью библиотеки Data.Bson.

Немного забегая вперед. Для построения Bson-конструкций, мы должны включить OverloadedStrings language extension, а также применять функцию "u" к значениям в BSON конструкциях, для конвертации из String в UString.
{-# LANGUAGE OverloadedStrings #-}
["key" =: u "value"]
Чуть выше мы уже применяли эту функцию в такой конструкции:
use (Database $ u "database_name")

Запрос информации из БД осуществляется с помощью совместного использования функций find и select:
find (select ["car" =: u"BMW"] "users_collection") { 
             sort    = ["_id" =: -1 :: Int] 
           , project = ["username" =: 1 :: Int, "age" =: 1 :: Int]
           }

Параметры в фигурных скобках - это поля из алгебраического типа Query. Запомнить следует лишь основные:
  • project - какие поля включить в ответ. Грубо говоря, project = ["a" =: 1 :: Int, b =: 1 :: Int] равно sql-овскому "SELECT a, b".
  • sort - сортировка. 1 - по возрастанию, -1 - по убыванию.
  • skip - сколько документов пропустить. Заметьте, тип - Word32.
Остальные параметры будут использоваться редко, их посмотреть вы можете тут.

Процесс добавления записей в MongoDB не выглядит cложным:
info = ["author" =: u"kreed"
      , "blog" =: u"kreed131.blogspot.com"
       , "message" =: u"Hello Mongo!"]
access safe Master pool $ use (Database $ u "database") $ insert "collection" info

Как вы уже могли заметить, в названии статьи было что-то про простую жизнь.
По большей части наше "упрощение" заключается в том, чтобы убрать бессмысленные повторы одного и того же. Приступим!

Все это можно обернуть в модуль:
module Database.MongoDB.MyHelpers
import Database.MongoDB

Функция для создания пула соединений (при использовании стандартного порта):
newPool h = newConnPool 1 $ host h

Определяем нашу базу данных и создаем функцию для её использования:
dbName = "our_db"
withDb = use (Database $ u dbName)

Функция для запуска "экшенов" (мы её будем использовать как опорную для других функций):
run' p = access safe Master p . withDb
Функция для получения информации из БД:
dbRecv pool a = run' pool (rest =<< a)

Функция для отправки данных в БД:
dbSend pool = run' pool

Функция, которую мы будем использовать подобно "u" в Bson-конструкциях:
i :: (Integral a) => a -> Int
i = fromIntegral -- | w32 будет нужно для параметра skip. w32 :: (Integral a) => a -> Word32 w32 = fromIntegral
Т.е. вместо:
find (select ["target" =: u"car"] "transport") { 
             sort    = ["_id" =: -1 :: Int] 
           , project = ["msg" =: 1 :: Int, "target" =: 1 :: Int, "time" =: 1 :: Int, "_id" =: 1 :: Int]
           }
Мы будем писать:
find (select ["target" =: u"car"] "transport") { 
             sort    = ["_id" =: i (-1)] 
           , project = ["msg" =: i 1, "target" =: i 1, "time" =: i 1, "_id" =: i 1]
           }
Удобно, правда? ;)
Тут вы должны были во весь голос кричать "нет"! Но вариантов у нас не было, по крайней мере я так думал.
Однако с подачи анонимного комментатора, мы можем забыть об этих функциях. Достаточно включить ExtendedDefaultRules:
{-# LANGUAGE ExtendedDefaultRules #-}

Вы можете ознакомиться с приведенным им примером.

На этом все. Стоит лишь учесть, что из-за привязки к имени БД, эти функции нельзя назвать "общими".

Рад был бы увидеть как другие решают такие задачи, но увы, примеров с использованием MongoDB и Haskell почти нету в сети.

На этом все. Удачи!

4 комментария:

  1. Трэш, угар и содомия. Монго - это такая удобная штука, главная фича которой - об ней *не надо* думать. Поднял за 5 минут, взял удобный маппер и забыл навсегда.
    Писать
    > find (select ["target" =: u"car"] "transport") {
    > sort = ["_id" =: i (-1)]
    > , project = ["msg" =: i 1, "target" =: i 1, "time" > =: i 1, "_id" =: i 1]
    > }
    ради одного селекта вменяемый человек никогда не станет, даже с унылой реляционной СУБД и голым HDBC это будет проще.
    > Рад был бы увидеть как другие решают такие задачи
    У Yesod-а был человеческий маппер к монге.

    ОтветитьУдалить
  2. @anon
    Видимо плохо искал, но удобных mapper'ов, да и вообще mapper'ов не нашел. Благо это можно делать с помощью JavaScript.
    Поискав, я нашел замечательную библиотеку helper'ов, для MongoDB:
    https://github.com/MassiveTactical/mt-mongodb
    Она использует Template Haskell и выглядит весьма симпатично.

    ОтветитьУдалить
  3. If you use language extension "ExtendedDefaultRules" you don't need "u" or "i" converters. See http://github.com/TonyGen/mongoDB-haskell/blob/master/doc/Example.hs

    ОтветитьУдалить