Существует вопрос об IRepository и для чего он используется, что, по-видимому, имеет хороший ответ.
Хотя моя проблема: как бы я справился с сущностями, которые связаны друг с другом, и не является ли IRepository просто слоем без реальной цели?
Допустим, у меня есть следующие бизнес-объекты:
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;}
}
Есть правила:
- Каждый регион ДОЛЖЕН иметь хотя бы одно местоположение
- Вновь созданные Регионы создаются с местоположением
- НЕ ВЫБРАТЬ N + 1, пожалуйста
Так как будет выглядеть мой 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?
}
}
Итак, как вы видите, RegionRepository должен также выбирать местоположения. В моем примере я использую Linq To Sql EntitySet / EntiryRef, но теперь Region необходимо разобраться с отображением Locations на Business Objects (поскольку у меня есть два набора объектов: бизнес и объекты L2S).
Должен ли я изменить это на что-то вроде:
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)
}
}
Теперь я красиво разделил свой слой данных на атомарные репозитории, имея дело только с одним типом каждый. Я запускаю Профилировщик и ... Ой, ВЫБЕРИТЕ N + 1. Потому что каждый регион вызывает сервис определения местоположения. У нас всего дюжина регионов и около 40 мест расположения, поэтому естественная оптимизация заключается в использовании DataLoadOptions . Проблема в том, что RegionRepository не знает, использует ли LocationRepository тот же DataContext или нет. В конце концов, мы вводим сюда фабрики, поэтому LocationRepository может раскрутить свою собственную. И даже если это не так - я вызываю метод службы, который предоставляет бизнес-объекты, поэтому DataLoadOptions в любом случае нельзя использовать.
Ах, я что-то упустил. Предполагается, что IRepository имеет такой метод:
public IQueryable<T> 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)
}
Это выглядит хорошо. Вначале. Во второй проверке у меня есть отдельные объекты business и L2S, поэтому я до сих пор не понимаю, как это позволяет избежать SELECT N + 1, поскольку Query не может просто вернуть GetTable<DbLocation>
.
Кажется, проблема в двух разных наборах объектов. Но если я декорирую бизнес-объекты всеми атрибутами System.Data.LINQ ([Таблица], [Столбец] и т. Д.), Это нарушает абстракцию и отрицает назначение IRepository. Потому что, возможно, я хочу также иметь возможность использовать какой-то другой ORM, после чего мне теперь придется украшать свои бизнес-объекты другими атрибутами (также, если бизнес-объекты находятся в отдельной сборке .Business, потребителям теперь нужно ссылаться также на все ORM для атрибутов, которые необходимо разрешить - чёрт!).
Мне кажется, что IRepository должен быть IService, и приведенный выше класс должен выглядеть так:
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();
}
}
}
Это также решает проблему куриного яйца:
- DbRegion.ID генерируется базой данных (как newid ()) и IsDbGenerated = true устанавливается
- DbRegion.DefaultLocationId является необнуляемым GUID
- DbRegion.DefaultLocationId - это FK для Location.ID
- DbLocation.RegionId является необнуляемым GUID и FK в Region.ID
Сделать это без EntitySet практически невозможно, поэтому, если вы не пожертвуете целостностью данных в базе данных и не перенесете ее в бизнес-логику, невозможно нести ответственность за местоположения вне поставщика Region.
Я вижу, как эта публикация может рассматриваться как не реальный вопрос, субъективный и аргументированный, поэтому, пожалуйста, позвольте мне сформулировать объективные вопросы:
- Что именно должен излагать шаблон репозитория?
- В реальном мире, как люди оптимизируют свой слой базы данных, не нарушая абстракцию, которой должен достичь шаблон репозитория?
- В частности, как реальный мир справляется с SELECT N + 1 и проблемами целостности данных?
Полагаю, мой настоящий вопрос таков:
- Когда уже используется ORM (например, Linq To Sql), DataContext уже не является моим репозиторием, и поэтому репозиторий поверх DataContext просто абстрагирует ту же самую вещь снова ?