2010-02-27 11 views
14

Sono in procinto di progettare la mia applicazione ASP.NET MVC e mi sono imbattuto in un paio di pensieri interessanti.Progettazione del livello di astrazione del database: utilizzo dell'IRepository nel modo giusto?

Molti esempi che ho visto descrivono e utilizzano il modello di repository (IRepository), quindi questo è il modo in cui l'ho fatto mentre stavo imparando MVC.

Ora so cosa sta facendo tutto, comincio a guardare il mio attuale design e mi chiedo se è il modo migliore per andare.

Attualmente ho una base IUserRepository, che definisce i metodi quali FindById(), SaveChanges(), ecc

Attualmente, ogni volta che voglio caricare/interrogare la tabella utente nel DB, faccio qualcosa lungo le linee del seguente:

private IUserRepository Repository; 

    public UserController() 
     : this(new UserRepository()) 
    { } 

    [RequiresAuthentication] 
    [AcceptVerbs(HttpVerbs.Get)] 
    public ActionResult Edit(string ReturnUrl, string FirstRun) 
    { 
     var user = Repository.FindById(User.Identity.Name); 

     var viewModel = Mapper.Map<User, UserEditViewModel>(user); 
     viewModel.FirstRun = FirstRun == "1" ? true : false; 

     return View("Edit", viewModel); 
    } 

    [AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken(Salt = "SaltAndPepper")] 
    public ActionResult Edit(UserEditViewModel viewModel, string ReturnUrl) 
    { 
     //Map the ViewModel to the Model 
     var user = Repository.FindById(User.Identity.Name); 

     //Map changes to the user 
     Mapper.Map<UserEditViewModel, User>(viewModel, user); 

     //Save the DB changes 
     Repository.SaveChanges(); 

     if (!string.IsNullOrEmpty(ReturnUrl)) 
      return Redirect(ReturnUrl); 
     else 
      return RedirectToAction("Index", "User"); 
    } 

Ora io non capisco appieno come funziona MVC per quanto riguarda la creazione di un controller quando un utente crea un collegamento (non so se v'è 1 regolatore per utente o per applicazione di controllo 1), in modo da Non sono favorevole alla migliore linea d'azione.

Ho trovato una grande domanda riguardante l'uso di un'interfaccia repository generica IRepository<T>here e ho anche l'idea di una statica RepositoryFactory su un numero di blog. In pratica, solo 1 istanza del repository viene mantenuta sempre ed è ottenuta tramite questa factory

Quindi la mia domanda ruota attorno a come le persone lo fanno nelle app, e ciò è considerato una buona pratica.

Le persone dispongono di singoli repository basati su ogni tabella (IUserRepository)?
Usano un generico IRepository<T>?
Usano una fabbrica di archivi statici?
O qualcos'altro completamente?

EDIT: Ho appena realizzato probabilmente dovrei chiedere così:

sta avendo un privato IRepository su ogni controller di un buon modo per andare? o dovrei istanziare un nuovo IRepository ogni volta che voglio usarlo?

BOUNTY EDIT: Sto iniziando una taglia per avere più prospettive (non che Tim non sia stato utile).

Sono più curioso di sapere cosa fanno le persone nelle loro app MVC o cosa pensano sia una buona idea.

+1

Repository statici generici! SÌÌ! SingletonSquared, proprio quello di cui abbiamo bisogno! ;-) –

+0

@Sky, non si può mai avere abbastanza. Sto pensando personalmente a repository generici statici e parziali. –

risposta

24

Alcuni problemi molto evidenti con l'idea di un generico IRepository<T>:

  • Si presuppone che ogni entità utilizza lo stesso tipo di chiave, che non è vero in quasi tutti i sistemi non banale. Alcune entità utilizzeranno GUID, altri potrebbero avere una sorta di chiave naturale e/o composita. NHibernate può supportarlo abbastanza bene, ma Linq to SQL è piuttosto scadente: devi scrivere una buona quantità di codice hacker per eseguire la mappatura automatica dei tasti.

  • Significa che ogni repository può gestire solo un tipo di entità e supporta solo le operazioni più banali. Quando un repository è relegato a un wrapper CRUD così semplice, ha pochissimo uso. Si potrebbe anche solo dare al cliente uno IQueryable<T> o Table<T>.

  • Presuppone che si eseguano esattamente le stesse operazioni su ogni entità. In realtà questo sarà molto lontano dalla verità. Certo, forse vuoi ottenere quello Order dal suo ID, ma più probabilmente vuoi ottenere un elenco di oggetti Order per un cliente specifico e entro un certo intervallo di date. La nozione di un numero totale generico di IRepository<T> non consente il fatto che quasi certamente si desidera eseguire diversi tipi di query su diversi tipi di entità.

Il punto del modello repository è quello di creare un astrazione su modelli di accesso di dati comune. Penso che alcuni programmatori si annoiano con la creazione di repository quindi dicono "Ehi, lo so, creerò un repository über in grado di gestire qualsiasi tipo di entità!" Il che è grande, tranne per il fatto che il repository è praticamente inutile per l'80% di ciò che stai cercando di fare. Va bene come interfaccia di classe base, ma se è tutta la portata del lavoro che fai, sei solo pigro (e garantisci i mal di testa futuri).


Idealmente potrei iniziare con un repository generico che sembra qualcosa di simile:

public interface IRepository<TKey, TEntity> 
{ 
    TEntity Get(TKey id); 
    void Save(TEntity entity); 
} 

Noterete che questo non lo fa hanno una funzione List o GetAll - che è perché è assurdo pensare che sia accettabile recuperare i dati da un'intera tabella in un punto qualsiasi nel codice. Questo è quando hai bisogno di iniziare ad andare in repository specifici:

public interface IOrderRepository : IRepository<int, Order> 
{ 
    IEnumerable<Order> GetOrdersByCustomer(Guid customerID); 
    IPager<Order> GetOrdersByDate(DateTime fromDate, DateTime toDate); 
    IPager<Order> GetOrdersByProduct(int productID); 
} 

E così via - hai capito l'idea. In questo modo abbiamo il repository "generico" per se abbiamo davvero bisogno della semantica di recupero per ID incredibilmente semplicistica, ma in generale non lo faremo mai in realtà, certamente non per una classe controller.


Ora, come per i controllori, è necessario fare questo diritto, altrimenti hai praticamente negato tutto il lavoro che hai appena fatto a mettere insieme tutti i repository.

Un controller deve prelevare il proprio repository dal mondo esterno. Il motivo per cui hai creato questi repository è che puoi fare una sorta di Inversion of Control. Il tuo obiettivo finale qui è quello di essere in grado di scambiare un repository con un altro - ad esempio, per eseguire test di unità, o se decidi di passare da Linq a SQL a Entity Framework in futuro.

Un esempio di questo principio è:

public class OrderController : Controller 
{ 
    public OrderController(IOrderRepository orderRepository) 
    { 
     if (orderRepository == null) 
      throw new ArgumentNullException("orderRepository"); 
     this.OrderRepository = orderRepository; 
    } 

    public ActionResult List(DateTime fromDate, DateTime toDate) { ... } 
    // More actions 

    public IOrderRepository OrderRepository { get; set; } 
} 

In altre parole il controller ha idea di come creare un repository, né deve. Se ci sono delle costruzioni di repository in corso, sta creando un accoppiamento che davvero non vuoi. La ragione per cui i controller di esempio MV.NET ASP.NET hanno costruttori senza parametri che creano repository concreti è che i siti devono essere in grado di compilare ed eseguire senza forzare l'utente a configurare un intero framework Iniezione di dipendenza.

Ma in un sito di produzione, se non si passa la dipendenza del repository tramite un costruttore o una proprietà pubblica, si perde tempo con i repository, perché i controller sono ancora strettamente collegati al livello del database . È necessario essere in grado di scrivere codice di prova in questo modo:

[TestMethod] 
public void Can_add_order() 
{ 
    OrderController controller = new OrderController(); 
    FakeOrderRepository fakeRepository = new FakeOrderRepository(); 
    controller.OrderRepository = fakeRepository; //<-- Important! 
    controller.SubmitOrder(...); 
    Assert.That(fakeRepository.ContainsOrder(...)); 
} 

Non si può fare questo se il vostro OrderController sta andando fuori e la creazione di un proprio repository. Questo metodo di test non dovrebbe eseguire alcun accesso ai dati, ma si limita a fare in modo che il controller invochi il metodo di repository corretto in base all'azione.


Questo non è ancora DI, si badi, questo è solo finto/beffardo. Dove DI entra nell'immagine è quando decidi che Linq to SQL non sta facendo abbastanza per te e vuoi davvero l'HQL in NHibernate, ma ci vorranno 3 mesi per trasferire tutto e vuoi essere in grado di fai questo un repository alla volta. Così, ad esempio, utilizzando un quadro DI come Ninject, tutto ciò che dovete fare è cambiare questo:

Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>(); 
Bind<IOrderRepository>().To<LinqToSqlOrderRepository>(); 
Bind<IProductRepository>().To<LinqToSqlProductRepository>(); 

A:

Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>(); 
Bind<IOrderRepository>().To<NHibernateOrderRepository>(); 
Bind<IProductRepository>().To<NHibernateProductRepository>(); 

E ci sei tu, ora tutto ciò che dipende da IOrderRepository sta usando la versione di NHibernate, hai solo dovuto cambiare di una riga di codice anziché potenzialmente centinaia di linee. E stiamo eseguendo le versioni Linq to SQL e NHibernate fianco a fianco, trasferendo funzionalità su un pezzo per pezzo senza mai rompere nulla nel mezzo.


Quindi, per riassumere tutti i punti che ho fatto:

  1. Non fare affidamento unicamente su una generica interfaccia IRepository<T>. La maggior parte delle funzionalità desiderate da un repository è specifica, non generica. Se vuoi includere uno IRepository<T> ai livelli superiori della gerarchia di classe/interfaccia, va bene, ma i controllori dovrebbero dipendere dai repository specifici in modo da non dover cambiare il codice in 5 punti diversi quando lo trovi nel repository generico mancano metodi importanti.

  2. I controllori devono accettare i repository dall'esterno, non crearne di propri. Questo è un passo importante per eliminare l'accoppiamento e migliorare la testabilità.

  3. Normalmente si vorrà cablare i controllori usando un framework Iniezione di dipendenza, e molti di essi possono essere perfettamente integrati con ASP.NET MVC.Se questo è troppo per te da accettare, allora almeno dovresti usare un qualche tipo di fornitore di servizi statici in modo da poter centralizzare tutta la logica di creazione del repository. (A lungo termine, probabilmente troverai più semplice imparare e utilizzare un framework DI).

+0

questa è una risposta fantastica e veramente degna della generosità. Grazie per tutti i suggerimenti e i pensieri. –

+1

So che questa è una vecchia domanda, ma questa è un'ottima risposta. Ho appena risposto a tante domande sull'utilizzo dell'interfaccia IRepository standard. Grazie! –

+0

Ottima risposta indipendentemente da quanti anni ha. È un ottimo consiglio. – slimflem

3

Le persone dispongono di singoli repository basati su ciascuna tabella (IUserRepository)? Tendo ad avere un repository per ogni aggregato, non ogni tabella.

Usano un IRepository generico? se possibile, sì

Usano una fabbrica di archivi statici? Preferisco l'iniezione di un'istanza di repository tramite un contenitore di IOC

+3

Sono curioso di sapere cosa intendi per aggregato. –

+2

Penso che il riferimento all'aggregazione riguardi il modello di dominio. Ho letto di "aggregati" all'interno del contesto DDD (Domain Driven Design), ma suppongo che dovrebbe applicarsi a tutti i domini. Eric Evans definisce un aggregato come "un gruppo di oggetti che appartengono insieme (un gruppo di singoli oggetti che rappresenta un'unità)" – Ahmad

+0

è effettivamente ciò che intendevo dire –

1

Ecco come lo sto usando.Sto usando IRepository per tutte le operazioni che sono comuni a tutti i miei repository.

public interface IRepository<T> where T : PersistentObject 
{ 
    T GetById(object id); 
    T[] GetAll(); 
    void Save(T entity); 
} 

e io uso anche dedicato un ITRepository per ogni aggregate per il funzionamento che sono distinti a questo repository. Ad esempio per l'utente userò IUserRepository per aggiungere metodo che sono distinti a UserRepository:

public interface IUserRepository : IRepository<User> 
{ 
    User GetByUserName(string username); 
} 

L'implementazione sarà simile a questa:

public class UserRepository : RepositoryBase<User>, IUserRepository 
{ 
    public User GetByUserName(string username) 
    { 
     ISession session = GetSession(); 
     IQuery query = session.CreateQuery("from User u where u.Username = :username"); 
     query.SetString("username", username); 

     var matchingUser = query.UniqueResult<User>(); 

     return matchingUser; 
    } 
} 


public class RepositoryBase<T> : IRepository<T> where T : PersistentObject 
{ 
    public virtual T GetById(object id) 
    { 
     ISession session = GetSession(); 
     return session.Get<T>(id); 
    } 

    public virtual T[] GetAll() 
    { 
     ISession session = GetSession(); 
     ICriteria criteria = session.CreateCriteria(typeof (T)); 
     return criteria.List<T>().ToArray(); 
    } 

    protected ISession GetSession() 
    { 
     return new SessionBuilder().GetSession(); 
    } 

    public virtual void Save(T entity) 
    { 
     GetSession().SaveOrUpdate(entity); 
    } 
} 

Rispetto al UserController guarderà come:

public class UserController : ConventionController 
{ 
    private readonly IUserRepository _repository; 
    private readonly ISecurityContext _securityContext; 
    private readonly IUserSession _userSession; 

    public UserController(IUserRepository repository, ISecurityContext securityContext, IUserSession userSession) 
    { 
     _repository = repository; 
     _securityContext = securityContext; 
     _userSession = userSession; 
    } 
} 

Il repository viene istanziato utilizzando il modello di iniezione delle dipendenze utilizzando la factory del controller personalizzata. Sto usando StructureMap come livello di iniezione dipendente.

Il livello del database è NHibernate. ISession è il gateway per il database in questa sessione.

Ti suggerisco di dare un'occhiata alla struttura CodeCampServer, puoi imparare molto da esso.

Un altro progetto che è possibile conoscere è Who Can Help Me. Che non riesco ancora a scavare abbastanza.

+0

Che cos'è questa ISession? –

+0

Ho dimenticato di menzionare che usa NHibernate come livello di persistenza. L'interfaccia ISession ti fornisce ciò di cui hai bisogno per comunicare con il server per questa sessione. –

+0

Ahhh, ora ha senso :) –

0

Le persone dispongono di singoli repository basati su ogni tabella (IUserRepository)?

Sì, questa è stata la scelta migliore per 2 motivi:

  • mio DAL si basa su Linq to Sql (ma i miei DTOs sono interfacce basate sulle entità LTS)
  • le operazioni eseguite sono atomico (aggiunta è un'operazione atomica, il risparmio è un altro, ecc)

Usano un IRepository generico?

Sì, esclusivamente, ispirato schema/Valore DDD Entità ho creato IRepositoryEntity/IRepositoryValue e un IRepository generico per altre cose.

Usano una fabbrica di archivi statici?

Sì e no: io uso un contenitore CIO invocato tramite una classe statica. Beh ... possiamo dire che è una specie di fabbrica.

: Ho progettato questa architettura da sola e un mio collega l'ha trovato così bello che stiamo attualmente creando l'intera struttura aziendale su questo modello (sì, è una giovane azienda). Questo è sicuramente qualcosa che vale la pena provare, anche se ritengo che quadri di questo tipo saranno pubblicati dai principali attori.

0

È possibile trovare un ottimo Repopsitory Generico biblioteca che è stata scritta per consentirgli di essere utilizzato come asp WebForms: ObjectDataSource su CodePlex: MultiTierLinqToSql

Ognuno dei miei controllori hanno repository privati ​​per le azioni hanno bisogno di supporto.

Problemi correlati