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?
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. –
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. –
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? –