Зменшення сховищ до сукупних коренів


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

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

+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,

Крім того, ви підтверджуєте, що згідно з правилами DDD, методи у сховищі, що відповідає кореневому коду, повинні повертати кореневий, а не базові сутності в графі. Отже, у моєму прикладі будь-який метод у UserRepository може повертати лише тип користувача, незалежно від того, як виглядає решта графіку (або тієї частини графіку, яка мені справді цікава, наприклад, адреси чи телефони)?
e36M3,

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


2
На жаль, ця відповідь неправильна. До сховища слід ставитись як до сукупності об’єктів у пам’яті, а також уникати лінивого завантаження. Ось гарна стаття про те, що besnikgeek.blogspot.com/2010/07/…
Рафал 10ужинський

9

Ваш приклад із сукупним кореневим сховищем цілком чудовий, тобто будь-яка сутність, яка не може розумно існувати без залежності від іншого, не повинна мати власного сховища (у вашому випадку Телефон). Без цього врахування ви зможете швидко опинитися з вибухом сховищ в 1-1, що відображається у таблицях db.

Вам слід поглянути на використання шаблону одиниці роботи для змін даних, а не на самі сховища, оскільки, на мою думку, вони викликають у вас певну плутанину навколо намірів, коли справа доходить до постійних змін назад до db. У рішенні EF одиниця роботи - це, по суті, оболонка інтерфейсу навколо вашого контексту EF.

Що стосується вашого сховища для даних пошуку, ми просто створюємо ReferenceDataRepository, який відповідає за дані, які конкретно не належать до сутності домену (Країни, Кольори тощо).


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

5

Якщо телефон не має сенсу без користувача, це сутність (якщо Ви піклуєтеся про його ідентичність) або об’єкт цінності, і його завжди слід модифікувати за допомогою користувача та отримувати / оновлювати разом.

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

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

Б'юся об заклад, це проблеми, з якими ви стикаєтесь:

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

Я не знаю, як виправити першу проблему, але я помітив, що виправлення другої виправляє першу досить добре. Щоб зрозуміти, що я маю на увазі, орієнтуючись на поведінку, спробуйте цей документ .

Ps Зведення сховища до сукупного кореня не має сенсу.
Pps уникати "CodeRepositories". Це призводить до орієнтованості на дані -> процесуального кодексу.
Ppps Уникайте одиниці схеми роботи. Сукупні корені повинні визначати межі транзакцій.


1
Оскільки посилання на статтю вже не активне, використовуйте замість цього: web.archive.org/web/20141021055503/http://www.objectmentor.com/…
JwJosefy

3

Це давнє запитання, але думка, яку варто опублікувати, просте рішення.

  1. EF Context вже надає вам як одиницю роботи (відстежує зміни), так і сховища (посилання в пам'яті на матеріали з БД). Подальша абстракція не є обов’язковою.
  2. Видаліть DBSet з вашого контекстного класу, оскільки Phone не є сукупним коренем.
  3. Натомість скористайтеся навігаційною властивістю «Телефони» на Користувачі.

статична порожнеча updateNumber (int userId, рядок oldNumber, рядок 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'S ORM (наприклад, ліниве завантаження, запити), коли я намагаюся абстрагуватися до інтерфейсу сховища.
Chalky

Безумовно, це цікава дискусія. Оскільки ледаче завантаження дуже специфічне для реалізації, я вважаю, що його значення обмежується інфраструктурою (об'єкт домену входить і виходить із обмеженим перекладом шару). Багато реалізацій, які я бачив, стикаються з проблемами під час спроби загальної абстракції. Я схиляюся до явної реалізації, оскільки загальні методи мають дуже мало значення для домену. EF робить запити дуже придатними для використання, але проблема стає роллю сховища - тобто сховища, що використовуються контролерами, втрачають переваги абстракції.
Джозеф Ферріс

0

Якщо сутність телефону має сенс лише разом із сукупним кореневим користувачем, тоді я також вважаю, що має сенс, що за операцію додавання нового запису телефону відповідає об'єкт домену користувача за допомогою певного методу (поведінка DDD), і це може цілком зрозуміло з кількох причин, безпосередньою причиною є те, що ми повинні перевірити, чи існує об’єкт User, оскільки сутність Phone залежить від його існування, і, можливо, тримати блокування транзакції на ньому, виконуючи додаткові перевірки перевірки, щоб переконатися, що жоден інший процес не видалив кореневий сукупність раніше ми перевірили операцію. В інших випадках з іншими видами кореневих агрегатів, можливо, ви захочете агрегувати або обчислити якесь значення і зберегти його у властивостях стовпців кореневого агрегату для більш ефективної обробки іншими операціями пізніше.

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

Лише одне застереження щодо можливості видалити сутності телефону, використовуючи лише об’єкт домену, а не сховище телефону, щоб переконатися, що UserId є частиною первинного ключа телефону, або іншими словами, первинним ключем запису телефону є складений ключ складається з UserId та деяких інших властивостей (я пропоную автоматично згенерований ідентифікатор) у сутності Phone. Це має сенс інтуїтивно, оскільки запис телефону "належить" запису користувача, і його вилучення з колекції навігації користувача дорівнює його повному видаленню з бази даних.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.