Одним из интересных аспектов SOA является интеграция данных. Выдержка из Wikipedia:
Интеграция данных – это процесс комбинирования данных из разнообразных источников и предоставление их пользователи в неком унифицированном виде. Необходимость в разработке этого процесса появляется в различных областях. Например, в коммерческой (когда двум схожим компаниям необходимо объединить свои базы данных) и в научной (объединение данных из нескольких разных хранилищ биометрической информации).
Необходимость в таком виде интеграции возникла в одном из текущих проектов. Со стороны разработчика хотелось получить максимально прозрачный API, не зависящий от способа реализации и предоставляющий удобный и максимально "родной" механизм работы с данными.
Введение
Главный вопрос, на который пришлось найти ответ - это технология открытия доступа к удаленной базе данных. Выбор пал на технологию REST. Вкратце, она позволяет нам получать нужные данные, формируя запросы в виде GET-параметров к сервису. Дополнительно к этому хотелось бы получить транслятор с LINQ к REST, используя LINQ как раз как тот самый унифицированный и "родной" механизм, который позволит связать воедино данные из любых источников, для которых реализована соответствующая библиотека Linq2***.
Поиски соответствующей библиотеки были недолгими: не так давно Microsoft выпустило в составе .NET 3.5 SP1 свой проект Astoria, назвав его ADO.NET Data Services. С одной стороны эта библиотека позволяет легко создавать сервисы данных, открывающие доступ к данным на сервере посредством протокола REST. А с другой стороны, предоставляет удобные средства интеграции этих сервисов с клиентскими приложениями, оборачивая всю рутину по доступу к сервису в автосгенерированные прокси-классы. Ну и конечно же эти клиентские прокси-классы реализуют LINQ-транслятор. Кроме того, сервисы данных реализуются на базе стека WCF, предоставляя широкие возможности по тонкой низкоуровневой настройке.
Реализация сервиса
Для начала добавим в веб-проект сервис данных:
Исходный код сервиса будет таким:
public class WebDataService : DataService< /* TODO: put your data source class name here */ > { // This method is called only once to initialize service-wide policies. public static void InitializeService(IDataServiceConfiguration config) { // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc. // Examples: // config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead); // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All); } }
Если вы обратитесь к спецификации класса DataService<T>, то увидите, что к типу Т не предоставляются такие требования, как реализация какого-либо инетрфейса. Все дело в том, что необходимые требования невозможно описать в виде какой-либо простой модели, которая бы не усложнила разработку этих сервисов. Но требования, тем не менее, есть:
- Сервис данных может опубликовывать все IQueryable-свойства данного источника данных.
Наш источник данных может выглядеть таким образом:public class DomainDataContext { private readonly Supplier[] _suppliers; private readonly Product[] _products; public DomainDataContext() { // Инициализация _suppliers и _products тестовыми данными } public IQueryable<Supplier> Suppliers { get { return _suppliers.AsQueryable(); } } public IQueryable<Product> Products { get { return _products.AsQueryable(); } } }
- Классы-сущности должны быть "идентифицируемыми".
Это правило в данном случае означает, что в классах обязательно должны быть свойства, заканчивающиеся на ID:public class Product { public string ProductID { get; set; } public string SupplierID { get; set; } public string ProductName { get; set; } } public class Supplier { public string SupplierID { get; set; } public string CompanyName { get; set; } public string ContactName { get; set; } public string Phone { get; set; } }
И это не удивительно, что в качестве источников данных можно использовать модели Linq2Sql и ADO.NET Entity Framework.
Посмотрев на наш сервис в браузере, мы увидим такой XML:
<?xml version="1.0" encoding="utf-8" standalone="yes" ?> <service xml:base="http://localhost:7593/WebDataService.svc/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app"> <workspace> <atom:title>Default</atom:title> </workspace> </service>
Никаких упоминаний о наших Products и Suppliers. Мы забыли сконфигурировать наш сервис, указав доступ к коллекциям объектов и операциям. Но так как наш сервис не предоставляет никаких методов для манипуляции данными, нам нужно сконфигурировать только доступ к коллекциям в источнике данных:
public static void InitializeService(IDataServiceConfiguration config) { config.SetEntitySetAccessRule("*", EntitySetRights.AllRead); }
Открываем сервис в браузере и видим наши коллекции:
<?xml version="1.0" encoding="windows-1251" standalone="yes"?> <service xml:base="http://localhost:7593/WebDataService.svc/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app"> <workspace> <atom:title>Default</atom:title> <collection href="Suppliers"> <atom:title>Suppliers</atom:title> </collection> <collection href="Products"> <atom:title>Products</atom:title> </collection> </workspace> </service>
Итак, наш сервис готов к использованию.
Реализация клиента
Так как наш сервис данных построен на основе WCF, то подключение сервиса ничем не отличается от подключения любого другого WCF-сервиса. Самое же интересное начинается во время его использования.
В качестве примера реализуем логику поиска поставщиков по названию товара. Локально поиск будет проводиться по базе Northwind с использованием Linq2Sql, а потом результат локального поиска будет объединяться с поиском через сервис данных.
Локальная схема данных будет выглядеть следующим образом:
Поисковый запрос к локальной базе достаточно тривиален:
var localData = new LocalDataDataContext(); var localSuppliers = from p in localData.Products join s in localData.Suppliers on p.SupplierID equals s.SupplierID where p.ProductName == productName select new { Company = s.CompanyName, PhoneNumber = s.Phone };
Попробуем сделать то же самое для поиска по удаленному сервису данных:
var serviceData = new DomainDataContext(new Uri("http://localhost:7593/WebDataService.svc")); var dataServiceSuppliers = from p in serviceData.Products join s in serviceData.Suppliers on p.SupplierID equals s.SupplierID where p.ProductName == productName select new { Company = s.CompanyName, PhoneNumber = s.Phone };
При попытке получить данные по этому запросу мы получим исключение "The method 'Join' is not supported.". По сути, это Законы дырявых асбстракций в действии :) Все дело в том, что не вся функциональность Linq однозначно транслируется в REST-запросы. REST-сервисы предоставляют лишь несколько параметров запросов: expand, orderby, skip, top, filter. Как видно, никаких inner join и даже агрегатов, типа банального count тут нет.
Для решения этой проблемы разобьем наш запрос на несколько подзапросов: сначала найдем все товары, удовлетворяющие данному, а потом получим список поставщиков этих товаров.
var serviceData = new DomainDataContext(new Uri("http://localhost:7593/WebDataService.svc")); var matchedProducts = from p in serviceData.Products where p.ProductName == productName select p;
Следующий фрагмент кода динамически соберет условие для параметра filter так, чтобы получилось что-то в таком духе:
Func<ServiceReference1.Supplier, bool> filterCondition = s => s.SupplierID == "001" || s.SupplierID == "002";
Такой вариант фильтра однозначно транслируется в строку для параметра запроса filter.
var param = Expression.Parameter(typeof(ServiceReference1.Supplier), "s"); Func<string, IEnumerable<string>, Expression> buildOrElseExpression = null; Func<string, Expression> buildEqualExpression = val => Expression.Equal(Expression.Property(param, "SupplierID"), Expression.Constant(val)); Func<IEnumerable<string>, Expression> buildTailExpression = tail => tail.Count() == 1 ? buildEqualExpression(tail.First()) : buildOrElseExpression(tail.First(), tail.Skip(1)); buildOrElseExpression = (head, tail) => Expression.OrElse( buildEqualExpression(head), buildTailExpression(tail)); // Вычитываем данные из сервиса и получаем отключенный список идентификаторов поставщиков var serviceSupplierIDs = matchedProducts.ToArray().Select(p => p.SupplierID); var filterCondition = Expression.Lambda<Func<ServiceReference1.Supplier, bool>>( buildTailExpression(serviceSupplierIDs), param); // Получаем отключенную копию результатов поиска для дальнейших манипуляций var serviceSuppliers = serviceData.Suppliers. Where(filterCondition). ToArray(). Select(s => new { Company = s.CompanyName, PhoneNumber = s.Phone });
Ну и наконец-то финальное объединение:
// Перед объединением получаем отключенную копию данных. // В противном случае Linq2Sql попытается транслировать этот метод в sql-инструкцию UNION var allSuppliers = localSuppliers.ToArray().Union(serviceSuppliers);
Выводы
Конечно же, ADO.NET Data Services - это решение многих проблем интеграции данных внутри системы. Но использование этой системы накладывает и ряд требований:
- Модель данных для таких сервисов должна быть подготовлена специальным образом так, чтобы избежать множества повторяющихся простых запросов из-за ограничения протокола REST. В общем случае, эта подготовка сведется к денормализации исходной модели данных, к укрупнению классов сущностей и введению всяких агрегатных полей с данными из дочерних сущностей.
- Для вычисления агрегатных функций (таких, как count(), sum() и т.д.) нужно будет реализовать специальные методы вне контекста сервисов данных.
- Все операции проекции должны проводиться на отсоединенной выборке. Таким образом, с учетом требований в п.1 такими запросами не стоит злоупотреблять по причиные избыточности данных, передаваемых через транспортный уровень. Для решения этой проблемы можно было бы расширить нашу модель одинаковыми сущностями с различными уровнями детализации.
Наверное, стоит наедятся, что в будущем многие из недостающих сегодня возможностей будут реализованы. Но даже то, что у нас есть на сегодняшний день - это большой шаг вперед в области интеграции приложений.
Опубликовать | Tweet |
ADO.NET Data Services - это не Linq-over-HTTP. Не надо пытаться использовать реляционные операторы в запросах к такому сервису.
ОтветитьУдалитьADO.NET Data Services вполне поддерживает навигацию по связанным сущностям, используйте нормальную модель и вам не придется писать джоины на клиенте.
Почему же нельзя - можно :)
ОтветитьУдалитьДругое дело - как и насколько эффективно это будет.
Я как раз и показал, что в лоб этим способом задачи интеграции не решаются.