ИЮЛЬ 2016

ТОМ 31 НОМЕР 7

Работающий программист - MEAN: поговорим о DEAN

Тэд Ньюард | Июль 2016

Тэд НьюардС возвращением, дорогие «министы». Или на этот раз скорее «динеры» («DEANers»).

Одна из вещей, которые делают стек MEAN столь привлекательным, заключается в том, что этот стек по своей природе очень гибок: вы можете заменять компоненты стека другими, более-менее эквивалентными частями и создавать новый стек, способный удовлетворить конкретным корпоративным или бизнес-требованиям без каких-либо существенных изменений в этой архитектуре. В качестве демонстрации этой концепции я намерен поэкспериментировать с заменой MongoDB на Microsoft Azure DocumentDB (отсюда и появилась буква «D» вместо буквы «M»).

MongoDB и DocumentDB

Тем, кто не уделял много времени знакомству с DocumentDB, я настоятельно советую изучить некоторые из существующих ресурсов, в том числе рубрику Джули Лерман за июнь 2015 года (msdn.com/magazine/mt147238). Лерман дала отличный обзор DocumentDB, в том числе одной из ее самых интересных особенностей — выполнения кода на серверной стороне (да, как в хранимых процедурах, но на JavaScript). Если вы никогда не видели DocumentDB или если вам нужно краткое введение, прочитайте упомянутую рубрику, прежде чем продолжить.

На первый взгляд, MongoDB и DocumentDB очень похожи. Обе являются базами данных, ориентированными на документы и используют JSON в качестве основного формата представления документов. Каждая из них расширяет JSON для включения некоторых дополнительных типов данных. Каждая рассматривает данные в терминах наборов документов и использует эту концепцию как принципиальную схему агрегации (вместо таблиц). Обе базы данных не являются реляционными в том смысле, что база данных никак не проверяет идентификаторы документов, с помощью которых документы связываются друг с другом. Если на то пошло, они свободны и от схем в плане того, что содержимое документа находится целиком в руках разработчика, одинаковая структура документов не вводится принудительно для всех элементов набора.

Следовательно, замена одной базы данных на другую должна быть простой.

Однако, помимо некоторых более глубоких различий, которые вы вскоре обнаружите, прямо сейчас можно назвать одно принципиальное отличие: если MongoDB можно скачать и локально выполнять на лэптопе, то DocumentDB доступна лишь по подписке Azure, поэтому первым шагом в сторону DEAN является простая задача создания экземпляра DocumentDB в учетной записи Azure. Как это делается, подробно описано в нескольких других местах (в том числе в онлайновой документации на DocumentDB), поэтому я не стану расписывать здесь этот процесс, а лишь кратко суммирую его. Отправляйтесь на Azure Management Portal, создайте новый ресурс Azure DocumentDB, присвойте ему уникальное имя (я назвал свой как dean-db) и щелкните большую синюю кнопку Create. Портал поколдует несколько минут, и Azure создаст ваш экземпляр DocumentDB в облаке.

Прежде чем покончить с порталом, вам нужно получить от него пару частей информации, которые вы потом будете использовать в коде. Поэтому потратим секунду и запишем их сейчас. В частности, вам понадобится URL, к которому вы будете подключаться, и ключ авторизации, подтверждающий вашу идентификацию. В Azure Management Portal он называется PRIMARY KEY (есть и дополнительный ключ — SECONDARY KEY); на момент написания этой статьи эти ключи показываются на вкладке All Settings под Keys. Данные ключи являются общими секретами, поэтому не давайте их никому; если код помещается в общедоступный репозитарий исходного кода (а-ля GitHub), убедитесь, что ключ не является частью кода, включаемого в систему управления версиями. (Обычно приложения Node.js конфигурируются так, чтобы ключ был переменной окружения и подхватывался кодом входной точки приложения при запуске; вы уже делали аналогичные вещи в предыдущих итерациях этой кодовой базы, так что это не должно стать для вас сюрпризом.) Если ключ скомпрометирован (скажем, вы обнаружили, что ключ каким-то образом оказался в общем доступе), то обязательно сгенерируйте заново оба ключа и замените их. На самом деле будет хорошей идеей периодически менять их, например каждые 30 или 60 дней, — просто из принципа.

Таким образом, теперь config.js в существующей кодовой базе выглядит, как на рис. 1.

Рис. 1. Config.js в существующей кодовой базе

module.exports = {
  mongoServer : "localhost",
  mongoPort : "27017",
  docdbServer : "https://dean-db.documents.azure.com:443/",
  docdbKey :
    "gzk030R7xC9629Cm1OAUirYg8n2sqLF3O0xtrVl8JT
    ANNM1zV1KLl4VEShJyB70jEtdmwnUUc4nRYyHhxsjQjQ=="
};

if (process.env["ENV"] === "prod") {
  module.exports.mongoServer = "ds054308.mongolab.com";
  module.exports.mongoPort = "54308";
  module.exports.docdbServer = process.env["DOCDB_HOST"];
  module.exports.docdbKey = process.env["DOCDB_KEY"];
}

(Кстати, я уже менял ключи в своем репозитарии, поэтому показываю старый ключ здесь просто, чтобы вы получили представление о том, как он выглядит.)

Привет от Node.js

Следующий шаг предсказуем: вам нужен модуль Node.js для доступа к экземпляру DocumentDB, и для этого используется команда npm install --save documentdb. Естественно, вам понадобится затребовать его через require в коде app.js:

var express = require('express'),
  bodyParser = require('body-parser'),
  debug = require('debug')('app'),
  edge = require('edge'),
  documentdb = require('documentdb'),
  ...;

// Получаем наши конфигурационные параметры
var config = require('./config.js');
debug("Mongo is available at ",config.mongoServer,
  ":",config.mongoPort);
debug("DocDB is available at ",config.docdbServer);

С этого момента для открытия соединения достаточно сконструировать объект DocumentClient с помощью сервера и ключа авторизации:

// Подключение к DocumentDB
var docDB = documentdb.DocumentClient(config.docdbServer, {
  masterKey: config.docdbKey
});

Разумеется, теперь возникает вопрос посерьезнее: ну открыли мы соединение, и что с ним делать?

Использование DocumentDB

Как и MongoDB, DocumentDB использует для схемы формат на основе JSON, а также наборы документов, поэтому хранение данных в ней (например, презентаций) включает те же виды операций, что и в MongoDB. Однако API немного отличается, возможно, из-за некоторых различий между тем, как Microsoft предпочитает проектировать свои продукты, и тем, как развиваются проекты в мире открытого исходного кода.

(Кстати, лучший ресурс по DocumentDB/Node.js, который я нашел на данный момент, — это набор примеров на странице Azure Documentation [bit.ly/1TkqXaP]. Он представляет собой набор прямых ссылок на авторизованные Microsoft примеры проектов, хранящихся на GitHub, а также ссылок на документацию, сгенерированную из исходного кода DocumentDB Node.js API, и я использую это как дежурный справочник по DocumentDB API вместо чего-то более официального.)

Для начинающих замечу: как только объект клиента базы данных сконструирован, нужно подключиться к этой базе данных по databaseId; здесь все во многом аналогично с тем, как сервер MongoDB может быть хостом для нескольких баз данных. Однако в отличие от MongoDB в случае DocumentDB эту базу данных надо создать заранее. Это осуществляется либо программно (что отлично подходит для демонстраций и сценариев DevOps) API-вызовом createDatabase, либо через Azure Management Portal (что, по-видимому, является более распространенным подходом, поскольку обычно это разовая операция), используя вкладку Add Database на странице ресурса DocumentDB. Простоты ради в моем коде предполагается, что база данных conferencedb уже существует и предположительно была создана с помощью портала. В этом случае Node.js может подключиться к ней вызовом queryDatabases, что приводит нас к следующему крупному отличию в подходах между MongoDB и DocumentDB (рис. 2).

Рис. 2. Запрос у DocumentDB списка баз данных

docClient.queryDatabases({
  query: 'SELECT * FROM root r WHERE r.id = @id',
  parameters: [
    {
      name: '@id',
      value: 'conferencedb'
    }
  ]}).toArray(function (err, results) {
  if (err) {
    handleError(err);
  }
  
  if (results.length === 0) {
    // Ошибки не было, но никаких результатов не возвращено,
    // что указывает на отсутствие базы данных, подходящей
    // к данному запросу, поэтому явно возвращаем null
    debug("No results found");
  } else {
    // База данных найдена, поэтому возвращаем ее
    debug('Found a database:', results[0]);
    var docDB = results[0];
  }
});

Прежде всего обратите внимание на то, как DocumentDB API использует явную «спецификацию запроса» (даже для получения списка баз данных) в комплекте с параметрами. Это заметное отличие от подхода Mongo «запрос по примеру». Согласен, вы вовсе не обязаны использовать такой вариант — вы могли бы просто заполнить аргументы через конкатенацию строк; но любой разработчик, который когда-либо становился жертвой атаки с внедрением SQL-кода, подтвердит вам, что параметризованные запросы гораздо безопаснее.

Другое основное отличие — это, конечно, язык запросов: здравствуй, SQL, мой старый друг. Если честно, это не совсем SQL, поскольку у вас нет таблиц, полей и всего такого, но Microsoft приложили большие усилия, чтобы адаптировать привычный язык запросов для мира, ориентированного на документы. «Баг» это или «фича» — видимо, зависит от отношения разработчика к миру реляционных баз (любит или ненавидит), но эти (и другие) решения Microsoft четко позиционируют DocumentDB как компромиссный выбор между SQL Server и MongoDB (или другими базами данных документов), и это вовсе не плохо.

Как только база данных найдена, вы должны получить набор — снова по идентификатору. Как и базы данных, наборы являются «формальными» элементами, т. е. их нужно явно создавать либо API-вызовом createCollection, либо через Azure Management Portal. Учитывая, что DocumentDB рассматривает наборы как таблицы, это не удивительно. И вновь самый простой способ создать что-либо — использование портала, где (на момент написания этой статьи) следует щелкнуть название базы данных на плитке баз данных в плитке ресурсов DocumentDB. После этого появится новая вкладка, позволяющая добавить новый набор, презентации, а затем находить их по идентификаторам (рис. 3).

Рис. 3. Нашли базу данных? Тогда ищем наборы

// Мы нашли базу данных
debug('Found a database:', results[0]);
docDB = results[0];

debug('Looking for collections:');
docClient.readCollections('dbs/conferencedb').
toArray(function(err, colls) {
if (err) {
  debug(err);
}
else {
  if (colls.length === 0) {
    debug("No collections found");
  }
  else {
    for (var c in colls) {
      debug("Found collection",colls[c]);
      
      if (colls[c].id === 'presentations')
        presentationColl = colls[c];
      }
    }
  }
});

Обратите внимание на первый параметр в API-вызове readCollections; если он кажется вам указывающим какую-то разновидность пути URI/URL к базе данных, это так и есть. При каждом вызове Node.js DocumentDB API этот вид идентификаторов в стиле REST используется для того, чтобы упростить прямой доступ к набору (и в конечном счете к документу) — без навигации по сложной иерархии.

(Одно важное отличие от MongoDB: в Azure взимается оплата за обслуживание соразмерно количеству наборов, поэтому, если пользователь MongoDB думает о наборах только с точки зрения моделирования, в случае DocumentDB решение о создании нового набора прямо влияет на ежемесячную оплату.)

Наконец, чтобы найти конкретные документы в наборе, вы используете queryDocuments API. И вновь вы передаете идентификатор набора (который, поскольку вы знаете, с каким набором вы будете работать, можно просто вставить непосредственно в код) и получаете результаты, возвращая их напрямую как JSON-тело точно так же, как это делалось ранее для MongoDB (рис. 4).

Рис. 4. Список всех документов в наборе

var getAllPresentations = function(req, res) {
  debug("Getting all presentations from DocumentDB:");
  
  docClient.queryDocuments("dbs/conferencedb/colls/
    presentations",
  {
    query: "SELECT * FROM presentations p"
  }).toArray(function (err, results) {
    if (err) res.status(500).jsonp(err);
    else res.status(200).jsonp(results);
  });
};

// ...

app.get('/presentations', getAllPresentations);

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

Заключение

Начиная эту статью, я все время ссылался на то, что MongoDB и DocumentDB похожи, однако теперь вы уже чувствуете, что на самом деле между ними есть ряд различий концептуального характера. Совершенно очевидно, что Microsoft стремилась внести с помощью DocumentDB некую долю «корпоративности» в мир баз данных документов, и, учитывая ее клиентскую базу, это серьезный ход. Однако в настоящее время DocumentDB API явно недостает какой-либо «объектной» оболочки наподобие той, которую Mongoose предоставляет для Node.js MongoDB API. Создать свою оболочку для разработчиков несложно, но это влечет за собой увеличение кода, который этим разработчикам придется писать и поддерживать какое-то время. То есть, по крайней мере, пока сообщество разработчиков открытого исходного кода либо не портирует Mongoose с адаптацией под использование MongoDB или DocumentDB (маловероятно), либо кто-нибудь не создаст нечто похожее для DocumentDB.

Но еще важнее осознавать, что акроним «MEAN» — это просто… акроним. DocumentDB представляет собой лишь один из десятка или около того систем хранения, которые могут поддерживаться стеком серверной частью стека в стиле «*EAN», включая добрый старый SQL Server. Там, где MEAN не соответствует вашим корпоративным стандартам, операционному персоналу или бизнес-целям, вы можете без проблем заменить негодную часть чем-то более подходящим. В конечном счете, буквальное следование архитектуре, создающей проблемы вам как разработчику и/или организации в целом, просто глупо.

У меня кончилось место, так что… удачи в кодировании!


Тэд Ньюард (Ted Neward) — глава фирмы Neward & Associates, предоставляющей консалтинговые услуги по самым разнообразным технологиям. Автор и соавтор многочисленных книг, в том числе «Professional F# 2.0» (Wrox, 2010), более сотни статей, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области F#. С ним можно связаться по адресу ted@tedneward.com или почитать его блог blogs.tedneward.com.

Выражаю благодарность за рецензирование статьи эксперту Шону Уайлдермуту (Shawn Wildermuth).