Использование mongo-cl-driver в качестве провайдера БД mongo на common-lisp





Здравствуйте, все любители common-lisp.

В этой статье я расскажу вам о своем опыте внедрения common-lisp библиотеки доступа к объектной СУБД mongo, которая называется mongo-cl-driver.

Начитавшись в интернете про то, насколько mongo-db быстр, масштабируем и крут и имея далекий и очень скудный опыт взаимодействия с данной БД на с++, я решил попробовать эту БД в своем веб-ориентированном проекте, написанном на common-lisp. Имея, однако, некоторые сомнения в правильности выбора СУБД, могу назвать свой опыт удачно свершившимся, поскольку реализованный функционал как минимум работает.

Любой человек, который начинает программировать доступ к СУБД mongo так или иначе натыкается в интернете на ссылки на cl-mongo — первый появившийся провайдер доступа к БД mongo на common-lisp. Используя cl-mongo в своем проекте, я наткнулся на ряд проблем c преобразованием данных в json, которые начались, когда возникла необходимость перевода результатов запроса по цепочке СУБД->common-lisp-сервер->javascript-клиент. Кстати для подобного кодирования/декодирования существуют известные мне библиотеки:

1) yasson
2) cl-json

Далее по тексту следует несколько примеров использования mongo-cl-driver для общих задач программирования доступа к СУБД mongo на common-lisp. Если примеры покажутся читателю малопонятными, выдранными из контекста, то есть возможность посмотреть примеры использования в доступных исходных кодах. Наиболее ценные куски кода содержатся в файлах webserver.lisp и competitions.lisp

В общем, возьмем этого быка за рога:

Подключение и обращение к БД можно условно разделить на два шага: создание экземпляра класса database и экземпляра класса коллекции. Далее — очень удобно работать именно с экземпляром коллекции.

Создаем экземпляр CLOS класса database. О объектно-ориентированном программировании на common-lisp можно почитать в переводе книги practical-common-lisp. Конструктор этот, также может принимать в качестве аргументов хост, порт, пользователя и пароль, однако, автор пользовался только первыми двумя параметрами.

Создание экземпляра database, для доступа к БД именуемой ski73:

(defparameter *db-instance* (make-instance 'mongo:database :name "ski73"))


Для работы с конкретной коллекцией необходимо создать экземпляр класса коллекции:

(defparameter *competitions* (mongo:collection *db-instance* "competitions"))

Теперь мы можем использовать его в нашем проекте

Для выборки интересующей информации можно использовать одну из предоставляемых mongo-cl-driver функций: find-one или find-list. Для начала предлагается представить как выглядит запрос в консоли mongo для выборки наименования и даты проведения соревнования в коллекции competitions:

db.competitions.find({}, {title: 1, date: 1});


Для выбора всех полей документа, нужно вызвать find без параметров:

db.competitions.find();


В качестве первого члена функции find передаются условия выборки. Подобным образом мы указываем предложение where в SQL запросе. Вторым параметром является фильтр возращаемых значений — мы указываем интересующие нас имена целевых полей, помечая их 1-ками. Для получения более подробной информации можно обратиться к документации по запросам mongo.

В mong-cl-driver получение списка документов реализовано через функцию find-list, а выборка одного документа — через find-one. К сожалению, ни тот ни другой метод не могут (из субъективного опыта автора) поддерживать параметры query и filter по умолчанию. Вот так выглядит получение списка соревнований из коллекции competitions.

(find-list *competitions* :query (son) :fields (son "title" 1 "date" 1))


Для того чтобы исключить из выборки какие-либо фильтры и условия поиска, нужно передать в качестве значений параметров :query и :fields вызовы функции son без параметров.

Например, вот так:

(find-list *competitions* :query (son) :fields (son))


Для поиска единственного документа используется функция find-one. В качестве первого (filter) и второго (mask) параметров ей также необходимо передавать, если производится запрос без условий и фильтров, пустые вызовы son

Например:

(find-one *coll-instance* (son) (son))


Что такое son?

Для того, чтобы запрос find в common-lisp выполнялся таким же образом, как это делается в консоли mongo, необходимо передавать в нём две структуры с набором key-value значений. Кстати, в той же cl-mongo есть аналогичная функция, которая так и назыается kv (key-value). Я уже не помню точно, но есть и с ней, наряду с другими её функциями, некоторые неудобства.

В mongo-cl-driver же, есть функция son.

Вот придуманный на ходу пример конструирования запроса с непустыми параметрами query и fields:

(find-list *coll-instance* :query (son "name" "Ivan" "surname" "Ivanov")
		:fields (son "name" 1 "surname" 1 "address" 1 "telephone"))


На четных местах функции стоят целевые key (имена членов) на нечетных value.

Далее следует, простой пример метода получения key-value объекта из класса, представляющего спортивное соревнование.

(defmethod mongo-doc ((c-instance competition))
   (son "title" (title c-instance)
      "date" (date c-instance)
      "begin-time" (begin-time c-instance)
      "end-time" (end-time c-instance)
      "captions" (captions c-instance)
))


Поиск по Id

Сопутствующей задачей в разработке клиент-серверного http приложения явилась передача на сервер id-ка, полученного ранее — предыдущим запросом от клиента. Возможно, подход не совсем правильный, но я решил, что id надо передавать цельным куском. Следующий пример поможет прояснить ситуацию.

Представим что в базе хранятся объекты соревнований и в каждом есть вложенные списки забегов (раундов)

Вот так формируется список соревнований для отправки на клиент в виде json. Ответ формируется в контексте диспетчера http сервера hunchentoot. define-url-fn — это макрос, который создает и регистрирует функцию-обработчик для запроса competitions-list, например такого ski73.ru:4242/competitons-list. Удобно, правда?

;Список троек {id, title, date} для передачи списка соревнований
(define-url-fn (competitions-list)
   (str (encode-json-to-string (find-list *competitions* :query (son) :fields (son "title" 1 "date" 1)))) )

В данном примере мы регистрируем обработчик запроса на получение списка соревнований. Нас интересует наименование и дата проведения. find-list всегда будет возвращать для каждого найденного документа еще и его 12-байтный id-шкник вместе с указанными в параметре :fields полями. На стороне клиентского javascript-a он сохраняется в виде массива из 12 Number-ов. Было решено засериализовать его в строчку для передачи другого запроса на сервер, который получал бы более емкую структуру c информацией по забегам с конретными результатами атлетов.

Функция mongoId предназначена для конвертации массива чисел в строку, составленную из байт идентификатора в соответствии с содержимым 12-элементного массива mid.

/** Получаем строку, в которой хранятся 12 байт mongo id-шника для передачи в запросе */
function mongoId(mid) {
   var strId = new String();
   var plusByCode = function(el, index, arr) {
      return strId += String.fromCharCode(el);
   };
   mid.raw.forEach(plusByCode);
   return strId;
}


Полученную при помощи mongoId строку мы помещаем в POST запроc. И, далее, на стороне сервера запрашивается информация по этому id-шнику из mongo-db. Делается это так:

;Более подробная информация по соревнованию
(define-url-fn (competition-info)
   (let ((id (post-parameter "id")) )
      (str
         (encode-json-to-string (find-one *competitions* 
	    (son "_id" (make-instance 'object-id :raw (flexi-streams:string-to-octets id))) 
	    (son "rounds" 1 "captions" 1 "title" 1)))
)))


Ключевым местом в конструировании запроса к СУБД является функция получения 12-ти элементного вектора чисел из строки, переданной c клиента в виде POST-парметра — flexi-streams:string-to-octets. Полученный вектор помещается в конструктор класса object-id.

Ну и в заключение, для тех кто тоже хочет попробовать, чтобы было меньше проклятий в сторону автора, приводится ооочень краткое описание процесса установки mongo-cl-driver в среду common-lisp. Практически, использовалась реализация sbcl.

При установке mongo-cl-driver у начинающих lisp-программистов, к которым сам автор также себя относит, могут возникнуть кажущиеся неразрешимыми проблемы.
Устанавливается mongo-cl-driver при помощи вот такого простого заклинания:
(ql:quickload «mongo-cl-driver»)
В вашей лисп среде, к этому моменту, должен быть настроен quick-lisp — система загрузки и установки lisp-программ. На официальном сайте проекта можно прочитать как её поставить и поюзать — www.quicklisp.org/beta
Скорее всего, в середине процесса установки вылезет исключение (перезапуск), сообщение об ошибке, вещающее о том, что есть неразрешенная зависимость — cl-camel-case.
Берем её у автора mongo-cl-driver и устанавливаем при помощи одного из трех методов описанных на замечательном сайте lisper.ru (с красивой ящеркой), в разделе wiki
Надеюсь, ничего не упущено. Добавлю лишь то, что библиотека использовалась при создании авторского ресурса для спортсменов-лыжников. Доступны исходники

Отдельное спасибо автору mongo-cl-driver archimag-у за некоторые подсказки в ходе разработки ресурса. И удачи, друзья!

Похожие публикации

Средняя зарплата в IT

113 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 5 003 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 4

    0
    Помню как-то смотрел три библиотечки для json, и перечисленные вами чем-то не угодили. Использую st-json с двумя киллер-фичами:
    1. Легко и просто определив метод можно определить способ сериализации для любого типа.
    2. Позволяет выбрать между alist и plist для десериализации JS объектов.
      0
      Интересно, а есть ли для LISP библиотеки AMF? Также интересно как будет выглядеть DSL класс мэппинга на CL. Давно хотел поэкспериментировать сабжем на серверсайде.
        0
        Думаю стоит указать в статье, что функция son создаёт простую hash-table, это просто такой сахар.
          0
          Прошу прощения за некропост, но меня интересует вопрос по поводу id.

          Зачем связываться с этими внутриними идентификаторами базы _id, которые долго сравнивать и конвертировать, почему не ввести собственное числовое поле id и генератором вроде:
          (let ((counter 0)) (defun gen-id () (incf counter)))


          C чем связано использование именно _id поля?

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое