Пример использования Couchbase в связке с PHP

Дисклеймер


Эта статья не призывает бросать все, к чему вы привыкли и переходить на использование Couchbase, без оглядки на весь ваш прошлый опыт и косяки, с которыми вы сталкивались, при разработке собственных проектов. Эта статья имеет своей целью быть лишь кратким описанием технологии использования Couchbase Server в связке с PHP и только. Возможно она будет интересна некоторым как описание возможностей, а возможно и как оценочный взгляд на перспективы.

Что это такое и с чем его едят


Couchbase это очередное направление NoSQL баз данных, разрабатываемое компанией Couchbase, Inc, и является прямым наследником традиций и проблем своего родителя CouchDB. Это документоориентированная база данных, что подразумевает под собой хранение каждой отдельной записи как документа, хотя это не жесткое правило, и в качестве записи может выступать любое значение (вплоть до BLOB строк), но прелесть этой (да и других баз тоже), это именно документоориентированный метод хранения данных.

Документоориентированный метод хранения данных подразумевает под собой то, что все данные будут храниться в виде так называемых документов, т.е. наборов полей, объеденных в документ на принципах здравого смысла и общей логики, присутствующей в записи. Примером такой записи может выступать например профиль пользователя, со списком полей как: логин, пароль, email и прочие. Стандартом хранения документов в данном случае выступает формат документа в виде JSON строки. Данный формат был выбран создателями сознательно, так как является достаточно популярным, легко интерпретируемым и человеко-читаемым. Но не суть важно. Важно чтобы вы имели представление о том, что такое документ и как он выглядит внутри базы данных.

Требуемые компоненты для работы


Для работы с Couchbase при помощи PHP нам понадобится несколько пунктов программного обеспечения:


После успешной установки всего этого добра и успешного запуска, у нас появляется возможность использовать класс, под названием Couchbase, описание которого находится в официальном Git репозитории Couchbase. Для удобства дальнейшего использования советую вам добавить его к себе в проект, дабы в вашей любимой IDe успешно заработал автокомплит.

Кроме того, для удобной работы, вам понадобится завести в самом couchbase отдельный Bucket (аналог базы данных) благодаря которому вам не придется гадить в общий и стандартный default. Делается это путем захода на адрес localhost:8091/index.html#sec=buckets и нажиманием на кнопочку «Create new bucket».

Начинаем кодить


Кодить нечто абстактное не имеет смысла, потому возьмем вполне конкретный пример, приведенный выше, а именно — профиль пользователя. Допустим у нашего пользователя имеются несколько полей: логин, пароль, email ну и из неявного — это его идентификатор типа integer. В JSON представлении, полученный документ у нас будет иметь вид:
{
  "login": "megausername",
  "password": "my secured password!",
  "email": "email@example.com"
}


Для начала нам надо узнать, как это дело можно сохранить в базу данных и как его оттуда можно достать. Делается это достаточно просто, и прекрасно видно в следующем примере:
<?php

/**
 * В этой строке создаем подключение к базе данных Couchabse с указанием всех необходимых параметров, с некоторыми 
 * оговорками. Например имя хоста к которому нужно подключаться, гужно передавать в виде массива, потмоу что 
 * Couchbase чаще всего работает в виде кластера, и для успешной работы кластера типа master-master нужно будет 
 * указывать оба хоста, на которых размещен Couchbase Server. Имя пользователя, пароль базы данных и имя Bucket в 
 * описании не нуждается, а вот последний параметр, говорит нам о том, что подключение к базе данных не нужно 
 * устанавливать каждый раз при каждом запросе.
 */
$couchbase = new Couchbase(array('localhost'), 'couchbase_user', 'couchbase_password', 'users_bucket', TRUE);

/**
 * Создаем документ в виде обычного массива данных стандартными средствами PHP
 */
$document = array(
    'login' => 'megausername',
    'password' => 'my secured password!',
    'email' => 'email@example.com'
);

/**
 * Теперь производим сохранение данной записи в базу данных. Однако нам надо генерировать ID пользователя, что
 * средствами Couchbase делается несколько оригинально, т.к. в этой базе данных нет механизма автоматического
 * autoincrement полей. Для этого в базе данных мы заведем счетчик и будем его инкрементить при каждом создании
 * пользователя. Кстати, при создании нового значения, отсчет начинается с нуля. 
 */

$userId = $couchbase->increment('counter::users', 1, TRUE);

/**
 * Ну а теперь собственно можно сохранять пользовательские данные в базу данных. Для этого существуют два варианта
 * действий. Первый - это использование метода add, который попытается создать новую запись в бвзе данных, и в случае, 
 * если она существует, вернет ошибку. И второй - set, который перезапишет уже существуюущее значение по данному ключу,
 * или создаст новую запись в базе данных, если ее не существует. Что использовать - решать вам, но в данном случае 
 * целостность данных важнее и мы будем использовать метод add
 */

try
{
    $couchbase->add("profile::{$userId}", json_encode($document));
}
catch(\CouchbaseException $e) // в случае возникновения ошибки, будет сгенерирован Exception с ее описанием
{
    // который мы успешно выводим в лог и забываем о ней, так как пока не хотим ее обрабатывать.
    error_log("Совершенно неожиданная проблема с Couchbase: {$e->getMessage()}");
    exit(1);
}

/**
 * Ну и наконец мы достаем документ из базы данных и читаем его значения. Делается это при помощи метода get. 
 * В его писании просто нечего описывать, кроме того факта, что он вернет вам не массив данных, а JSON строку, 
 * которую необходимо будет распарсить и уже дальше работать с полученными данными. 
 */

try
{
    $userData = json_decode($couchbase->get("profile::{$userId}")); 
}
catch(\CouchbaseException $e) // в случае возникновения ошибки, будет сгенерирован Exception с ее описанием
{
    // который мы успешно выводим в лог и забываем о ней, так как пока не хотим ее обрабатывать.
    error_log("Опять проблемы с Couchbase: {$e->getMessage()}");
    exit(1);
}


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

Что такое View и как с ним работать


Если вы зайдете в панель управления сервером Couchbase, то заметите там одну чудную вещь, под названием Views. В ней будут два подпункта «Development views» и «Production views». Это как не сложно догадаться отображения (а в разрезе Couchbase это выборки). Пока там пусто, но разберемся как туда нагадить.

View это по своей сути индексы, правила для создания которых можно описывать на языке JavaScript. Да, да. Индексы тут создаются на основании логики описанной вами и несут не просто перечисляемую функцию, но и некоторую смысловую. Например в индекс можно включить пользователей у которых длинна email больше n символов, или присутствуют только определенные поля. Для работы с индексами у нас имеется только JavaScript но и его достаточно с лихвой. Обновление индекса происходит либо по требованию (при запросе на получение данных), либо автоматически, когда фрагментация базы данных достигает определенного процента (заданного в настройках Bucket). Этот момент тоже нужно учитывать при разработке.

Создать view можно двумя способами. Первый из них — это писать JavaScript правила напрямую в панели управления в разделе Development и переносить их на Production, либо напрямую из PHP дергать метод setDesignDoc с описанием скриптов, которые будут напрямую попадать в Production секцию.

Сначала рассмотрим скрипт и из чего он должен состоять. Скрипт представляет из себя функцию, на входе у которой описание meta информации документа и его содержимое. Давайте рассмотрим создание индекса, на основании логина пользователя.
function (doc, meta) {
    // если тип документа является JSON и в документе присутствует поле login
    if (meta.type == "json" && doc.login) {
        // то из ключа записи достаем userId (он у нас записан через двойное двоеточие)
        var userid = meta.id.split("::");
        // и добавлем полученные данные в индекс ключ/значение
        emit(doc.login, parseInt(userid[1]));
    }
}

Благодаря вышеописанному JavaScript методу можно понять, что в индекс (которые формируется при помощи метода emit) попадут только записи у которых присутствует поле login. Как видим, функция JavaScript выполнена в виде callback функции, которая будет применена к каждой записи, находящейся в данном Bucket. Следует учесть что view можно создавать в любой момент существования жизненного цикла bucket. Т.е. если у вас возникла необходимость добавить новый функционал, вы без проблем можете добавить новые view и жить дальше, как вам хочется.

И так. Разберемся как же мы можем узнать идентификатор пользователя, если мы знаем только его login. Для этого нам надо создать новый индекс (мы его создадим сразу из PHP кода) и вызовем его.
/**
 * Создаем описание нашего view 
 */
$designedDocument = array(
    'language' => 'javascript',
    'views' =>array(
        'login' => array(
            'map' => 'function (doc, meta) {if (meta.type == "json" && doc.login) {var userid = meta.id.split("::"); emit(doc.login, parseInt(userid[1]));}}'
        )
    ),
);

/**
 * Вызываем сохранение данного дизайн документа в базу данных 
 */
$couchbase->setDesignDoc('userFields', json_encode($designedDocument));


Если сразу после выполнения данного кода, мы зайдем в панель управления Couchbase то увидим (на больших объема) справа вверху прогресс создания индекса. По завершении которого, можно проверить его работу, открыв его в панели управления и нажав на кнопку «Show Results». В ответе мы увидим пары ключ/значение, которые были генерированны JavaScript callback методом.

Из PHP кода мы можем получить выборку по следующему запросу:
// ответ придет не в виде JSON строки, а в виде массива данных
$result = $couchbase->view('userFields', "login");

В ответе будет массив из двух элементов: total_rows — где будет содержаться общее количество записей по данному индексу и поле rows — в котором будет массив массивов из нашей выборки в виде: array('id' => 'profile::0', 'key'=>'megausername', 'value'=>0). В этом массиве поля: _id — это идентификатор документа, который попал в выборку. Key — ключ который был указан при формировании индекса, и value — значение которое мы сформировали.

Но следует учесть тот факт, что таким образом мы получим весь индекс, что не совсем подходит нам для поиска. А если нам понадобится найти только идентификатор? Не перебирать же весь индекс вручную. Конечно нет. И для этого, при выполнении каждого запроса к view можно указать дополнительные параметры. Например если мы хотим узнать только идентификатор заданного по login пользователя, то мы должны указать конкретный ключ в запросе к view. Делается это так:
$result = $couchbase->view('userFields', "login", array('key'=>'megausername'));


И вот этот счастливый момент, когда в результате, у нас будет содержаться только запись, ключ у которой равен 'megausername'. С которой мы можем работать и быть счастливыми. Вот только есть один подводный камень. Как говорилось выше, индекс перестраивается не в момент добавления или изменения записи в заданный Bucket, а только лишь при достижении определенного процента фрагментации Bucket.

Допустим у нас есть необходимость проверить на уникальность имени пользователя перед выполнением какой либо операции. Например при регистрации пользователя. Естественно мы выполним запрос к этому view и будем анализировать ответ базы данных. Однако существует вероятность того, что в этот же самый момент только что зарегистрировался пользователь с таким же именем, а индекс еще не успел перестроиться. Естественно нам придет информация что в индексе такой записи нет и мы получим некоторую коллизию. Дабы избежать такой ситуации, есть возможность, при вызове view индекса, указать ему необходимость перестроения индекса. Т.е. все операции, которые не были выполнены по данному индексу, сначала будут завершены, а после чего уже будет выполнен запрос и возвращен результат. Делается это путем добавления опции stale со значением FALSE. Делается это вот так:
$result = $couchbase->view('userFields', "login", array('key'=>'megausername', 'stale'=>FALSE));


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

Заключение


Как вы могли убедиться, работать с Couchbase Server не так уж и сложно, важно перед началом работы досконально изучить документацию и не забывать думать и анализировать свои действия. Я не буду агитировать всех переходить на Couchbase, но хотелось бы сказать, что лично для меня, возможность работать с базой данных без кардинального изменения структуры при добавлении новых полей, была весьма «вкусным» фактором при разработке системы, описанной выше. Однако суровая реальность возвращает все на свои места. В моем конкретном случае, встал вопрос о формировании статистики по отдельно взятым полям в реальном времени, и взаимодействии с системой хранения/анализа статистики, запущенной на MSSQL движке. Этот факт привел к организации «костылей» для удобной работы наших DBA разработчиков, что вылилось фактически к дублированию системы управления полями в формате SQL. Если вы захотите использовать NoSQL движки баз данных в своих проектах, то советую начать со standalone проектов, которые не завязаны на внутреннюю инфраструктуру, т.к. интегрироваться безболезненно у вас не получится.

Если у вас возникнут вопросы, советую изучить документацию, которая находится по адресу www.couchbase.com/documentation
Если у вас сохранится интерес к этой теме, то ее можно будет раскрыть на несколько более сложном уровне. Рассмотреть методологию миграции с SQL мышления на NoSQL документооринтированную в следующих статьях. Рассмотреть как можно организовать GROUP BY, ORDER и прочие интересности при помощи Couchbase, а также рассмотреть более глубоко вопросы по оптимизации и проектированию документооориентированных баз данных.
Поделиться публикацией

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

    0
    А какие плюсы по сравнению с CouchDB? Пока навскидку всё описанное относится и к ней, да и другоим документно-ориентированным базам. Хотя, конечно, перестраивать индекс при каждом запросе на чтение характерно для Couch :)
      0
      К сожалению не имел удовольствия работать с CouchDB, однако из основных отличий от CouchDB это измененные алгоритмы определения фрагментации данных, более удобная работа с кластерами, устранение проблем с хранением избыточной информации (ревизии документа). Но в целом этому нужно посвящать отдельную статью. Я просто хотел поделиться опытом своего сотрудничества с Couchbase. И отразил в статье вопросы, которые скорее будут интересны новичкам.
        0
        CouchBase это более продвинутая CouchDB + неудачный Membase
        0
        Ура! Как хорошо что в сети появилось хоть что-то на русском языке об этой хорошей базе данных.
          0
          Да. Мне тоже нравится эта база данных, но к сожалению не имею возможности использовать ее в своих проектах. Т.к. они очень сильно завязаны на SQL движки
            –1
            меняй могз руководства
          +1
          это не реляционная база данных и в ней нет механизма autoincrement полей

          Как связаны реляционность БД и autoincrement?

          Во-первых, далеко не во всех реляционных базах данных есть механизм autoincrement.

          Во-вторых, использование auto increment полей в большинстве случаев нарушает принцип нормализации БД поскольку добавляет в таблицу избыточное поле, не являющееся естественным ключом — фактически email и password являются зависимыми от login, которое уникально в рамках сервиса, но при этом все три этих поля вдруг становятся зависимыми от какого то нового поля id, и получается что каждое поле в таблице начинает описывать id, который является ключом, и login, который не является ключом, хотя должен был — это явное противоречие 3 нормальной форме.

          Есть интересное обсуждение на тему использования искусственных и естественных ключей на stackoverflow:
          stackoverflow.com/questions/63090/surrogate-vs-natural-business-keys

          Если что, лично я не против использования auto increment там, где это надо, просто хочу заметить что наличие/отсутствие этого механизма никак не связано с реляционностью/нереляционностью СУБД, и метод замены auto_increment поля с помощью счетчика, который автор использует в статье, примерно так же реализуется и в реляционных СУБД, не имеющих механизма auto increment — с помощью sequence либо на триггерах
            0
            Спасибо за уточнение. Просто в большинстве реляционных БД, что я использовал (да и не я один), этот механизм присутствовал. Да, этот механизм не является обязательным, но в большинстве своем он присутствует именно в реляционных БД.
              0
              Согласен,
              автоинкремент — это не стандарт SQL, а имплементация SEQUENCE во внутренний механизм ISAM/MyISAM/InnoDB etc
              0
              При создании документа поле «type»: «json» было ошибочно пропущено, правильно?
              Новые статьи?

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

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