Re[18]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 04.10.17 07:57
Оценка:
Здравствуйте, Sinclair, Вы писали:
S>Хотелось бы убедительный пример.
100%-но убедительный пример привести не смогу, так как конкретно это технологией не пользовался и реального опыта её применения не имею.
Но могу описать на основании каких представлений у меня сложилось озвученное мнение.
Во-первых, то что бизнес-сущности не всегда один-к-одному соответствуют таблицам я думаю вы согласитесь. В простейших случаях это следствие преднамеренной денормализации (храним список Id строкой, не охота городить таблицу; слабоструктурированные динамические данные — храним JSON в строке; ...). Работу с такими штуками на уровне linq не выразить.
Во-вторых, поведение часто бывает динамическим. Давайте разовьём пример с заказами. Пусть всего в системе у заказа может быть пять способов его оплаты. Кроме того, заказ может входить в некие программы поставок двух типов (а может и не входить). Грубо говоря, это нам даёт максимум 3х5=15 вариаций поведения на каждый аспект заказа. Окей, происходит бизнес-операция — изменение заказа, включающее в себя изменение способа оплаты и перенос в программу поставок. Рассмотрим один из аспектов — стоимость заказа. Стоимость определяется ценой (которая зависит от способа оплаты) и скидкой (которая зависит от программы поставок). В итоговом запросе мы должны получить что-то вроде
UPDATE order SET
  ...
  Cost = x.Price * quantity * y.discount,
  ...
FROM Orders
    INNER JOIN PriceListFromPaymentMethod x ON ....
    INNER JOIN SupplyPrograms y ON ...
    ...

Это и есть одна из 15 вариаций, и только для свойства Cost.

Когда всё на сущностях, то всё просто. Способ определения цены — это абстракция (некая стратегия), способ определения скидки — тоже. Имплементации смотрят на сущность заказа и вычисляют значение по соответствующему алгоритму. Первая стратегия выдаёт "используйте цену 1000", вторая — "используйте скидку 10%". Код, выполняющий операцию, посчитал итоговую стоимость (900), проставил её в сущность. По другим аспектам отработали свои стратегии, заполнились остальные свойства. В итоге свойства сущности были изменены как надо, дёргаем DAL, он всё сохранил, красота.
Теперь как это провернуть на linq? Вижу два варианта.
а) Какие-то динамические запросы... Каждая стратегия вместо того чтобы просто "взять и посчитать" будет куда-то добавлять свою часть запроса. В итоге будет построен какой-то огромный запрос, которые таки да, одной операцией всё пересчитает и обновит. Но зачем эта промежуточная модель? Попробуйте её отладить...
б) Можно апдейтить свойства по одному — одним запросом обновили цену заказа, другим — другое свойство. Будет ли это просто, понятно и эффективно? Тоже сомневаюсь. Какие там ещё проблемы всплывут в конкурентной среде? Теоретически ведь можем в разных запросах использовать одни и те же данные — значит уже нужен repeatable read...
Brainbench transcript #6370594
Отредактировано 04.10.2017 7:59 Poul_Ko . Предыдущая версия .
Re[19]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 05.10.17 03:19
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

S>>Хотелось бы убедительный пример.

P_K>100%-но убедительный пример привести не смогу, так как конкретно это технологией не пользовался и реального опыта её применения не имею.
Не-не-не. Вы мне не linq пример приведите, в пример логики, которая "плохо соответствует таблицам".
P_K>Но могу описать на основании каких представлений у меня сложилось озвученное мнение.
P_K>Во-первых, то что бизнес-сущности не всегда один-к-одному соответствуют таблицам я думаю вы согласитесь. В простейших случаях это следствие преднамеренной денормализации (храним список Id строкой, не охота городить таблицу; слабоструктурированные динамические данные — храним JSON в строке; ...).
Откуда в бизнес-логике взялся JSON? Это же всего лишь представление — оно появляется только при взаимодействии с другими системами.
P_K>Во-вторых, поведение часто бывает динамическим. Давайте разовьём пример с заказами. Пусть всего в системе у заказа может быть пять способов его оплаты. Кроме того, заказ может входить в некие программы поставок двух типов (а может и не входить). Грубо говоря, это нам даёт максимум 3х5=15 вариаций поведения на каждый аспект заказа. Окей, происходит бизнес-операция — изменение заказа, включающее в себя изменение способа оплаты и перенос в программу поставок. Рассмотрим один из аспектов — стоимость заказа. Стоимость определяется ценой (которая зависит от способа оплаты) и скидкой (которая зависит от программы поставок). В итоговом запросе мы должны получить что-то вроде
P_K>
P_K>UPDATE order SET
P_K>  ...
P_K>  Cost = x.Price * quantity * y.discount,
P_K>  ...
P_K>FROM Orders
P_K>    INNER JOIN PriceListFromPaymentMethod x ON ....
P_K>    INNER JOIN SupplyPrograms y ON ...
P_K>    ...
P_K>

P_K>Это и есть одна из 15 вариаций, и только для свойства Cost.


P_K>Когда всё на сущностях, то всё просто. Способ определения цены — это абстракция (некая стратегия), способ определения скидки — тоже. Имплементации смотрят на сущность заказа и вычисляют значение по соответствующему алгоритму. Первая стратегия выдаёт "используйте цену 1000", вторая — "используйте скидку 10%". Код, выполняющий операцию, посчитал итоговую стоимость (900), проставил её в сущность. По другим аспектам отработали свои стратегии, заполнились остальные свойства. В итоге свойства сущности были изменены как надо, дёргаем DAL, он всё сохранил, красота.

Давайте напишем для начала всё это на чистом С#.
Вот у нас, допустим, класс Order. У него есть операция CalculateCost(). Как она устроена?
Мы будем делать 15 наследников класса Order c перегрузками?
Или у нас будут свойства PaymentMethod и SupplyProgram, у которых методы GetPrice() и GetDiscount() — виртуальные?
Давайте детализировать.
P_K>Теперь как это провернуть на linq? Вижу два варианта.
P_K>а) Какие-то динамические запросы... Каждая стратегия вместо того чтобы просто "взять и посчитать" будет куда-то добавлять свою часть запроса. В итоге будет построен какой-то огромный запрос, которые таки да, одной операцией всё пересчитает и обновит. Но зачем эта промежуточная модель? Попробуйте её отладить...
Ничего сложного.
Предположим, к примеру, что PaymentMethod — это один из пяти well-known типов, т.е. покрыт перечислением (и добавлять новый метод без перекомпиляции мы не планируем).
У нас есть где-то на более-менее корневом уровне сервис получения прайс-листа: GetPriceList(PaymentMethod paymentMethod).
В Linq-мире он возвращает IQueryable<PriceListItem>. Внутри он может быть устроен более-менее как угодно:
{
  switch(paymentMethod)
  {
    case PaymentMethod.Cash: 
      return from db.CashPrices select new PriceListItem(itemId, price);
    case PaymentMethod.Visa: 
      return from db.CCPrices select new PriceListItem(itemId, visaPrice);
    case PaymentMethod.MasterCard:
      return from db.CCPrices select new PriceListItem(itemId, masterCardPrice);
  }
}

То есть у нас тут и разные таблицы, и разные колонки в одной таблице.
В итоге, метод заказа устроен как-то примерно так:
var totalCost = (from pl in GetPriceList(PaymentMethod) join i in Items on pl.itemId equals i.itemId select i.quantity * pl.price).Sum();

И его вычисление вместо нудных N+1 запросов превратится в нормальный join c агрегатом на стороне СУБД. Писать и отлаживать это ещё проще, чем пошаговую логику в традиционном ERP-приложении с Rich ORM и Lazy Load.
При этом производительность будет как минимум на порядок выше.

P_K>б) Можно апдейтить свойства по одному — одним запросом обновили цену заказа, другим — другое свойство. Будет ли это просто, понятно и эффективно? Тоже сомневаюсь. Какие там ещё проблемы всплывут в конкурентной среде? Теоретически ведь можем в разных запросах использовать одни и те же данные — значит уже нужен repeatable read...
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[20]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 05.10.17 04:39
Оценка:
Здравствуйте, Sinclair, Вы писали:
P_K>>Во-первых, то что бизнес-сущности не всегда один-к-одному соответствуют таблицам я думаю вы согласитесь. В простейших случаях это следствие преднамеренной денормализации (храним список Id строкой, не охота городить таблицу; слабоструктурированные динамические данные — храним JSON в строке; ...).
S>Откуда в бизнес-логике взялся JSON? Это же всего лишь представление — оно появляется только при взаимодействии с другими системами.
Но ведь linq оперирует объектами, напрямую отражающими структуру таблиц?
Предположим что у нас прайс-лист для какого-то одного случая хранится в базе в JSON-строке. Когда я пишу логику на сущностях мне пофиг — DAL достанет этот JSON и переведёт его в объекты, которыми я уже и буду оперировать. А вот когда я пишу логику на linq? Как я смогу достать из этого прайс-листа несколько нужных мне позиций запросом? Вот я о чём.

P_K>>Во-вторых, поведение часто бывает динамическим. Давайте разовьём пример с заказами. Пусть всего в системе у заказа может быть пять способов его оплаты. Кроме того, заказ может входить в некие

S>Давайте напишем для начала всё это на чистом С#.
S>Вот у нас, допустим, класс Order. У него есть операция CalculateCost(). Как она устроена?
S>Мы будем делать 15 наследников класса Order c перегрузками?
Нет, всё не так. Заказ не считает свою цену
S>Или у нас будут свойства PaymentMethod и SupplyProgram, у которых методы GetPrice() и GetDiscount() — виртуальные?
Опять мимо. Я же написал, будут использоваться стратегии. Как-то так:
public interface IPricingStrategy {    // Будет реализация, которая смотрит на заказ и по его свойствам определяет откуда брать цену, плюс простая реализация для тестов
  decimal GetPrice(Order order);
}

public interfact IDiscountingStrategy {    // Будет реализация, которая смотрит на заказ и по его свойствам определяет откуда брать скидку, плюс простая реализация для тестов
  decimal GetDiscountPercents(Order oreder);
}

// Пересчёт стоимости заказа после изменений
private void RecalcOrderCost(Order order) {
  var price = _pricingStrategy.GetPrice(order);
  var discount = _discountingStrategy.GetDiscountPercents(order) / 100M;
  order.Cost = order.Quantity * price * (1M - discount);
}

Нетестовые реализации стратегий могут быть разные — где-то в виде swith или набора if, где-то более изощрённо-динамически, но не суть.

S>Давайте детализировать.

S>
S>var totalCost = (from pl in GetPriceList(PaymentMethod) join i in Items on pl.itemId equals i.itemId select i.quantity * pl.price).Sum();
S>

S>И его вычисление вместо нудных N+1 запросов превратится в нормальный join c агрегатом на стороне СУБД. Писать и отлаживать это ещё проще, чем пошаговую логику в традиционном ERP-приложении с Rich ORM и Lazy Load.
Но опять же, это будет отдельный запрос, который вернёт сразу сумму. А стоимость нужно будет потом проапдейтить в заказе — другим запросом.
Точно так же будет и в моём случае — реализация стратегии сбегает в базу, посчитает стоимость и отдаст сразу в виде decimal. А не в виде IQueriable<что-то>, что нужно ещё потом правильно сджинить. Кроме того, стратегия может бросить исключение, например, "для товара нет цены в прайс-листе", что является предсказуемой бизнес-ситуацией и обрабатывается. А вот как это выяснить для IQueriable?
S>При этом производительность будет как минимум на порядок выше.
Будет выше — скорее всего. На порядок или нет — спорить не готов.

Вернёмся к исходной проблеме. Мы хотим обеспечить ситуацию, чтобы на время обновления заказа использованный прайс-лист не мог быть изменён.
Что в вашем случае, что в моём поведение с т.з. базы одинаково — будет последовательность запросов:
1) найти заказ
2) найти цены
3) проапдейтить заказ
Ни тот ни другой подход не обеспечат того, что между 2 и 3 использованные в шаге 2 сущности не поменяются. Поэтому нет смысла продолжать спорить в рамках этой темы о том как лучше выбирать данные — через linq или нет.
Brainbench transcript #6370594
Re[21]: Блокировки в бизнес-слое
От: samius Япония http://sams-tricks.blogspot.com
Дата: 05.10.17 04:49
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Ни тот ни другой подход не обеспечат того, что между 2 и 3 использованные в шаге 2 сущности не поменяются. Поэтому нет смысла продолжать спорить в рамках этой темы о том как лучше выбирать данные — через linq или нет.


Если хранить не текущую цену, а историю изменения цен, а при формировании Order выбирать последнюю установленную цену на момент формирования, то станет при любом пододе (linq или не linq) сложно накосячить. Да и тема блокировки в бизнес-слое исчезает.
Re[21]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 05.10.17 04:57
Оценка:
Здравствуйте, Poul_Ko, Вы писали:
P_K>Но ведь linq оперирует объектами, напрямую отражающими структуру таблиц?
Да.
P_K>Предположим что у нас прайс-лист для какого-то одного случая хранится в базе в JSON-строке. Когда я пишу логику на сущностях мне пофиг — DAL достанет этот JSON и переведёт его в объекты, которыми я уже и буду оперировать. А вот когда я пишу логику на linq? Как я смогу достать из этого прайс-листа несколько нужных мне позиций запросом? Вот я о чём.
Во-первых, как минимум — не хуже, чем в "логике на сущностях".
Во-вторых, если СУБД умеет работать с JSON, то у вас есть шанс подпилить маппер так, чтобы парсинг выполнялся на стороне СУБД.
В-третьих, если СУБД не умеет работать с JSON, но вы всё равно храните данные в нём, и при этом вам надо работать с отдельными позициями, то вам пора уволить архитектора, пока он вас не загнал в банкротство.

P_K>Опять мимо. Я же написал, будут использоваться стратегии. Как-то так:

P_K>[c#]
P_K>public interface IPricingStrategy { // Будет реализация, которая смотрит на заказ и по его свойствам определяет откуда брать цену, плюс простая реализация для тестов
P_K> decimal GetPrice(Order order);
P_K>}
О
P_K>public interfact IDiscountingStrategy { // Будет реализация, которая смотрит на заказ и по его свойствам определяет откуда брать скидку, плюс простая реализация для тестов
P_K> decimal GetDiscountPercents(Order oreder);
P_K>}
Ок. Откуда берутся эти стратегии? Приведите реализацию какой-нибудь из этих стратегий. Пока что всё ещё непонятно, что там будет за код, и почему его трудно переписать на linq.

P_K>Но опять же, это будет отдельный запрос, который вернёт сразу сумму. А стоимость нужно будет потом проапдейтить в заказе — другим запросом.

P_K>Точно так же будет и в моём случае — реализация стратегии сбегает в базу, посчитает стоимость и отдаст сразу в виде decimal.
Хорошо, если так. Пока я не увижу кода реализации, я не могу понять, что вы имеете в виду под "сбегает в базу".

P_K>А не в виде IQueriable<что-то>, что нужно ещё потом правильно сджинить. Кроме того, стратегия может бросить исключение, например, "для товара нет цены в прайс-листе", что является предсказуемой бизнес-ситуацией и обрабатывается. А вот как это выяснить для IQueriable?

В целом — точно так же. В идеале, "стратегия" будет просто скомпилирована в SQL, который выполнится прямо в базе. И вместо отдачи decimal, который на клиенте нужен только для того, чтобы тут же отдать его обратно в базу, вернёт expression, который можно использовать в update order set TotalCost = <>.

S>>При этом производительность будет как минимум на порядок выше.

P_K>Будет выше — скорее всего. На порядок или нет — спорить не готов.
Тут всё очень просто: как правило, такие "маленькие" запросы на стороне сервера не стоят почти ничего. Основная стоимость — это roundtrip. Когда в заказе примерно 10 позиций, как раз и получаем примерно десятикратное улучшение производительности.

P_K>Вернёмся к исходной проблеме. Мы хотим обеспечить ситуацию, чтобы на время обновления заказа использованный прайс-лист не мог быть изменён.

P_K>Что в вашем случае, что в моём поведение с т.з. базы одинаково — будет последовательность запросов:
P_K>1) найти заказ
P_K>2) найти цены
P_K>3) проапдейтить заказ
P_K>Ни тот ни другой подход не обеспечат того, что между 2 и 3 использованные в шаге 2 сущности не поменяются. Поэтому нет смысла продолжать спорить в рамках этой темы о том как лучше выбирать данные — через linq или нет.
Я вам попробую ещё раз объяснить, что ключ к успеху — это
1. Использовать возможности СУБД для изоляции транзакций. То есть следить за тем, чтобы все три пункта выполнялись в рамках одной транзакции с подходящим уровнем изоляции.
2. Минимизировать количество раундтрипов между СУБД и миддл-тир, потому что они увеличивают время выполнения транзакции и, соответственно, увеличивают шансы напороться на ожидание или дедлок.
3. Традиционным ответом на эти два требования является написание хранимок. К сожалению, у хранимок есть несколько фундаментальных проблем:
3.1. Плохие возможности по декомпозиции. Введение table-valued функций несколько помогает, но даже с ними современный SQL на поколения отстаёт от полноценных языков программирования.
3.2. Проблемы по синхронизации версий клиента и сервера. Можно запросто напороться на ситуацию, когда версия кода в миддл-тир не совпадает с версией кода в хранимках, и происходят трудноуловимые глюки.
3.3. Проблемы с рефакторингом и отладкой. В отличие от C#, статически гаратировать корректность кода на SQL очень тяжело.
4. Потенциальным решением для этих проблем является linq — как способ порождения SQL кода из C#-кода. В таком варианте ваш код всегда корректен, дружественен оптимизатору СУБД, и при этом его всё ещё может читать и поддерживать живой человек.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[22]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 05.10.17 06:46
Оценка: +1
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, Poul_Ko, Вы писали:

P_K>>Но ведь linq оперирует объектами, напрямую отражающими структуру таблиц?
S>Да.
P_K>>Предположим что у нас прайс-лист для какого-то одного случая хранится в базе в JSON-строке. Когда я пишу логику на сущностях мне пофиг — DAL достанет этот JSON и переведёт его в объекты, которыми я уже и буду оперировать. А вот когда я пишу логику на linq? Как я смогу достать из этого прайс-листа несколько нужных мне позиций запросом? Вот я о чём.
S>Во-вторых, если СУБД умеет работать с JSON, то у вас есть шанс подпилить маппер так, чтобы парсинг выполнялся на стороне СУБД.
Юмор оценил
S>В-третьих, если СУБД не умеет работать с JSON, но вы всё равно храните данные в нём, и при этом вам надо работать с отдельными позициями, то вам пора уволить архитектора, пока он вас не загнал в банкротство.
Здесь согласен. Но в жизни не всё так просто. Бывают случаи когда сначала работать с отдельными позициями не надо, а потом вдруг стало надо в каком-то одном редком случае. И возникает два пути — либо нормализовать денормализованное, либо работать как с объектами после десериализации. В случае архитектуры "на сущностях" этот выбор есть, в случае linq — увы.

S>Ок. Откуда берутся эти стратегии? Приведите реализацию какой-нибудь из этих стратегий. Пока что всё ещё непонятно, что там будет за код, и почему его трудно переписать на linq.

Реализация будет какой-то такой:
public class PricingStrategy : IPricingStrategy {
  public decimal GetPrice(Order order) {
    if (order.PaymentMethod == PaymentMethods.Method1) {
      var priceListItem = _method1PriceListDal.FindForProduct(order.ProductId);            // Выльется в SELECT ... FROM PriceList WHERE ProductId = @p1
      if (priceListItem == null) throw new Method1ProductPriceNotFound(order.ProductId);    // Либо какая-то логика, например, взять из "дефолтного" прайса
      return priceListItem.Price;
    }
  ...
  }

Для простоты здесь в заказе нет набора айтемов, всегда один продукт. Но расширить до коллекции не сложно — будет всё равно один запрос (с IN) и возвращаться будет, например, IDictionary<int, decimal>.

P_K>>А не в виде IQueriable<что-то>, что нужно ещё потом правильно сджинить. Кроме того, стратегия может бросить исключение, например, "для товара нет цены в прайс-листе", что является предсказуемой бизнес-ситуацией и обрабатывается. А вот как это выяснить для IQueriable?

S>В целом — точно так же. В идеале, "стратегия" будет просто скомпилирована в SQL, который выполнится прямо в базе. И вместо отдачи decimal, который на клиенте нужен только для того, чтобы тут же отдать его обратно в базу, вернёт expression, который можно использовать в update order set TotalCost = <>.
Вот это очень важное уточнение вы почему-то опустили в предыдущем посте, оно в корне меняет смысл.
Но сначала о другом. Чем ещё не нравится использование IQueriable в бизнес коде: "не шибко опытные" разработчики могут запросто сделать с ним что-то, что генератор запросов не переварит (.Select(x => new SomeClass(x.Prop1, x.Prop2)). То есть, сам по себе IQueriable/IEnumerable не говорит что он связан с базой, и это нужно всегда держать в голове. Это больше вопрос дисциплины и т.д., но чем меньше шансов совершить ошибку, тем лучше. Когда работа с базой лежит строго в отдельном слое, то можно неопытных туда не пускать. А когда она выставлена наружу вот так неявно, это уже становится опасно.

S>Я вам попробую ещё раз объяснить, что ключ к успеху — это

S>1. Использовать возможности СУБД для изоляции транзакций. То есть следить за тем, чтобы все три пункта выполнялись в рамках одной транзакции с подходящим уровнем изоляции.
Это уже есть сейчас в системе, от используемого подхода не зависит.
S>2. Минимизировать количество раундтрипов между СУБД и миддл-тир, потому что они увеличивают время выполнения транзакции и, соответственно, увеличивают шансы напороться на ожидание или дедлок.
Здесь полностью согласен. Описанный подход на linq действительно позволит перенести какие-то операции в БД, сделав один запрос вместо нескольких.
S>3. Традиционным ответом на эти два требования является написание хранимок. К сожалению, у хранимок есть несколько фундаментальных проблем:
Хранимки не решают первую проблему, это мы уже выяснили раньше. В остальном согласен — устаревшая монструозная технология, подходит для совсем простых задач.
S>4. Потенциальным решением для этих проблем является linq — как способ порождения SQL кода из C#-кода. В таком варианте ваш код всегда корректен, дружественен оптимизатору СУБД, и при этом его всё ещё может читать и поддерживать живой человек.
При этом код становится хитрым, за счёт оперирования какими-то отложенными запросами, и в него просачиваются ограничения СУБД. Для каких-то задач это несущественно, для каких-то — будет проблемой.

Если вам интересно — можем дальше продолжить обсуждение "как сделать Х на linq2db", я ещё вижу некоторые проблемы.
Если оперировать книжными терминами, то мне кажется что linq2db идеально подходит для паттерна transaction script, но вот со сложной domain model его не подружить.

А пока на всякий случай обозначу: считаю, что помимо озвученных вами вариантов решения (хранимки и экономия запросов) имеет право на жизнь вариант "ручных крупногранулярных блокировок", который просто исключает саму возможность ожиданий и дедлоков на уровне БД. Да, ожидание будет, просто на другом уровне, и да не всегда система будет работать максимально эффективно. Но решение выглядит наиболее простым и понятным, а это по моему мнению очень важно в enterprise-разработке.
Brainbench transcript #6370594
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.