Entity Framework

Новые возможности в июньской CTP-версии Entity Framework

Срикант Мандади

Продукты и технологии:

Entity Framework 4.2, Visual Studio 2010

В статье рассматриваются:

  • новые средства — перечислимые, табличные функции (table-valued functions) и пространственные типы;
  • использование подходов Code First и Database First;
  • автоматически компилируемые LINQ-запросы.

Недавно выпущенная CTP-версия Microsoft Entity Framework (EF) за июнь 2011 г. включает поддержку ряда средств, о введении которых нас часто просили, в частности перечислимых и пространственных типов, а также функций, возвращающих табличные значения (table-valued functions, TVF) (далее для краткости — табличные функции). Мы рассмотрим эти средства на простых примерах. Я исхожу из того, что вы знакомы с EF (http://bit.ly/oLbjp0) и шаблоном разработки Code First (http://bit.ly/oQ77Hm), введенным в EF 4.1.

Вот что вам понадобится для работы с примерами в этой статье:

  • Visual Studio 2010 Express и SQL Server 2008 R2 Express или выше. Редакции Express продуктов Visual Studio и SQL Server можно скачать по ссылке bit.ly/rsFvxJ.
  • Microsoft EF и EF Tools June 2011 CTP
  • база данных Northwind; ее можно скачать по ссылке bit.ly/pwbDoQ.

Итак, приступим.

Перечислимые типы

Начнем с одного из самых часто запрашиваемых средств в EF — перечислимых. Многие языки программирования, в том числе .NET-языки вроде C# и Visual Basic, изначально поддерживают перечислимые типы. В EF поставлена цель разрешить разработчикам создавать перечислимые типы в своих CLR-типах и сопоставлять с нижележащей Entity Data Model (EDM), после этого сохранять соответствующие значения в базе данных. Прежде чем углубиться в детали, рассмотрим простой пример. Перечислимые поддерживаются в Code First, Database First и Model First. Я начну с подхода Database First (сначала база данных), а затем покажу пример, где используется подход Code First.

Для примера Database First воспользуемся таблицей Products в базе данных Northwind. Прежде чем добавлять модель, переключитесь на EF June 2011 CTP. Для этого сделайте следующее.

  1. Запустите Visual Studio 2010 и создайте новый проект C# Console Application.
  2. Щелкните свой проект правой кнопкой мыши в Solution Explorer и выберите Properties.
  3. Выберите Microsoft Entity Framework June 2011 CTP из раскрывающего списка Target framework (рис. 1).
  4. Нажмите Ctrl+S, чтобы сохранить проект. Visual Studio предложит закрыть проект и заново открыть его — согласитесь, щелкнув Yes.
  5. Добавьте новую модель в проект, выбрав Project | Add New Item (или нажав Ctrl+Shift+A) и указав ADO.NET Data Entity Model в Visual C# Items (мы назовем нашу модель «CTP1EnumsModel.edmx»), затем щелкните Add.
  6. С помощью мастера укажите базу данных Northwind. Выберите таблицу Products и щелкните Finish.
  7. В Entity Model, которая получится в результате такого выбора, содержится одна сущность (Product), как показано на рис. 2.

Ориентация на Entity Framework June 2011 CTP
Рис. 1. Ориентация на Entity Framework June 2011 CTP

Entity Model для сущности Product
Рис. 2. Entity Model для сущности Product

Ниже приведен LINQ-запрос для получения всех продуктов, относящихся к категории Beverages. Заметьте, что CategoryID для Beverages равен 1:

var ctx = new NorthwindEntities();
var beverageProducts = from p in ctx.Products
                       where p.CategoryID == 1

Конечно, чтобы написать этот запрос, вы должны были бы знать, что CategoryID для Beverages равно 1, что возможно лишь в двух случаях: вы запомнили это значение или нашли его, проанализировав базу данных. Другая проблема с этим запросом в том, что непонятно, какую категорию запрашивает этот код. В таком случае нужно либо документировать категорию в каждом месте, где используется CategoryID, либо иметь каждому, кто читает этот код, соответствующие знания о значениях CategoryID для различных категорий.

Еще одна проблема вылезает, когда вы пытаетесь вставить новый Product. Для вставки в таблицу Products используйте следующий код:

var ctx = new NorthwindEntities();
var product = new Product() { ProductName = "place holder",
  Discontinued = false, CategoryID = 13 };
ctx.AddToProducts(product);
ctx.SaveChanges();

Запустив этот код, вы получите от базы данных исключение из-за ограничения внешнего ключа. Это исключение генерируется потому, что категории с идентификатором 13 нет. Было бы куда лучше, если бы у программиста был какой-то способ выяснить список категорий и назначить правильный идентификатор вместо того, чтобы все время помнить набор правильных целочисленных значений.

Давайте введем в модель перечислимые и посмотрим, насколько эту улучшит ситуацию.

Ниже перечислены шаги, необходимые для преобразования CategoryID в перечислимый тип.

  1. Откройте модель в дизайнере, дважды щелкнув файл CTP1EnumsModel.edmx.
  2. Щелкните правой кнопкой мыши свойство CategoryID в Product Entity и выберите Convert to Enum.
  3. 3. Создайте перечислимый тип (enum) и введите значения для его членов в новом диалоге, который появится на экране (рис. 3). Присвойте этому типу имя Category и выберите Byte в качестве нижележащего типа. Нижележащим считается тип, который предоставляет пространство значений для перечислимого типа. Вы можете выбирать его на основе количества членов в перечислении. В данном перечислении восемь членов, что как раз и соответствует байту. Введите эти члены в порядке возрастания значений CategoryID. Укажите Value для первой категории (Beverages) как 1 и оставьте поле Value для остальных членов пустым, потому что их значения автоматически увеличиваются на 1 в базе данных. Этот вариант и для EF является поведением по умолчанию. Но, если бы значения в базе данных были другими, вам пришлось бы заполнять поле Value для каждой категории. Если бы значение для Beverages было 0 вместо 1, вы также могли бы оставить остальные поля пустыми, так как EF выбирает 0 в качестве значения по умолчанию для первого члена перечисления.
  4. Создавая перечислимый тип, вы можете обозначить его как флаг, используя параметр «Is Flag?». Он используется только при генерации кода; если этот флаг установлен, перечислимый тип будет сгенерирован с атрибутом Flags (подробнее о таких перечислениях см. по ссылке bit.ly/oPqiMp). В этом примере оставьте данный параметр неустановленным.
  5. Перекомпилируйте приложение для повторной генерации кода, и полученный в результате этого код будет теперь включать перечислимые.

Окно для создания перечислимого типа
Рис. 3. Окно для создания перечислимого типа

Вы можете переписать запрос для получения всех продуктов, относящихся к категории Beverages, следующим образом:

var ctx = new NorthwindEntities();
var beverageProducts = from p in ctx.Products
                       where p.Category == Category.Beverages
                       select p;

Теперь IntelliSense будет помогать в написании запроса, и вам не придется изучать базу данных в поисках значений Beverages. Аналогично при обновлениях IntelliSense будет показывать корректные значения для Category.

Мы только что рассмотрели перечислимые, используя подход Database First. А сейчас я применю подход Code First для написания запроса, который возвращает все продукты категории Beverages с использованием перечислимых. Для этого создайте еще одно консольное приложение и добавьте файл исходного кода на C# с типами, показанными на рис. 4..

Рис. 4. Применение перечислимых в подходе Code First

public enum Category : byte
{
  Beverages = 1,
  Condiments,
  Confections,
  Dairy,
  Grains,
  Meat,
  Produce,
  Seafood
}

public class Product
{
  public int ProductID { get; set; }
  public string ProductName { get; set; }
  public int? SupplierID { get; set; }
  [Column("CategoryID", TypeName = "int")]
  public Category Category { get; set; }
  public string QuantityPerUnit { get; set; }
  public decimal? UnitPrice { get; set; }
  public short? UnitsInStock { get; set; }
  public short? UnitsOnOrder { get; set; }
  public short? ReorderLevel { get; set; }
  public bool Discontinued { get; set; }
}

public class EnumsCodeFirstContext : DbContext
{
  public EnumsCodeFirstContext() : base(
    "data source=<server name>; initial catalog=Northwind;
    integrated security=True;multipleactiveresultsets=True;")
  {
  }
  public DbSet<Product> Products { get; set; }
}

Класс EnumsCodeFirstContext наследует от DbContext — нового типа в EF 4.1, аналогичного ObjectContext, но гораздо более простого в использовании. (Подробнее о применении DbContext API см. по ссылке bit.ly/eeEsyt.)

В коде на рис. 4 стоит обратить внимание на пару моментов.

  • Атрибут Column над свойством Category используется для сопоставления CLR-свойств и столбцов, когда у них разные имена или типы.
  • Конструктор в EnumsCodeFirstContext вызывает конструктор базового класса, передавая строку подключения. По умолчанию DbContext создает базу данных в локальном экземпляре SqlExpress с полным именем класса, производного от DbContext. В этом примере мы просто используем существующую базу данных Northwind.

Теперь вы можете написать код, аналогичный тому, который использовался в контексте Database First, чтобы получить все продукты, относящиеся к категории Beverages:

EnumsCodeFirstContext ctx = new EnumsCodeFirstContext();
var beverageProducts = from p in ctx.Products
                       where p.Category == Category.Beverages
                       select p;

 

Другое важное средство, добавленное в эту CTP-версию, — поддержка табличных функций.

Табличные функции

Другое важное средство, добавленное в эту CTP-версию, — поддержка табличных функций (TVF). TVF очень похожи на хранимые процедуры с одним важным отличием: результат, возвращаемый TVF, является компонуемым (composable). Это означает, что результаты от TVF можно использовать во внешнем запросе. Таким образом, главное для разработчиков, использующих EF, заключается в том, что TVF можно указывать в LINQ-запросе, а хранимую процедуру — нет. Я продемонстрирую пример использования TVF в EF-приложении. Попутно вы увидите, как задействовать преимущества функциональности полнотекстового поиска (Full-Text Search, FTS) в SQL Server (подробнее см. по ссылке bit.ly/qZXG9X). 

Функциональность FTS предоставляется через несколько предикатов и TVF. В предыдущих версиях EF вы могли задействовать полнотекстовые TVF, либо вызывая их в скрипте на T-SQL с применением ExecuteStoreCommand, либо используя хранимую процедуру. Но оба этих механизма не обеспечивают компонуемость, и их нельзя применять в LINQ to Entities. В моем примере вы увидите, как задействовать эти функции в качестве компонуемых с поддержкой TVF в этой CTP-версии. Для этого я позаимствовал запрос из документации MSDN для ContainsTable (bit.ly/q8FFws). Этот запрос ищет все названия продуктов, в которых есть слова «breads», «fish» или «beers», и этим словам назначаются разные весовые доли (значимость). Для каждой полученной строки, отвечающей этим критериям поиска, показывается относительная близость совпадения (классификационное значение):

SELECT FT_TBL.CategoryName, FT_TBL.Description, KEY_TBL.RANK
  FROM Categories AS FT_TBL
    INNER JOIN CONTAINSTABLE(Categories, Description,
    'ISABOUT (breads weight (.8),
    fish weight (.4), beers weight (.2) )' ) AS KEY_TBL
      ON FT_TBL.CategoryID = KEY_TBL.[KEY]
ORDER BY KEY_TBL.RANK DESC;

Попробуем написать тот же запрос в LINQ to Entities. К сожалению, предоставить ContainsTable напрямую EF нельзя, так как эта инфраструктура ожидает, что в первых двух параметрах имена таблицы и столбца передаются как идентификаторы без кавычек, т. е. как Categories, а не 'Categories', и нет никакого способа сообщить EF особым образом интерпретировать эти параметры. Чтобы обойти это ограничение, оберните ContainsTable в другую пользовательскую TVF. Выполните следующий SQL-код для создания TVF с именем ContainsTableWrapper (TVF выполняет функцию ContainsTable применительно к столбцу Description в таблице Categories):

Use Northwind;
Create Function ContainsTableWrapper(
  @searchstring nvarchar(4000))
returns table
as
return (select [rank], [key] from ContainsTable(
  Categories, Description, @searchstring))

Теперь создайте EF-приложение и используйте эту TVF. Придерживайтесь той же схемы, что и в примере с перечислением, чтобы создать консольное приложение и добавить модель сущностей, связанную с Northwind. Включите Categories, Products и только что созданную TVF. Модель будет выглядеть, как показано на рис. 5. {Рисунок}

Модель сущностей с Products и Categories из Northwind
Рис. 5. Модель сущностей с Products и Categories из Northwind

TVF в рабочей области дизайнера не отображается, но вы можете увидеть ее в Model Browser, раскрыв Stored Procedures/Functions в разделе Store.

Чтобы задействовать эту функцию в LINQ, добавьте функцию-заглушку (function stub) (как описано по ссылке bit.ly/qhIYe2). Я добавил функцию-заглушку в частичный класс для класса ObjectContext — в данном случае NorthwindEntities:

public partial class NorthwindEntities
{
  [EdmFunction("NorthwindModel.Store", "ContainsTableWrapper")]
  public IQueryable<DbDataRecord> ContainsTableWrapper(
    string searchString)
  {
    return this.CreateQuery<DbDataRecord>(
      "[NorthwindModel.Store].[ContainsTableWrapper](
      @searchstring)", new ObjectParameter[] {
      new ObjectParameter("searchString", searchString)});
  }
}

Теперь вы можете пользоваться этой функцией в своих запросах. Просто выведите в консоль Key, т. е. CategoryId и Rank для полнотекстового запроса, упомянутого ранее:

var ctx = new NorthwindEntities();
var fulltextResults = from r in ctx.ContainsTableWrapper(
  "ISABOUT (breads weight (.8),
  fish weight (.4), beers weight (.2) )")
                    select r;
foreach (var result in fulltextResults)
{
  Console.WriteLine("Category ID:" + 
  result["Key"] + "   Rank :" + result["Rank"]);
}

Вывод в консоли выглядит так:

Category ID:1   Rank :15
Category ID:3   Rank :47
Category ID:5   Rank :47
Category ID:8   Rank :31

Но это не тот запрос, который мы пытались написать. Нужный нам делает нечто большее. Наряду со значимостью (rank) он предоставляет Category Name и Description, которые интереснее, чем просто Category ID. Вот исходный запрос:

SELECT FT_TBL.CategoryName, FT_TBL.Description, KEY_TBL.RANK
  FROM Categories AS FT_TBL
    INNER JOIN CONTAINSTABLE(Categories, Description,
    'ISABOUT (breads weight (.8),
    fish weight (.4), beers weight (.2) )' ) AS KEY_TBL
      ON FT_TBL.CategoryID = KEY_TBL.[KEY]
ORDER BY KEY_TBL.RANK DESC;

Чтобы использовать LINQ для этого запроса, нужно сопоставить TVF с Function Import в EDM с типом Complex или возвращаемым типом Entity, так как Function Import, которые возвращают Row Types, не являются компонуемыми.

Для сопоставления проделайте следующее.

  1. Дважды щелкните функцию Store в Model Browser, чтобы открыть диалог Add Function Import, показанный на рис. 6.
  2. Введите Function Import Name как ContainsTableWrapperModelFunction.
  3. Установите флажок «Function Import is Composable?».
  4. Выберите функцию ContainsTableWrapper из раскрывающегося списка Stored Procedure/Function Name.
  5. Щелкните кнопку GetColumnInformation, чтобы заполнить таблицу под этой кнопкой информацией о типе результата, возвращаемого функцией.
  6. Щелкните кнопку Create New Complex Type. Это приведет к переключению с Returns a Collection Of на Complex со сгенерированным именем для комплексного типа.
  7. Щелкните OK.

Сопоставление TVF с Function Import
Рис. 6. Сопоставление TVF с Function Import

Для TVF, сопоставленных с Function Import в модели, вам не потребуется добавлять соответствующую функцию в коде, поскольку такая функция будет сгенерирована автоматически.

Теперь вы можете написать запрос на T-SQL, использовавший FTS-функцию ContainsTable в LINQ, следующим образом:

var ctx = new NorthwindEntities();
var fulltextResults = from r in
  ctx.ContainsTableWrapperModelFunction("ISABOUT
  (breads weight (.8), fish weight (.4), beers weight (.2) )")
                     join c in ctx.Categories
                     on r.key equals c.CategoryID
                     select new { c.CategoryName,
                       c.Description, Rank = r.rank };

foreach (var result in fulltextResults)
{
  Console.WriteLine("Category Name:" +
    result.CategoryName + "   Description:" +
    result.Description + "   Rank:" + result.Rank);
}

Запустив этот код, вы получите в консоли такой вывод:

Category Name:Beverages   Description:Soft drinks, coffees,
  teas, beers, and ales   Rank:15
Category Name:Confections   Description:Desserts, candies,
  and sweet breads   Rank:47
Category Name:Grains/Cereals   Description:Breads, crackers,
  pasta, and cereal Rank:47
Category Name:Seafood   Description:Seaweed and fish   Rank:31

Поддержка пространственных типов

Теперь рассмотрим новую функциональность, вызвавшую большой интерес: поддержку пространственных типов. В EDM добавлены два новых типа: DbGeometry и DbGeography. В следующем примере я покажу, как пользоваться пространственными типами в EF с применением Code First.

Создайте проект Console Application с именем EFSpatialSample, затем добавьте в него файл исходного кода на C# со следующими типами:

namespace EFCTPSpatial
{
  public class Customer
  {
    public int CustomerID { get; set; }
    public string Name { get; set; }
    public DbGeography Location { get; set; }
  }

  public class SpatialExampleContext : DbContext
  {
    public DbSet<Customer> People { get; set; }
  }
}

Свойство Location в Customer имеет тип DbGeography, который был добавлен в пространство имен System.Data.Spatial в этой CTP-версии. DbGeography преобразуется в SqlGeography в случае SQL Server. Вставьте какие-нибудь пространственные данные, используя эти типы, а затем запросите их с помощью LINQ (рис. 7).

Рис. 7. Работа с пространственными данными

static void Main(string[] args)
{
  var ctx = new SpatialExampleContext();
  ctx.Customers.Add(new Customer() { CustomerID = 1,
    Name = "Customer1", Location = DbGeography.Parse((
    "POINT(-122.336106 47.605049)")) });
  ctx.Customers.Add(new Customer() { CustomerID = 2,
    Name = "Customer2", Location = DbGeography.Parse((
    "POINT(-122.31946 47.625112)")) });
  ctx.SaveChanges();

  var customer1 = ctx.Customers.Find(1);
  var distances = from c in ctx.Customers
        select new { Name = c.Name, DistanceFromCustomer1 =
        c.Location.Distance(customer1.Location)};
  foreach (var item in distances)
  {
    Console.WriteLine("Customer Name:" + item.Name + ",
      Distance from Customer 1:" +
      (item.DistanceFromCustomer1 / 1609.344 ));
  }
}

Ничего сложного в этом коде не происходит. Вы создаете два объекта Customer с разными местоположениями и указываете их через Well-Known Text (об этом протоколе читайте по ссылке bit.ly/owIhfu). Эти изменения сохраняются в базе данных. Далее запрос LINQ to Entities получает расстояние до каждого клиента в таблице из Customer1. Это расстояние делится на 1609,344 для преобразования метров в мили. Вот вывод программы:

Customer Name:Customer1,  Distance from Customer 1:0
Customer Name:Customer2,  Distance from Customer 1:1.58929160985881

Как и следовало ожидать, расстояние от Customer1 до Customer1 равно 0. Расстояние от Customer1 до Customer2 указано в милях. Операция Distance в запросе выполняется в базе данных с помощью функции STDistance. Ниже показан SQL-запрос, отправляемый базе данных:

SELECT 1 AS [C1], [Extent1].[Name] AS [Name], [Extent1].
[Location].STDistance(@p__linq__0)
  AS [C2]
FROM [dbo].[Customers] AS [Extent1]
Компиляция дерева выражений в SQL создает некоторые издержки — особенно в более сложных запросах.

Автоматически компилируемые LINQ-запросы

В настоящее время, когда вы пишете запрос LINQ to Entities, EF проходит по дереву выражений, генерируемому компилятором C# или Visual Basic, и транслирует (или компилирует) его в SQL. Компиляция дерева выражений в SQL создает некоторые издержки — особенно в более сложных запросах. Чтобы избежать этих издержек при каждом выполнении LINQ-запроса, вы можете компилировать свои запросы, а затем повторно использовать их. Класс CompiledQuery позволяет платить за издержки лишь раз, а затем передает вам делегат, который указывает непосредственно на скомпилированную версию запроса в кеше EF.

Июньская CTP-версия поддерживает новую функциональность Auto-Compiled LINQ Queries, которая позволяет автоматически компилировать и помещать в кеш запросов EF каждый выполняемый вами запрос LINQ to Entities. При каждом последующем выполнении запроса EF будет искать его в своем кеше и не станет снова инициировать весь процесс компиляции. Эта функциональность также ускоряет выполнение запросов, выдаваемых с помощью WCF Data Services, так как «за кулисами» использует LINQ. Подробнее о деревьях выражений см. по ссылке bit.ly/o5X3rA.

Заключение

Как видите, в следующем выпуске EF появится ряд интересных средств. На самом деле их даже больше, чем я смог описать в этой статье, в том числе усовершенствования в генерации SQL-кода для Table per Type (TPT), возможность получения нескольких наборов результатов от хранимых процедур и др. Данная CTP-версия дает шанс ознакомиться с будущими новинками и высказать свои замечания, чтобы разработчики устранили имеющиеся ошибки или улучшили удобство использования новых средств. О любых ошибках можно сообщать через сайт Microsoft Data Developer Connect (connect.microsoft.com/data), а предложения по новой функциональности — через сайт (ef.mswish.net).

Срикант Мандади (Srikanth Mandadi) — руководитель разработок в группе Entity Framework.

Выражаю благодарность за рецензирование статьи группе Entity Framework.