Есть вопрос о IRepository и для чего он используется, на который есть, казалось бы, хороший ответ.
Моя проблема: как мне аккуратно разобраться с сущностями, которые связаны друг с другом, и не является ли IRepository просто слоем без реальной цели?
Допустим, у меня есть следующие бизнес-объекты:
public class Region {
public Guid InternalId {get; set;}
public string Name {get; set;}
public ICollection 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;}
}
Есть правила:
Так как же будет выглядеть мой RegionRepository?
public class RegionRepository : IRepository
{
// Linq To Sql, injected through constructor
private Func _l2sfactory;
public ICollection GetAll(){
using(var db = _l2sfactory()) {
return db.GetTable()
.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
Locations = dbRegion.Locations.Select(loc => MapLoc(loc)).ToList(),
// DefaultLocation is EntityRef
DefaultLocation = MapLoc(dbRegion.DefaultLocation)
}
}
private Location MapLoc(DbLocation dbLocation) {
// Where should this come from?
}
}
Итак, как вы видите, RegionRepository также должен получать местоположения. В моем примере я использую Linq To Sql EntitySet / EntiryRef, но теперь регион должен иметь дело с сопоставлением местоположений с бизнес-объектами (потому что у меня есть два набора объектов, бизнес-объекты и объекты L2S).
Должен ли я реорганизовать это до чего-то вроде:
public class RegionRepository : IRepository
{
private IRepository _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)
}
}
Теперь я аккуратно разделил свой уровень данных на атомарные репозитории, каждый из которых имеет дело только с одним типом. Я запускаю Профайлер и ... Ой, ВЫБЕРИТЕ N + 1. Потому что каждый регион вызывает службу определения местоположения. У нас всего дюжина регионов и около 40 местоположений, поэтому естественной оптимизацией является использование DataLoadOptions . Проблема в том, что RegionRepository не знает, использует ли LocationRepository тот же DataContext или нет. В конце концов, мы вводим сюда фабрики, поэтому LocationRepository может развиваться самостоятельно. И даже если это не так - я вызываю метод службы, который предоставляет бизнес-объекты, поэтому DataLoadOptions все равно нельзя использовать.
Ах, я кое-что упустил.Предполагается, что IRepository имеет такой метод:
public IQueryable Query()
Итак, теперь я бы использовал
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)
}
Это выглядит хорошо. Вначале. При второй проверке у меня есть отдельные бизнес-объекты и объекты L2S, поэтому я все еще не понимаю, как это позволяет избежать SELECT N + 1, поскольку Query не может просто вернуть GetTable
.
Кажется, проблема заключается в наличии двух разных наборов объектов. Но если я украсил Business Objects всеми атрибутами System.Data.LINQ ([Table], [Column] и т. Д.), Это нарушит абстракцию и лишит IRepository цели.Поскольку, возможно, я хочу также иметь возможность использовать какой-то другой ORM, в этот момент мне теперь придется украсить свои бизнес-объекты другими атрибутами (а также, если бизнес-объекты находятся в отдельной сборке .Business, потребителям этого теперь необходимо ссылаться на все ORM, чтобы атрибуты были разрешены - фу!).
Мне кажется, что IRepository должен быть IService, а приведенный выше класс должен выглядеть так:
public class RegionService : IRegionService {
private Func _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();
}
}
}
Это также решает проблему куриного яйца:
Сделать это без EntitySet практически невозможно, поэтому, если вы не пожертвуете целостностью данных в базе данных и не перенесете их в бизнес-логику, невозможно сохранить ответственность за Locations за пределами провайдера региона.
Я понимаю, что эта публикация может рассматриваться как не настоящий вопрос, субъективный и аргументированный, поэтому позвольте мне сформулировать объективные вопросы:
Я полагаю, что мой реальный вопрос в следующем: