Интересности      Книги      Утилиты    

14 октября 2011 г.

Пишем слой доступа к данным c POCO и EF

Не так давно обновился Entity Framework (EF) до версии 4.1, основное нововведение, которое появилось – это Code First, подход и возможность работать с POCO объектами. Но для людей, у которых уже была готовая и спроектированная база, все равно не получалось работать с Code First, кроме того многие, имя толковых экспертов по базам данных, выбирают традиционный путь data-driven development.

Список изменений в версии 4.1 можно найти в статье - What's New (Entity Framework 4.1), занимательно, что существует также статья - What's Not Supported (Entity Framework 4.1)

Итак, вполне такая себе практическая задача – быстро создать слой доступа к данным (data access layer), который будет оперировать с POCO объектами, с использованием Entity Framework, отталкиваясь от уже существующей базы данных.

Многие наверное подумают, что есть смысл выбрать nHibernate или еще какие другие архитектурные решения. Но это тема других холиворов типа EF vs nHibernate, или, хорошо или плохо принимать архитектурные решения в стиле “потому-что Майкрософт”, или “потому-что не-Майкрософт”, или “потому что гладиолус”. Тут же имеем вполне практическую задачу и имеем EF.

Итак, вариант 1 - Entity Framework Power Tools

Недавно также обновилось расширение к Visual Studio 2010 – Entity Framework Power Tools. Оно еще в CTP1, но свое дело делает отлично, по крайней мере у меня нареканий не было. Расширение добавляет контекстное меню к проекту.

image

Конектимся к базе:

image

Сказать, что у вас может не возникнуть проблем с EF Power tools, – не могу. Но для того, чтобы POCO сгенерились по базе, достаточно, чтобы к проекту был подключен EF, а если точнее, сборка EntityFramework.dll версии 4.1, и аккаунт, который вы указываете на подключение к базе обладает всеми необходимыми правами, чтобы сделать свое черное дело. Как вариант EF 4.1 можно установить используя NuGet - Install-Package EntityFramework.

При соединении необходимо указывать аккаунт, который имеет доступ к БД master. Не спрашивайте почему – это требование, иначе получите ошибку в окно Output, которая отправит вас создавать такой аккаунт.

Reverse engineering нагенерит нам POCO сущностей по базе и служебные классы маппингов.

POCO классы вида:

    
public class Document
{
public Document()
{
this.DocumentAttributes = new List<documentattribute>();
this.Categories = new List<category>();
this.Tags = new List<tag>();
}

public long DocumentID { get; set; }

public int UserID { get; set; }

public Guid GUID { get; set; }

public string Name { get; set; }

public string Hash { get; set; }

public Nullable<long> Size { get; set; }

public int DocumentTypeID { get; set; }

public Nullable<long> ImageID { get; set; }

public Nullable<guid> ConvertedGUID { get; set; }

public Nullable<guid> SnapshotGUID { get; set; }

public Nullable<datetime> CreatedDate { get; set; }

public Nullable<datetime> ModifiedDate { get; set; }

public bool Processed { get; set; }

public virtual ICollection<documentattribute> DocumentAttributes { get; set; }

public virtual DocumentType DocumentType { get; set; }

public virtual Image Image { get; set; }

public virtual ICollection<category> Categories { get; set; }

public virtual ICollection<tag> Tags { get; set; }
}

Как видим в них ничего лишнего, разве что наличие Nullable или виртуальных свойств может выдать такой класс. Связанные таблицы реализуются как виртуальные свойства типа ICollection. Тем не менее это чистые объекты без лишних зависимостй

Маппинги вида:

    public class DocumentMap : EntityTypeConfiguration<Document>
{
public DocumentMap()
{
// Primary Key
this.HasKey(t => t.DocumentID);

// Properties
this.Property(t => t.Name)
.HasMaxLength(256);

this.Property(t => t.Hash)
.HasMaxLength(30);

// Table & Column Mappings
this.ToTable("Documents");
this.Property(t => t.DocumentID).HasColumnName("DocumentID");
this.Property(t => t.UserID).HasColumnName("UserID");
this.Property(t => t.GUID).HasColumnName("GUID");
this.Property(t => t.Name).HasColumnName("Name");
this.Property(t => t.Hash).HasColumnName("Hash");
this.Property(t => t.Size).HasColumnName("Size");
this.Property(t => t.DocumentTypeID).HasColumnName("DocumentTypeID");
this.Property(t => t.ImageID).HasColumnName("ImageID");
this.Property(t => t.ConvertedGUID).HasColumnName("ConvertedGUID");
this.Property(t => t.SnapshotGUID).HasColumnName("SnapshotGUID");
this.Property(t => t.CreatedDate).HasColumnName("CreatedDate");
this.Property(t => t.ModifiedDate).HasColumnName("ModifiedDate");
this.Property(t => t.Processed).HasColumnName("Processed");

// Relationships
this.HasMany(t => t.Tags)
.WithMany(t => t.Documents)
.Map(m =>
{
m.ToTable("Metadata.Document_Tag");
m.MapLeftKey("DocumentID");
m.MapRightKey("TagID");
});

this.HasRequired(t => t.DocumentType)
.WithMany(t => t.Documents)
.HasForeignKey(d => d.DocumentTypeID);

this.HasOptional(t => t.Image)
.WithMany(t => t.Documents)
.HasForeignKey(d => d.ImageID);

}
}

Маппинги придется немного подредактировать. Если у вас указывается схема БД в названии таблиц, то необходимо в классах маппингов добавить эту схему в “магических строках” вида:

this.ToTable("Documents");

На строки вида:

this.ToTable("Metadata.Documents");

В принципе с эти можно жить имея класс контекст. Но нам этого мало. Хочется иметь для этого нормальный Data access layer. А для гурманов хочется такой слой с использованием паттернов Repositorу, UnitOfWork. Чтобы не делать много рутиной работы будем использовать T4 Template.

Этот Т4 возьмем здесь - https://github.com/danemorgridge/efrepo, естественно с благодарностью автору. Нас интересует именно IRepository.tt. Он должен находится рядом с edmx файлом.

Запустим его. Получи базовые классы и реализации показанные ниже:

Интерфейс IRepository

    public interface IRepository<T>
{
IUnitOfWork UnitOfWork { get; set; }
IQueryable<T> All();
IQueryable<T> Where(Expression<Func<T, bool>> expression);
void Add(T entity);
void Delete(T entity);
}

Интерфейс IUnitOfWork

    public interface IUnitOfWork
{
DbContext Context { get; set; }
void Commit();
bool LazyLoadingEnabled { get; set; }
bool ProxyCreationEnabled { get; set; }
string ConnectionString { get; set; }
}

Базовая конкретная реализация интерфейса IRepository от этой реализации можно наследовать свои более конкретные репозитории:

    public class EFRepository<T> : IRepository<T> where T : class
{
public IUnitOfWork UnitOfWork { get; set; }

private IDbSet<T> _objectset;
private IDbSet<T> ObjectSet
{
get
{
if (_objectset == null)
{
_objectset = UnitOfWork.Context.Set<T>();
}
return _objectset;
}
}

public virtual IQueryable<T> All()
{
return ObjectSet.AsQueryable();
}

public IQueryable<T> Where(Expression<Func<T, bool>> expression)
{
return ObjectSet.Where(expression);
}

public void Add(T entity)
{
ObjectSet.Add(entity);
}

public void Delete(T entity)
{
ObjectSet.Remove(entity);
}
}

Конкретная реализация интерфейса IUnitOfWork:

    public class EFUnitOfWork : IUnitOfWork
{
public DbContext Context { get; set; }

public EFUnitOfWork(DbContext context)
{
this.Context = context;
}

public void Commit()
{
Context.SaveChanges();
}

public bool LazyLoadingEnabled
{
get { return Context.Configuration.LazyLoadingEnabled; }
set { Context.Configuration.LazyLoadingEnabled = value; }
}

public bool ProxyCreationEnabled
{
get { return Context.Configuration.ProxyCreationEnabled; }
set { Context.Configuration.ProxyCreationEnabled = value; }
}

public string ConnectionString
{
get { return Context.Database.Connection.ConnectionString; }
set { Context.Database.Connection.ConnectionString = value; }
}
}

Далее код показывать не буду так как в принципе по интерфейсам и базовым классам можно понять о чем речь. Остальной пример уровня доступа к данным в приложении по ссылке в конце.

Покажу еще код использования такого слоя доступа к данным:

IUnitOfWork uow = DocumentRepositoryHelper.GetUnitOfWork();
IRepository<Document> repo = DocumentRepositoryHelper.GetDocumentRepository(uow);
var all = repo.All().ToList();

Вариант 2 - ADO.NET C# POCO Entity Generator

Есть и другой вариант если не гнаться за репозиториями и unit of work в своем коде. Тоже генерирует сущности. Вот только способ доступа к БД с использованием этих сущностей через класс контекст DbContext напрямую, хотя аналогичным образом можно все заврапить в репозитории. По большому счету что Entity Framework Power Tools, что ADO.NET C# POCO Entity Generator – дают нам объекты-сущности и маппинги.

А дальше можем к примеру заиспользовать Т4 скрипт из первого способа.

ADO.NET C# POCO Entity Generator находится в галереи расширений.

Также есть набор достаточно полезных ссылок, в том числе с использованием ADO.NET C# POCO Entity Generator.

Entity Framework 4.0 Resources – documentation links, best blog posts and more

Там серди них есть ссылки на то как генерить сущности с помощью ADO.NET C# POCO Entity Generator или еще один более упрощенный вариант:

Walkthrough: POCO Template for the Entity Framework
EF 4.1 Model & Database First Walkthrough

Хотя лично для меня первый вариант, который я описал предпочтительнее


Как использовать POCO объекты с репозиторием или без, это уже дело техники или Т4 скрипта. Создание достойного слоя доступа к данным в Entity Framework теперь сводится к его генерации, как впрочем и в nHibernate

Комментариев нет:

Отправить комментарий