- PVSM.RU - https://www.pvsm.ru -
Объектно-ориентированная парадигма — стандарт для прикладного ПО. Реляционные СУБД — стандарт хранения данных в прикладном ПО. Да, можно писать и на Haskell и хранить данные исключительно в ClickHouse. Но речь о мейнстриме.
ORM позволяет натянуть сову на глобус сделать вид, что RDBMS'а нет и данные хранятся в объектной модели, более подходящей для ООП. Остается «маленькая» такая проблемка — эта абстракция, как и многие другие, «течет». Там где в объектной модели ссылка на другой объект в базе данных foreign key и id. В момент материализации сущности мы встаем перед выбором:
Какую-же ногу себе отрезать: левую или правую?
TLDR Lazy Load не так плох, если использовать только для записи и не использовать при чтении. Но все не так просто и есть куча нюансов.
Со временем я пришел ко мнению, что Lazy Load и/или зависимость сущностей от реализации ORM -меньшее из зол при соблюдении некоторых условий.
В 90% случаев проблемы с Lazy Load возникают именно при чтении. Получаем список сущностей, пробегаемся по нему циклом и начинаем выбирать все необходимые данные. Получаем вал запросов к БД. При этом чаще всего единственное, что нужно сделать — это получить данные, сериализовать и отправить их в ответ в виде JSON. Зачем же тогда вообще загружать сущности? Нет никакой нужды добавлять эти данные в change tracker UOW, читать целиком сущность вместе с «лишними» полями. Вместо этого можно всегда писать либо Select
, либо ProjectTo [2]
. Lazy Load не потребуется, потому что C#-код из Select
будет транслирован в SQL и выполнен на стороне БД.
Client Evaluation [3] я рекомендую держать выключенным. Во первых, можно «помочь» и дописать поддержку необходимых функций прямо в субд [4]. Не самый плохой вариант, если речь идет о простых вычислениях, а не бизнес-правилах. Вариант номер два: выделить интерфейс из сущности и реализовать его и в сущности и в DTO.
Например, в БД есть два поля: «цена без скидки» и «цена со скидкой». Если поле «цена со скидкой» заполнено, то используем его, если нет — то используем поле с обычной ценой. Добавим еще одно правило. При покупке 3 товаров вы платите только за 2 самых дорогих, при этом обычные скидки также учитываются.
Реализация может быть такой:
public interface IHasProductPrice
{
decimal BasePrice { get; }
decimal? SalePrice { get; }
}
public class Product: IHasProductPrice
{
// ... a lot of code
public decimal BasePrice { get; protected set;}
public decimal? SalePrice { get; protected set;}
}
public class ProductDto: IHasProductPrice
{
public decimal BasePrice { get; set;}
public decimal? SalePrice { get; set;}
}
public static class ProductCalculator
{
public static void decimal Calculate(IEnumerable<IHasProductPrice> prices)
}
Во write-подсистеме, наоборот, довольно часто только id для записи не достаточно. Всевозможные проверки не редко заставляют читать сущность целиком, потому что объектная парадигма предполагает совмещение данных и операций над ними в рамках объекта класса и его инварианта. Если в проекте используется DDD, то операции записи/изменения должны производиться через корень агрегации, а значит только над одним объектом и его зависимостями. Большое количество запросов может возникнуть только при работе со связанными коллекциями.
Если в агрегате слишком много данных, это может свидетельствовать о проблемах с проектированием. Типичные корни агрегации — корзина, заказ, посылка. Люди обычно не работают с данными из тысяч строк, поэтому загрузка всей связанной коллекции может быть не самой производительной, но не смертельной операцией. А вот если в коллекции тысячи объектов, возможно, что такого корня агрегации на самом деле нет и его придумали разработчики, потому то было очень просто это сделать с помощью подручных инструментов.
Передайте DbContext
в конструктор [5] и читайте из него только необходимые в контексте операции данные. Да, нарушаем DIP. Либо так, либо вообще не использовать агрегат в этом случае.
Импорт файла на 10.000 строк отличная мишень для Lazy Load. Здесь ко всем проблемам read-подсистемы добавляются еще и тормоза ChangeTracker'а. Для массовой записи нужно использовать отдельные инструменты [6]. Я отдаю предпочтения Batch Extensions, потому что опять можно обойтись без создания сущностей. Для особо тяжелых случаев существуют старые добрые хранимые процедуры и даже специальные средства СУБД [7].
Если нужно реализовать и массовую операцию и обычную, нужно начинать с массовой. Обычная операция — просто частный случай массовой, кода в последовательности только один элемент.
Автор: marshinov
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/306416
Ссылки в тексте:
[1] tell don't ask: https://martinfowler.com/bliki/TellDontAsk.html
[2] ProjectTo: http://docs.automapper.org/en/stable/Queryable-Extensions.html
[3] Client Evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval
[4] прямо в субд: http://anthonygiretti.com/2018/01/11/entity-framework-core-2-scalar-function-mapping/
[5] конструктор: https://docs.microsoft.com/en-us/ef/core/modeling/constructors#injecting-services
[6] инструменты: https://github.com/borisdj/EFCore.BulkExtensions
[7] специальные средства СУБД: https://docs.microsoft.com/en-us/sql/t-sql/statements/bulk-insert-transact-sql?view=sql-server-2017
[8] Источник: https://habr.com/ru/post/437418/?utm_campaign=437418
Нажмите здесь для печати.