Начинаю делать программу для конвертации контрагентов из старой базы в новую.
На данный момент, интересуют следующие таблицы, которые есть в базе данных: Организации, Контрагенты, Договора.
Организации — это наши организации, их несколько.
Как я понимаю, слой бизнес-логики должен состоять из следующих "элементов":
— бизнес-сущности для "объектов" из старой БД: "ОрганизацияСтарая", "КонтрагентСтарый", "ДоговорСтарый"
— бизнес-сущности для "объектов" в новой БД: "ОрганизацияНовая", "КонтрагентНовый", "ДоговорНовый"
— репозитории для этих сущностей
— метод "Конвертация", который выполняет основную работу: читает с помощью репозиториев старые данные, формирует из старых сущностей новые сущности, и с помощью других репозиториев записывает новые сущности в новую базу.
Верно?
Вопрос возник по агрегатам.
Договор — это как бы связь между нашей организацией и контрагентом. То есть он не может отдельно существовать без организации и без контрагента.
Следовательно, договор не может быть отдельным агрегатом, он должен быть одновременно частью и агрегата "Организация" и агрегата "Контрагент".
Однако, такого же не может быть?
Может быть сделать договор отдельным агрегатом? Соответственно придется делать отдельный Репозиторий.
Там выделение агрегата завязано на инвариантах. А какие тут инварианты?
Инвариант только один — договор не может существовать без организации и без контрагента.
А контрагент не может существовать без договора. И как это влияет на выделение агрегата?
Z>Верно?
Ну в теории, допустим, верно.
Но, как кто-то из коллег уже писал в твоей предыдущей теме, пытаться прикрутить классический DDD к задаче миграции — это натягивание совы на глобус.
Здесь просто нет того уровня сложности, который раскроет преимущества DDD-подхода.
Взять данные из одной БД, преобразовать по элементарному алгоритму, положить в другую БД. В чем тут сложность?
Z>Вопрос возник по агрегатам. Z>Договор — это как бы связь между нашей организацией и контрагентом. То есть он не может отдельно существовать без организации и без контрагента. Z>Следовательно, договор не может быть отдельным агрегатом, он должен быть одновременно частью и агрегата "Организация" и агрегата "Контрагент". Z>Однако, такого же не может быть?
Вот тут по DDD подходу ты должен сесть за стол в domain experts и проговаривать неясные моменты до общего просветления.
Как в системе появляется Организация? Кто ее заводит? Или бизнес-процесса заведения Организации вообще нет и предполгается, что она просто существует изначально? А организация меняется? А кто, как и почему ее меняет? А кому интересен факт этих изменений?
Как в системе появляется Контрагент? Заведение контрагента тождественно Заключению Договора с ним? Да? А договор с данным контрагентом всегда один? Один, да? Хм.. Ну тогда может у нас нет сущности Контрагент а есть cущность "Договор с контрагентом", в которой в том числе хранятся реквизиты контрагента? Что вы говорите? Договоров все же может быть несколько, а Контрагенты используются еще для выставление Счетов? Вон оно что. Похоже у нас все же будет сущность Договор (и, возможно, сущность Счет) А как Договор появляется в системе? И т.д. и т.п.
И из этого процесса пошагово формируется твоя доменная модель, понятная тебе как программисту и, что важно по DDD, понятная domain-экспертам, как представителям бизнеса.
Z>Может быть сделать договор отдельным агрегатом? Соответственно придется делать отдельный Репозиторий.
Ну да. И что?
Будет отдельная сущность "Договор", со ссылками на "Организацию" и "Контрагента" (это нормально).
Будет репозиторий договором с методами вроде "ПолучитьДоговорПоНомеру", "ПолучитьВсеДоговораОрганизации", "ПолучитьАктивныйДоговорКонтрагента" и т.п.
Здравствуйте, paradok, Вы писали:
P>Здравствуйте, zelenprog, Вы писали:
P>имхо договор может в пределе быть сам по себе P>например оба участника договора давно перестали существовать P>и были удалены из базы
нельзя удалять из базы КА иначе договор станет неполным.
только после удаления последней ссылки на КАК его можно удалять. нарушите целостность базы.
Z>Следовательно, договор не может быть отдельным агрегатом, он должен быть одновременно частью и агрегата "Организация" и агрегата "Контрагент".
"Организация" -> договор -> Контрагент
если уж хотите ДДД то не мечитесь, а следуйте ему.
просто в корне мы Identity предоставляем конечному пользователю, а в значениях-свойствах прячем.
по сути в базе будет точно такая же связь
Здравствуйте, RushDevion, Вы писали:
Z>>Может быть сделать договор отдельным агрегатом? Соответственно придется делать отдельный Репозиторий. RD>Ну да. И что? RD>Будет отдельная сущность "Договор", со ссылками на "Организацию" и "Контрагента" (это нормально). RD>Будет репозиторий договором с методами вроде "ПолучитьДоговорПоНомеру", "ПолучитьВсеДоговораОрганизации", "ПолучитьАктивныйДоговорКонтрагента" и т.п.
Согласен.
С одним контрагентом запросто может быть несколько договоров по разным поводам.
Иногда заказчик хочет еще хранить в базе текст договора в виде блоба (как pdf или doc).
Пообщайтесь с заказчиками для уточнения ТЗ.
Иногда в организациях учет договоров бывает очень мудренным.
RD>Здесь просто нет того уровня сложности, который раскроет преимущества DDD-подхода. RD>Взять данные из одной БД, преобразовать по элементарному алгоритму, положить в другую БД. В чем тут сложность?
Сложность в правилах конвертации. Они достаточно сложные и неочевидные.
Где же их еще реализовавыть как не в Бизнес-слое (domain-model)?
RD>Вот тут по DDD подходу ты должен сесть за стол в domain experts и проговаривать неясные моменты до общего просветления. RD>Как в системе появляется Организация? Кто ее заводит? Или бизнес-процесса заведения Организации вообще нет и предполгается, что она просто существует изначально? А организация меняется? А кто, как и почему ее меняет? А кому интересен факт этих изменений? RD>Как в системе появляется Контрагент? Заведение контрагента тождественно Заключению Договора с ним? Да? А договор с данным контрагентом всегда один? Один, да? Хм.. Ну тогда может у нас нет сущности Контрагент а есть cущность "Договор с контрагентом", в которой в том числе хранятся реквизиты контрагента? Что вы говорите? Договоров все же может быть несколько, а Контрагенты используются еще для выставление Счетов? Вон оно что. Похоже у нас все же будет сущность Договор (и, возможно, сущность Счет) А как Договор появляется в системе? И т.д. и т.п. RD>И из этого процесса пошагово формируется твоя доменная модель, понятная тебе как программисту и, что важно по DDD, понятная domain-экспертам, как представителям бизнеса.
Это все мы обсуждали с нашими "экспертами".
Схема "бизнеса" классическая. Есть несколько организаций, заводятся очень редко.
Контрагенты появляются часто. С каждым контрагентом может быть несколько договоров. На данный момент перенести нужно только тот договор, который отмечен как основной. Но вполне вероятно, что в дальнейшем потребуется перенести и остальные договора.
Вопросы создания\изменения объектов пока не интересуют, так как они не касаются данной программы конвертации. Задача программы — перенести в новую базу то, что есть на текущий момент.
Р>нельзя удалять из базы КА иначе договор станет неполным. Р>только после удаления последней ссылки на КАК его можно удалять. нарушите целостность базы.
на практике так часто бывает и никакая целостность не нарушается
подвисшие договора в базе — это норма
по ним все также можно делать поиск и тд.
кроме подписавших сторон у договоров есть масса других атрибутов по которым можно делать поиск и агрегацию
Здравствуйте, zelenprog, Вы писали:
Z>Как лучше сделать?
Делай так, чтобы быстрее решить задачу
Z>Может быть сделать договор отдельным агрегатом? Соответственно придется делать отдельный Репозиторий.
На какие пользовательские характеристики это влияет?
Как будет быстрее написать?
Будет работать быстрее или медленнее?
Как повлияет на взаимодействие с программой?
Z>Сложность в правилах конвертации. Они достаточно сложные и неочевидные. Z>Где же их еще реализовавыть как не в Бизнес-слое (domain-model)?
Опять же, допустим.
Я просто пытаюсь донести, что DDD — это не только про код, это прежде всего про shared mental model системы, выраженную в коде.
И есть у меня ощущение, что если спросить вашего доменного эксперта о правилах конвертации из старой БД в новую, то он сильно удивится.
Потому что в его картине мира есть Организации, Договора, Контрагенты. А вот БД там скорей всего нет.
Как нет и процесса конвертации из одной БД в другую.
Т.е. конвертация — это такая чисто техническая деталь, с доменом вообще никак не связанная.
Опять же, допустим, что я ошибаюсь.
И в картине мира доменного эксперта присутствуют и Договор Старого Формата, и Договор Нового Формата.
И он выдал вам вполне конкретные правила конвертации.
Тогда в терминах DDD у нас есть сущности ДоговорСтарогоФормата и ДоговорНовогоФормата и есть процесс конвертации.
А такой процесс обычно моделируется доменным сервисом, e.g.
// Это лежит в сборке Domain.dllclass ContractConvertor {
public Contract Convert(OldContract contract) {
// Тут создание нового контракта
// Применение бизнес-правил конвертации
}
}
А где-то уровнем выше есть application service, который этим доменным сервисом оперирует:
// Это лежит в сборке Application.dll, которая рефренсит Domain.dll и, e.g. Infrastructure.Storage.Contracts - для интерфейсов репозиториевclass ConvertionService {
private IContractRepo _contracts;
private IOldContractRepo _oldContracts;
private ContractConvertor _convertor;
public Contract ConvertContract(int oldContractId) {
var oldContract = _oldContracts.LoadById(oldContractId);
var newContract = _convertor.Convert(oldContract);
_contracts.Save(newContract);
return newContract;
}
}
RD>Опять же, допустим. RD>Я просто пытаюсь донести, что DDD — это не только про код, это прежде всего про shared mental model системы, выраженную в коде. RD>И есть у меня ощущение, что если спросить вашего доменного эксперта о правилах конвертации из старой БД в новую, то он сильно удивится. RD>Потому что в его картине мира есть Организации, Договора, Контрагенты. А вот БД там скорей всего нет.
Эксперт понимает про старые БД и про новую БД.
Если раньше каждый филиал работал в своей локальной базе, то теперь все должны работать в одной централизованной базе.
Перенос данных — это как раз инициатива группы экспертов. Причем они (эта группа) прекрасно понимают некоторые сложности, которые при этом переносе возникнут.
Например, вот одна из сложностей.
Один и тот же контрагент есть во всех базах: он есть во всех старых базах и даже уже есть в новой базе (был заведен вручную).
У контрагента есть реквизиты (телефон) для связи. С данным контрагентом в разных наших филиалах работали разные менеджеры, они взаимодейтсовали с разными представителями контрагента.
То есть за каждым менеджером филиала был закреплен какой-то представитель контрагента. Соответственно, в БД каждого филиала для этого контрагента вбиты разные данные представителя контрагента (ФИО, телефон и пр.).
Эксперты понимают, что "объединить" эти данные в одной новой базе не так просто.
Кроме того, в разных старых базах иногда забиты разные значения и в других реквизитах контрагента.
При последовательной загрузке контрагентов из старых баз, реквизиты будут просто перезаписываться.
И получится, что значения реквизитов у контрагента в новой базе будут просто совпадать с контрагентом из последней загруженной старой базы.
Эти сложности, мы пока совместно не придумали как решить.
Я просто к тому, что эксперты кое-что все-таки понимают.
RD>Опять же, допустим, что я ошибаюсь. RD>И в картине мира доменного эксперта присутствуют и Договор Старого Формата, и Договор Нового Формата. RD>И он выдал вам вполне конкретные правила конвертации.
RD>Тогда в терминах DDD у нас есть сущности ДоговорСтарогоФормата и ДоговорНовогоФормата и есть процесс конвертации. RD>А такой процесс обычно моделируется доменным сервисом, e.g. RD>
RD>// Это лежит в сборке Domain.dll
RD>class ContractConvertor {
RD> public Contract Convert(OldContract contract) {
RD> // Тут создание нового контракта
RD> // Применение бизнес-правил конвертации
RD> }
RD>}
RD>
RD>А где-то уровнем выше есть application service, который этим доменным сервисом оперирует: RD>
RD>// Это лежит в сборке Application.dll, которая рефренсит Domain.dll и, e.g. Infrastructure.Storage.Contracts - для интерфейсов репозиториев
RD>class ConvertionService {
RD> private IContractRepo _contracts;
RD> private IOldContractRepo _oldContracts;
RD> private ContractConvertor _convertor;
RD> public Contract ConvertContract(int oldContractId) {
RD> var oldContract = _oldContracts.LoadById(oldContractId);
RD> var newContract = _convertor.Convert(oldContract);
RD> _contracts.Save(newContract);
RD> return newContract;
RD> }
RD>}
RD>
Спасибо за пример кода!
В связи с этим два вопроса:
1) А как в этом коде предусмотреть ситуацию, что договор уже есть в новой базе?
2) Получается, вы считаете, что договор — это все-таки отдельный агрегат? Правильно?
Насколько я помню, Репозитории в DDD дожны работать именно с агрегатами.
Тогда как этот договор-агрегат будет связан с Сущностями "Организация" и "Контрагент"?
Здравствуйте, zelenprog, Вы писали:
Z>Договор — это как бы связь между нашей организацией и контрагентом. То есть он не может отдельно существовать без организации и без контрагента.
Может. Шаблон договора, к примеру. Юристы кстати используют — т.е. это жизненная, интуитивно-понятная абстракция.
Z>Следовательно, договор не может быть отдельным агрегатом, он должен быть одновременно частью и агрегата "Организация" и агрегата "Контрагент".
Организация и контрагент — это параметры договора. См. выше — берешь шаблон, задаешь параметры.
Z>https://habr.com/ru/articles/660599/
Чего ты усложняешь? Примитивная же задача. Делай так, как будет естественно и интуитивно понятно — иначе сопровождать замахаешься ты или твой последователь — поди потом вспомни/пойми/разберись в доках, что ты имел в виду какой-то навороченной архитектурой и как при правках правильно сохранить твой замысел.
Z>>Как лучше сделать? G>Делай так, чтобы быстрее решить задачу
Так в этом то и проблема.
Если структура (архитектура) программы продумана хорошо, то и задачу получиться решить быстрее.
А если корявая архитектура, то ее доработки и исправление косяков будут бесконечны.
Z>>Может быть сделать договор отдельным агрегатом? Соответственно придется делать отдельный Репозиторий. G>На какие пользовательские характеристики это влияет?
Как я понимаю, внутренние технические решения никак не влияют на пользовательские характеристики.
Наоборот, пользовательские характеристики задают требования для технической реализации.
Если техническая реализация выполняет эти требования — то мы получим желаемые пользовательские характеристки, и все довольны.
Если мы не смогли придумать и сделать техническую реализацию, удовлетворяющую пользовательским характеристкам, то у нас и нету рабочей программы.
G>Как будет быстрее написать?
Чтобы ответить на этот вопрос, надо сравнить несколько вариантов ("проектов") будущей программы.
А как построить эти варианты? Какие выбрать варианты? Вот тут я как раз и не имею достаточного опыта.
Но судя по рекомендациям ведущих "мыслителей" разработки ПО, правильнее и быстрее написать получится при умении создать правильную архитектуру.
G>Будет работать быстрее или медленнее? G>Как повлияет на взаимодействие с программой?
Это второстепенные вопросы.
Самые важные вопросы — это работоспособная программы, выполняющая определенные требования, срок разработки, и возможность дальнейшей доработки при расширении исходных требований.
V>Чего ты усложняешь? Примитивная же задача. Делай так, как будет естественно и интуитивно понятно — иначе сопровождать замахаешься ты или твой последователь — поди потом вспомни/пойми/разберись в доках, что ты имел в виду какой-то навороченной архитектурой и как при правках правильно сохранить твой замысел.
Нужна не навороченная, а "правильная" архитектура.
При правильной архитектуре код читается легко и понятно.
Z>Спасибо за пример кода!
Пожалуйста.
Z>В связи с этим два вопроса: Z>1) А как в этом коде предусмотреть ситуацию, что договор уже есть в новой базе?
Ну, есть несколько вариантов.
Для начала, я бы посмотрел, есть ли у документа то, что в DDD называют "естественный идентификатор".
Например, им может быть номер договора, который, формируется по определенным правилам и глобально уникален в пределах всей вашей организации.
Если такой идентификатор есть — то все просто:
public Contract ConvertContract(string oldContractNumber) {
var oldContract = _oldContracts.LoadByNumber(oldContractNumber);
var existingNewContract = _contracts.LoadByNumber(oldContractNumber);
if(existingNewContract is not null) {
_logger.LogWarn("Contract {oldContractNumber} is already imported");
return existingNewContract;
}
var newContract = _convertor.Convert(oldContract);
_contracts.Save(newContract);
return newContract;
}
Если же естественного идентификатора нет и им выступает какой-нибудь Identity столбец в БД + в разных БД филиалов есть пересечения по identity,
то придется научиться однозначно сопоставлять: Филиал + Id старого документа => Id нового.
Как вариант:
// Имплементация может хранить мапинг в какой-то таблице БД, e.g. ContractsConvertionHistory
// Это и для разбора проблем будет полезноinterface IConvertedContractsTracker {
void RegisterContract(string departmentId, long oldDocumentId, long newDocumentId);
void IsContractRegistered(string departmentId, long oldDocumentId);
}
public Contract ConvertContract(long oldContractId) {
var oldContract = _oldContracts.LoadById(oldContractNumber);
var isImported = _convertionTracker.IsContractRegistered(oldContract.DepartmentId, oldContract.Id);
if (isImported) {
// TODO: logreturn;
}
var newContract = _convertor.Convert(oldContract);
_contracts.Save(newContract);
_convertionTracker.RegisterContract(oldContract.DepartmentId, oldContract.Id, newContract.Id);
return newContract;
}
Или можно договориться, что в новой БД номера контрактов будут составные, типа такого:
class IdentityConvertor {
string GenerateNewContractNumber(string departmentId, long oldContractId) => $"{departmentId:oldContractId}";
}
// Тогда будет что-то типа такогоpublic Contract ConvertContract(long oldContractId) {
var oldContract = _oldContracts.LoadById(oldContractId);
var newContractNumber = _identityConvetor.GenerateNewContractNumber(oldContract.DepartmentId, oldContract.Id);
var existingContract = _contracts.LoadByNumber(newContractNumber);
if (existingContract is not null) {
// TODO: logreturn existingContract;
}
// ...
}
Z>2) Получается, вы считаете, что договор — это все-таки отдельный агрегат? Правильно?
Да, договор — это отдельный агрегат.
Z>Насколько я помню, Репозитории в DDD дожны работать именно с агрегатами. Z>Тогда как этот договор-агрегат будет связан с Сущностями "Организация" и "Контрагент"?
Имхо, проще всего — по Id.
class Document {
public long OrganizationId { get; private set; }
public long PartnerId { get; private set; }
}
Еще я встречал вот такой изврат:
class Document {
public EntityRef<Organization, long> Organization { get; private set; }
public EntityRef<Partner, long> Partner { get; private set; }
}
// Здесь EntityRef<Organization, long> - это read-only, lazy-loading обертка для соответствующей entity
Z>Один и тот же контрагент есть во всех базах: он есть во всех старых базах и даже уже есть в новой базе (был заведен вручную). Z>У контрагента есть реквизиты (телефон) для связи. С данным контрагентом в разных наших филиалах работали разные менеджеры, они взаимодейтсовали с разными представителями контрагента. Z>То есть за каждым менеджером филиала был закреплен какой-то представитель контрагента. Соответственно, в БД каждого филиала для этого контрагента вбиты разные данные представителя контрагента (ФИО, телефон и пр.). Z>Эксперты понимают, что "объединить" эти данные в одной новой базе не так просто.
Конкретно тут проблемы не вижу. Просто в доменной модели появляется сущность ContactPerson
class Partner {
private readonly List<ContactPersons> _contactPersons = new();
public IReadOnlyCollection<ContactPersons> ContactPersons { get; } => _contactPersons.AsReadOnly();
public void AddContact(ContactPerson person) {
// TODO: реализация инвариантов для агрегата Patner, e.g.
// должен быть активный контакт, активный контакт может быть только один и с российским телефоном,
// последний добавленый контакт становится активным и т.д и т.п.
_contactsPersons.Add(person);
}
public ContactPerson ActiveContact => ...
}
Z>Кроме того, в разных старых базах иногда забиты разные значения и в других реквизитах контрагента. Z>При последовательной загрузке контрагентов из старых баз, реквизиты будут просто перезаписываться. Z>И получится, что значения реквизитов у контрагента в новой базе будут просто совпадать с контрагентом из последней загруженной старой базы.
Имхо, это не проблема моделирования домена. Это проблема неконсистентности данных. Ее надо решать не программными, а административными методами.
Здравствуйте, zelenprog, Вы писали:
Z>Например, вот одна из сложностей. Z>Один и тот же контрагент есть во всех базах: он есть во всех старых базах и даже уже есть в новой базе (был заведен вручную). Z>У контрагента есть реквизиты (телефон) для связи. С данным контрагентом в разных наших филиалах работали разные менеджеры, они взаимодейтсовали с разными представителями контрагента. Z>То есть за каждым менеджером филиала был закреплен какой-то представитель контрагента. Соответственно, в БД каждого филиала для этого контрагента вбиты разные данные представителя контрагента (ФИО, телефон и пр.). Z>Эксперты понимают, что "объединить" эти данные в одной новой базе не так просто.
Не понял в чём сложность. Нельзя создать организационную структуру предприятия и на эту структуру делать ссылки в таблице контрагентов?
Здравствуйте, zelenprog, Вы писали:
Z>Нужна не навороченная, а "правильная" архитектура. Z>При правильной архитектуре код читается легко и понятно.
Ставишь телегу впереди лошади. Наоборот — та архитектура, которая понятная и легко читается — правильная. Ты пытаешься найти некую "правильность", исходя из неких общепрограммерских принципов, которые кто-то сформулировал (ты дал ссылку). И думаешь, будто, если сделаешь "правильно" согласно этой чьей-то философии (а это именно философия, не некая точная наука), то вдруг бац — и станет легко читаться. Да ничего подобного. Просто делай сразу так, как будет легко читаться. Т.е. чтобы было интуитивно понятно исходя из конкретной прикладной задачи. Ладно бы у тебя были какие-то абстракции, которые не имеют аналогов в жизни — но тут то обычная прикладнуха, слепок из жизни, из конкретных бумажек и несложных бизнес-процессов.
Z>Эксперт понимает про старые БД и про новую БД. Z>Если раньше каждый филиал работал в своей локальной базе, то теперь все должны работать в одной централизованной базе. Z>Перенос данных — это как раз инициатива группы экспертов. Причем они (эта группа) прекрасно понимают некоторые сложности, которые при этом переносе возникнут.
Z>Например, вот одна из сложностей. Z>Один и тот же контрагент есть во всех базах: он есть во всех старых базах и даже уже есть в новой базе (был заведен вручную). Z>У контрагента есть реквизиты (телефон) для связи. С данным контрагентом в разных наших филиалах работали разные менеджеры, они взаимодейтсовали с разными представителями контрагента. Z>То есть за каждым менеджером филиала был закреплен какой-то представитель контрагента. Соответственно, в БД каждого филиала для этого контрагента вбиты разные данные представителя контрагента (ФИО, телефон и пр.). Z>Эксперты понимают, что "объединить" эти данные в одной новой базе не так просто.
Компетентность ваших "экспертов" вызывает очень большие сомнения . То что вы описываете весьма тривиальная задача при объединении баз.
В итоговую таблицу выгружаются все записи компаний , добавляется bool поле "default" .
По таблице строится дерево значений со связями , например по ИНН , где вершина компания с выставленным значением true в поле "default"
Именно эта компания и используется в дальнейшей работе, остальные связанные компании нужны для уточнения реквизитов и архива документов.
... Хорошо уметь читать между строк. Это иногда
приносит большую пользу