2011-11-12 11 views
8

C'è un question about IRepository e a cosa serve, che ha una risposta apparentemente buona.Come funziona il modello di repository se le entità sono correlate tra loro?

Il mio problema però: come gestirò in modo pulito entità collegate tra loro, e non è IRepository, ma solo un livello senza uno scopo reale?

Diciamo che ho questi oggetti business:

public class Region { 
    public Guid InternalId {get; set;} 
    public string Name {get; set;} 
    public ICollection<Location> Locations {get; set;} 
    public Location DefaultLocation {get; set;} 
} 

public class Location { 
    public Guid InternalId {get; set;} 
    public string Name {get; set;} 
    public Guid RegionId {get; set;} 
} 

ci sono delle regole:

  • Ogni Regione deve avere almeno una sede
  • Regioni appena creati vengono creati con una posizione
  • No SELEZIONA N + 1 per favore

Quindi, come sarebbe il mio RegionRepository?

public class RegionRepository : IRepository<Region> 
{ 
    // Linq To Sql, injected through constructor 
    private Func<DataContext> _l2sfactory; 

    public ICollection<Region> GetAll(){ 
     using(var db = _l2sfactory()) { 
      return db.GetTable<DbRegion>() 
         .Select(dbr => MapDbObject(dbr)) 
         .ToList(); 
     } 
    } 

    private Region MapDbObject(DbRegion dbRegion) { 
     if(dbRegion == null) return null; 

     return new Region { 
      InternalId = dbRegion.ID, 
      Name = dbRegion.Name, 
      // Locations is EntitySet<DbLocation> 
      Locations = dbRegion.Locations.Select(loc => MapLoc(loc)).ToList(), 
      // DefaultLocation is EntityRef<DbLocation> 
      DefaultLocation = MapLoc(dbRegion.DefaultLocation) 
     } 
    } 

    private Location MapLoc(DbLocation dbLocation) { 
     // Where should this come from? 
    } 
} 

Quindi, come vedete, un Repository Region deve recuperare anche le posizioni. Nel mio esempio, utilizzo Linq To Sql EntitySet/EntiryRef, ma ora Region deve occuparsi della mappatura di Locations to Business Objects (perché ho due gruppi di oggetti, business e oggetti L2S).

Devo refactoring questo a qualcosa di simile:

public class RegionRepository : IRepository<Region> 
{ 
    private IRepository<Location> _locationRepo; 

    // snip 

    private Region MapDbObject(DbRegion dbRegion) { 
     if(dbRegion == null) return null; 

     return new Region { 
      InternalId = dbRegion.ID, 
      Name = dbRegion.Name, 
      // Now, LocationRepo needs to concern itself with Regions... 
      Locations = _locationRepo.GetAllForRegion(dbRegion.ID), 
      // DefaultLocation is a uniqueidentifier 
      DefaultLocation = _locationRepo.Get(dbRegion.DefaultLocationId) 
     } 
    } 

ora ho ben separato il mio livello di dati in repository atomiche, solo che fare con un tipo di ciascuno. Accendo il Profiler e ... Whoops, SELECT N + 1. Perché ogni Regione chiama il servizio di localizzazione. Abbiamo solo una dozzina di regioni e una quarantina di luoghi, quindi l'ottimizzazione naturale è l'utilizzo di DataLoadOptions. Il problema è che RegionRepository non sa se LocationRepository sta utilizzando lo stesso DataContext o meno. Dopo tutto, stiamo iniettando le fabbriche, quindi LocationRepository potrebbe girare per conto proprio. E anche se così non fosse - sto chiamando un metodo di servizio che fornisce oggetti business, quindi le Opzioni DataLoad potrebbero non essere comunque utilizzate.

Ah, ho trascurato qualcosa. IRepository si suppone di avere un metodo come questo:

public IQueryable<T> Query() 

Così ora vorrei fare

  return new Region { 
      InternalId = dbRegion.ID, 
      Name = dbRegion.Name, 
      // Now, LocationRepo needs to concern itself with Regions... 
      Locations = _locationRepo.Query() 
         .Select(loc => loc.RegionId == dbRegion.ID) 
         .ToList(), 
      // DefaultLocation is a uniqueidentifier 
      DefaultLocation = _locationRepo.Get(dbRegion.DefaultLocationId) 
     } 

che sembra buono. All'inizio. Alla seconda ispezione, ho oggetti business separati e L2S, quindi continuo a non vedere come questo eviti SELECT N + 1 poiché Query non può semplicemente restituire GetTable<DbLocation>.

Il problema sembra avere due diversi gruppi di oggetti. Ma se decorassi Business Objects con tutti gli attributi System.Data.LINQ ([Table], [Column] ecc.), Ciò interrompe l'astrazione e vanifica lo scopo di IRepository. Perché forse voglio anche essere in grado di usare un altro ORM, a quel punto ora dovrei decorare le mie Entità Aziendali con altri attributi (anche, se le entità di business sono in un assembly separato .Business, i consumatori ne hanno ora bisogno fare riferimento a tutti gli ORM anche per gli attributi da risolvere - yuck!).

A me, sembra che IRepository dovrebbe essere IService, e la classe di cui sopra dovrebbe essere simile a questo:

public class RegionService : IRegionService { 
     private Func<DataContext> _l2sfactory; 

     public void Create(Region newRegion) { 
     // Responsibility 1: Business Validation 
     // This could of course move into the Region class as 
     // a bool IsValid(), but that doesn't change the fact that 
     // the service concerns itself with validation 
     if(newRegion.Locations == null || newRegion.Locations.Count == 0){ 
      throw new Exception("..."); 
     } 

     if(newRegion.DefaultLocation == null){ 
      newRegion.DefaultLocation = newRegion.Locations.First(); 
     } 

     // Responsibility 2: Data Insertion, incl. Foreign Keys 
     using(var db = _l2sfactory()){ 
      var dbRegion = new DbRegion { 
       ... 
      } 

      // Use EntitySet to insert Locations as well 
      foreach(var location in newRegion.Locations){ 
       var dbLocation = new DbLocation { 

       } 
       dbRegion.Locations.Add(dbLocation); 
      } 

      // Insert Region AND all Locations 
      db.InsertOnSubmit(dbRegion); 
      db.SubmitChanges(); 
     } 
     } 
} 

Questo risolve anche un problema di pollo-uovo:

  • DbRegion.ID è generato dal database (come newid()) e IsDbGenerated = true è impostato
  • DbRegion.DefaultLocationId è un non annullabile GUID
  • DbRegion.DefaultLocationId è un F K in Location.ID
  • DbLocation.RegionId è un GUID non annullabile e FK in Region.ID

Fare questo senza EntitySet è praticamente impossibile, quindi a meno che non sacrificare l'integrità dei dati sul database e mossa è nella logica del business, è impossibile mantenere la responsabilità delle posizioni fuori dal provider della regione.

vedo come questo intervento può essere visto come non è una questione reale, soggettivo e polemico, quindi per favore mi permetta di formulare una domanda oggettivi:

  • Che cosa è esattamente il Pattern Repository dovrebbe astrarre?
  • Nel mondo reale, in che modo le persone ottimizzano il livello del database senza interrompere l'astrazione che il modello di repository dovrebbe raggiungere?
  • In particolare, in che modo il mondo reale si occupa di SELECT N + 1 e dei problemi di integrità dei dati?

Credo che la mia vera domanda è questa:

  • Quando già utilizzando un ORM (come LINQ to SQL), non è DataContext già mio repository, e quindi un repository in cima DataContext è solo l'astrazione della stessa identica cosa di nuovo?

risposta

1

Ho diverse considerazioni su tutto questo: 1. Il pattern AFAIK Repository è stato inventato un po 'prima dell'ORM. Tornando ai giorni su semplici query SQL, è stata una buona idea implementare il repository e acquistare questo codice astratto dal database effettivo utilizzato. 2. Potrei dire che il repository non è completamente necessario ora, ma sfortunatamente, dalla mia esperienza, non posso dire che qualsiasi ORM possa veramente astrarti da tutti i dettagli del database. Per esempio. Non ho potuto creare una mappatura ORM e usarla con qualsiasi altro server DB, che ORM sostiene di supportare (in particolare sto parlando di Microsoft EF). Quindi, se si vuole davvero poter utilizzare diversi server di database, è probabile che sia ancora necessario utilizzare il repository. 3. Un'altra preoccupazione è molto semplice: duplicazione del codice. Di sicuro, ci sono alcune domande che chiami spesso il tuo codice. Se lasci solo ORM come repository, allora dovrai duplicare tali query, quindi sarà meglio avere un certo livello di astrazione sul contenitore ORM, che mantenga quelle query usate comunemente.

4

Quando si progettano i repository, è necessario pensare a ciò che è noto come radice aggregata. In sostanza, ciò significa che se un'entità può esistere da sola all'interno del dominio, sarà più che probabile che abbia il proprio repository. Nel tuo caso questa sarebbe la Regione.

Considerare il classico scenario cliente/ordine.L'archivio clienti fornirebbe l'accesso agli ordini poiché un ordine non può esistere senza un cliente e pertanto, a meno che non si disponga di un business case valido, è improbabile che sia necessario un repository ordini separato.

In una semplice applicazione la tua ipotesi potrebbe essere corretta ma ricorda che, a meno che non fornisci un'astrazione del contesto L2S, farai fatica a eseguire test unitari efficaci. Coding contro un'interfaccia, che sia un IServiceX, IRepositoryX o qualunque altro ti dia quel livello di separazione.

La decisione sul fatto che le interfacce di servizio entrino nel progetto è generalmente ricondotta alla complessità della logica aziendale e alla necessità di un'API estensibile in quella logica che potrebbe essere consumata da diversi client disparati.

+1

Il problema che vedo è con l'esempio di ordine è un effetto a catena: un ordine ha OrderItem, che non può esistere senza un ordine. Tuttavia, potrei avere una funzione per Inventory Tracking che tiene traccia di quante unità sono state vendute. Questa è una semplice SUM (quantità) FROM OrderItems GROUP BY ItemId' chiamata SQL, ma ora il mio inventario deve andare all'archivio clienti per ottenere tutti gli ordini per ottenere tutti gli ordini. A meno che CustomerRepoistory non fornisca un IDictionary GetItemSalesQuantities' che sembra fuori luogo in un CustomerRepository. –

+1

Non considererei irragionevole vedere un InventoryRepository in questa istanza. Non esiste una relazione diretta tra cliente e inventario e quindi non vi è alcun requisito per un'operazione basata su set tra i due qui che è ciò che causa il dolore e selectn + 1. –

+0

E ora stiamo ottenendo una nuova regola aziendale: voglio i conteggi, ma solo per i clienti VIP in Sud America, e solo per gli ordini effettuati negli ultimi tre mesi. (È successo qualcosa del genere :(). Questo è ancora il lavoro di InventoryRepository, ma ora ha bisogno di accedere a più di una tabella di database. A quel punto direi che non è più un semplice repository ma una classe di servizio business? –

Problemi correlati