Доступ к данным

Простой доступ к данным в JavaScript — да, в JavaScript

Джули Лерман

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

Джули ЛерманВ статье рассматривается бета-версия Breeze. Любая изложенная здесь информация может быть изменена.

Хотя я считаю себя разработчиком инфраструктурного кода для создателей приложений, мне все же приходится немало заниматься программированием на клиентском стороне. Даже в этой рубрике я влезаю в клиентские приложения в тех местах, где они пересекаются с доступом к данным. Но когда на клиентской стороне находится JavaScript-код, особых успехов у меня нет — мои навыки в JavaScript, увы, все еще недостаточны, и каждый виток обучения дается мне нелегко. Но это всегда того стоит, когда в конце концов добиваешься успеха. И я приветствую все, что упрощает мне работу в JavaScript. Поэтому, когда в ходе презентации по одностраничным приложениям на Vermont.NET User Group Уорд Белл (Ward Bell) из IdeaBlade представил нам API доступа к данным с открытым исходным кодом для JavaScript, которым занимаются он и его команда, я была очень заинтересована. С точки зрения моего опыта работы с Entity Framework, то, что я увидела, было сравнимо с применением EF для веб-разработки на клиентской стороне. Этот API назвали Breeze, и на момент написания данной статьи он находился на стадии бета-версии. Белл щедро уделил мне время, помогая больше узнать о Breeze для написания этой статьи. Вы можете скачать текущую версию Breeze с сайта breezejs.com, где вы также найдете внушительные залежи документации, видеороликов и примеров.

В июньской статье за 2012 г. «Data Bind OData in Web Apps with Knockout.js» (msdn.microsoft.com/magazine/jj133816) я рассказывала об использовании библиотеки Knockout.js, которая упрощает связывание с данными на клиентской стороне. Breeze без проблем работает с Knockout, поэтому я намерена вернуться к примеру из той статьи. Моя цель — посмотреть, насколько введение Breeze могло бы упростить кодирование того примера для:

  • получения данных от сервера;
  • связывания и отображения этих данных;
  • передачи изменений обратно на сервер.

Я пошагово пройду самые важные части обновленного решения, чтобы вы могли увидеть, как складываются воедино фрагменты головоломки. Если вы хотите следовать за мной и проверить все это на практике, скачайте полное решение по ссылке archive.msdn.microsoft.com/mag201212DataPoints.

Исходный пример

Вот ключевые элементы моего прежнего решения.

  • На клиентской стороне я определила класс person, который Knockout может использовать для связывания с данными:
function PersonViewModel(model) {
      model = model || {};
      var self = this;
      self.FirstName = ko.observable(model.Name || ' ');
      self.LastName = ko.observable(model.Name || ' ');
    }
  • Мои данные предоставлялись через OData-сервис данных, поэтому я обращалась к ним через datajs — инструментальный набор для использования OData из JavaScript.
  • Я принимала результаты запроса (возвращавшиеся в формате JSON) и создавала экземпляр PersonViewModel, инициализированный значениями.
  • Затем мое приложение позволяло Knockout обработать связывание с данными, которая также координировала изменения, вносимые пользователем.
  • Я принимала модифицированный экземпляр PersonViewModel и обновляла свой JSON-объект на основе его значений.
  • Наконец, я передавала этот JSON-объект в datajs для сохранения на сервере через OData.

Я даже не стала рассматривать случай со связанными данными, так как это сильно усложнило бы мой небольшой пример.

Обновленный сервис, использующий ASP.NET Web API

С помощью Breeze я могу выдавать HTTP-вызовы своему OData-сервису или сервису, определенному ASP.NET Web API (asp.net/web-api). Я переключила свой сервис на ASP.NET Web API, который работает с той же моделью EF, что и использовавшаяся мной ранее, — но с одним добавлением. В прошлом примере предоставлялись только данные Person. Теперь у меня имеются связанные данные в виде класса Device, который подобно всем разработчикам, известным мне, имеет небольшой набор персональных устройств. Релевантные функции, предоставляемые моим ASP.NET Web API, — GET, возвращающая данные Person, другая GET для данных Device и единственная POST для сохранения изменений. Я также использую функцию Metadata для предоставления схемы моим данным, как показано на рис. 1. Breeze задействует эту Metadata, чтобы распознать мою модель.

Рис. 1. Ключевые ингредиенты моего сервиса, использующего ASP.NET Web API

readonly EFContextProvider<PersonModelContext> _contextProvider = 
  new EFContextProvider<PersonModelContext>();
[AcceptVerbs("GET")]
public IQueryable<Person> People()
{
  return _contextProvider.Context.People;
}
[AcceptVerbs("GET")]
public IQueryable<Device> Devices()
{
  return _contextProvider.Context.Devices;
}
[AcceptVerbs("POST")]
public SaveResult SaveChanges(JObject saveBundle)
{
  return _contextProvider.SaveChanges(saveBundle);
}
[AcceptVerbs("GET")]
public string Metadata()
{
  return _contextProvider.Metadata();
}

Breeze.NET на сервере

Обратите внимание на переменную _contextProvider, используемую в этих методах. Я не вызываю методы своего EF DbContext (PersonModelContext) напрямую. Вместо этого я обертываю их в Breeze EFContextProvider. Вот откуда берется метод _contextProvider.Metadata, равно как и сигнатура SaveChanges, которая принимает параметр saveBundle. Breeze с помощью saveBundle позволяет мне отправлять набор изменений данных из моего приложения, которые он передает в мой DbContext для сохранения в базе данных.

Я назвала приложение на основе ASP.NET Web API «BreezyDevices», поэтому теперь могу запросить схему, используя http://localhost:19428/api/breezydevices/metadata. А также запросить данные, указав один из методов GET: http://localhost:19428/api/breezydevices/people.

Поскольку Breeze на клиентской стороне будет запрашивать удаленный сервис ASP.NET Web API и сохранять в нем изменения, я могу удалить datajs из клиентского приложения.

Как Breeze поможет в моем примере

В рамках этого примера я буду использовать Breeze, чтобы устранить три болевые точки.

  1. Мой сервис возвращает и принимает чистый JSON, но мне нужно работать с JavaScript-объектами с наблюдаемыми Knockout свойствами для привязки данных к UI.
  2. Мне нужно включать связанные данные, но это трудно сделать на клиенте.
  3. Мне нужно отправлять множество изменений на сервер для сохранения.

Благодаря Breeze я могу напрямую связывать данные со своими конечными данными. Я сконфигурирую Breeze на использование Knockout, и в ответ тот сам создаст наблюдаемые свойства Knockout. Это означает, что работа со связанными данными намного упрощается, поскольку мне не нужно преобразовывать их из JSON в связываемые объекты (bindable objects) и прикладывать дополнительные усилия для переопределения графов на клиентской стороне, используя результаты своего запроса.

При использовании Breeze требуются некоторые усилия в конфигурировании на серверной стороне. Детали этой процедуры вы прочитаете в документации Breeze, а здесь я сосредоточусь на клиентской части доступа к данным в моем примере. Кроме того, учтите, что я использую в примере лишь малую толику Breeze, поэтому, как только вы прочувствуете его пользу, отправляйтесь на сайт breezejs.com, чтобы узнать о Breeze больше.

На рис. 2 показано, где Breeze встраивается в рабочий процесс на серверной и клиентской сторонах.

Breeze.NET API помогает вам на сервере, а BreezeJS API — на клиенте
Рис. 2. Breeze.NET API помогает вам на сервере, а BreezeJS API — на клиенте

Server Сервер
ASP.NET Web API Controller Контроллер ASP.NET Web API
Breeze.NET API Breeze.NET API
Entity Framework & EF Model Entity Framework и EF-модель
Client Клиент
BreezeJS API BreezeJS API
Knockout Knockout
Markup Разметка

Запросы из Breeze

Поддержка запросов в Breeze во многом напомнила мне таковую в OData и Entity Framework. Я буду работать с Breeze-классом EntityManager. Этот класс может считать модель данных, предоставляемую через метаданные вашего сервиса и самостоятельно генерировать JavaScript-объекты сущностей; вам не потребуется определять классы сущностей или писать мэпперы.

На клиентской стороне тоже выполняется некоторая настройка. Например, в следующем фрагменте кода создаются сокращения отдельных пространств имен Breeze, а затем Breeze конфигурируется на использование Knockout и ASP.NET Web API:

var core = breeze.core,
           entityModel = breeze.entityModel;
core.config.setProperties({
  trackingImplementation: entityModel.entityTracking_ko,
  remoteAccessImplementation: entityModel.remoteAccess_webApi
});

Breeze можно сконфигурировать на использование ряда альтернативных инфраструктур связывания с данными (например, Backbone.js или Windows Library for JavaScript) и технологий доступа к данным (скажем, OData).

Далее я создаю EntityManager, которому известен относительный uri моего сервиса. EntityManager сравним с контекстом Entity Framework или OData. Он действует как шлюз к Breeze и кеширует данные:

var  manager = new entityModel.EntityManager('api/breezydevices');

Теперь я могу определить запрос и сообщить своему EntityManager выполнить его за меня. Этот код не слишком сильно отличается от того, который применяется в случае Entity Framework и LINQ to Entities или при работе с любым из клиентских API в OData, так что моей любимой часть было изучение того, как пользоваться Breeze:

function getAllPersons(peopleArray) {
    var query = new entityModel.EntityQuery()
      .from("People")
      .orderBy("FirstName, LastName");
    return manager
      .executeQuery(query)
      .then(function (data) {
        processResults(data,peopleArray); })
      .fail(queryFailed);
  };

Я запускаю этот код на клиентской стороне и могу выполнять запрос асинхронно — вот почему метод executeQuery позволяет определять, что делать как при успешном выполнении запроса (.then), так и при неудачном (.fail).

Заметьте, что я передаю в getAllPersons массив (который, как вы вскоре увидите, является наблюдаемым Knockout массивом). Если запрос выполняется успешно, я передаю этот массив в метод processResults, который опустошает массив, а затем заполняет его данными от сервиса. Раньше мне пришлось бы перебирать результаты в цикле и самой создавать каждый экземпляр PersonViewModel. С помощью Breeze я могу использовать возвращаемые данные напрямую:

function processResults(data, peopleArray) {
    var persons = data.results;
    peopleArray.removeAll();
    persons.forEach(function (person) {
      peopleArray.push(person);
    });
  }

Это дает мне массив объектов person, которые я отображаю в представлении.

Функция getAllPersons находится внутри объекта, который я назвала dataservice. Я задействую этот объект в следующей части кода.

Самозаполняемая модель представления

В примере из июньской статьи по Knockout запрос и результаты были отделены от класса PersonViewModel, использовавшегося для связывания с данными в представлении. Поэтому я выполняла запрос и транслировала результаты в экземпляр PersonViewModel с помощью написанного мной кода преобразования. Так как при работе с Breeze не требуется ни код преобразования, ни PersonViewModel, я сделаю на этот раз свое приложение немного «интеллектуальнее» и заставлю его отображать массив объектов person, извлеченных из базы данных моим dataservice. Для этого у меня теперь есть объект PeopleViewModel. Он предоставляет свойство people, которое я определила как наблюдаемый Knockout массив, заполняемый через dataservice.getAllPersons:

(function (root) {
  var app = root.app;
  var dataservice = app.dataservice;
  var vm = {
    people: ko.observableArray([]),
    }
  };
  dataservice.getAllPersons(vm.people);
  app.peopleViewModel = vm;
}(window));

В сопутствующем коде вы найдете файл main.js, который является стартовой точкой логики приложения. Он содержит следующую строку кода, вызывающую Knockout-метод applyBindings:

ko.applyBindings(app.peopleViewModel, $("content").get(0));

Метод applyBindings подключает свойства и методы модели представления к HTML-элементам управления UI через привязки данных, объявленные в представлении.

Представлением в данном случае является небольшой блок HTML в моем index.cshtml. Обратите внимание на Knockout-разметку для связывания с данными и отображение первого и последнего имен из каждого объекта person в массиве people:

<ul data-bind="foreach: people">
  <li class="person" >
    <label data-bind="text: FirstName"></label>
    <label data-bind="text: LastName"></label>
  </li>
</ul>

Запустив приложение, я получаю представление данных person только для чтения, как показано на рис. 3.

Применение Breeze и Knockout упрощает использование данных в JavaScript

Рис. 3. Применение Breeze и Knockout упрощает использование данных в JavaScript

Настройка JavaScript и Knockout для поддержки редактирования

Возможно, вы помните из июньской статьи, что Knockout упрощает связывание с данными для их редактирования. Совместно с Breeze вы получаете отличную комбинацию для простой поддержки редактирования данных и сохранения их обратно на сервере.

Сначала я добавляю функцию в объект dataservice, которая вызывает Breeze-метод manager.saveChanges. При вызове Breeze EntityManager собирает в пакет отложенные изменения и передает их через POST в сервис на основе Web API:

function saveChanges() {
     manager.saveChanges();
  }

Далее я предоставлю новую функцию saveChanges как функционал dataservice:

var dataservice = {
    getAllPersons: getAllPersons,
    saveChanges: saveChanges,
  };

Теперь мой объект PeopleViewModel должен предоставлять собственный метод save для связывания с представлением; функция save модели представления делегируется методу saveChanges в dataservice. Здесь я использую «анонимную функцию» JavaScript, чтобы определить функцию save модели представления:

var vm = {
    people: ko.observableArray([]),
    save: function () {
      dataservice.saveChanges();
    },
  };

Далее я заменяю свои метки элементами input (текстовыми полями), чтобы пользователь мог редактировать объекты Person. Я должна переключиться с ключевого слова text на ключевое слово value в Knockout, чтобы разрешить двухстороннюю привязку с пользовательским вводом. Кроме того, я добавляю изображение через событие click, связанное с методом PeopleViewModel.save:

<img src="../../Images/save.png" 
  data-bind="click: save" title="Save Changes" />
<ul data-bind="foreach: people">
  <li class="person" >
    <form>
      <label>First: </label><input 
        data-bind="value: FirstName" />
      <label>Last: </label> <input
        data-bind="value: LastName" />
    </form>
  </li>
</ul>

Вот и все. Об остальном позаботятся Breeze и Knockout! Вы можете увидеть, как отображаются данные для редактирования на рис. 4.

Использование Breeze для сохранения данных через JavaScript

Рис. 4. Использование Breeze для сохранения данных через JavaScript

Я могу редактировать любое из полей (или все поля) и щелкать кнопку сохранения. Breeze EntityManager соберет все изменения в данных и передаст их на сервер, что в свою очередь отправит их в Entity Framework для обновления базы данных. Хотя я не стану расширять свою демонстрационную программу включением операций вставки и удаления, Breeze определенно умеет обрабатывать и такие изменения.

И для большего эффекта: добавление связанных данных

Эта часть любого приложения на JavaScript всегда ужасала многих разработчиков — именно это послужило причиной, по которой я решила написать эту статью.

Я внесу одно небольшое изменение в свой скрипт и добавлю немного разметки на форму, которая будет превращать каждый person в редактируемое представление «основные/подробные сведения».

Изменение в скрипте будет в dataservice, где я модифицирую запрос, добавив Breeze-метод расширения запроса для интенсивной загрузки (eager load) объектов device каждого person. Расширение (expand) — термин, который, вероятно, знаком вам по OData или NHibernate и аналогичен Include в Entity Framework (в Breeze тоже есть поддержка для простой загрузки связанных данных):

var query = new entityModel.EntityQuery()
           .from("People")
           .expand("Devices")
           .orderBy("FirstName, LastName");

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

Рис. 5. Модификация представления для отображения данных Device

<ul data-bind="foreach: people">
  <li class="person" >
    <form>
      <label>First: </label><input
        data-bind="value: FirstName" />
      <label>Last: </label> <input
        data-bind="value: LastName" />
      <br/>
      <label>Devices: </label>
      <ul class="device" data-bind="foreach: Devices">
        <li>
          <input data-bind="value: DeviceName"/>
        </li>
      </ul>
    </form>
  </li>
</ul>

И это все. Как видно на рис. 6, Breeze обрабатывает интенсивную загрузку и формирует графы на клиентской стороне. Он также управляет данными, которые подлежат отправке обратно сервису для обновления. На серверной стороне Breeze EFContextProvider рассортировывает все измененные данные, принятые им, и заботится о том, чтобы Entity Framework получила все, что нужно для их сохранения в базе данных.

Использование и сохранение связанных данных
Рис. 6. Использование и сохранение связанных данных

Хотя это было очень просто при отношении «один ко многим», на момент написания этой статьи бета-версия Breeze не поддерживала отношения «многие ко многим».

Беспроблемный доступ к данным на клиентской стороне

Белл сказал мне, что на создание Breeze его вдохновил собственный, весьма болезненный опыт работы над одним проектом, где интенсивно использовались как JavaScript, так и доступ к данным. В его компании, IdeaBlade, всегда уделяли внимание созданию решений для задач обработки отсоединенных данных, и разработчики этой компании смогли внести большой вклад в этот проект с открытым исходным кодом. Раньше я с большой неохотой бралась за проекты с большими объемами кода на JavaScript, потому что, во-первых, была несильна в этом языке, а во-вторых, знала, как трудно обращаться на нем к данным. Я очень заинтересовалась Breeze, как только увидела этот проект. И хотя я лишь поверхностно рассказала о нем, главное, что по-настоящему привлекло меня, — насколько легко использовать и сохранять связанные данные.


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

Выражаю благодарность за рецензирование статьи эксперту Уорду Беллу (Ward Bell).