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

Существует вопрос об 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 просто абстрагирует ту же самую вещь снова ?

Ответы [ 2 ]

4 голосов
/ 12 ноября 2011

При проектировании своих репозиториев вы должны думать о том, что известно как совокупный корень. По сути, это означает, что если сущность может существовать одна внутри домена, у нее будет больше, чем просто собственный репозиторий. В вашем случае это будет Регион.

Рассмотрим классический сценарий клиент / заказ. Хранилище Клиента предоставит доступ к Заказам, так как заказ не может существовать без клиента, и поэтому, если у вас нет действующего бизнес-обоснования для него, вам вряд ли понадобится отдельный репозиторий заказов.

В простом приложении ваше предположение вполне может быть правильным, но помните, что если вы не предоставите абстракцию контекста L2S, вам будет сложно выполнить эффективное модульное тестирование. Кодирование на интерфейсе, будь то IServiceX, IRepositoryX или что-то еще, дает вам такой уровень разделения.

Решение о том, входят ли интерфейсы служб в проект, как правило, снова связано со сложностью бизнес-логики и необходимостью расширения API в этой логике, которая может использоваться несколькими разрозненными клиентами.

1 голос
/ 12 ноября 2011

У меня есть несколько мыслей по поводу всего этого: 1. Шаблон репозитория AFAIK был изобретен немного раньше, чем ORM.В те времена, когда речь шла о простых SQL-запросах, было неплохо реализовать Repository и купить этот абстрагированный код в реальной базе данных.2. Я мог бы сказать, что репозиторий сейчас совершенно не нужен, но, к сожалению, по своему опыту я не могу сказать, что любой ORM может действительно абстрагировать вас от всех деталей базы данных.Например, я не мог однажды создать отображение ORM и просто использовать его с любым другим сервером БД, который, как утверждают, поддерживает ORM (особенно я говорю о Microsoft EF).Так что если вы действительно хотите иметь возможность использовать разные серверы баз данных, то вам, вероятно, все еще нужно использовать репозиторий.3. Другая проблема очень проста: дублирование кода.Конечно, есть некоторые запросы, которые вы часто называете своим кодом.Если вы оставите в качестве хранилища только ORM, то вы будете дублировать эти запросы, поэтому будет лучше иметь некоторый уровень абстракции над контейнером ORM, который будет содержать эти часто используемые запросы.

...