Работающий программист

Куда идет NoSQL с MongoDB

Тэд Ньюард

Загрузка образца кода

За прошедшее десятилетие с момента объявления Microsoft .NET Framework в 2000 г. и ее первого выпуска в 2002 г. разработчики под .NET всячески старались идти в ногу со всеми новинками, которыми их забрасывала Microsoft. И будто бы этого им недостаточно «сообщество» — под ним я подразумеваю разработчиков, использующих .NET как повседневно, так и от случая к случаю, — вдруг взяло да и создало еще несколько новинок, которые были призваны заткнуть не охваченные пока Microsoft области или просто посеять хаос и путаницу (выбирайте, что вам больше нравится).

Одной из таких «новинок» от сообщества (без всякого участия Microsoft) является движение NoSQL — группа разработчиков, которые открыто бросили вызов идее насчет того, что все данные хранятся, будут и должны храниться в той или иной форме системы управления реляционными базами данных (СУРБД). Таблицы, строки, столбцы, основные ключи, ограничения внешнего ключа, аргументы за null-значения, должен ли основной ключ быть естественным или неестественным… неужели не осталось ничего святого?

В этой статье и последующих работах я буду исследовать один из основных средств, которые посоветовали мне специалисты по NoSQL. MongoDB, название которых образовано от «гомонголус», если верить веб-сайту MongoDB (нет, это я не придумал). Я постараюсь рассмотреть все, что связано с MongoDB: установка, изучение и работа с ним из .NET Framework, включая предложенную поддержку LINQ; использование ее из других сред (приложения настольных сред и веб-приложения и службы), а также ее настройка таким образом, чтобы администраторы Windows не спалили ваше чучело.

Проблема (или почему я снова должен волноваться?)

Прежде чем углубляться в детали MongoDB, логично спросить, почему любые разработчики под .NET Framework должны жертвовать получасом своего времени на чтение этой статьи и следовать ей на своих лэптопах. В конце концов, SQL Server поставляется и в бесплатном варианте с возможностью редистрибуции — редакции Express, которая предоставляет хранилище данных, облегченное по сравнению с традиционными реляционными базами данных, ориентированных на корпоративное применение или использование в информационных центрах. Более того, существует масса инструментов и библиотек, обеспечивающих более простой доступ к этим хранилищам данных, в том числе Microsoft LINQ и Entity Framework.

Проблема в том, что сильная сторона реляционной модели — сама реляционная модель — это и ее самая большая слабость. Большинство разработчиков (что бы они ни использовали — .NET, Java или нечто совершенно иное) после нескольких лет работы может в красках описать, что не все так ладно с «квадратной» моделью таблиц/строк/столбцов. Попытка смоделировать иерархические данные может довести до бешенства даже самых искушенных разработчиков, причем настолько, что о концепции моделирования иерархических данных в реляционной модели была написана целая книга — Джо Келко (Joe Celko) «SQL for Smarties, Third Edition» (Morgan-Kaufmann, 2005). И если к этому добавить базовую «данность», которая заключается в том, что реляционные базы данных предполагают отсутствие гибкости в структуре данных (вспомните схему базы данных), то попытка поддерживать «дополнения» в данные по месту приводит к весьма громоздким и запутанным конструкциям. (Быстро голосуем поднятием рук: кто из вас работал с базами данных, в которых был столбец Notes, или еще лучше, столбцы Note1, Note2, Note3…?)

Никто в движении NoSQL не спорит, что у реляционной модели есть свои сильные стороны или что реляционные базы данных никуда не денутся, но на основе опыта двух прошедших десятилетий можно считать фактом, что разработчики часто хранят в реляционных базах такие данные, которые по своей природе вовсе (или отчасти) не являются реляционными.

База данных, ориентированная на документы, хранит не «отношения», а «документы» (тесно переплетенные наборы данных, которые в целом не связаны с другими элементами данных в системе). Например, записи в системе блогов совершенно не связаны друг с другом, и даже когда в одной из них есть ссылка на какую-то другую, чаще всего они соединены гиперссылкой, которая разбирается браузером, а не является некоей внутренней связью. Комментарии к записи в блоге видимы исключительно в рамках этой записи, и вряд ли кому придет в голову агрегировать все комментарии независимо от того, к какой записи они относятся.

Более того, базы данных, ориентированные на документы, обычно эффективнее в высокопроизводительных средах или средах с высокой степенью параллелизма; MongoDB, в частности, нацелена на высокую производительность, а ее близкая родственница, CouchDB, — в большей мере рассчитана на среды с высокой степенью параллелизма. В обеих системах отказались от какой-либо поддержки транзакций с участием нескольких объектов, исходя из того, что, хотя они поддерживают параллельную модификацию одного объекта в базе данных, любая попытка изменить более одного объекта одновременно оставляет небольшое временное окно, когда эти модификации могут быть видны «в процессе их применения». Документы обновляются атомарно, но концепции транзакции, охватывающей обновления нескольких документов сразу, нет. Это не значит, что в MongoDB нет никаких средств обеспечения отказоустойчивости, — просто экземпляр MongoDB не переживет, например, испытания сбоем электроснабжения в отличие от экземпляра SQL Server. Системы, требующие полной семантики ACID (atomicity, consistency, isolation and durability), лучше строить на основе традиционных систем управления реляционными базами данных, поэтому скорее всего в ближайшей перспективе никто не будет хранить в экземпляре MongoDB критически важные данные, кроме, возможно, уже реплицированных или кешируемых на веб-сервере.

В целом, MongoDB будет отлично работать для приложений и компонентов, где нужно хранить данные, которые часто используются и к которым можно быстро обращаться. Системы аналитики на веб-сайтах, пользовательские предпочтения и настройки — или любые системы, где данные не полностью структурированы или должны быть гибко структурируемыми, — естественные кандидаты для работы с MongoDB. Это не означает, что MongoDB не является полностью готовым основным хранилищем данных для операционной информации; это значит лишь то, что MongoDB отлично подходит в тех ситуациях, где традиционные СУРБД буксуют, а также для тех случаев применения, где нормально работала бы любая из них.

Приступаем к работе

Как упоминалось ранее, MongoDB — это пакет программного обеспечения с открытым исходным кодом, который можно загрузить с веб-сайта mongodb.com. Чтобы найти ссылки на скачивание набора двоичных файлов для Windows, достаточно открыть этот сайт в браузере; ищите вверху в правой части страницы ссылку Downloads. Или, если вы предпочитаете прямые ссылки, используйте mongodb.org/display/DOCS/Downloads. На момент написания этой статьи стабильной версией является выпуск 1.2.4. Это не более чем ZIP-архив файлов, поэтому его установка, фигурально выражаясь, смехотворно проста: просто распакуйте содержимое архива в подходящее вам место на диске.

Нет, серьезно. Это все.

ZIP-файл создает три каталога: bin, include и lib. Единственный интересный каталог — bin, в котором содержатся восемь исполняемых файлов. Никакие другие двоичные зависимости (или исполняющая среда) не нужны, а на данный момент нас интересуют лишь два из этих исполняемых файлов. Это mongod.exe (процесс самой базы данных MongoDB) и mongo.exe (клиент оболочки командной строки, который обычно используется так же, как старый isql.exe в SQL Server, — для проверки правильно установки и корректности работы, прямого просмотра данных и выполнения административных задач.

Проверить корректность установки так же легко, как запустить mongod из клиента командной строки. По умолчанию MongoDB хранит данные по пути c:\data\db, но это можно изменить с помощью текстового файла, передаваемого по имени в командной строки через --config. Проверка кошерности установки делается, как показано на рис. 1; все очень просто, если на момент запуска mongod существует подкаталог db.

image: Firing up Mongod.exe to Verify Successful Installation

Рис. 1. Запуск Mongod.exe для проверки успешности установки

Если этого каталога нет, MongoDB не станет создавать его. Заметьте:в моей системе Windows 7 при запуске MongoDB обычно выскакивает окошко с «This application wants to open a port» («Это приложение собирается открыть порт»). Убедитесь, что порт доступен (по умолчанию — 27017), иначе подключение к нему будет… сильно затруднено в лучшем случае. (Об этом я подробнее расскажу в следующей статье, когда буду обсуждать размещение MongoDB в производственной среде.)

После запуска сервера подключение к нему с помощью оболочки весьма тривиально:приложение mongo.exe запускает среду командной строки, которая обеспечивает прямое взаимодействие с сервером (рис. 2).

image: Mongo.exe Launches a Command-Line Environment that Allows Direct Interaction with the Server

Рис. 2 Mongo.exe запускает среду командной строки, которая обеспечивает непосредственное взаимодействие с сервером

По умолчанию оболочка соединяется с «тестовой» базой данных test. Так как сейчас нам нужно лишь проверить, что все работает, test отлично подходит для наших целей. Конечно, с этого момента можно сравнительно легко создать какие-то данные для работы с MongoDB, например объект, описывающий некую персону. Некоторое представление о функционировании MongoDB можно получить по рис. 3.

image: Creating Sample Data

Рис. 3. Создание образцов данных

По сути, MongoDB использует нотацию данных JSON (JavaScript Object Notation), что объясняет ее гибкость и то, как с ней взаимодействуют клиенты. На внутреннем уровне MongoDB хранит все в BSON — двоичном надмножестве JSON — для упрощения хранилища и индексации. Однако JSON остается предпочтительным форматом ввода-вывода MongoDB, и обычно именно этот формат используется между веб- и вики-сайтами MongoDB. Если вы не знакомы с JSON, советую подучить его, прежде чем всерьез браться за MongoDB. А пока, просто смеха ради, загляните в каталог, в котором mongod хранит данные, и вы увидите, что в нем появилась пара файлов с именами, начинающимися с «test».

Но хватит развлекаться — пора написать немного кода. Для выхода из оболочки достаточно ввести «exit», а для закрытия сервера нажмите Ctrl+C в окне или просто закройте окно; сервер захватывает уведомление о закрытии и корректно завершает все до прекращения работы своего процесса.

Сервер MongoDB (и оболочка, хотя это не столь важно) написан как неуправляемое приложение на C++ —помните такой? Поэтому обращение к нему требует своего рода драйвера для .NET Framework, которому известно, как соединяться через открытый сокет для передачи команд и данных В дистрибутиве MongoDB нет драйвера для .NET Framework, но, к счастью, сообщество позаботилось о нем, где под «сообществом» в данном случае скрывается разработчик по имени Сэм Кордер (Sam Corder), который создал драйвер для .NET Framework и поддержку LINQ для доступа к MongoDB Его работа доступна как в виде исходного, так и двоичного кода по ссылке github.com/samus/mongodb-csharp. Скачайте либо двоичные файлы с этой страницы (смотрите в верхний правый угол) или исходный код и скомпилируйте его. В любом случае в результате вы получите две сборки: MongoDB.Driver.dll и MongoDB.Linq.dll. Быстро открываете Add Reference в узле References проекта, и .NET Framework готова к работе.

Написание кода

По сути, открытие соединения с выполняемым сервером MongoDB мало чем отличается от открытия соединения с любой другой базой данных, как показано на рис. 4.

Рис. 4. Открытие подключения к серверу MongoDB

using System;
using MongoDB.Driver; 

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      Mongo db = new Mongo();
      db.Connect(); //Connect to localhost on the default port
      db.Disconnect();
    }
  }
}

Обнаружение созданного ранее объекта — дело нетрудное, просто… оно отличается от того, к чему привыкли разработчики под .NET Framework (рис. 5).

Рис.5. Обнаружение созданного объекта Mongo

using System;
using MongoDB.Driver; 

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      Mongo db = new Mongo();
      db.Connect(); //Connect to localhost on the default port.
      Database test = db.getDB("test");
      IMongoCollection things = test.GetCollection("things");
      Document queryDoc = new Document();
      queryDoc.Append("lastname", "Neward");
      Document resultDoc = things.FindOne(queryDoc);
      Console.WriteLine(resultDoc);
      db.Disconnect();
    }
  }
}

Если это показалось вам слишком громоздким, расслабьтесь — это написано «длинным путем» потому, что MongoDB хранит все иначе, чем традиционные базы данных.

Начинающим следует помнить, что данные, вставленные ранее, имели три поля — firstname, lastname и age, — и любое из них является элементом, с помощью которого данные могут быть извлечены. Но, что важнее, строка, в которой они сохранялись, отброшенная довольно бесцеремонно, была «test.things.save()» — а это подразумевает, что данные сохраняются в чем-то под названием «things». В терминологии MongoDB «things» — это набор, и неявно все данные помещаются в него. Наборы в свою очередь хранят документы, а те — пары «ключ-значение», где значения могут быть дополнительными наборами. В нашем случае things — набор, хранящийся в базе данных test.

В итоге выборка данных заключается в том, что сначала мы подключаемся к серверу MongoDB, затем к базе данных test, а потом ищем набор things. Именно это и делают первые четыре строки на рис. 5:создается объект Mongo, представляющий соединение, осуществляется подключение к серверу, потом к базе данных test и далее возвращается набор things.

Получив набор, код может выдать запрос для поиска одного документа через вызов FindOne. Но, как и во всех базах данных, клиент не захочет извлекать каждый документ из набора, а затем искать среди них нужный; поэтому запрос нужно каким-то образом ограничить. В MongoDB для этого создается Document, который содержит поля и данные для поиска в этих полях, — такая концепция известна под названием «запрос по образцу» (query by example, QBE). Поскольку цель заключается в поиске документа, содержащего поле lastname со значением «Neward», создается Document с одним полем lastname и таким значением, а потом передается в качестве параметра в FindOne. Если запрос завершается успешно, возвращается другой Document со всеми интересующими нас данными (плюс одно поле); в ином случае возвращается null.

Кстати, краткая версия этого описания может выглядеть вот так лаконично:

Document anotherResult = 
         db["test"]["things"].FindOne(
           new Document().Append("lastname", "Neward"));
       Console.WriteLine(anotherResult);

После выполнения этого кода показываются не только исходные значения, но и одно новое — поле _id, содержащее объект ObjectId. Это уникальный идентификатор объекта; он автоматически вставляется базой данных при сохранении в ней новых данных. При любой попытке модифицировать этот объект вы должны сохранять значение этого поля, иначе база данных будет считать, что ей передается новый объект.Как правило, это осуществляется модификацией Document, который был возвращен запросом:

anotherResult["age"] = 39;
       things.Update(resultDoc);
       Console.WriteLine(
         db["test"]["things"].FindOne(
           new Document().Append("lastname", "Neward")));

Однако всегда можно создать новый экземпляр Document и вручную заполнить поле _id значением, совпадающим с ObjectId:

Document ted = new Document();
       ted["_id"] = new MongoDB.Driver.Oid("4b61494aff75000000002e77");
       ted["firstname"] = "Ted";
       ted["lastname"] = "Neward";
       ted["age"] = 40;
       things.Update(ted);
       Console.WriteLine(
         db["test"]["things"].FindOne(
           new Document().Append("lastname", "Neward")));

Конечно, если значение _id уже известно, его можно использовать и в качестве критерия запроса.

Обратите внимание, что Document в конечном счете не типизирован — в поле можно сохранить почти что угодно по любому имени, в том числе некоторые базовые значимые типы .NET Framework, например DateTime. С технической точки зрения, MongoDB, как уже говорилось, хранит BSON-данные, которые включают некоторые расширения традиционных JSON-типов (string, integer, Boolean, double и null, хотя null-значения допустимы только в объектах, а не в наборах) вроде ранее упомянутого ObjectId, двоичных данных, регулярных выражений и встроенного JavaScript-кода. На данный момент последние два мы пока не будем рассматривать.Просто имейте в виду:тот факт, что BSON позволяет хранить двоичные данные, означает следующее:база данных способна хранить все, что можно преобразовать в байтовый массив, а это означает, что в конечном счете MongoDB может хранить все — только выдавать запросы к такому большому двоичному объекту (blob) нельзя.

Это еще не конец!

Нам предстоит еще многое обсудить по поводу MongoDB, в том числе поддержку LINQ, выполнение более сложных запросов на серверной стороне, возможности которых превосходят таковые для запросов в простом стиле QBE, и размещение MongoDB на производственной серверной ферме. Ну а пока, эта статья и внимательное изучение IntelliSense должны дать отправную точку работающим программистам.

Кстати, если вас интересует какая-то конкретная тематика, пишите мне без всяких сомнений. В конце концов это рубрика для вас. Удачи в кодировании!

Тэд Ньюард  (Ted Neward) — глава компании Neward and Associates, специализирующейся на гибких и надежных корпоративных системах с применением .NET и Java. Автор многочисленных книг, более сотни статей, лектор INETA, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области архитектуры. С ним можно связаться по адресу blogs.tedneward.com..

Выражаю благодарность за рецензирование статьи Кайлу Бэнкеру (Kyle Banker) и Сэму Кордеру (Sam Corder)