About entities, DTO, ORM and Lazy Load
Object Oriented Paradigm is the standard for application software. Relational DBMS is a standard for storing data in application software. Yes, you can write in Haskell and store data exclusively in ClickHouse. But talking about the mainstream.
The ORM allows you topull an owl on a globe to pretend that there is no RDBMS and the data is stored in an object model that is more suitable for OOP. There remains a “small” such problem — this abstraction, like many others, “flows”. Where in the object model there is a link to another object in the foreign key and id database. At the moment of the materialization of the entity, we face a choice:
Which leg should you cut off: left or right?
TLDR Lazy Load is not so bad if used only for writing and not used when reading. But everything is not so simple and there are a lot of nuances.
Over time, I came to the conclusion that Lazy Load and / or the dependence of entities on the implementation of ORM is the lesser of evils under certain conditions.
In 90% of cases, problems with Lazy Load arise precisely when reading. We get a list of entities, run over it in a cycle and start to select all the necessary data. We receive a shaft of inquiries to a DB. In this case, most often the only thing that needs to be done is to get the data, serialize it and send it back as JSON. Why, then, load entities at all? There is no need to add this data to the change tracker UOW, to read the whole entity along with the “extra” fields. Instead, you can always write either
I recommend to keep Client Evaluation off. First, you can "help" and add support for the necessary functions directly in the subd . Not the worst option if it is a question of simple calculations, but not business rules. Option number two: select the interface from the entity and implement it in essence and in the DTO.
For example, there are two fields in the database: “price without discount” and “price with discount”. If the “discount price” field is filled, then use it; if not, then use the field with the regular price. Add another rule. When you buy 3 products you pay only for the 2 most expensive, while the usual discounts are also taken into account.
The implementation may be:
In the write subsystem, on the contrary, quite often only the id for writing is not enough. All sorts of checks are not rarely forced to read the essence of the whole, because the object paradigm involves the combination of data and operations on them within the object of the class and its invariant. If DDD is used in the project, then write / change operations should be performed through the aggregation root, which means only on one object and its dependencies. A large number of queries can occur only when working with related collections.
If there is too much data in the aggregate, this may indicate design problems. Typical aggregation roots are basket, order, package. People usually do not work with data from thousands of rows, so loading the entire associated collection may not be the most productive, but not lethal operation. But if there are thousands of objects in the collection, it is possible that there really is no such aggregation root and it was invented by the developers, because it was very easy to do this with the help of improvised tools.
Pass
Importing a 10,000-line file is an excellent target for Lazy Load. Here, the ChangeTracker brakes are also added to all the problems of the read subsystem. For mass recording, you need to use separate tools . I prefer Batch Extensions, because again you can do without creating entities. For especially severe cases, there are good old stored procedures and even special DBMS tools .
If you want to implement a mass operation and an ordinary one, you need to start with a mass one. A normal operation is just a special case of a mass, the code in the sequence is only one element.
The ORM allows you to
- Download everything and drop out of memory / timeout
- Clearly specify what we want depending on load, and what - no, and violate the principle of tell do not ask
- Load dependencies implicitly on demand with Lazy Load and get performance problems somewhere in the called code.
Which leg should you cut off: left or right?
TLDR Lazy Load is not so bad if used only for writing and not used when reading. But everything is not so simple and there are a lot of nuances.
Over time, I came to the conclusion that Lazy Load and / or the dependence of entities on the implementation of ORM is the lesser of evils under certain conditions.
In the read subsystem, always read only DTO
In 90% of cases, problems with Lazy Load arise precisely when reading. We get a list of entities, run over it in a cycle and start to select all the necessary data. We receive a shaft of inquiries to a DB. In this case, most often the only thing that needs to be done is to get the data, serialize it and send it back as JSON. Why, then, load entities at all? There is no need to add this data to the change tracker UOW, to read the whole entity along with the “extra” fields. Instead, you can always write either
Select
, or ProjectTo
. Lazy Load is not required because the C # code from Select
will be translated to SQL and executed on the database side.What if my logic is not translated to SQL?
I recommend to keep Client Evaluation off. First, you can "help" and add support for the necessary functions directly in the subd . Not the worst option if it is a question of simple calculations, but not business rules. Option number two: select the interface from the entity and implement it in essence and in the DTO.
For example, there are two fields in the database: “price without discount” and “price with discount”. If the “discount price” field is filled, then use it; if not, then use the field with the regular price. Add another rule. When you buy 3 products you pay only for the 2 most expensive, while the usual discounts are also taken into account.
The implementation may be:
publicinterfaceIHasProductPrice
{
decimal BasePrice { get; }
decimal? SalePrice { get; }
}
publicclassProduct: IHasProductPrice
{
// ... a lot of codepublicdecimal BasePrice { get; protectedset;}
publicdecimal? SalePrice { get; protectedset;}
}
publicclassProductDto: IHasProductPrice
{
publicdecimal BasePrice { get; set;}
publicdecimal? SalePrice { get; set;}
}
publicstaticclassProductCalculator
{
publicstaticvoiddecimalCalculate(IEnumerable<IHasProductPrice> prices)
}
The Lazy Load write subsystem is not so bad
In the write subsystem, on the contrary, quite often only the id for writing is not enough. All sorts of checks are not rarely forced to read the essence of the whole, because the object paradigm involves the combination of data and operations on them within the object of the class and its invariant. If DDD is used in the project, then write / change operations should be performed through the aggregation root, which means only on one object and its dependencies. A large number of queries can occur only when working with related collections.
Related collections in aggregates
If there is too much data in the aggregate, this may indicate design problems. Typical aggregation roots are basket, order, package. People usually do not work with data from thousands of rows, so loading the entire associated collection may not be the most productive, but not lethal operation. But if there are thousands of objects in the collection, it is possible that there really is no such aggregation root and it was invented by the developers, because it was very easy to do this with the help of improvised tools.
What if there are thousands of entries in the aggregate
Pass
DbContext
to the constructor and read from it only the data necessary in the context of the operation. Yes, we break the DIP. Either that or not to use the unit at all in this case.Mass operations
Importing a 10,000-line file is an excellent target for Lazy Load. Here, the ChangeTracker brakes are also added to all the problems of the read subsystem. For mass recording, you need to use separate tools . I prefer Batch Extensions, because again you can do without creating entities. For especially severe cases, there are good old stored procedures and even special DBMS tools .
Life hacking
If you want to implement a mass operation and an ordinary one, you need to start with a mass one. A normal operation is just a special case of a mass, the code in the sequence is only one element.