Как работает шаблон репозитория, если сущности связаны друг с другом?

Есть вопрос о 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;}
}

Есть правила:

  • Каждый регион ДОЛЖЕН иметь хотя бы одно местоположение
  • Вновь созданные регионы создаются с местоположением
  • Нет ВЫБРАТЬ N + 1, пожалуйста

Так как же будет выглядеть мой 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();
        }
      }
}

Это также решает проблему куриного яйца:

  • DbRegion.ID генерируется базой данных (как newid ( )) и IsDbGenerated = true
  • DbRegion.DefaultLocationId - это GUID, не допускающий значения NULL
  • DbRegion.DefaultLocationId - это FK в Location.ID
  • DbLocation.RegionId - это GUID, не допускающий значения NULL, и FK в Region .ID

Сделать это без EntitySet практически невозможно, поэтому, если вы не пожертвуете целостностью данных в базе данных и не перенесете их в бизнес-логику, невозможно сохранить ответственность за Locations за пределами провайдера региона.

Я понимаю, что эта публикация может рассматриваться как не настоящий вопрос, субъективный и аргументированный, поэтому позвольте мне сформулировать объективные вопросы:

  • Что именно должен абстрагироваться от шаблона репозитория?
  • в реальном мире, как люди оптимизируют уровень своей базы данных, не нарушая абстракции, которую должен достичь шаблон репозитория?
  • В частности, как реальный мир справляется с SELECT N + 1 и проблемами целостности данных?

Я полагаю, что мой реальный вопрос в следующем:

  • Когда уже используется ORM (например, Linq To Sql), разве DataContext уже не является моим репозиторием, и, таким образом, репозиторий поверх DataContext просто абстрагирует одно и то же снова ?

8
задан Community 23 May 2017 в 10:24
поделиться