Сокращение репозиториев до агрегированных корней


83

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

Предположим, что у меня есть следующие таблицы, Userи Phone. У каждого пользователя может быть один или несколько телефонов. Без понятия совокупного корня я мог бы сделать что-то вроде этого:

//assuming I have the userId in session for example and I want to update a phone number
List<Phone> phones = PhoneRepository.GetPhoneNumberByUserId(userId);
phones[0].Number = “911”;
PhoneRepository.Update(phones[0]);

Концепцию совокупных корней легче понять на бумаге, чем на практике. У меня никогда не будет телефонных номеров, которые не принадлежат пользователю, так что имеет ли смысл отказаться от PhoneRepository и включить методы, связанные с телефоном, в UserRepository? Предполагая, что да, я собираюсь переписать предыдущий пример кода.

Могу ли я иметь в UserRepository метод, который возвращает номера телефонов? Или он всегда должен возвращать ссылку на пользователя, а затем проходить отношения через пользователя, чтобы добраться до номеров телефонов:

List<Phone> phones = UserRepository.GetPhoneNumbers(userId);
// Or
User user = UserRepository.GetUserWithPhoneNumbers(userId); //this method will join to Phone

Независимо от того, каким образом я приобрету телефоны, если я модифицировал один из них, как мне их обновить? Мое ограниченное понимание состоит в том, что объекты под корнем должны обновляться через корень, что подтолкнет меня к выбору № 1 ниже. Хотя это будет отлично работать с Entity Framework, это кажется крайне не описательным, потому что, читая код, я понятия не имею, что я на самом деле обновляю, хотя Entity Framework отслеживает измененные объекты в графе.

UserRepository.Update(user);
// Or
UserRepository.UpdatePhone(phone);

И, наконец, если предположить , у меня есть несколько справочных таблиц, которые на самом деле не привязанные ни к чему, например CountryCodes, ColorsCodes, SomethingElseCodes. Я мог бы использовать их для заполнения раскрывающихся списков или по любой другой причине. Это автономные репозитории? Можно ли их объединить в какую-то логическую группу / хранилище, например CodesRepository? Или это противоречит передовой практике.


2
Действительно, очень хороший вопрос, потому что я много боролся с собой. Похоже на один из тех компромиссов, где нет "правильного" решения. Хотя ответы, доступные в то время, когда я пишу это, хороши и охватывают большинство проблем, я не чувствую, что они предоставляют какие-либо "окончательные" решения .. :(
cwap 01

Я слышу вас, нет предела тому, насколько близко к "правильному" решению можно подойти. Думаю, мы должны приложить все усилия, пока не научимся лучше :)
e36M3 02

+1 - Я тоже борюсь с этим. Раньше у меня был отдельный уровень репо и обслуживания для каждой таблицы. Я начал комбинировать их там, где это имело смысл, но потом у меня получился уровень репо и обслуживания с более чем 1 тыс. Строк кода. В моем последнем фрагменте приложения я сделал небольшую резервную копию, чтобы поместить только тесно связанные концепции в один и тот же уровень репо / службы, даже если этот элемент является зависимым. например - для блога я добавлял комментарии к агрегату репозитория сообщений, но теперь я выделил их для отдельного репо / сервиса комментариев.
jpshook

Ответы:


12

Вы можете использовать любой метод в своем репозитории :) В обоих случаях, которые вы упомянули, имеет смысл вернуть пользователя с заполненным списком телефонов. Обычно пользовательский объект не может быть полностью заполнен всей вспомогательной информацией (скажем, всеми адресами, номерами телефонов), и у нас могут быть разные методы для заполнения пользовательского объекта другой информацией. Это называется отложенной загрузкой.

User GetUserDetailsWithPhones()
{
    // Populate User along with Phones
}

Для обновления в этом случае обновляется пользователь, а не сам номер телефона. Модель хранилища может хранить телефоны в разных таблицах, и поэтому вы можете подумать, что обновляются только телефоны, но это не так, если вы думаете с точки зрения DDD. Что касается читабельности, то строка

UserRepository.Update(user)

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

Для таблиц поиска и даже в противном случае полезно иметь GenericRepository и использовать его. Пользовательский репозиторий может наследовать от GenericRepository.

public class UserRepository : GenericRepository<User>
{
    IEnumerable<User> GetUserByCustomCriteria()
    {
    }

    User GetUserDetailsWithPhones()
    {
        // Populate User along with Phones
    }

    User GetUserDetailsWithAllSubInfo()
    {
        // Populate User along with all sub information e.g. phones, addresses etc.
    }
}

Найдите Generic Repository Entity Framework, и вы получите много хороших реализаций. Используйте один из них или напишите свой.


@amit_g, спасибо за информацию. Я уже использую общий / базовый репозиторий, от которого наследуются все остальные. Моя идея для логической группировки "справочных" таблиц в один репозиторий заключалась в простой экономии времени и сокращении количества репозиториев. Поэтому вместо создания ColorCodeRepository и AnotherCodeRepository я бы просто создал CodesRepository.GetColorCodes () и CodesRepository.GetAnotherCodes (). Но я не уверен, что логическая группировка несвязанных сущностей в один репозиторий - плохая практика.
e36M3 01

Кроме того, вы затем подтверждаете, что по правилам DDD методы в репозитории, соответствующем корню, должны возвращать корень, а не базовые объекты в графе. Итак, в моем примере любой метод в UserRepository может возвращать только тип User, независимо от того, как выглядит остальная часть графика (или часть графика, которая меня действительно интересует, например, адреса или телефоны)?
e36M3 01

CodesRepository - это хорошо, но было бы сложно последовательно поддерживать то, что ему нужно. То же самое можно сделать просто с помощью GenericRepository <ColorCodes> GetAll (). Поскольку GenericRepository будет иметь только очень общие методы (GetAll, GetByID и т. Д.), Он отлично подойдет для таблиц поиска.
amit_g 01


2
К сожалению, это неверный ответ. Репозиторий следует рассматривать как набор объектов в памяти, и вам следует избегать отложенной загрузки. Вот хорошая статья об этом besnikgeek.blogspot.com/2010/07/…
Рафал Лужиньски

9

Ваш пример с репозиторием Aggregate Root в порядке, т.е. любой объект, который не может разумно существовать без зависимости от другого, не должен иметь своего собственного репозитория (в вашем случае Phone). Без этого рассмотрения вы можете быстро обнаружить бурный рост репозиториев в сопоставлении 1-1 с таблицами БД.

Вам следует обратить внимание на использование шаблона Unit of Work для изменения данных, а не на сами репозитории, поскольку я думаю, что они вызывают у вас некоторую путаницу в отношении намерений, когда дело доходит до сохранения изменений обратно в базу данных. В решении EF единица работы, по сути, является оболочкой интерфейса для вашего контекста EF.

Что касается вашего репозитория для данных поиска, мы просто создаем ReferenceDataRepository, который становится ответственным за данные, которые конкретно не принадлежат объекту домена (страны, цвета и т. Д.).


1
благодарю вас. Я не уверен, как Unit of Work заменяет репозиторий? Я уже использую UOW в том смысле, что в конце каждой бизнес-транзакции (конце HTTP-запроса) будет один вызов SaveChanges () к контексту Entity Framework. Однако я все еще прохожу через репозитории (в которых находится контекст EF) для доступа к данным. Такие как UserRepository.Delete (пользователь) и UserRepository.Add (пользователь).
e36M3 01

5

Если телефон не имеет смысла без пользователя, это сущность (если вы заботитесь об ее идентичности) или объект значения, который всегда должен изменяться пользователем и извлекаться / обновляться вместе.

Подумайте об агрегированных корнях как о средствах определения контекста - они рисуют локальные контексты, но сами находятся в глобальном контексте (ваше приложение).

Если вы следуете дизайну, основанному на домене, предполагается, что репозитории должны быть 1: 1 на совокупные корни.
Никаких оправданий.

Бьюсь об заклад, это проблемы, с которыми вы столкнулись:

  • технические трудности - несоответствие импеданса объектному отношению. Вы с легкостью боретесь с сохранением целых графов объектов, а структура сущности не может помочь.
  • модель предметной области ориентирована на данные (в отличие от модели поведения). из-за этого - вы теряете знания об иерархии объектов (ранее упомянутых контекстах), и волшебным образом все становится совокупным корнем.

Я не уверен, как исправить первую проблему, но я заметил, что исправление второй исправляет первую достаточно хорошо. Чтобы понять, что я имею в виду, говоря о поведении, попробуйте эту статью .

Ps сворачивать репозиторий до агрегатного рута нет смысла.
Pps Избегайте "CodeRepositories". Это приводит к ориентированному на данные -> процедурному коду.
Ppps Избегайте шаблона единицы работы. Совокупные корни должны определять границы транзакции.


1
Поскольку ссылка на статью больше не активна, используйте вместо
нее следующую

3

Это старый вопрос, но подумал, что стоит опубликовать простое решение.

  1. EF Context уже предоставляет вам как единицу работы (отслеживает изменения), так и репозитории (ссылка в памяти на данные из БД). Дальнейшее абстрагирование не обязательно.
  2. Удалите DBSet из вашего класса контекста, так как Phone не является совокупным корнем.
  3. Вместо этого используйте свойство навигации "Телефоны" на странице "Пользователь".

static void updateNumber (int userId, string oldNumber, string newNumber)

static void updateNumber(int userId, string oldNumber, string newNumber)
    {
        using (MyContext uow = new MyContext()) // Unit of Work
        {
            DbSet<User> repo = uow.Users; // Repository
            User user = repo.Find(userId); 
            Phone oldPhone = user.Phones.Where(x => x.Number.Trim() == oldNumber).SingleOrDefault();
            oldPhone.Number = newNumber;
            uow.SaveChanges();
        }

    }

Абстракция не является обязательной, но рекомендуется. Entity Framework по-прежнему остается всего лишь поставщиком и частью инфраструктуры. Дело даже не только в том, что должно было произойти, если поставщик изменился, но в более крупных системах у вас может быть несколько типов поставщиков, которые сохраняют различные концепции домена на разных средах сохранения. Это своего рода абстракция, которую чрезвычайно легко сделать на ранней стадии, но болезненно провести рефакторинг в течение достаточного времени и сложности.
Джозеф Феррис,

1
Мне очень трудно сохранить преимущества EF ORM (например, отложенная загрузка, запросы), когда я пытаюсь абстрагироваться от интерфейса репозитория.
Chalky

Безусловно, это интересная дискуссия. Поскольку ленивая загрузка очень специфична для реализации, я считаю, что ее значение ограничено инфраструктурой (входящий и исходящий объект домена с переводом, ограниченным уровнем). Многие реализации, которые я видел, сталкиваются с проблемами при попытке создания общей абстракции. Я предпочитаю явную реализацию, потому что общие методы имеют очень маленькую ценность для предметной области. EF действительно делает запрашиваемые объекты очень удобными, но проблема заключается в роли репозитория, т. Е. Репозитории, используемые контроллерами, упускают преимущества абстракции.
Джозеф Феррис,

0

Если объект Phone имеет смысл только вместе с совокупным корневым пользователем, то я также думаю, что имеет смысл, что операция по добавлению новой записи Phone является обязанностью объекта домена User посредством определенного метода (поведение DDD), и это может имеет смысл по нескольким причинам, непосредственная причина заключается в том, что мы должны проверить, существует ли объект User, поскольку объект Phone зависит от его существования и, возможно, сохранить блокировку транзакции на нем, выполняя больше проверок, чтобы убедиться, что никакой другой процесс не удалил корневой агрегат раньше мы закончили проверку операции. В других случаях с другими типами корневых агрегатов вы можете захотеть агрегировать или вычислить какое-то значение и сохранить его в свойствах столбца корневого агрегата для более эффективной обработки другими операциями позже.

Кроме того, если вы хотите использовать методы, которые извлекают все телефоны независимо от пользователей, владеющих ими, вы все равно можете, хотя через репозиторий пользователей вам нужен только один метод, который возвращает всех пользователей как IQueryable, тогда вы можете сопоставить их, чтобы получить все телефоны пользователей и выполнить уточненный запрос с этим. Так что в этом случае вам даже не понадобится PhoneRepository. Кроме того, я бы предпочел использовать класс с методом расширений для IQueryable, который я могу использовать где угодно, а не только из класса репозитория, если я хочу абстрагировать запросы за методами.

Только одно предостережение в отношении возможности удалять объекты Phone, используя только объект домена, а не репозиторий Phone, вам необходимо убедиться, что UserId является частью первичного ключа Phone или, другими словами, первичный ключ записи Phone является составным ключом. состоящий из UserId и некоторого другого свойства (я предлагаю автоматически сгенерированный идентификатор) в объекте Phone. Это интуитивно понятно, поскольку запись телефона «принадлежит» записи пользователя, и ее удаление из коллекции навигации пользователя равносильно ее полному удалению из базы данных.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.