ADO.NET Entity Framework 4.1 была выпущена в апреле и включает ряд новых средств, построенных на основе существующей функциональности Entity Framework 4 — инфраструктуры, предоставляемой в Microsoft .NET Framework 4 и Visual Studio 2010.
Entity Framework 4.1 доступна отдельно (msdn.microsoft.com/data/ee712906) как NuGet-пакет EntityFramework, а также включена в ASP.NET MVC 3.01.
В Entity Framework 4.1 появились две основные новые возможности: DbContext API и Code First. В этой статье я расскажу, как задействовать эти возможности при разработке приложений. Мы кратко рассмотрим, как приступить к работе с Code First, а затем подробно обсудим некоторые более продвинутые возможности.
DbContext API — это упрощенная абстракция существующего типа ObjectContext и некоторых других типов, которые были введены в предыдущих выпусках Entity Framework. DbContext API оптимизирован для распространенных задач и шаблонов кодирования. Стандартная функциональность предоставляется на корневом уровне, а более продвинутая — доступна при углублении в этот API.
Code First — новый шаблон разработки для Entity Framework и альтернатива существующим шаблонам Database First и Model First. Code First позволяет определить модель, используя CLR-классы, а затем сопоставить эти классы с существующей базой данных или сгенерировать на их основе схему базы данных. Дополнительное конфигурирование обеспечивается через аннотации данных или через текучий API.
Приступаем к работе с шаблоном Code First
Code First существует уже некоторое время, поэтому я не стану вдаваться в детали того, как начать с ним работу. Если вы не знакомы с основами, то можете последовать «Code First Walkthrough» (bit.ly/evXlOc). На рис. 1 приведен полный исходный код, помогающий быстро начать разработку приложения на основе шаблона Code First.
Рис. 1. Приступаем к работе с шаблоном Code First
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System;
namespace Blogging
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer<BlogContext>(
new BlogInitializer());
// TODO: заставьте эту программу что-нибудь делать!
}
}
public class BlogContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(
DbModelBuilder modelBuilder)
{
// TODO: любое конфигурирование
// через текучий API делается здесь!
}
}
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Abstract { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class RssEnabledBlog : Blog
{
public string RssFeed { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public byte[] Photo { get; set; }
public virtual Blog Blog { get; set; }
}
public class BlogInitializer :
DropCreateDatabaseIfModelChanges<BlogContext>
{
protected override void Seed(BlogContext context)
{
context.Blogs.Add(new RssEnabledBlog
{
Name = "blogs.msdn.com/data",
RssFeed = "http://blogs.msdn.com/b/data/rss.aspx",
Posts = new List<Post>
{
new Post { Title = "Introducing EF4.1" },
new Post { Title = "Code First with EF4.1" },
}
});
context.Blogs.Add(new Blog { Name = "romiller.com" });
context.SaveChanges();
}
}
}
Простоты ради я предпочел сделать так, чтобы Code First генерировал базу данных. В моем случае база данных будет создана при первом использовании BlogContext для сохранения и запроса данных. Остальная часть статьи в равной мере применима к тем случаям, где Code First сопоставляется с существующей базой данных. Вы увидите, что на протяжении всей статьи я использую инициализатор базы данных для удаления и повторного создания базы данных после внесения любых изменений в модель.
Сопоставление с помощью текучего API
Code First начинает с анализа ваших CLR-классов для логического определения «контуров» вашей модели. При распознавании многих вещей вроде основных ключей применяется ряд соглашений. Вы можете переопределить (или добавить) то, что распознается по соглашению, используя аннотации данных (Data Annotations) или текучий API (fluent API). О решении распространенных задач с помощью текучего API уже написано довольно много статей, поэтому я намерен рассмотреть некоторые из более сложных случаев. В частности, я сосредоточусь на разделах «сопоставления» (mapping) этого API. Конфигурацию сопоставления можно использовать для сопоставления со схемой существующей базы данных или для изменения сгенерированной схемы. Текучий API предоставляется через тип DbModelBuilder, и к нему проще всего обращаться с помощью переопределенного метода OnModelCreating объекта DbContext.
В Entity Framework 4.1 появились две основные новые возможности: DbContext API и Code First. |
Расщепление сущностей Позволяет распределять свойства типа сущности по нескольким таблицам. Например, я хочу выделить фотоснимки для публикации в отдельную таблицу, чтобы их можно было хранить в другой файловой группе. При расщеплении сущности используется серия вызовов Map для сопоставления подмножества свойств с конкретной таблицей. На рис. 2 я сопоставляю свойство Photo с таблицей PostPhotos, а остальные свойства — с таблицей Posts. Заметьте, что я не включил основной ключ в список свойств. Основной ключ всегда должен быть в каждой таблице; я мог бы включить его самостоятельно, но Code First добавляет его за меня автоматически.
Рис. 2. Расщепление сущностей
protected override void OnModelCreating(
DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.Map(m =>
{
m.Properties(p => new { p.Title, p.Content });
m.ToTable("Posts");
})
.Map(m =>
{
m.Properties(p => new { p.Photo });
m.ToTable("PostPhotos");
});
}
Наследование Table-per-Hierarchy (TPH) В случае TPH данные для иерархии наследования хранятся в одной таблице и используется поле различения (discriminator) для идентификации типа каждой записи. Если вы не предоставляете свою конфигурацию, Code First будет использовать TPH по умолчанию. Поле различения названо соответствующим образом — Discriminator, и его значением является имя каждого CLR-типа.
Однако вам может понадобиться настроить то, как осуществляется сопоставление TPH. Для этого используйте метод Map, чтобы задать значения в полях различения для базового типа, а затем вызывайте Map<TEntityType>, чтобы сконфигурировать каждый производный тип. В данном случае я использую поле HasRssFeed для хранения значения true/false, чтобы различать экземпляры Blog и RssEnabledBlog:
protected override void OnModelCreating(
DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Map(m => m.Requires("HasRssFeed").HasValue(false))
.Map<RssEnabledBlog>(m => m.Requires(
"HasRssFeed").HasValue(true));
}
В предыдущем примере я все еще применяю отдельное поле для различения типов, но знаю, что экземпляры RssEnabledBlog можно идентифицировать по наличию у них RSS-канала. Я могу переписать сопоставление, чтобы сообщить Entity Framework о необходимости использования поля, где хранится Blog.RssFeed, для различения типов. Если в поле содержится значение, отличное от null, значит, это RssEnabledBlog:
protected override void OnModelCreating(
DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Map<RssEnabledBlog>(m => m.Requires(
b => b.RssFeed).HasValue());
}
Наследование Table-per-Type (TPT) В случае TPT все свойства из базового типа хранятся в одной таблице. Любые дополнительные свойства помещаются в отдельные таблицы производных типов с внешним ключом, указывающим на базовую таблицу. Сопоставление TPT осуществляется вызовом Map, чтобы указать имя базовой таблицы, и последующим вызовом Map<TEntityType>, чтобы сконфигурировать таблицу для каждого производного типа. В следующем примере я сохраняю данные, общие для всех блогов, в таблице Blogs, а данные, специфичные для блогов с RSS-каналом, — в таблице RssBlogs:
modelBuilder.Entity<Blog>()
.Map(m => m.ToTable("Blogs"))
.Map<RssEnabledBlog>(m => m.ToTable("RssBlogs"));
Наследование Table-per-Concrete Type (TPC) В случае TPC данные для каждого типа хранятся в отдельной таблице без ограничений по внешнему ключу между ними. Конфигурирование выполняется аналогично сопоставлению TPT, но при конфигурировании каждого производного типа добавляется вызов MapInheritedProperties. Этот метод сообщает Code First заново сопоставить все свойства, унаследованные от базового класса, с новыми полями в таблице производного класса:
protected override void OnModelCreating(
DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Map(m => m.ToTable("Blogs"))
.Map<RssEnabledBlog>(m =>
{
m.MapInheritedProperties();
m.ToTable("RssBlogs");
});
}
По соглашению, Code First будет использовать для целочисленных основных ключей поля идентификации (identity columns). Однако в случае TPC у вас не будет единственной таблицы, содержащей все блоги, на основе которой можно сгенерировать основные ключи. Из-за этого Code First отключит идентификацию при использовании сопоставления TPC. Если сопоставление осуществляется с существующей базой данных, которая была настроена на генерацию уникальных значений между несколькими таблицами, то можно заново включить идентификацию через раздел конфигурирования свойств в текучем API.
Гибридные сопоставления Конечно, ваша схема не всегда будет соответствовать одному из перечисленных шаблонов, особенно при сопоставлении с существующей базой данных. Хорошая новость в том, что API сопоставления является композиционным и вы можете комбинировать несколько стратегий сопоставления. На рис. 3 показан пример, демонстрирующий, как скомбинировать расщепление сущностей с сопоставлением TPT. Данные, общие для блогов, разделяются между таблицами Blogs и BlogAbstracts, а данные, специфичные для блогов с RSS-каналом, хранятся в отдельной таблице RssBlogs.
Рис. 3. Комбинация расщепления сущностей и сопоставления TPT
protected override void OnModelCreating(
DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Map(m =>
{
m.Properties(b => new { b.Name });
m.ToTable("Blogs");
})
.Map(m =>
{
m.Properties(b => new { b.Abstract });
m.ToTable("BlogAbstracts");
})
.Map<RssEnabledBlog>(m =>
{
m.ToTable("RssBlogs");
});
}
API для отслеживания изменений
Рассмотрев конфигурирование сопоставлений с базой данных, немного поработаем с данными и обсудим некоторые более сложные сценарии. (Если вы не знакомы с базовыми принципами доступа к данным, сначала прочитайте документ «Code First Walkthrough», о котором я уже упоминал.)
Информация о состоянии единственной сущности Во многих случаях, например при ведении журнала, полезно иметь доступ к информации о состоянии сущности. DbContext предоставляет доступ к такой информации для индивидуальных сущностей через метод Entry. Фрагмент кода на рис. 4 загружает один Blog из базы данных, модифицирует одно из свойств, а затем выводит в консоль текущее и исходное значения каждого свойства.
Рис. 4. Получение информации о состоянии сущности
static void Main(string[] args)
{
Database.SetInitializer<BlogContext>(new BlogInitializer());
using (var db = new BlogContext())
{
// Изменяем название одного из блогов
var blog = db.Blogs.First();
blog.Name = "ADO.NET Team Blog";
// Выводим исходное и текущее значения каждого свойства
var propertyNames =
db.Entry(blog).CurrentValues.PropertyNames;
foreach (var property in propertyNames)
{
System.Console.WriteLine(
"{0}\n Original Value: {1}\n Current Value: {2}",
property,
db.Entry(blog).OriginalValues[property],
db.Entry(blog).CurrentValues[property]);
}
}
Console.ReadKey();
}
При выполнении кода с рис. 4 вы получите следующий вывод в консоли:
BlogId
Original Value: 1
Current Value: 1
Name
Original Value: blogs.msdn.com/data
Current Value: ADO.NET Team Blog
Abstract
Original Value:
Current Value:
RssFeed
Original Value: http://blogs.msdn.com/b/data/rss.aspx
Current Value: http://blogs.msdn.com/b/data/rss.aspx
Информация о состоянии нескольких сущностей DbContext позволяет получать информацию о состоянии нескольких сущностей через метод ChangeTracker.Entries. Он существует в двух версиях: обобщенной (для получения сущностей конкретного типа) и обычной (для получения всех сущностей). Параметр обобщенной версии не обязательно должен быть типом сущности. Например, вы могли бы получать записи по всем загруженным объектам, которые реализуют специфический интерфейс. Код на рис. 5 демонстрирует загрузку всех объектов блогов в память, модификацию свойства одного из них и вывод в консоль состояния каждого отслеживаемого объекта блога.
Рис. 5. Доступ к информации о нескольких сущностях с помощью DbContext
static void Main(string[] args)
{
Database.SetInitializer<BlogContext>(new BlogInitializer());
using (var db = new BlogContext())
{
// Загружаем в память все блоги
db.Blogs.Load();
// Изменяем название одного из них
var blog = db.Blogs.First();
blog.Name = "ADO.NET Team Blog";
// Выводим состояние каждого блога,
// который находится в памяти
foreach (var entry in db.ChangeTracker.Entries<Blog>())
{
Console.WriteLine("BlogId: {0}\n State: {1}\n",
entry.Entity.BlogId,
entry.State);
}
}
Console.ReadKey();
}
При выполнении кода с рис. 5 вы получите следующий вывод в консоли:
BlogId: 1
State: Modified
BlogId: 2
State: Unchanged
Запрос локальных экземпляров Всякий раз, когда вы выполняете LINQ-запрос к DbSet, запрос отправляется в базу данных для обработки. Это гарантирует, что вы всегда будете получать актуальные результаты, но, если известно, что все необходимые данные уже находятся в памяти, можно избежать цикла обмена данными с базой данных, запросив локальные данные. Код на рис. 6 загружает в память все объекты блогов, а затем выполняет два LINQ-запроса, не требующие обращения к базе данных.
Рис. 6. Выполнение LINQ-запросов к данным в памяти
static void Main(string[] args)
{
Database.SetInitializer<BlogContext>(new BlogInitializer());
using (var db = new BlogContext())
{
// Загружаем в память все блоги
db.Blogs.Load();
// Запрашиваем блоги, упорядоченные по названию
var orderedBlogs = from b in db.Blogs.Local
orderby b.Name
select b;
Console.WriteLine("All Blogs:");
foreach (var blog in orderedBlogs)
{
Console.WriteLine(" - {0}", blog.Name);
}
// Запрашиваем все блоги с RSS-каналом
var rssBlogs = from b in db.Blogs.Local
where b is RssEnabledBlog
select b;
Console.WriteLine("\n Rss Blog Count: {0}",
rssBlogs.Count());
}
Console.ReadKey();
}
При выполнении кода с рис. 6 вы получите следующий вывод в консоли:
All Blogs:
- blogs.msdn.com/data
- romiller.com
Rss Blog Count: 1
Навигационное свойство как запрос DbContext позволяет получить запрос, представляющий содержимое навигационного свойства для данного экземпляра сущности. Тем самым вы можете фильтровать элементы, которые вы хотите поместить в память, и избежать загрузки ненужных данных.
Расщепление сущностей позволяет распределять свойства типа сущности по нескольким таблицам. |
Например, у меня есть экземпляр блога, и мне надо знать, сколько в нем сообщений. Я мог бы написать код, показанный на рис. 7, но он полагается на отложенную загрузку, а значит, поместит в память все сообщения только для того, чтобы подсчитать их количество.
Рис. 7. Получение количества элементов в базе данных с помощью отложенной загрузки
static void Main(string[] args)
{
Database.SetInitializer<BlogContext>(new BlogInitializer());
using (var db = new BlogContext())
{
// Загрузка всего блога
var blog = db.Blogs.First();
// Вывод количества сообщений
Console.WriteLine("Blog {0} has {1} posts.",
blog.BlogId,
blog.Posts.Count());
}
Console.ReadKey();
}
По сравнению с единственным целочисленным результатом, который и был реально нужен, пришлось загрузить из базы данных огромный объем информации, занявшей много памяти.
К счастью, этот код можно оптимизировать, используя метод Entry объекта DbContext для получения запроса, который представляет набор сообщений, связанных с блогом. Поскольку LINQ-запросы поддерживают композицию, я могу объединить в цепочку оператор Count и отправить в базу данных весь запрос, чтобы она вернула мне лишь один целочисленный результат (рис. 8).
Рис. 8. Применение DbContext для оптимизации кода запроса и экономии ресурсов
static void Main(string[] args)
{
Database.SetInitializer<BlogContext>(new BlogInitializer());
using (var db = new BlogContext())
{
// Загрузка всего блога
var blog = db.Blogs.First();
// Запрос количества сообщений
var postCount = db.Entry(blog)
.Collection(b => b.Posts)
.Query()
.Count();
// Вывод количества сообщений
Console.WriteLine("Blog {0} has {1} posts.",
blog.BlogId,
postCount);
}
Console.ReadKey();
}
Соображения по развертыванию
До сих пор мы обсуждали доступ к данным. Теперь заглянем немного в будущее и поразмыслим о некоторых вещах, которые следует учитывать по мере приближения к концу разработки приложения.
Строки подключения До этого момента я просто позволял Code First генерировать базу данных в localhost\SQLEXPRESS. Когда придет пора развертывать мое приложение, я, вероятно, захочу сменить базу данных, которая была указана Code First. Рекомендуемый подход для этого — добавить строку подключения в файл App.config (или Web.config для веб-приложений). Такой же подход рекомендуется и при использовании Code First для сопоставления с существующей базой данных. Если имя строки подключения совпадает с полным именем типа контекста, Code First автоматически выберет ее в период выполнения. Однако лучше использовать конструктор DbContext, который принимает имя соединения по синтаксису имя=<имя_строки_подключения>. Тогда Code First будет всегда использовать конфигурационный файл. Если в нем не окажется строки подключения, будет сгенерировано исключение. В следующем примере показан раздел строк подключения, через который можно было бы менять базу данных, обрабатываемую нашим приложением-примером:
<connectionStrings>
<add
name="Blogging"
providerName="System.Data.SqlClient"
connectionString="Server=MyServer;Database=Blogging;
Integrated Security=True;MultipleActiveResultSets=True;" />
</connectionStrings>
Вот как выглядит обновленный код контекста:
public class BlogContext : DbContext
{
public BlogContext()
: base("name=Blogging")
{}
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
Заметьте, что рекомендуется включить MultipleActiveResultSets. Благодаря этому одновременно могут быть активны два запроса. Это очень удобно, например, при запросе сообщений, связанных с блогом, в цикле перечисления всех блогов.
Инициализаторы базы данных По умолчанию Code First будет создавать базу данных автоматически, если целевой базы данных нет. Для некоторых такая функциональность будет предпочтительной даже при развертывании — производственная база данных будет создаваться просто при первом запуске приложения. Если в вашей производственной среде имеется DBA, то гораздо вероятнее, что именно DBA будет создавать за вас производственную базу данных, и, как только ваше приложение будет развернуто, вы получите сообщение об ошибке. В этой статье я также переопределял логику инициализатора по умолчанию и при каждом изменении схемы удалял и заново создавал базу данных. Это явно не то, что вы предпочтете оставить при развертывании приложения в производственной среде.
Рекомендуемый подход к изменению или отключению логики инициализатора при развертывании — использование файла App.config (или Web.config для веб-приложений). В раздел appSettings добавьте запись с ключом DatabaseInitializerForType, а за ним имя типа контекста и сборку, в которой он определен. Значением может быть либо «Disabled», либо имя типа инициализатора с именем сборки, где он определен.
TВ следующем примере отключается любая логика инициализатора для контекста:
<appSettings>
<add
key="DatabaseInitializerForType Blogging.BlogContext,
Blogging"
value="Disabled" />
</appSettings>
А здесь инициализатору возвращается исходная логика, в соответствии с которой он создает базу данных, только если она отсутствует:
<appSettings>
<add
key="DatabaseInitializerForType Blogging.BlogContext,
Blogging"
value="System.Data.Entity.CreateDatabaseIfNotExists
EntityFramework" />
</appSettings>
Учетные записи пользователей Если вы решили, что базу данных будет создавать ваше производственное приложение, то оно должно сначала запускаться под учетной записью, в которой есть разрешения на создание базы данных и модификацию схемы. Если эти разрешения так и останутся, резко возрастет потенциальная угроза безопасности. Настоятельно советую в дальнейшем выполнять приложение с минимальным набором разрешений, позволяющих только запрашивать и сохранять данные.
По умолчанию Code First будет создавать базу данных автоматически, если целевой базы данных нет. |
Где узнать больше
В этой статье я показал, как приступить к разработке с применением Code First и использовать новый DbContext API; обе эти возможности появились в ADO.NET Entity Framework 4.1. Вы увидели, как можно использовать текучий API для сопоставления с существующей базой данных или изменения схемы, генерируемой Code First. Затем мы рассмотрели API для отслеживания изменений и то, как применять его для запроса локальных экземпляров сущностей и дополнительной информации об этих экземплярах. Наконец, я высказал несколько соображений по развертыванию приложения, использующего Code First для доступа к данным.
Если вы хотите узнать больше о любых средствах, появившихся в Entity Framework 4.1, зайдите на сайт msdn.com/data/ef. Кроме того, на форуме Data Developer Center (bit.ly/166o1Z) вам всегда помогут в использовании Entity Framework 4.1.