Hoje venho demonstrar como podemos implementar o design pattern Repository e Unit Of Work. Esses são alguns dos padrões mais comuns utilizados na construção de um software. A implementação desses padrões ajudam a isolar a camada de negócio da camada de armazenamento de dados e facilita de forma significativa a implementações de Unit Tests ou – Test Driven Development (TDD) .
O Design Pattern Repository e o Design Pattern Unit Of Work
O padrão Repository é responsável por intermediação entre as camadas de domínio e mapeamento de dados, agindo como uma coleção objeto de domínio abstraindo a implementação de acesso a banco de dados.
Conceitualmente a classe que implementa o padrão Repository encapsula o conjunto de objetos e as operações executadas sobre eles , proporcionando uma visão mais orientada a objeto da camada de acesso a dados.
O padrão Unit of Work controla o que fazemos durante uma transação geralmente executada sobre uma regra de negócio e coordenando essas alterações para a camada de acesso a dados.
Agora que sabemos os que esses padrões representam a pergunta é como implementar? Vamos demonstrar esses padrões utilizando o Entity Framework.
Para esse tutorial, estarei utilizando o Entity Framework 6 Code First
Criando um Contexto Genérico Base
Para demonstrar esses padrões, vamos criar um contexto e o que seria o contexto ? O Contexto dentro do Entity Framework representa a abstração de escrita e leitura em um banco de dados. O Entity Framework utiliza o DbContext para fazer o “meio de campo” entre seus objetos mapeados e o banco de dados em si.
A principal ideia aqui é criar um contexto que seja fácil de ser utilizado em qualquer lugar e em qualquer projeto. Assim poderemos utilizar ao máximo do poder da orientação a objeto. Vamos criar uma classe chamada BaseContext.cs e adicione o código abaixo:
public class BaseContext<T> : DbContext where T : class { public DbSet<T> DbSet { get; set; } public BaseContext(): base("EFConnectionString") { //Caso a base de dados não tenha sido criada, //ao iniciar a aplicação iremos criar Database.SetInitializer<BaseContext<T>>(null); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { //Neste momento não iremos fazer nada, //iremos voltar mais para frente para criar nosso mapeamos dinamicos base.OnModelCreating(modelBuilder); } public virtual void ChangeObjectState(object model, EntityState state) { //Aqui trocamos o estado do objeto, //facilita quando temos alterações e exclusões ((IObjectContextAdapter)this) .ObjectContext .ObjectStateManager .ChangeObjectState(model, state); } }
Agora estamos com nossa classe base de contexto criada, devemos criar nossos objetos de domínio.
Criando os Objetos de Domínio
Neste tutorial irei criar somente dois objetos de domínio Usuário e Receita. Vamos ao código.
public class User { public Guid Id { get; set; } public String Nome { get; set; } public String Email { get; set; } public DateTime DtNascimento { get; set; } public virtual IList<Recipe> Recipes { get; set; } }
public class Recipe { public Guid Id { get; set; } public String Title { get; set; } public String Ingredients { get; set; } public String Steps { get; set; } public Guid UserId { get; set; } }
Mapeando os objetos com o EntityTypeConfiguration
Com nosso objetos de domínio criados devemos agora criar nossos mapeamentos. Os mapeamentos servem para dizer ao Entity Framework como que ele deve gravar e ler os dados no banco de dados. Tenho uma dica muito importante eu sempre crio uma Interface chamada IMapping, essa interface servirá para fazer nosso mapeamento dinâmicos que iremos criar mais para a frente.
Vamos criar nossos mapeamentos e a interface, conforme código abaixo:
public interface IMapping { }
public class UserMapping : EntityTypeConfiguration<User>, IMapping { public UserMapping() { this.ToTable("User"); this.HasKey(x => x.Id); this.Property(x => x.Id); this.Property(x => x.Nome).IsRequired().HasMaxLength(255); this.Property(x => x.Email).IsRequired().HasMaxLength(255); this.Property(x => x.DtNascimento).IsRequired(); this.HasMany(x => x.Recipes).WithOptional().HasForeignKey(x => x.UserId); } }
public class RecipeMapping : EntityTypeConfiguration<Recipe>, IMapping { public RecipeMapping() { this.ToTable("Recipe"); this.HasKey(x => x.Id); this.Property(x => x.Id); this.Property(x => x.Ingredients).HasMaxLength(1024); this.Property(x => x.Steps).HasMaxLength(1024); this.Property(x => x.Title).IsRequired().HasMaxLength(150); } }
Carregando os Mapeamentos Dinamicamente
Com nossos mapeamentos criados, devemos agora carregá-los no Entity Framework. Esses mapeamentos serão carregados no evento do contexto chamando OnModelCreating e qual o grande problema? Devemos carregá-los um a um conforme exemplo abaixo.
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new UserMapping()); modelBuilder.Configurations.Add(new RecipeMapping()); base.OnModelCreating(modelBuilder); }
O evento acima está implementado na classe BaseContext.cs que acabamos de criar, porém isso não é muito inteligente concorda ? Se nosso banco tiver 30 tabelas vamos fazer isso na mão ? Se fosse 50 tabelas ?
Para resolver essa situação que iremos criar o mapeamento dinâmico, a principal ideia é caso seja criado um novo mapeamento não será necessário alterar o evento OnModelCreating, ele será capaz de identificar um novo modelo e carregá-lo dinamicamente. Parece complicado não acham porém e bem simples.
protected override void OnModelCreating(DbModelBuilder modelBuilder) { //Fazendo o mapeamento com o banco de dados //Pega todas as classes que estão implementando a interface IMapping //Assim o Entity Framework é capaz de carregar os mapeamentos var typesToMapping = (from x in Assembly.GetExecutingAssembly().GetTypes() where x.IsClass && typeof(IMapping).IsAssignableFrom(x) select x).ToList(); // Varrendo todos os tipos que são mapeamento // Com ajuda do Reflection criamos as instancias // e adicionamos no Entity Framework foreach (var mapping in typesToMapping) { dynamic mappingClass = Activator.CreateInstance(mapping); modelBuilder.Configurations.Add(mappingClass); } }
Agora com a implementação acima nosso Repositório é capaz de identificar um novo mapeamento e carregá-lo em tempo de execução.
Nosso contexto base está quase pronto, devemos implementar as operações de banco ou seja implementar o Unit Of Work Pattern
Implementando o Unit of Work Pattern
Com nosso contexto base quase pronto devemos adicionar as operações de manipulação de objeto. Vamos criar uma interface chamada IUnitOfWork essa interface será a responsável pelas as operações de banco de dados.
public interface IUnitOfWork<T> where T : class { int Save(T model); int Update(T model); void Delete(T model); IEnumerable<T> GetAll(); T GetById(object id); IEnumerable<T> Where(Expression<Func<T, bool>> expression); IEnumerable<T> OrderBy(Expression<System.Func<T, bool>> expression); }
Nossa interface tem todas as operações para manipular um objeto e consultar um objeto a partir de uma fonte de dados. Agora devemos implementar essa interface, e claro, essa interface será implementada pela nossa classe base de contexto e graças ao poder da orientação a objetos todos os repositórios que herdarem da base terão essas operações.
Adicione o código abaixo na BaseContext.cs:
public virtual int Save(T model) { this.DbSet.Add(model); return this.SaveChanges(); } public virtual int Update(T model) { var entry = this.Entry(model); if (entry.State == EntityState.Detached) this.DbSet.Attach(model); this.ChangeObjectState(model, EntityState.Modified); return this.SaveChanges(); } public virtual void Delete(T model) { var entry = this.Entry(model); if (entry.State == EntityState.Detached) this.DbSet.Attach(model); this.ChangeObjectState(model, EntityState.Deleted); this.SaveChanges(); } public virtual IEnumerable<T> GetAll() { return this.DbSet.ToList(); } public virtual T GetById(object id) { return this.DbSet.Find(id); } public virtual IEnumerable<T> Where(Expression<Func<T, bool>> expression) { return this.DbSet.Where(expression); } public IEnumerable<T> OrderBy(Expression<Func<T, bool>> expression) { return this.DbSet.OrderBy(expression); }
Nosso contexto base está implementado e para finalizar devemos criar nosso Repositório de Usuário e o Repositório de Receitas. Veja como fica a implementação deles utilizando os padrões.
public class UserRepository : BaseContext<User>, IUnitOfWork<User> { }
public class RecipeRepository : BaseContext<Recipe>, IUnitOfWork<Recipe> { }
Ficou muito simples não é mesmo ? Com o poder da orientação temos todos os recursos para criar um acesso ao banco padronizado.
Um exemplo de como poderíamos usar nosso repositório em um Controller MVC
public class UserController : Controller { IUnitOfWork<User> UnitOfWorkUser { get; set; } public UserController() { this.UnitOfWorkUser = new UserRepository(); } // GET: User public ActionResult Index() { return View(); } // GET: User/Details/5 public ActionResult Details(Guid id) { var model = this.UnitOfWorkUser.GetById(id); return View(model); } // GET: User/Create public ActionResult Create() { return View(); } // POST: User/Create [HttpPost] public ActionResult Create(User model) { try { this.UnitOfWorkUser.Save(model); return RedirectToAction("Index"); } catch { return View(); } } // GET: User/Edit/5 public ActionResult Edit(Guid id) { var model = this.UnitOfWorkUser.GetById(id); return View(); } // POST: User/Edit/5 [HttpPost] public ActionResult Edit(Guid id, User model) { try { model.Id = id; this.UnitOfWorkUser.Update(model); return RedirectToAction("Index"); } catch { return View(); } } // POST: User/Delete/5 [HttpPost] public ActionResult Delete(Guid id) { try { // TODO: Add delete logic here var model = this.UnitOfWorkUser.Where(x => x.Id == id).FirstOrDefault(); this.UnitOfWorkUser.Delete(model); return RedirectToAction("Index"); } catch { return View(); } } }
Chegamos ao final desse post, espero que ele possa ajudar a vocês a criarem um padrão de acesso a dados. Com a implementação acima não estamos limitados ao Entity Framework e podemos substituir facilmente por outro ORM como NHibernate e utilizar os mesmo conceitos.
O código está disponivel no GitHub através desse link
Abs e até a próxima.
Este modelo de desenvolvimento torna lento demais o acesso a dados. Quando temos de tratar grande volume de dados se torna impraticável sua utilização.
Oi ivan,
Isso e verdade, quando temos que tratar um grande volume de dados o entity framework fica muito lento, não so ele como outros OR/M como NHibernate.
Agora um mini OR/M como Dapper não fica lento e consegue tratar um volume de dados com alta performance. Essa arquitetura dá para ser usada também com o Modelo ADO.NET sem problemas. Bastando abstrair o conceito de OR/M e utilizando pura e simplesmente o ADO.NET
Abs e obrigado pela visita