Доступ к данным - Обзор Microsoft Azure DocumentDB

Джули Лерман

Исходный код можно скачать по ссылке

Джули ЛерманВ ноябре 2011 года в своей рубрике я написала статью «What the Heck Are Document Databases?» (msdn.microsoft.com/magazine/hh547103), в которой обсудила некоторые из наиболее известных баз данных документов: MongoDB, CouchDB и RavenDB. Все эти три NoSQL-базы данных все еще занимают сильные позиции. На тот период у Microsoft не было базы данных документов, которую она могла бы предложить на рынке, но была Microsoft Azure Table Storage — NoSQL-база данных, основанная на парах «ключ-значение». Однако в августе 2014 года была объявлена Microsoft Azure DocumentDB, которая, как и предполагает ее название, является сервисом NoSQL-базы данных документов, доступным в Azure.

В этой статье я дам обзор Azure DocumentDB, который, как я надеюсь, в достаточной мере заинтересует вас, что самостоятельно продолжить исследование этого сервиса. Данный сервис доступен в Azure и даже уже использовался до 8 апреля, когда статус сервиса был повышен с предварительного до общедоступного ресурса. Например, есть отличный отчет компании SGS, которая реализовала DocumentDB как часть своего решения (bit.ly/1GMnBd9). Один из разработчиков того проекта прислал мне твит об этом и сообщил, что заказчик до сих пор по-настоящему доволен этим решением.

Что такое база данных документов?

На этот вопрос я ответила в той статье, но вкратце напомню суть (советую прочитать мою статью за ноябрь 2011 года). База данных документов хранит данные как документы, в основном как индивидуальные JSON-документы. (MongoDB немного отличается тем, что сжимает свои JSON-документы в двоичный формат BSON.) Это хранилище обеспечивает гораздо более высокую производительность при работе с огромными объемами данных, поскольку нет нужды «скакать» по всей базе данных, чтобы извлечь связанные данные. Связанные данные можно объединять в один JSON-документ. Еще одна важная особенность базы документов и других NoSQL-баз данных в том, что они не используют схемы. В отличие от реляционной базы данных, таблицы которой требуют наличия предопределенных схем для хранения и выборки данных, база данных документов позволяет определять собственную схему в каждом документе. Поэтому такая база данных состоит из наборов документов. На рис. 1 приведен простой пример того, как может выглядеть индивидуальный JSON-документ. Заметьте, что в документе имя свойства указывается наряду со значением и что документ содержит связанные данные.

Рис. 1. Простой JSON-документ

{
  "RecipeName": "Insane Ganache",
  "DerivedFrom": "Café Pasqual’s Cookbook",
  "Comments":"Insanely rich. Estimate min 20 servings",
  "Ingredients":[
    {
      "Name":"Semi-Sweet Chocolate",
      "Amount":"1.5 lbs",
      "Note":"Use a bar, not bits. Ghiradelli FTW"
    },
    {
      "Name":"Heavy cream",
      "Amount":"2 cups"
    },
    {
      "Name":"Unsalted butter",
      "Amount":"2 tbs"
    }
],
  "Directions": "Combine chocolate,
    cream and butter in the top ..."
}

Все эти данные не только находятся в формате JSON и описывают сами себя — документ содержит еще и связанные данные (ингредиенты). Другая особенность, общая для некоторых из баз данных документов, — они доступны через HTTP-вызовы. Об этом еще будет сказано, но, опять же, в более ранней статье эти и другие особенности, общие для таких баз данных, были описано гораздо подробнее.

Структура Azure DocumentDB

На рис. 1 видно, как выглядит типичный документ, хранимый в базе данных документов. Однако Azure DocumentDB включает не только эти документы. Документы в Azure DocumentDB считаются ресурсом и могут группироваться в наборы, которые также являются ресурсами в DocumentDB. Вы можете создавать, обновлять, удалять и запрашивать наборы через HTTP-вызовы точно так же, как документы. По сути, DocumentDB полностью состоит из ресурсов разных типов. Наборы группируются в одну базу данных DocumentDB. По учетной записи DocumentDB у вас может быть несколько баз данных, и вы можете располагать несколько учетных записей.

Все эти ресурсы являются полноправными элементами экосферы. Помимо этого, есть другой набор ресурсов, сопутствующих документам. Они именуются по схеме, привычной пользователям реляционных баз данных: хранимые процедуры, пользовательские функции (user-defined functions, UDF), индексы и триггеры.

Последний ресурс, относящийся к документам, — вложение (attachment), которое является разновидностью двоичных данных, прикрепляемых к JSON-документу. Двоичные данные вложения хранятся в Azure Blob Storage, но метаданные — в DocumentDB, что позволяет вам запрашивать эти вложения по самым разнообразным свойствам.

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

Взаимодействие с DocumentDB

Работать с ресурсами в Azure DocumentDB можно несколькими способами, включая SQL, REST API и различные клиентские API, в том числе .NET API, который позволяет использовать LINQ для запросов к базе данных. Подробнее о запросах см. по ссылке bit.ly/1aLm4bC.

Azure Portal — то место, где вы создаете учетную запись DocumentDB и управляете ею (см. документацию по ссылке bit.ly/1Cq8zE7). Кроме того, вы можете управлять на портале своей DocumentDB, а также использовать Document Explorer и Query Explorer для просмотра и запроса документов. В Query Explorer можно применять синтаксис SQL, как это сделала я в простом запросе на рис. 2.

Применение синтаксиса SQL для запроса документов в Azure Portal Query Explorer
Рис. 2. Применение синтаксиса SQL для запроса документов в Azure Portal Query Explorer

Этот SQL применим и в ваших приложениях. Например, вот фрагмент кода из «Build a Node.js Web Application Using DocumentDB» (bit.ly/1E7j5Wg), где запрос выражается на синтаксисе SQL:

getOrCreateDatabase: function (client, databaseId, callback) {
  var querySpec = {
    query: 'SELECT * FROM root r WHERE r.id=@id',
    parameters: [{
      name: '@id',
      value: databaseId
    }]
  };

На этом раннем этапе развития DocumentDB вы, вероятно, сочтете данный синтаксис SQL ограниченным, но учитывайте, что можно дополнять существующий SQL с помощью UDF. Например, можно написать собственную функцию CONTAINS для построения предикатов, которые оценивают строки, скажем, CONTAINS(r.name, "Chocolate").

Подобно многим другим ресурсам Azure в DocumentDB есть REST API, к которому можно обращаться через HTTP. У каждого ресурса имеется уникальный URI. Вот пример HTTP-запроса на конкретное разрешение DocumentDB:

GET https://contosomarketing.documents.azure.com/dbs/ruJjAA==/users
/ruJjAFjqQAA=/permissions/ruJjAFjqQABUp3QAAAAAAA== HTTP/1.1
x-ms-date: Sun, 17 Aug 2014 03:02:32 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%
3dGfrwRDuhd18ZmKCJHW4OCeNt5Av065QYFJxLaW8qLmg%3d
x-ms-version: 2014-08-21
Accept: application/json
Host: contosomarketing.documents.azure.com

Подробнее о работе с REST API напрямую см. по ссылке bit.ly/1NUIUd9. Но работа с любым REST API может оказаться довольно утомительной. Существует ряд клиентских API для взаимодействия с Azure DocumentDB: .NET, Node.js, JavaScript, Java и Python. Скачайте нужные SDK и прочитайте документацию к ним по ссылке bit.ly/1Cq9iVJ.

.NET-разработчики оценят тот факт, что .NET-библиотека поддерживает запросы, использующие LINQ. Хотя поддержка LINQ со временем определенно расширится, в настоящее время поддерживаются следующие LINQ-выражения: Queryable.Where, Queryable.Select и Queryable.SelectMany.

Прежде чем взаимодействовать с DocumentDB, нужно указать учетную запись, базу данных и набор, с которыми вы хотите работать. Ниже определяется Microsoft.Azure.Documents.ClientDocument, использующий .NET API:

string endpoint = ConfigurationManager.AppSettings["endpoint"];
string authKey = ConfigurationManager.AppSettings["authKey"];
Uri endpointUri = new Uri(endpoint);
client = new DocumentClient(endpointUri, authKey);

Этот пример кода взят из руководства по ASP.NET MVC и DocumentDB, которому я следовала на странице Azure Documentation (bit.ly/1HS6OEe). Это руководство весьма основательное и начинается с этапов создания учетной записи DocumentDB в Azure Portal. Настоятельно рекомендую использовать его или (в качестве альтернативы) одно из руководств, которые демонстрируют работу DocumentDB с другими языками, например упомянутую ранее статью по Node.js. В приложении-примере имеется единственный тип — класс Item (рис. 3).

Рис. 3. Класс Item

public class Item
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "name")]
    public string Name { get; set; }

    [JsonProperty(PropertyName = "descrip")]
    public string Description { get; set; }

    [JsonProperty(PropertyName = "isComplete")]
    public bool Completed { get; set; }
  }

Заметьте, что каждое свойство класса Item помечено как JsonProperty PropertyName. Это не обязательно, но позволяет .NET-клиенту сопоставлять хранимые JSON-данные с моим типом Item, а мне — именовать свойства класса как угодно, независимо от того, как они именуются в базе данных. Используя определенный клиент, можно выразить LINQ-запрос, возвращающий экземпляр Microsoft.Azure.Documents.Database по известному Id базы данных:

var db = Client.CreateDatabaseQuery()
               .Where(d => d.Id == myDatabaseId)
               .AsEnumerable()
               .FirstOrDefault();

После этого вы можете определить набор внутри базы данных и, наконец, запросить набор с помощью LINQ-выражения вроде следующего, которое возвращает один JSON-документ:

return Client.CreateDocumentQuery(Collection.DocumentsLink)
             .Where(d => d.Id == id)
             .AsEnumerable()
             .FirstOrDefault();

Различные объекты в .NET API также поддерживают операции для вставки, обновления и удаления документов с помощью методов CreateDocumentAsync, UpdateDocumentAsync и DeleteDocumentAsync (CUD), которые обертывают HTTP-вызовы в REST API. Как и в случае запросов, существуют и релевантные CUD-методы для ресурсов других типов, таких как хранимые процедуры и вложения.

Новый неожиданный поворот в CAP

Один из наиболее интересных аспектов DocumentDB, который ставит ее особняком среди других баз данных документов, заключается в том, что она позволяет настраивать согласованность. В моей старой статье по базам данных документов говорилось о теореме согласованности, доступности и отказоустойчивости разделов (Consistency, Availability and Partition Tolerance, CAP), утверждающей, что при работе в распределенных системах можно получить только две из трех гарантий. Реляционные базы данных обеспечивают согласованность за счет доступности (например, ожидания выполнения транзакции). С другой стороны, NoSQL-базы данных более толерантны к конечной согласованности (eventual consistency), где данные могут быть не на 100% текущими; основной акцент делается на доступности.

Azure DocumentDB предоставляет новый способ решения теоремы CAP, разрешая настраивать уровень согласованности, тем самым давая возможность одновременного выигрыша от доступности и отказоустойчивости разделов. Вы можете выбрать один из четырех уровней согласованности: строгий (strong), ограниченного устаревания (bounded staleness), сеансовый (session) и конечный (eventual); такой уровень можно определять индивидуально для операции — не только применительно ко всей базе данных. Вместо согласованности «все или ничего» вы можете подобрать подходящий в вашем решении уровень согласованности. Подробнее на эту тему см. страницу Azure DocumentDB Documentation (bit.ly/1Cq9p3v).

JavaScript-код на серверной стороне

Многие из вас, видимо, знакомы с хранимыми процедурами и UDF в реляционных базах данных, и Azure DocumentDB в отличие от других баз данных документов включает эти концепции, хотя их код пишется на JavaScript. JavaScript может естественным образом взаимодействовать с JSON, поэтому такой вариант крайне эффективен при взаимодействии с JSON-документами и другими ресурсами. Никаких преобразований, трансляций или сопоставлений не требуется. Другое преимущество наличия JavaScript на серверной стороне в виде триггеров хранимых процедур и UDF в том, что вы получаете атомарные транзакции, охватывающие множество документов, — все, что находится в области транзакции, будет откачено, если один из процессов потерпит неудачу. Определение хранимых процедур и UDF заметно отличается от такового в реляционных базах данных вроде SQL Server. Портал пока не поддерживает эту возможность. Вместо этого вы определяете серверный код в своем клиентском коде. Советую изучить раздел «Server-Side Script» в Azure DocumentDB .NET Code Samples (bit.ly/1FiNK4y).

Теперь я покажу, как создать и сохранить хранимую процедуру, а потом выполнить ее. На рис. 4 приведен простой пример, в котором для вставки хранимой процедуры в DocumentDB используется код .NET API.

Рис. 4. Вставка хранимой процедуры в DocumentDB

public static async Task<StoredProcedure>
  InsertStoredProcedure() {
  var sproc = new StoredProcedure
              {
                Id = "Hello",
                Body = @"
                  function() {
                    var context = getContext();
                    var response = context.getResponse();
                    response.setBody(
                      'Stored Procedure says: Hello World');
                  };"
              };

  sproc = await Client.CreateStoredProcedureAsync(
    setup.Collection.SelfLink, sproc);
  return sproc;
}

Я инкапсулировала всю логику в один метод для простоты. Мой объект StoredProcedure состоит из ID и Body. Body — это серверный JavaScript. Вероятно, вы предпочтете создать JavaScript-файлы для каждой процедуры и считывать их содержимое при создании объекта StoredProcedure. Код предполагает, что StoredProcedure пока не существует в базе данных. В примере из пакета сопутствующего кода для этой статьи вы увидите, что я вызываю пользовательский метод, который запрашивает базу данных, чтобы убедиться в отсутствии процедуры до ее вставки. Наконец, я использую свойство SetupDocDb<T>.Client (оно предоставляет экземпляр DocumentClient) для создания хранимой процедуры.

Теперь, когда в базе данных есть хранимая процедура, ее можно вызывать. Это было для меня несколько труднее, так как я привыкла к SQL Server, а здесь вызов осуществляется иначе. Хотя мне известно, что Id процедуры — «Hello», в текущем API этого недостаточно для ее идентификации при вызове ExecuteStoredProcedureAsync. Каждый ресурс имеет SelfLink, созданный DocumentDB. SelfLink — это неизменяемый ключ, который поддерживает REST-средства DocumentDB. Он гарантирует, что у каждого ресурса будет неизменный HTTP-адрес. Мне нужен SelfLink, чтобы сообщить базе данных, какую хранимую процедуру следует выполнить. Это означает, что сначала я должна запросить базу данных найти хранимую процедуру по известному Id («Hello»), чтобы я могла найти ее значение SelfLink. Такой рабочий процесс довольно замысловат для разработчиков, и группа DocumentDB сейчас стремится к тому, чтобы исключить любую необходимость в SelfLink. Такое изменение может появиться даже к моменту публикации этой статьи. А пока я буду запрашивать процедуру так же, как и для любого ресурса DocumentDB: я буду использовать метод CreateStoredProcedureQuery. Затем с помощью SelfLink можно запустить процедуру и получить ее результаты:

public static async Task<string> GetHello() {
  StoredProcedure sproc = Client.CreateStoredProcedureQuery(
    Collection.SelfLink)
    .Where(s => s.Id == "Hello")
    .AsEnumerable()
    .FirstOrDefault();

  var response = (await Client.ExecuteStoredProcedureAsync
    <dynamic>(sproc.SelfLink)).Response;
  return response.ToString();
}

UDF создаются аналогично. Вы определяете UDF как JavaScript в объекте UserDefinedFunction и вставляете его в DocumentDB. Как только UDF появляется в базе данных, вы можете использовать эту функцию в своих запросах. Изначально это было возможно только при использовании синтаксиса SQL как параметра метода CreateDocumentQuery, хотя поддержка LINQ была добавлена как раз перед официальным выпуском DocumentDB в начале апреля 2015 г. Вот пример SQL-запроса с использованием UDF:

select r.name,udf.HelloUDF() AS descrip from root r
  where r.isComplete=false

Эта UDF просто выдает некий текст, поэтому никаких параметров она не принимает.

Заметьте, что я использую JsonProperty-имена в запросе, поскольку они будут обрабатываться на сервере применительно к JSON-данным. В случае LINQ-запросов я использовала бы имена типа Item.

Вы найдете аналогичный запрос в примере из пакета сопутствующего кода, хотя там моя UDF называется HelloUDF.

Производительность и масштабируемость

Когда речь заходит о производительности и масштабируемости, в игру вступают очень много факторов. Даже архитектура ваших моделей данных и разделов может повлиять на эти две крайне важные характеристики любого хранилища данных. Я настоятельно рекомендую прочитать великолепное руководство по моделированию данных в DocumentDB (bit.ly/1Chrjqa). В этой статье взвешиваются все за и против архитектуры на основе графов объектов и отношений и то, как они влияют на производительность и масштабируемость DocumentDB. Автор, Райен Крокур (Ryan CrawCour), который является старшим менеджером программ в группе DocumentDB, объясняет, какие шаблоны дают преимущество в скорости чтения, а какие — в скорости записи. По сути, я считаю, что это руководство полезно для проектирования моделей в целом — не только для Azure DocumentDB.

Разбиение базы данных на разделы также должно определяться вашими требованиями к чтению и записи. В статье по разбиению данных на разделы в DocumentDB (bit.ly/1y5T4FG) дается больше информации по использованию наборов DocumentDB для определения разделов и поясняется, как определять наборы в зависимости от того, как вам требуется обращаться к данным.

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

Индексы — другой важный фактор, влияющий на производительность, и DocumentDB позволяет настраивать правила индексации наборов. Без индексации вы сможете использовать для запросов только SelfLink и Id ресурсов, как было показано ранее. Политика индексации по умолчанию пытается найти баланс между производительностью запросов и эффективностью работы хранилища, но вы можете переопределить эту политику, чтобы подобрать другой баланс. Индексы также согласованны, а значит, операции поиска, использующие индексацию, получат немедленный доступ к новым данным. Подробнее об индексации см. по ссылке bit.ly/1GMplDm.

Не бесплатно, но экономично

Управление производительностью и масштабируемостью влияет не только на доступность ваших данных, но и на стоимость предоставления этих данных. Сервис DocumentDB не является бесплатным. При этом предлагаются три ценовых уровня в зависимости от того, какой из трех уровней производительности вы выбираете. Поскольку Microsoft постоянно оптимизирует расценки на свои сервисы, надежнее всего изучить веб-страницу, на которой подробно описаны все тарифы использования DocumentDB (bit.ly/1IKUUMo). Как и любая NoSQL-база данных, DocumentDB нацелена на предоставление хранилища для огромных объемов данных и поэтому может оказаться кардинально экономичнее, чем реляционное хранилище в релевантных сценариях.


Джули Лерман (Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором серии книг «Programming Entity Framework» (O’Reilly Media, 2010), в том числе «Code First Edition» (2011) и «DbContext Edition» (2012), также выпущенных издательством O’Reilly Media. Вы можете читать ее заметки в twitter.com/julielerman и смотреть ее видеокурсы для Pluralsight на juliel.me/PS-Videos.

Выражаю благодарность за рецензирование статьи эксперту Райену Крокуру (Ryan CrawCour).