27 May 2012

List-функции в CouchDB

NoSQL
На Хабре часто встречается комментарий о том, что документацию разработчики не дочитывают до конца. Столкнулся с этим сам, когда открыл для себя List-функции в CouchDB.

Мне показался вопрос достаточно сложным и не очень хорошо объясненным в документации, решил поделиться с уважаемым сообществом своим исследованием.

List-функции в design-документах CouchDB нужны для того, чтобы иметь возможность обработать всю базу данных одной функцией. Т.е. это некий аналог Full Table Scan в реляционных базах.

Рассмотрим design-документ из реально работающей инсталляции CouchDB.

{
    "_id": "_design/complete",
    "_rev": "2-45c7b0280b529d99b1d34f362e457860",
    "views": {
        "freq": {
            "map": "function(doc) {  emit(doc.REQUEST, 1);}",
            "reduce": "function (key, values, rereduce){return sum(values);}"
        }
    },
    "lists": {
        "basicJSON": "function(head, req) { start({headers :{'Content-Type' : 'text/plain;charset=utf-8'}}); send('{\"head\":'+toJSON(head)+', ');send('\"req\":'+toJSON(req)+', ');send('\"rows\":[');var row;var prev = null;while (row = getRow()){if (prev != null && prev.key == row.key) {} else {if (prev != null) { send(',');} send(toJSON({id: row.id, key: row.key}));} prev = row;} send(']}');}"
    }
}


CouchDB сильно не любит переводы каретки в функциях, поэтому все функции идут в одну строку. Для лучшей читаемости разверну функцию basicJSON в lists:

function (head, req) {
    start({
        headers: {
            'Content-Type': 'text/plain;charset=utf-8'
        }
    });
    send('{\"head\":' + toJSON(head) + ', ');
    send('\"req\":' + toJSON(req) + ', ');
    send('\"rows\":[');
    var row;
    var prev = null;
    while (row = getRow()) {
        if (prev != null && prev.key == row.key) {} else {
            if (prev != null) {
                send(',');
            }
            send(toJSON({
                id: row.id,
                key: row.key
            }));
        }
        prev = row;
    }
    send(']}');


Что здесь интересного?
Ключевым звеном является цикл обработки:

    var row;
    ...
    while (row = getRow()) {
         ...
         send(',');
         ...
         send(toJSON({
             id: row.id,
             key: row.key
        }));
        ...
    }


Поскольку мы работаем с HTTP, когда обращаемся к CouchDB, то list-функция строит http-ответ. Т.е. по сути генерирует текст в какой-то кодировке. За это отвечает фукнция send(); Она возвращает текстовую строку в http-ответ сервера. Можно использовать конструкцию send(toJSON()); для возврата текстового представления JSON-объектов.

getRow() получает следующую запись в БД. Собственно, на её основе формируется цикл обработки.

Функция start отвечает за формирование заголовка http-ответа.

Вызывается приведённая list-функция так:

http://localhost:5984/requests-db/_design/complete/_list/basicJSON/freq?reduce=false

Указывается имя БД, design-документ, _list, название функции списка, представление. Затем идут параметры представления. Параметры можно указывать так же, как при обращении к любому представлению.

Таким образом, с помощью list-функций можно произвести нужную обработку данных, такую, которая не всегда доступна с помощью Map/Reduce или будет очень сложной на клиенте. С помощью условий в цикле можно часть записей отсеивать, а можно на одну запись делать несколько вызовов send(), что приведет к увеличению количества записей в ответе.

В итоге строится http-текст, содержащий все записи в БД, прошедшие через функциональную обработку.

Да, конечно, это не очень быстрая обработка, но ведь и в реляционных БД full table scan — это самая медленная операция.

List-функции, это очень удобный и полезный механизм, до которого многие не дочитывают в документации. По крайней мере я.

Хорошего вам кода!

PS. В CouchDB есть еще show-функции, но про это как-нибудь в другой раз.
Tags:couchdb
Hubs: NoSQL
+5
1.8k 9
Comments 2