Pull to refresh

Comments 88

1)
Передача OrdersContext в метод доменного объекта нарушает принцип разделения ответственности, т.к. класс Order в этом случае содержит информацию о том, как он сохраняется в базе.
public virtual void RemoveLine(OrderLine line, OrdersContext db)
{
Lines.Remove(line);
db.OrderLines.Remove(line);
}

Если бы вместо OrdersContext был интерефейс IOrdersContext, то Order не знал бы КАК он сохраняется в бд, а просто знал бы, ЧТО он сохраняется в неком хранилище. И это было бы гораздо лучше, чем гибернейтовская автомагия.

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

В гибернейте есть специальных флаг отложенной загрузки связанных сущностей LazyLoad. Беда в том, что необходимость отложенности загрузки зависит от места использования. В итоге для сущностей, которые используются в нескольких местах, или куча ненужной информации из бд тянется (если не установлен LazyLoad), или из-за отложенной загрузки для каждого бизнес объекта из выборки (коих может быть миллион) по несколько запросов в бд идёт (т.е. несколько миллионов запросов) (это если установлен LazyLoad), или в коде несколько одинаковых маппингов, отличающихся лишь флагами LazyLoad. То есть или получается нарушение DRY, или всё жутко тормозит. Подход с идентификаторами мне кажется более приемлемым. Невелика беда, что данные хранятся не в хранилище List
(Часть комментария не сохранилась, видимо из-за наличия в нём <ob_ject> без подчёркивания)
<ob_ject>, а в Dictionary<int, ob_ject> (под хранилищем-словарём имеется ввиду бд).

3)
Customer customer = CreateCustomer(dto);
session.Save(customer);
ids.Add(customer.Id);
Если вы используете для этой задачи Entity Framework, то он будет вставлять записи в БД по мере сохранения их в сессии для того, чтобы получить Id, т.к. единственная доступная стратегия генерации целочисленных идентификаторов в EF — database identity.

А вот и неправда. Пока не будет вызвано session.SaveChanges(), ничего в базу данных сохраняться не будет, а будут подставлены фиктивные идентификаторы. Поэтому собирать идентификаторы смысла нет, вместо этого нужно использовать ссылки на связанные сущности (NavigationProperty).

4)
Работа с закешированными объектами
public Order(Customer customer, OrdersContext context)
{
context.Entry(customer).State = EntityState.Unchanged;
Customer = customer;
}
… устанавливает зависимость между доменной и инфраструктурной логикой

А что мешает, вынести context.Entry(customer).State = EntityState.Unchanged в класс, который занимается кэшированием?
Подумал по поводу флага LazyLoad — в принципе можно динамически генерировать маппинг на основе декларативных указаний где нужен LazyLoad, а где нет, но тогда маппинг станет сложнее и велик риск неправильно указать необходимость LazyLoad, и если ошибиться, то потом будет сложно разобраться, почему программа тормозит.
Если же для извлечения данных использовать джойны только по нужным таблицам, то не ошибёшься, но писанины станет чуть больше. Я предпочитаю решения, в которых сложнее ошибиться. Подход с идентификаторами можно совместить с ссылками на связанные сущности (NavigationProperty). Тогда можно обеспечить хорошее быстродействие там, где это нужно, и удобство использования в остальных местах.
В любом случае, не вижу серьёзного нарушения дизайна в добавлении поля с идентификатором. В конце концов, можно просто этим полем не пользоваться, и добавить юнит-тест, проверяющий что нигде в вашем коде это поле не используется.
Lazy load не нужен нигде, от слова вообще.
Почему не нужен? В некоторых случаях Lazy load позволяет уменьшить время запроса в несколько раз.
Именно Lazy, т.е. отложенная загрузка, или Partial — когда грузится только то, что нужно для работы?

Если первое, то вы не могли бы привести пример?
Например в маппинге fluent nhibernate заказа есть следующий код:
References(x => x.Customer).Column(«CUSTOMER_ID»).LazyLoad();
Тогда при загрузке из бд заказа поле Customer не заполнится, но оно считается из бд при первом обращении к этому полю.
Если грузится сразу много заказов и по факту нужна только треть таких полей (связанных сущностей из других таблиц), то запрос может занимать несколько минут, и его оптимизация оправдана.
Проблема этого подхода в том, что если у вас из запроса на n сущностей нужно m деталей, то при очень малых m вы в выигрыше, а вот при больших можете оказаться, наоборот, в проигрыше. И вот эта недетерминированность очень опасна — вы никогда не знаете, сколько именно запросов в БД порождает ваш код.
Не совсем понимаю. По-моему, всегда в выигрыше. Если без использования LazyLoad NH сгенерирует примерно следующий код
select * from Table
join Table2 on…
join Table3 on…
Если указать что Table2 и Table3 LazyLoad, то NH сгенерирует «select * from Table».
Если потом в коде не обращаться к Table.Table2 и Table.Table3, то всегда в выигрыше.
Если в коде для каждого объекта Table обратиться к Table.Table2 и Table.Table3, то всегда в проигрыше.
Если всегда не обращаться, то в выигрыше тот код, который не грузит — это как раз partial loading.
Если всегда обращаться, то в выигрыше код, который грузит сразу (eager loading).
А вот если обращаться не всегда, то, в зависимости от соотношения сколько раз надо обратиться, и сколько — нет, может быть быстрее eager или lazy.
Так я как раз и писал о том,
что необходимость отложенности загрузки зависит от места использования
Что значит «зависит от места использования»? Код, который гуляет по списку ордеров, сам определяет, надо ли грузить кастомеров? В этом случае проблема вашего кода — недетерминированная производительность.
Не совсем понимаю, почему если точно знать будут ли в этом месте использоваться заказчики и перед загрузкой заказов указать нужно ли сразу загрузить и заказчиков, то возникает недетерминированная производительность? Очень даже определенная производительность — можно даже план запроса посмотреть. А будут использоваться заказчики или же нет, можно и по коду посмотреть. Но вот если до того, как мы достали заказы точно неизвестно, нужны заказчики или нет, да ещё для некоторых заказов нужны, а для некоторых нет, то лучше их сразу всех загрузить одним запросом. По крайней мере мы точно можем определить по коду (или по постановке задачи): или заказчики точно не нужны, или возможно понадобятся.
Если точнее, не программист запрос к бд на всех заказчиков указанных заказов напишет, а NH автоматически джойн заказов с заказчиками сделает в момент доставания заказов из бд и соответствующие свойства Заказ.Заказчик заранее заполнит, если флаг LazyLoad не выставлен.
Это все равно медленнее, чем затянуть сразу все заказы с кастомерами + применить проекцию.
Похоже, проще на примерах.

1. Частичная загрузка:
foreach(var order in db.Orders(customerLoadOption: LoadOption.DontLoad))
{
  //NullReferenceException при попытке обратиться к order.Customer
  Console.WriteLine("{0}: {1}", order.Id, order.Customer.Name); 
}


2. Жадная (eager) загрузка:
foreach(var order in db.Orders(customerLoadOption: LoadOption.Load))
{
  //никакого NullReferenceException
  //зато из базы - одним запросом - вытащены все данные по заказам и покупателям
  Console.WriteLine("{0}: {1}", order.Id, order.Customer.Name); 
}

Это вариант медленнее, чем (1), зато работает.

3. Проекция
foreach(var order in db.Orders().Select(o => new {orderId = o.Id, customerName = o.Customer.Name}))
{
  //из базы вытащено ровно два поля
  Console.WriteLine("{0}: {1}", order.orderId, order.customerName); 
}

Этот вариант быстрее, чем (2) (и чем (1) — тоже), и при этом все еще работает.

4. Ленивая (lazy) загрузка:
foreach(var order in db.Orders(customerLoadOption: LoadOption.Lazy))
{
  //никакого NullReferenceException
  //зато для каждого заказа в цикле делается дополнительное обращение в БД
  Console.WriteLine("{0}: {1}", order.Id, order.Customer.Name); 
}

Это самый медленный вариант (даже медленнее чем (2)).

5. Ленивая (lazy) загрузка с условием:
foreach(var order in db.Orders(customerLoadOption: LoadOption.Lazy))
{
  if (order.IsSuspicious)
    Console.WriteLine("{0}: {1}", order.Id, order.Customer.Name); 
}

А производительность этого варианта недетерминирована, потому что напрямую зависит от количества подозрительных заказов. Если их ни одного — производительность будет как у (1), то есть разумной, а если их больше некоего количества — то производительность станет хуже, чем у (2), и в худшем случае будет как у (4).

Вот и возникает вопрос — а зачем, все-таки, нужна ленивая загрузка (т.е. варианты 4 и 5)?
В таких сценариях она не нужна. Но полезна в следующих примерах:
public class Account
{
    ...
    public double GetTaxes()
    {
        if (SomeComplexCondition())
        {
            GetTaxes(this.DomainTrasactions)  // load this.DomainTransaction lazily
        }
        else
        {
            GetTaxes(this.ForiegnTrasactions)  // load this.ForiegnTrasactions lazily
        }
    }
    ...
} 


Lazy load must have в rich-модели, поскольку пользователь такой модели не имеет никакого представления о проекции данных, которая на самом деле требуется entity для выполнения запрошенной им функции. Конечно, при применении такой функции к листу объектов, автоматически получаем какой-то из вариантов «SELECT N+1»-проблемы. Поскольку EF в настоящий момент не дружит с rich подходом, то и без lazy load вполне можно обойтись.
Прямо скажем, в EF lazy load есть, так что EF этот сценарий обработает из коробки без проблем.

Другое дело, что меня «SELECT N+1» пугает весьма сильно в таких сценариях.
Это проблема NH. EF не генерирует джоины если явно не попросишь об этом — через include или связанные сущности в linq-запросе. Так что утверждение про LL верно только в контексте NH.

А вообще неявные джоины это жопа для быстродействия. Только из за них nh категорически не стоит использовать.
Именно Lazy, т.е. отложенная загрузка, или Partial — когда грузится только то, что нужно для работы?
Если первое, то вы не могли бы привести пример?


Пример: в сущности Order есть ссылки на справочники (OrderType, OrderState, City)
Запрос выбирает 10 млн. Order, если LazyLoad включен, основной запрос будет без join, что сильно его ускорит, а отдельно загрузятся всего лишь десятки элементов. Если бизнес-операция сложная (несколько запросов к Orders), большая вероятность, что эти lazy-сущности и так уже в кеше сессии.
(я не буду спрашивать, зачем грузить десять миллионов заказов с подключенными справочниками)

Вы делали реальное сравнение, в котором было бы видно, что оверхед от дублей в результирующем наборе записей больше, чем потери от отдельных запросов в БД за каждым элементом справочника (которых тоже может набежать достаточно много в сумме)?
Вопрос не только в производительности, но и в удобстве.
Я хочу писать ф-ции, принимающие entity, при вызове которых не надо думать, к каким полям они теоретически могут обратиться, чтобы заранее их прогрузить. Если из-за lazy будет много догрузок, я это замечу и добавлю в запрос join (обычно до этого не доходит: если понятно, что поле будет использоваться и это не справочник, join сразу пишется в запрос).

Самый оптимальный вариант, с проекциями, вообще предлагает каждый сценарий обрабатывать уникально и ни о каких общих ф-циях, принимающих entity, речи нет. Понятно, что для каких-то узких случаев проекции это самое лучшее, но я бы не стал категорично утверждать, что у LazyLoad вообще нет применений.
А я этого и не утверждал. Я просто считаю, что в среднем lazy load — это жертва скоростью в пользу удобства, и каждый для себя выбирает, где ему комфортно остановиться.
Это я утверждал.

В любой системе есть разделение запросов и операций изменений данных. В вебе это просто два разных HTTP запроса на сервер.

Далее:
1) Если у вас запрос, который тянет много строк, то без покрывающих индексов не обойтись уже на десятке тысяч записей в базе. А использовать покрывающие индексы без проекций невозможно. Так что как ни крутись, но для любых серьезных объемов надо делать проекции.
2) Операции изменения в 90% и более случаев затрагивают одну запись или небольшое множество записей, так как пользователь физически не сможет на одном экране обработать одной корневой и нескольких связанных записей. В этом случае вполне можно тянуть и полные объекты (сущности).

Это все создает сложности? Совершенно не создает. Запрос\команда пользователя всегда приходит в разные контроллеры\application services. Соотвественно в них и можно решать что загружать, а что — нет.

Вы и так это уже делаете
Если из-за lazy будет много догрузок, я это замечу и добавлю в запрос join (обычно до этого не доходит: если понятно, что поле будет использоваться и это не справочник, join сразу пишется в запрос)

Остается сделать маленький шаг в сторону правильной архитектуры — отказаться в запросах от методов, работающих с полными сущностями и работать только с IQueryable, навешивая проекцию только в контроллере (ибо только в контроллере мы точно знаем как данные будут отображаться). При обработке команд изменения можно оставить как есть, ибо большого влияния на быстродействие это не окажет.

Если у вас, не дай бог, двузвенная архитектура и вы ходите в базу напрямую из клиентского кода, то вам тоже надо делать проекции, так как сеть часто становится узким местом. В отличие от трехзвенки, где сервер и база находятся в одной сети с толстым каналом, клиент двухзвеки обычно «далеко» (относительно небольшая пропускная способность, высокая латентность) от базы и тянуть полные объекты, тем более с джоинами — очень сильно забивает канал и снижает отзывчивость. Это прекрасно видно на примере толстого клиента 1С.

Единственный вариант когда можно (и даже нужно) не делать проекции, а работать с lazy load — локальная (embedded) база для небольшого клиентского приложения. В этом случае вы можете спокойно держать ссылку на целый объект сколько угодно долго и не беспокоиться, что получение данных забьет канал, так как чтение данные, считанные с диска уже попадают в память приложения и вы не выиграете ничего от проекций.

В Hibernate достаточно добавить пустую критерию на lazy-сущность, чтобы она загрузилась
var qOrder = DetachedCriteria.For<Order>();
var qClient = qOrder.CreateCriteria("Client");
qOrder.Add(Restriction.Gt("Sum", 1000000));
var OrdersWithClients = Session.List<Order>(qOrder);
UFO just landed and posted this here
Имеется ввиду, что единственный способ в EF получить целочисленный идентификатор сущности — это сохранить ее в БД. Соответственно если в процессе операции вы решите отменить эту операцию, то вам придется удалить все сохраненные к этому моменту записи вручную. В NH есть различные стратегии генерации ID, что позволяет генерировать их не сохраняя саму сущность в базе.

В EF7 это кстати поправили, Rowan Miller говорит, что там уже есть Hi/Lo стратегия как в NH.
Соответственно если в процессе операции вы решите отменить эту операцию, то вам придется удалить все сохраненные к этому моменту записи вручную.

Зачем же вручную, когда есть транзакция и ее откат?
Либо через транзакцию, да. Суть в том, что в случае отмены операции эти данные придется удалять (либо вручную либо через транзакцию), что приводит к проседанию производительности. Гораздо эффективнее просто не вставлять эти данные в БД до момента завершения транзакции.
Как этот сценарий разруливается в EF, вам показали (как раз через честный UoW).
Вы про ваш коммент? Если да, то это не совсем то, что нужно. Необходимо получение идентификаторов не в конце операции, а именно по мере создания сущностей.
В любом случае, сделать честный UoW, с database generated идентификаторами невозможно.
А зачем вам получать идентификаторы именно по мере создания сущностей?
В задаче, к кот. я ссылаюсь в посте, они были нужны для целей real-time репортинга.
в реалтайме отдавались ИД незакоммиченных записей???? Зачем?
В примере, приведенном в посте, идентификаторы по мере создания сущностей никому не нужны, они получаются после сохранения. Либо вы из примера выкинули рил-таймовую часть, либо я в нем чего-то не понимаю.

Можете описать задачу поподробнее?

(впрочем, будем честными, я очень давно не использую целочисленные идентификаторы, поэтому с такими проблемами сталкиваюсь только теоретически)
Да, я убрал эту часть из примера чтобы не делать нагромождений. У меня в целом была задача показать, что EF хуже поддерживает UoW из-за того, что у него только одна стратегия генерации идшников. Пример я думаю не выбран не слишком удачный для этого, с этим я согласен.
EF нормально поддерживает UoW. Стратегии генерации идентификаторов тут уже особого значения не имеют (вы же можете идентификаторы и самостоятельно генерить).
Да, могу. Проблема в том, что этой функциональности нет из коробки.
А NH как будто может отслеживать такие ситуации, что на двух компьютерах попытались почти одновременно сохранить две сущности, в результате чего им сгенерировались одинаковые идентификаторы? Исключение ведь выбросится. Такие проблемы решаются на уровне бд и решение применимо к любой ORM.
Насколько я понял, там говорится про sequence из базы данных, т.е. примерно тоже самое, что и я предлагал. Метод, который бы генерировал идентификаторы также, как и NH, был бы в пару строчек кода и умел бы работать с любым sequence. В крайнем случае, можно написать эту пару строчек кода и выложите в Nuget, и проблема решена.
Да, в целом вы правы. Проблема тут только в том, что этой функциональности нет «out of the box»
EF генерит вам временные идентификаторы, которые вы можете использовать для внешних ключей в других сущностях. После фиксации изменений во всех местах (и в первичных ключах и во внешних ключах) временные идентификаторы заменяются на сгенерированные БД. В EF 7 только добавили поддержку sequence, т.к. она появилась в MSSQL.

Минутка некропостинга

Не выкупаю смысл использования целочисленных идентификаторов (если конечно это не легаси)? Почему не использовать uuid/guid который будет 100% уникальным, не создаст проблем в будущем, и может генерироваться через конструктор или иными способами.. т.е. будет заранее вам известен.

Я не предъявляю претензий, но хочу понять "лучшие практики", но вижу в треде споры о вкусовщине.

Потому что автогеновые id - это просто, гарантированно без коллизий, занимают меньше памяти и лучше индексируются, чем guid?

UFO just landed and posted this here
Если у вас database generated id, то не вставлять нельзя. А если нет, то кто мешает руками нужные ид сохранить самостоятельно?
>кто мешает руками нужные ид сохранить самостоятельно?
В EF6 есть только одна стратегия генерации целочисленных Ids — database generated ids, в этом как раз и проблема. Вставка идшников руками решила бы проблему, как это решает Hi/Lo в NH.
А кто мешает отключить автогенерацию и задавать ид руками?
А кто же мешает не вставлять? Просто не вызывать save changes для контекста, а id получить после сохранения,
А зачем вам нужны идентификаторы? Наверное чтобы сослаться на эту сущность в других сущностях, так? Для этого есть NavigationProperty.
Но даже если вам всё таки нужны идентификаторы, то это вполне реализуемо для любой ORM. Храним максимальный идентификатор в каком-нибудь sequence или отдельной таблице, и в рамках одной транзакции увеличиваем его на единицу и достаём из бд. Получаем уникальный идентификатор без необходимости сохранения сущностей в базу.
Пример 3: Read-only коллекция связанных сущностей

В EF можно легко добиться того же разделения, что в NH, за счет либо (а) иной области видимости LinesInternal (тогда можно будет конфигуратор вынести за пределы класса), либо (б) построения выражения для маппинга не через лямбду, а вручную.

Пример 4: Паттерн Unit of Work

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

public IList<int> MigrateCustomers(IEnumerable<CustomerDto> customerDtos, CancellationToken token)
{
    List<Customer> customers = new List<Customer>();
 
    using (var db = new CustomerDbContext())
    {
        foreach (CustomerDto dto in customerDtos)
        {
            token.ThrowIfCancellationRequested();
 
            Customer customer = CreateCustomer(dto);
            customers.Add(db.Add(customer))
        }
 
        db.SaveChangesAsync(token).Wait(); //метод снаружи не асинхронный - а зря
    }

    return customers.Select(c => c.Id).ToList();
}


Если же медленнно работает само сохранение в БД, и отмену надо сделать между двумя строками, то транзакция выгоднее (пусть даже мы и тратим время на ее откат).

Пример 5: Работа с закешированными объектами
[...]
С Entity Framework вы не можете присвоить новому объекту ссылку на detached объект. Если вы напишете код как на примере выше, EF попытается вставить клиента в БД, т.к. он не был приаттачен к текущему контексту.

Это, скажем так, неточно. Действительно, эта проблема существует, но она возникает не в момент присвоения клиента в конструкторе заказа, а при сохранении заказа в БД. В EF достаточно возможностей по анализу графа сущностей перед сохранением, чтобы сделать необходимую проверку и не сохранять лишние объекты в БД.
Изоляция доменной логики означает, что доменные классы могут взаимодействовать только с другими доменными классами

А как вы собираетесь «изолировать логику» проверки уникальности? Например надо не продать два билета на одно место на концерт.
Или как сделать целостность на несколько сущностей? Например не создавать заказ, если на складе больше нет данной позиции.

Теория DDD красива только в теории. На практике сделать прикладную логику только на взаимодействии классов, которые не знают о базе данных — невозможно, кроме самых примитивных моделей.
Теория DDD красива только в теории. На практике сделать прикладную логику только на взаимодействии классов, которые не знают о базе данных — невозможно, кроме самых примитивных моделей.

Это зависит от трактовки DDD. Почему-то каждый раз, когда рассматривают «применимость ORM для DDD», забывают, что в DDD есть не только доменные сущности, но и доменные сервисы — а вот этим уже вполне можно знать про перзистентный слой.
Доменные сервисы вообще-то в DDD и появились потому что изначальная идея о взаимодействии доменных классов нежизнеспособна.
Ну, я не очень хочу спорить о том, почему доменные сервисы появились в DDD, мне достаточно того, что у Эванса они уже есть, и на этом фоне как-то странно, что они регулярно пропадают из дискуссий «как нам сделать на DDD вот такое-то».
Domain Service, к сожалению, разрушает целостную картину DDD.
Возникает много вопросов:
1) Как распределеть отвественность между сервисами, репозитариями и сущностями?
2) Кто вызвает сущности\репозитарии\сервисы?
У Эванса, кстати, нет ответа на эти вопросы.

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

Ответственность репозитория понятна: он должен обеспечить сохранение и восстановление агрегата в/из БД.
Разделение ответственности между сущностями и сервисами, очевидно, не так уж и просто, ну так и работа архитектора — вообще не очень простая. Есть несколько правил большого пальца, одно из которых сводится к тому, что если все данные для работы есть в агрегате, то это его ответственность, если нужно координировать несколько агрегатов — ответственность сервиса.

Кто вызвает сущности\репозитарии\сервисы?

«Следующий» слой. Для приложения это будет либо презентационный слой, либо слой прикладных сервисов, для сервисов — фасад.
Как раз интересует кто кого вызывает в связке app services — domain services — domain entities — repository. Я видел трехдневный холивар на эту тему, причем не в интернатах а вживую.

С репозитариями тоже не все однозначно. Вот требуется вывести список заказов с суммами заказов, рассчитаными на основе OrderLines. Вопрос: где должен быть расчет — в entity, domain service или repository?
Как раз интересует кто кого вызывает в связке app services — domain services — domain entities — repository. Я видел трехдневный холивар на эту тему, причем не в интернатах а вживую.

Холиварить можно на любую тему, было бы желание. Насколько я помню, формально картина выглядит так: прикладные сервисы могут вызывать всех трех, доменные сервисы могут вызывать репозиторий и сущности, сущности могут (но лучше избегать) вызывать доменные сервисы.

Вот требуется вывести список заказов с суммами заказов, рассчитаными на основе OrderLines. Вопрос: где должен быть расчет — в entity, domain service или repository?

Формально этот расчет — ответственность сущности. Сложности возникают вокруг того, как добиться, чтобы у сущности были все данные, нужные для этого расчета, и не было дикого количества запросов к БД.
А как вы собираетесь «изолировать логику» проверки уникальности? Например надо не продать два билета на одно место на концерт.
Или как сделать целостность на несколько сущностей? Например не создавать заказ, если на складе больше нет данной позиции.

Эти задачи вполне решаются передачей их на aggregation root
Да что вы?

Ну вот простая ситуация: две точки продаж билетов по городу. В обоих разные люди пытаются купить билеты. Запросы от точек приходят на разные инстнасы веб-приложения. Что нужно сделать в aggregation root, чтобы гарантированно не продать два билета на место?
Счетчик там храните. Вторая транзакция не зафиксируется, из-за того что первая изменит счетчик в руте и изменит тем самым rowversion рута. Получите во второй транзакции ConcurrencyException и обработаете его.
P.S. Это более общее решение. В данном случае вам AR вообще не нужен, можно просто отловить нарушение UNIQUE KEY.
Именно.

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

Поэтому и не стоит заморачиваться на DDD, лучше работу переложить на БД, генерируя хорошие запросы (что с EF очень легко делать), а в приложении уже разбираться с отображением данных, валидацией ввода. и обработкой ошибок.
Если у вас доменная модель должна учитывать физическую модель развертывания, то вам нужно явно вводить front-end-ы в доменную модель со всеми вытекающими артефактами доменной модели для распределенной обработки. Стоит ли это делать? В случае data-centric application, каким, явно, является заказ билетов, этого не стоит, доменная модель не для него. Ваша объективная реальность лежит среди таких приложений (а их вокруг, действительно, очень много). Поэтому доменная модель вам и кажется необоснованной. Однако это не повод отрицать существование приложений, ориентированных на бизнес-логику, особенно инжектированную бизнес логику. В этом случае доменная модель вполне себе работает.
Любое многопользовательское приложение с бд по вашему будет data-centric. Ведь в сухом остатке важно что попадет в базу, а не какие классы в приложении. Получается единственная область применения DDD — простые, в основном однопользовательские, приложения без конкурентного доступа к данным. Но для таких приложений DDD — избыточные приседания.
Заказ билетов нифига не data-centric, там очень много всего любопытного и интересного может случиться. Просто это типовой пример на concurrency.
в DDD нет нормальной возможности выразить всю логику в терминах операций с доменными объектами.
Почитайте Эванса, помимо доменного уровня в DDD есть и другие уровни. Не нужно запихивать всю логику в доменный уровень.

1) Как распределеть отвественность между сервисами, репозитариями и сущностями?
2) Кто вызвает сущности\репозитарии\сервисы?
У Эванса, кстати, нет ответа на эти вопросы.
У Эванса подробно и детально разжёвано, кто и к кому обращается и какой слой за что несёт ответственность. Процитирую отрывок: «уровни прикладных операций и предметной области обращаются к СЛУЖБАМ, предоставляемым инфраструктурным уровнем.»

И в какой уровень по вашему нужно запихнуть проверку уникальности? И как это соотносится с DDD?

У эванса в книге вообще не расписано как распределять функционал между доменными службами и сущностями. Может вы скажете — расчет суммы заказа по позициям где должен выполняться?
И в какой уровень по вашему нужно запихнуть проверку уникальности?
Если мы проверяем, не продан ли билет на это место перед тем, как начать создание и сохранение билета, то это бизнес-логика приложения: «нельзя продавать два билета на одно и то же место». Проверка в процессе сохранения того, что мы не пытаемся сохранить в персистентное хранилище два билета на одно и то же место — это уже не бизнес-логика приложения, а всего лишь необходимость, возникающая из-за того, что несколько клиентов могут попытаться почти одновременно создать два билета на одно место. А так как это не бизнесс-логика и не координирование задач, то, согласно DDD, проверку уникальности в момент сохранения нужно поместить в инфраструктурный уровень. Процитирую определение Эванса, которое он даёт инфраструктурному уровню: инфраструктурный уровень обеспечивает непосредственную техническую поддержку для верхних уровней: передачу сообщений на операционном уровне, непрерывность существования объектов на уровне модели, вывод элементов управления на уровне пользовательского интерфейса и т.д. Итак, проверка должна находиться в инфраструктурном уровне.
Давайте теперь определимся в каком именно классе. Согласно ООП, данные и код, работающий с этими данными, следует поместить в одном классе. Таким образом, проверка уникальности должна находиться в коде персистентного хранилища. Если в качестве персистентного хранилища у вас используется файл на жёстком диске, то нужно написать класс, который будет производить чтение\запись в этот файл и контролировать уникальность. Если в качестве персистентного хранилища вы используете базу данных, то вам повезло — этот код уже написан за вас и вам нужно лишь декларативно указать, что два билета на одно место сохранять в хранилище нельзя.

расчет суммы заказа по позициям где должен выполняться?
Расчёт суммы заказа это бизнес-логика, она должна находиться в доменном уровне.

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

Ну собственно что и требовалось доказать, важная часть логики (на самом деле САМАЯ важная часть) выпала из структуры DDD вообще.

Расчёт суммы заказа это бизнес-логика, она должна находиться в доменном уровне.

Вы не ответили на конкретный вопрос, это будет класс сущности, domain service или репозиторий?

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

Я её читал более одного раза и очень внимательно. Там нет ответа на этот вопрос. Вообще эванс в книге очень много токних моментов обходит, а потом они всплывают в разработке.
Ну собственно что и требовалось доказать, важная часть логики (на самом деле САМАЯ важная часть) выпала из структуры DDD вообще.
Инфраструктурный уровень, по вашему, не имеет ничего общего со структурой DDD? DDD — это концепция проектирования программ, и понятие инфраструктурного уровня — часть этой концепции.

Вы не ответили на конкретный вопрос, это будет класс сущности, domain service или репозиторий?
Алгоритм расчёта суммы заказа будет или в доменной сущности, или в доменном сервисе — в зависимости от предметной области и сложности самого расчёта.
DDD не описывает инфраструктурный уровень если что. Максимум что описывает DDD — репозиторий, как компонент между инфраструктурой и доменом.

Алгоритм расчёта суммы заказа будет или в доменной сущности, или в доменном сервисе — в зависимости от предметной области и сложности самого расчёта.

Предметная область — интернет-магазин. Расчет — как обычно просто сумма по позициям, но бизнес меняется, в будущем будут и скидки и, возможно, что-то еще.
DDD не описывает инфраструктурный уровень если что. Максимум что описывает DDD — репозиторий, как компонент между инфраструктурой и доменом.
Повторюсь, DDD — это концепция проектирования. Цитируя Эванса, DDD — это «система взглядов и подходов». Один из таких подходов — разделить приложение на уровни, в том числе на инфраструктурный уровень. И репозиторий не является компонентом между инфраструктурным и доменным уровнями.
Если честно, мне немного непонятно, зачем человек, который не знает определения DDD, не понимает какой уровень за что отвечает и пр, берётся пропагандировать, что
Теория DDD красива только в теории.


Расчет — как обычно просто сумма по позициям
Тогда в доменной сущности.
У меня есть предположение, что может не давать вам покоя в вопросе про проверку уникальности билета на стороне хранилища. Сначала нужно сделать проверку в доменном уровне на уникальность, а потом ещё и в инфраструктурном. Но этот вопрос легко решается. В одном месте задаём правило, а потом на основе этого правила и делаем проверку в доменном уровне, и генерируем правило для хранилища. Правила для хранилища можно генерировать например вот так.
Как делать индексы в codefirst я знаю. А как сделать проверку уникальности в доменном уровне? Так чтобы приложение не легло под нагрузкой от 10 таких проверок параллельно.
Повторюсь, DDD — это концепция проектирования.
Это болтология.
Для начала стоит придумать дифференцирующее определение что есть DDD, а что им не является. Причем с точки зрения кода. Иначе вообще нет смысла ни о чем говорить. Эванс такое определение дал в DDD Pattern Language, предложив конкретную архитектуру для DDD.
Вот только эта архитектура получилась дырявая со всех сторон, далеко не всю бизнес-логику можно хорошо выразить в этой архитектуре.

Еще раз повторю, что меня интересует код, а не философия.

Тогда в доменной сущности.
И вы только что сделали получение списка заказов в разы медленнее, чем стоило бы.
И если бы такой случай был единичным, то все было бы ок. Но когда такой подход распространяется на все приложение то начинает тормозить очень сильно. В приложениях довольно часто встречаются master-detail связи и каждый раз надо выводить список master c агрегированием detail.
Как уже говорил lair, холиварить можно на любую тему, было бы желание. Например, знает ли Эванс что такое DDD, или же определение, которое он дал в своей книге, неверно. Имеет ли деление кода на слои и уровни какое то отношение к коду, или это просто болтология. Можно ли реализовать приложение в духе DDD с приемлемым быстродействием. Но у меня нет желания холиварить на эти темы. Так что будем считать что каждый остался при своей точке зрения.
> Расчет — как обычно просто сумма по позициям, но бизнес меняется, в будущем будут и скидки и, возможно, что-то еще.

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

То же самое для сложных валидаций с композитными политиками: собирать политики и дирижировать ими — это работа для сервиса.

Сам по себе доменный объект должен проверить лишь то, о чём он имеет _полную_ информацию. Полнота информации — ключевой фактор! Если объект хранит коллекцию, и у него гарантировано имеется полная информация о всех элементах коллекции — значит нужно проверять и коллекцию. Например, объект и его подчинённые сущности только что созданы, никогда не сохранялись в хранилище с конкурентным доступом и посему гарантированно существуют в единственном экземпляре. Или, скажем, если получение объекта из хранилища автоматически влечёт блокировку этого объекта в хранилище (отдельные места в зале временно блокируются в момент когда пользователь выбирает их, и разблокируются лишь при отмене заказа или по таймауту).

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

Если доменная область такова, что вообще трудно дать какие-то гарантии внутри сущности, и все вопросы приходится решать в сервисах — то это вырождается в anemic domain model + transaction script. Тоже бывает.
Пример 1: Удаление дочерней сущности из корня агрегата
Объясните мне простую вещь. Зачем грузить сущность заказа и все его позиции, чтобы удалить одну позицию?
Более того, даже сам OrderLine грузить не надо, надо лишь присоединить к контексту объект к ID в состоянии Deleted.

Описанная проблема исключительно надуманная (созданная NH). В реальности её не существует.

Пример 2: Ссылка на связанную сущность
Это просто вранье. В EF ты почему-то рассматриваешь Code First, который очевидно генерит схему по свойствам и нужно все поля выписывать в объектах, чтобы сделать мэпинг. А в database first вполне можно замапить связанную сущность без поля внешнего ключа. (также как в NH) Более того, в EF первой версии только так и работало.

НО! Когда создавался EF4 это был самый популярный запрос — сделать поля внешних ключей. Потому что все прекрасно понимают, что для добавления order line вовсе необязательно грузить Order.

Пример 3: Read-only коллекция связанных сущностей
Во-первых это настолько редкий кейс, что любой программист суммарно напишет гораздо меньше кода для реализации всех таких кейсов, чем ты написал для этого примера.

Во вторых, это просто выдавание желаемого за действительное.

Вот EF
    protected virtual ICollection<OrderLine> LinesInternal { get; set; }
    public virtual IReadOnlyList<OrderLine> Lines
    {
        get { return LinesInternal.ToList(); }
    }


Вот NH
    private IList<OrderLine> _lines;
    public virtual IReadOnlyList<OrderLine> Lines
    {
        get { return _lines.ToList(); }
    }


Разница только в видимости, но это прекрасно решается с помощью модификатора internal.

«Проблема» высосана из пальца. На практике вовсе не проблема.

Пример 4: Паттерн Unit of Work
Прости, но это просто бред. Это в NH надо вызывать сохранение объекта в сессии, чтобы id генерировались. А в EF этого делать не надо. Поэтому в EF надо только оставить SaveChanges в конце цикла и все. Даже транзакция не нужна.

Кстати создание ID на клиенте — ахтунг при конкурентном доступе. Так что в этой ситуации EF сработает даже лучше NH.

Пример 5: Работа с закешированными объектами
Снова выдаешь желаемое за действительное.

Во-первых что в EF, что в NH у Order будет parameterless конструктор, так что смысла в создании конструктора с параметром Customer нету.

Во-вторых в EF можно просто id кастомера присвоить свойству-внешнему ключу, для этого даже список кастомеров в памяти держать не надо.

То есть для NH такая проблема есть и она как-то решается. Для EF этой проблемы просто нет.

Короче ты показал как EF не умеет решать проблемы, которые создает NH. Можно ли в этом обвинять EF — сомневаюсь.
Причем в обоих случаях это вообще не решение для encapsulates collections т.к. во-первых .ToList() при каждом обращении, а во вторых если эту коллекцию использовать в запросе, то на SQL это, очевидно, не провалится.
Как можно видеть, по умолчанию, в Entity Framework нужно добавить дополнительное свойство с идентификатором

Насколько я помню — это не обязательно (для CodeFirst, по крайней мере).
Ключи можно указать в маппинге просто строкой (именем столбца-внешнего ключа).
А такое разве сработает если база генерируется по коду?
Есть возможность сконфигурировать такое поведение (точно через fluent configuration, возможно через атрибуты тоже).
Да. Даже промежуточная таблица для связи многие-ко-многим автоматически создастся.
С NHibernate вы можете выбрать стратегию Hi/Lo, так что записи просто не будут сохраняться в БД до момента закрытия сессии. Идентификаторы в этом случае генерируются на клиенте, так что нет необходимости сохранять записи в БД для того, чтобы получить их.

А что будет, если возникнет конфликт генерации Id (во время генерации на клиенте в БД уже появилась запись с таким Id)?
Sign up to leave a comment.

Articles