PHP → Разработка масштабируемых PHP-приложений с использованием MongoDB
Содержание
- Что такое NoSQL?
- Что такое документо-ориентированная база данных?
- MongoDB
- Установка
- Основы использования
- Поддержка индексов
- Реальные приложения
- Хранение файлов в MongoDB
- Map-Reduce
- Авто-партиционирование (sharding)
- Заключение и планы на будущее
Что такое NoSQL?
NoSQL — это база данных, которая, в отличие от реляционных БД, не предоставляет SQL-интерфейса для управления данными. Обычно данные в NoSQL-бд организованы другим образом.NoSQL базы данных делятся на три категории: column-oriented, пара ключ-значение и документо-ориентированные. Эта статья про третий тип БД — документо-ориентированные, так как они являются лучшим решением для большинства веб-сайтов.
Реляционные базы данных плохо масштабируются, когда они разделены по разным частям кластера. Разделение данных не так то просто осуществить, когда приложение использует JOIN-запросы и транзакции.
NoSQL базы данных не являются чем-то новым. Вообще говоря, они были базами, основанными на принципе пар ключ-значение до того как реляционные БД приобрели популярность.
Что такое документо-ориентированная база данных?
Для документо-ориентированных БД, документ — это структура данных, которая имеет переменное число свойств. У каждого свойства есть значение, которе может быть скалярным (номер, строка и т.п.) или векторным (массивы или объекты).Вы можете представлять документ как объект или ассоциативный массив в PHP. Чтобы лучше понять эту концепцию, давайте разберем ее на примере документа "персона" (person):
$person = array(
"name" => "Cesar Rodas",
"country" => "Paraguay",
"languages" => array("Spanish", "English", "Guarani"),
);
У документов нет какой-от предопределенной структуры, такой как таблицы в реляционных БД. У документов может быть множество свойств. Также документы группируются в коллекции. Термин «коллекция» будет использоваться для отличия от таблиц реляционных бд, в которых хранится фиксированное число полей.
Другая важная характеристика документов — они могут содержать поддокументы. Поддокументы используются вместо таблиц родитель-потомок для связи таблиц в реляционных базах данных.
MongoDB
MongoDB — очень интересная реализация документо-оринетированной базы данных. Вот ее основные качества:- Она использует JSON вместо XML
- Быстрая, написана на C++
- Поддерживает создание индексов
- Поддерживает простой в использовании интерфейс запросов, который очень похож на слой абстракции в некоторых БД
- Поддерживает операции с поддокументами
- Предоставляет «родное» расширение для PHP
- Поддерживает авто партиционирование (auto-sharding)
- Поддерживает map-reduce для трансформации данных
Установка
Установка MongoDB-сервера очень проста, но она лежит вне пределов этой статьи, поэтому тут мы не будем рассматривать этот процесс. Вы можете скачать исходники и скомпилировать их по инструкциям в readme-файле, а можете сразу скачать исполняемые файлы для своей платформы.Расширение PHP MongoDB client устанавливается командой PECL:
pecl install mongoТакже вы можете скачать расширение MongoDB в виде исходных кодов и установить его самостоятельно:
phpize ./configure --enable-mongo make install
Основы использования
Подключение к базе данныхКак было сказано выше, MongoDB оперирует документами, которые для PHP-разработчиков очень похожи на обычные ассоциативные массивы. Это значит, что все операции с MongoDB происходят с помощью массивов, даже запросы.
Код подключения к MongoDB-серверу очень похож на код подключения к другим типам БД. Пример подключения к серверу localhost на порт 27017:
$connection = new Mongo();Подключение к удаленному серверу на другом порту:
$connection= new Mongo( "192.168.2.1" ); $connection = new Mongo( "192.168.2.1:65432" );Когда подключение к MongoDB-серверу установлено, необходимо выбрать базу данных, с которой мы хотим работать. Если баз еще нет, будет создана новая база. Существует два способа сделать это:
$db = $connection->selectDB('dbname');
$db = $connection->dbname;
Далее необходимо выбрать коллекцию, с которой мы хотим работать, это что-то вроде выбора таблицы у реляционных БД:
$collection = $db->selectCollection('people');
или просто
$collection = $db->people;
Вставка новых документов
Объект коллекции используется для подготовки базовых операции манипуляции его информацией. К примеру, если вы хотите хранить информацию о человеке, ваш код будет выглядеть следующим образом:
$person = array( 'name' => 'Cesar Rodas', 'email' => 'crodas@php.net', 'address' => array( array( 'country' => 'PY', 'zip' => '2160', 'address1' => 'foo bar' ), array( 'country' => 'PY', 'zip' => '2161', 'address1' => 'foo bar bar foo' ), ), 'sessions' => 0, ); $safe_insert = true; $collection->insert($person, $safe_insert); $person_identifier = $person['_id'];Как вы могли заметить, в функцию вставки передан параметр $safe_insert. Это значит, что MongoDB-клиент будет ждать окончания запроса, чтобы можно было определить был ли он удачным или нет.
Если что-то пойдет не так — будет выброшено исключение. По умолчанию параметр safe insert установлен в false. В этом случае запрос на вставку выполняется мгновенно, но вы сразу же не узнаете прошла ли вставка успешно. В любом случае, эта возможность может быть полезной, когда требуется вставка большого количества записей.
Заметьте также, что объект коллекции передается по ссылке, так что MongoDB-клиент может установить свойство id, чтобы там содержался идентификатор только что созданного объекта.
Обновление документов
Обновление документов происходит немного сложнее и вначале может показаться слегка запутанным. Если вы обновляете обычный документ — он будет заменен целиком. Чтобы сделать это правильно, MongoDB поддерживает специальные свойства, которые работают как модификаторы операций.
Если хотите обновить только некоторые свойства, к примеру, увеличить значение параметра sessions, добавить свойство address2 к первому адресу и удалить свойство второй адрес, нужно сделать следующее:
Во-первых, необходимо определить фильтр, который сообщит MongoDB какой документ надо обновить.
$filter = array('email' => 'crodas@php.net');
$new_document = array(
'$inc' => array('sessions' => 1),
'$set' => array(
'address.0.address2' => 'Some foobar street',
),
'$unset' => array('address.1' => 1),
);
$options['multiple'] = false;
$collection->update(
$filter,
$new_document,
$options
);
MongoDB также поддерживает множественные обновления, такие же как в реляционных базах данных, то есть может обновлять все документы, соответствующие определенному критерию. Для этого необходимо установить параметр multiple в true.
Выборка документов
Чтобы выбрать один или более документов, которые соответствуют определенным параметрам, необходимо задать фильтр условий при помощи селекторов запросов, как показано в следующих примерах: 1. Выбрать людей по адресу e-mail:
$filter = array('email' => 'crodas@php.net');
$cursor = $collection->find($filter);
foreach ($cursor as $user) {
var_dump($user);
}
2. Выбрать людей, у которых больше десяти сессий:
$filter = array('sessions' => array('$gt' => 10));
$cursor = $collection->find($filter);
3. Выбрать людей, у которых не установлено свойство sessions:
$filter = array(
'sessions' => array('$exists' => false)
);
$cursor = $collection->find($filter);
4. Выбрать людей, у которых страна Paraguay и более 15 сессий:
$filter = array(
'address.country' => 'PY',
'sessions' => array('$gt' => 10)
);
$cursor = $collection->find($filter);
Стоит отметить одну важную деталь: запрос выполняется только тогда, когда фактически будут запрошены результаты выборки. В первом примере запрос выполнится, когда программа дойдет до цикла foreach.
Это очень полезное свойство, так как оно позволяет устанавливать дополнительные опции объекту курсора сразу после определения запроса, но перед его выполнением. К примеру, вы можете установить опции для организации постраничного вывода или для определения количества документов.
$total = $cursor->total();
$cursor->limit(20)->skip(40);
foreach($cursor as $user) {
}
Агрегация полученнх документов
MongoDB, также как и реляционные базы данных, поддерживает агрегацию результатов. Вы можете использовать такие агрегирующие операторы как count, distinct и group.
Аггрегационные запросы, в отличие от обычных, возвращают массивы вместо объектов документов.
Группировка позволяет определить функции на стороне MongoDB-сервера, написанные на javasript, для выполнения операций над группами. Это более гибкий способ, потому что вы можете выполнить множество типов операций со сгруппированными значениями, но это немного сложнее, чем выполнение простых групповых операций SQL врод SUM(), AVG() и т.д.
Вот пример того, как можно получить страны из списка адресов и сколько раз страна встречается в адресах.
$countries = $collection->distinct(
array("address.country")
);
$result = $collection->group(
/* keys to group by */
array("address.country" => True),
/* initial value */
array("sum" => 0),
/* js code to reduce */
"function (obj, prev) { prev.sum += 1; }",
/* Filter condition */
array("session" => array('$gt' => 10))
);
Удаление документов
Удаление документа происходит аналогично получению или обновлению.
$filter = array('field' => 'foo');
$collection->remove($filter);
Будьте осторожны. По умолчанию будут удалены все документы, соответствующие критерию. Если вы хотите удалить только первый документ, вы должны передать true в качестве второго параметра функции удаления.
Поддержка индексов
Очень важная особенность, которая может повлиять на ваше решение выбрать MongoDB среди других аналогичных документ-ориентированных баз данных, является поддержка индексов, которые очень похожи на индексы таблиц реляционных баз данных. Не во всех документ-ориентированных базах данных есть встроенная поддержка индексов.В MongoDB вы можете создавать индексы, чтобы избежать сканирования всех документов в процессе выборки, так же как реляционные базы данных используют индексы, чтобы избежать полного сканирования таблицы. Это позволяет ускорить запросы документов, соответствующих условиям, которые содержат индексированные свойства.
Например, если вы хотите завести уникальный индекс на свойстве email-адреса, вы можете определить его так:
$collection->ensureIndex(
array('email' => 1),
array('unique' => true, 'background' => true)
);
Первый массив описывает свойства, которые должны быть частью индекса. Это может быть либо одно свойство, либо несколько.
По умолчанию создание индекса — это синхронная операция, но возможно будет лучше, чтобы индексы создавались в фоне, особенно если в коллекции много документов. Это показано в примере выше.
Создание индекса по одному полю иногда может быть неэффективно. В следующем примере показано как ускорить запрос, определив индекс по двум свойствам:
$collection->ensureIndex(
array('address.country' => 1, 'sessions' => 1),
array('background' => true)
);
Значение, присвоенное индексу, определяет сортировку индекса: 1 — по возрастанию (ascending), -1 — по убыванию (descending). Это бывает полезно, когда вам необходимо сортировать результаты выборки:
$filter = array(
'address.country' => 'PY',
);
$cursor = $collection->find($filter)->order(
array('sessions' => -1)
);
$collection->ensureIndex(
array('address.country' => 1, 'sessions' => -1),
array('background' => true)
);
Реальные приложения
Некоторые разработчики боятся использовать новый тип баз данных, потому что он работает по-другому.Изучение новых вещей в теории отличается от изучения того, как использовать их на практике. Этот раздел был написан для того, чтобы объяснить как разрабатывать реальные приложения с помощью MongoDB в сравнении с реляционными базами данных, такими как MySQL, чтобы вы увидели отличия этих подходов.
Скажем, вы хотите сделать систему блогов с пользователями, записями и комментариями. Если бы вы делали это с помощью реляционной базы данных, схема таблиц была бы примерно такой:

Схема таблиц базы данных блогов
Эквивалент, определенный с помощью документов, определяющий такую же структуру с использованием MongoDB будет таким:
$users = array( 'username' => 'crodas', 'name' => 'Cesar Rodas', ); $posts = array( 'uri' => '/foo-bar-post', 'author_id' => $users->_id, 'title' => 'Foo bar post', 'summary' => 'This is a summary text', 'body' => 'This is the body', 'comments' => array( array( 'name' => 'user', 'email' => 'foo@bar.com', 'content' => 'nice post' ) ) );Как вы могли заметить, нам нужен только один документ для представления записей и комментариев. Это возможно, потому что комментарии являются поддокументами документов записей.
Это делает реализацию намного проще. Также это уменьшает время запроса к базе данных, когда вам нужно получить доступ к записи или ее комментариям.
Чтобы сделать это еще короче, информация о пользователях, которые оставляют комментарии, может быть объединена с самимим комментариями, чтобы вы могли получить сообщения, комментарии и пользователей одним запросом.
$posts = array( 'uri' => '/foo-bar-post', 'author_id' => $users->_id, 'author_name' => 'Cesar Rodas', 'author_username' => 'crodas', 'title' => 'Foo bar post', 'summary' => 'This is a summary text', 'body' => 'This is the body', 'comments' => array( array( 'name' => 'user', 'email' => 'foo@bar.com', 'comment' => 'nice post' ), ) );Недостаток этого в том, что может появиться некоторая дублированность информации, но всегда имейте ввиду, что дисковый объем стоит намного дешевле, чем процессорное время или ОЗУ, и, что еще более важно, чем время и терпение ваших пользователей.
Если вас беспокоит синхронизация дублированной информации, вы можете решить эту проблему путем выполнения этого запроса на обновление, когда автор обновляет свой профиль:
$filter = array( 'author_id' => $author['_id'], ); $data = array( '$set' => array( 'author_name' => 'Cesar D. Rodas', 'author_username' => 'cesar', ) ); $collection->update($filter, $data, array( 'multiple' => true) );С учетом этих оптимизаций нашей модели данных, давайте перепишем некоторые SQL-запросы их эквивалентами к MongoDB.
SELECT * FROM posts INNER JOIN users ON users.id = posts.user_id WHERE URL = :url; SELECT * FROM comments WHERE post_id = $post_id;Сначала надо разово добавить индекс:
$collection->ensureIndex(
array('uri' => 1),
array('unique' => 1, 'background')
);
$collection->find(array('uri' => '<uri>'));
INSERT INTO comments(post_id, name, email, contents) VALUES(:post_id, :name, :email, :comment);
$comment = array(
'name' => $_POST['name'],
'email' => $_POST['email'],
'comment' => $_POST['comment'],
);
$filter = array(
'uri' => $_POST['uri'],
);
$collection->update($filter, array(
'$push' => array('comments' => $comment))
);
SELECT * FROM posts WHERE id IN ( SELECT DISTINCT post_id FROM comments WHERE email = :email );Сначала надо разово добавить индекс:
$collection->ensureIndex(
array('comments.email' => 1),
array('background' => 1)
);
$collection->find( array('comments.email' => $email) );
Хранение файлов в MongoDB
MongoDB также предоставляет возможность выйти за рамки операций с базами данных. Например, в нем есть хорошее решение для хранения маленьких и больших файлов в базе данных.Файлы автоматически разбиваются на куски. Если MongoDB работает в авто-партиционированном окружении, куски файла также реплицируются между несколькими серверами.
Хранение файлов на удивление очень трудная задача, особенно когда необходимо управлять большим количеством файлов. Хранить файлы в локальной файловой системе часто является не очень хорошим выбором.
Одним из примеров такой сложности является задача эффективного обслуживания картинок миллионов видео на сайте YouTube, или даже хаки исполнении Facebook, чтобы эффективно обслуживать миллиарды фотографий.
MongoDB решает эту проблему при помощи создания двух внутренних коллекций: коллекция файлов, которая хранит информацию о метаданных файлов, и коллекция кусков файлов, которая содержит информацию о кусках файлов.
Если вы хотите хранить в MongoDB большой видеофайл, используйти такой код:
$metadata = array(
"filename" => "path.avi",
"downloads" => 0,
"comment" => "This file is foo bar",
"permissions" => array(
"crodas" => "write",
"everybody" => "read",
)
);
$grid = $db->getGridFS();
$grid->storeFile("/file/to/path.avi", $metadata);
Как вы видите, это очень просто реализовать и очень легко понять!
Map-Reduce
В процессе…Авто партиционирование (auto-sharding)
* Мы будем называть такое партиционирование шардингом за неимением русского эквивалента.Шардинг упоминается несколько раз выше, но вы можете не быть знакомы с концепцией.
Шардинг — это техника распределения базы данных по нескольким серверам.
К сожалению, рассмотрение вопросов шардинга выходит за рамки этой статьи. Возможно будет отдельная статья про шардинг MongoDB.
Заключение и планы
В этой статье было рассказано о новом типе базы данных, который, возможно, изменит процесс создания веб-приложений.В настоящее время я работаю над ActiveRecord-фреймворком для MongoDB, который упростит работу с объектами MongoDB. Вскоре он будет опубликован на PHPClasses. Также я работаю над оберткой, чтобы упростить хранение и извлечение файлов из MongoDB, так же как если бы они были обычными файлами.
Источник: Developing scalable PHP applications using MongoDB
