Кирилл Безпалый, .NET Developer, Ciklum
- 1. ORM OR NOT ORM?
THAT IS THE QUESTION
BEZPALYI KYRYLO, .NET DEVELOPER @ CIKLUM
- 2. ABOUT ME
• .Net developer @ Ciklum
• Microsoft Student Partner
• An avid programmer
- 3. BEFORE WE BEGIN
ALL NON-TRIVIAL ABSTRACTIONS,
TO SOME DEGREE, ARE LEAKY.
Joel Spolsky,
The Law of Leaky Abstractions
YOU SHOULD ALWAYS
UNDERSTAND AT LEAST ONE
LAYER BELOW WHAT YOU ARE
CODING.
Lee Campbell, author of IntroToRX.com
- 4. SMALL HISTORY
• ADO.NET
• Data Sets
• Strong-typed Data Sets
• NHibernate
• LINQ to SQL
• Entity Framework
• EF Code First
• EF Core
- 5. WHAT IS .NET WAY TODAY?
• .NET
• ASP.NET (WebApi/MVC)
• Sql Server
• Entity Framework
- 6. WHAT’S .NET DEVELOPERS THINK ABOUT EF?
• Too much configuration (Fluent API, attributes, navigation properties, keys,
relations, etc.)
• Leaking abstractions (LINQ)
• Object model does not fit business model
• Things become complicated very fast
- 8. PROBLEM OF ENTITY FRAMEWORK
• Performance
• Parameter Sniffing
• Lazy Load
• Data Modification
• Some crazy stuff
- 10. HIDDEN POTENTIAN IQUERYABLE
• Do you know difference between IEnumerable and IQueryable. Lot of tutorials
don`t show full power of IQueryable showing only some simple examples.
• In practice: IQueryable has generic logic of query using Expression Trees data
structure and Provider that can transform this logic in query to some data source.
- 11. MORE ABOUT EXPRESSION TREE
• Expression tree it’s a tree of operations and their operands which
can be other operations
• Generally Expression tree is a serialized code
• Difference between IEnumerable and IQueryable:
• IEnumerable represent sequence of values, values processed by delegates
• IQueryable represent query collected with Expression Tree
• Expressions has deep integration in VS, so in some cases programmers don’t understand that their
lambda will be converted to some data structure instead of simple delegate.
• Example:
• Expression<Func<OrderLine, bool>> foo = (x) => x.Quantity > 10;
• Looks like simple lambda, but left side says that compiler should use Expression Tree instead of delegate
- 12. COLD QUERY EXECUTION PROCESS
Code User Writes Action Performance Impact
using(var db = new WideWorldImporters())
{
Context creation Low
var q1 =
from c in db. Orders
where c.Id == 1000
select c;
Query expression creation Low
var c1 = q1.First(); LINQ query execution - Metadata loading: High but cached
- View generation: Medium but cached
- Parameter evaluation: Low
- Query translation: Medium but cached
- Materializer generation: Medium but cached
- Database query execution: Potentially high (Better queries in some
situations)
+ Connection.Open
+ Command.ExecuteReader
+ DataReader.Read
Object materialization: Medium (Faster than EF5)
- Identity lookup: Medium
} Connection.Close Low
- 13. WARM QUERY EXECUTION PROCESS
Code User Writes Action Performance Impact
using(var db = new WideWorldImporters())
{
Context creation Low
var q1 =
from c in db. Orders
where c.Id == 1000
select c;
Query expression creation Low
var c1 = q1.First(); LINQ query execution - Metadata loading lookup: High but cached Low
- View generation lookup: Medium but cached Low
- Parameter evaluation: Low
- Query translation lookup: Medium but cached Low
- Materializer generation lookup: Medium but cached Low
- Database query execution: Potentially high (Better queries in some
situations)
+ Connection.Open
+ Command.ExecuteReader
+ DataReader.Read
Object materialization: Medium (Faster than EF5)
- Identity lookup: Medium
} Connection.Close Low
- 14. SOME USEFUL OPTIMIZATIONS
Find Find with AutoDetectChanges disabled
Find with AutoDetectChanges disabled
cached
Find 85491 3525 39
1
10
100
1000
10000
100000
Using DbSet<T>.Find
- 15. HOW EF CACHE WORKS
The cleanup algorithm is as follows:
• Once the cache contains a set number of entries (800), we start a timer that periodically (once-per-
minute) sweeps the cache.
• During cache sweeps, entries are removed from the cache on a LFRU (Least frequently – recently used)
basis. This algorithm takes both hit count and age into account when deciding which entries are
ejected.
• At the end of each cache sweep, the cache again contains 800 entries.
All cache entries are treated equally when determining which entries to evict. This means the store
command for a CompiledQuery has the same chance of eviction as the store command for an Entity SQL
query.
Note that the cache eviction timer is kicked in when there are 800 entities in the cache, but the cache is
only swept 60 seconds after this timer is started. That means that for up to 60 seconds your cache may
grow to be quite large.
- 16. CACHE TEST RESULTS
Test EF6 no cache EF6 cached
Enumerating all 18723 queries 124.3 125.3
Avoiding sweep (just the first 800 queries, regardless of complexity) 40.5 5.4
Just the AggregatingSubtotals queries (178 total - which avoids
sweep)
38.1 4.6
- 17. CONTAINS<T>
var ids = new int[10000];
...
using (var context = new WideWorldImporters("TestConString"))
{
var orders = context.Orders.Where(e => ids.Contains(e.CustomerID)).ToList();
...
}
- 18. PARAMETER SNIFFING
using (var context = new WideWorldImporters())
{
var myObject = new NonMappedType();
var query = from entity in context.Orders
where entity.CustomerPurchaseOrderNumber
.StartsWith(myObject.Mask)
select entity;
var results = query.ToList();
}
- 19. PARAMETER SNIFFING
using (var context = new WideWorldImporters())
{
var myObject = new NonMappedType();
var value = myObject.OrderNumberMask;
var query = from entity in context.Orders
where entity.CustomerPurchaseOrderNumber
.StartsWith(value)
select entity;
var results = query.ToList();
}
- 20. CACHING AGAIN
int[] ids = new int[10000];
using (var context = new WideWorldImporters())
{
var firstQuery = from c in context.Customers
where ids.Contains(c.CustomerId)
select c;
var secondQuery = from c in context.Customers
where firstQuery.Any(otherEntity => otherEntity.CustomerId == c.CustomerId)
select c;
var results = secondQuery.ToList();
}
- 21. LAZY LOAD
using (var context = new WideWorldImporters())
{
var customers = context.Customers.Include(e=>e.Orders)
.Where(c => c.BuyingGroup
.BuyingGroupName == "Tailspin Toys");
var chosenCustomer = customers.First();
Console.WriteLine("Customer Id: {0} has {1} orders",
chosenCustomer?.CustomerId,
chosenCustomer?.Orders.Count);
}
- 22. SELECT
[Project1].[BuyingGroupID1] AS [BuyingGroupID],
[Project1].[CustomerID] AS [CustomerID],
[Project1].[CustomerName] AS [CustomerName],
[Project1].[BillToCustomerID] AS [BillToCustomerID],
[Project1].[CustomerCategoryID] AS [CustomerCategoryID],
[Project1].[BuyingGroupID] AS [BuyingGroupID1],
[Project1].[PrimaryContactPersonID] AS [PrimaryContactPersonID],
[Project1].[AlternateContactPersonID] AS [AlternateContactPersonID],
[Project1].[DeliveryMethodID] AS [DeliveryMethodID],
[Project1].[DeliveryCityID] AS [DeliveryCityID],
[Project1].[PostalCityID] AS [PostalCityID],
[Project1].[CreditLimit] AS [CreditLimit],
[Project1].[AccountOpenedDate] AS [AccountOpenedDate],
[Project1].[StandardDiscountPercentage] AS [StandardDiscountPercentage],
[Project1].[IsStatementSent] AS [IsStatementSent],
[Project1].[IsOnCreditHold] AS [IsOnCreditHold],
[Project1].[PaymentDays] AS [PaymentDays],
[Project1].[PhoneNumber] AS [PhoneNumber],
[Project1].[FaxNumber] AS [FaxNumber],
[Project1].[DeliveryRun] AS [DeliveryRun],
[Project1].[RunPosition] AS [RunPosition],
[Project1].[WebsiteURL] AS [WebsiteURL],
[Project1].[DeliveryAddressLine1] AS [DeliveryAddressLine1],
[Project1].[DeliveryAddressLine2] AS [DeliveryAddressLine2],
[Project1].[DeliveryPostalCode] AS [DeliveryPostalCode],
[Project1].[PostalAddressLine1] AS [PostalAddressLine1],
[Project1].[PostalAddressLine2] AS [PostalAddressLine2],
[Project1].[PostalPostalCode] AS [PostalPostalCode],
[Project1].[LastEditedBy] AS [LastEditedBy],
[Project1].[ValidFrom] AS [ValidFrom],
[Project1].[ValidTo] AS [ValidTo],
[Project1].[C1] AS [C1],
[Project1].[OrderID] AS [OrderID],
[Project1].[CustomerID1] AS [CustomerID1],
[Project1].[SalespersonPersonID] AS [SalespersonPersonID],
[Project1].[PickedByPersonID] AS [PickedByPersonID],
[Project1].[ContactPersonID] AS [ContactPersonID],
[Project1].[BackorderOrderID] AS [BackorderOrderID],
[Project1].[OrderDate] AS [OrderDate],
[Project1].[ExpectedDeliveryDate] AS [ExpectedDeliveryDate],
[Project1].[CustomerPurchaseOrderNumber] AS [CustomerPurchaseOrderNumber],
[Project1].[IsUndersupplyBackordered] AS [IsUndersupplyBackordered],
[Project1].[Comments] AS [Comments],
[Project1].[DeliveryInstructions] AS [DeliveryInstructions],
[Project1].[InternalComments] AS [InternalComments],
[Project1].[PickingCompletedWhen] AS [PickingCompletedWhen],
[Project1].[LastEditedBy1] AS [LastEditedBy1],
[Project1].[LastEditedWhen] AS [LastEditedWhen]
FROM ( SELECT
[Limit1].[CustomerID] AS [CustomerID],
[Limit1].[CustomerName] AS [CustomerName],
[Limit1].[BillToCustomerID] AS [BillToCustomerID],
[Limit1].[CustomerCategoryID] AS [CustomerCategoryID],
[Limit1].[BuyingGroupID1] AS [BuyingGroupID],
[Limit1].[PrimaryContactPersonID] AS [PrimaryContactPersonID],
[Limit1].[AlternateContactPersonID] AS [AlternateContactPersonID],
[Limit1].[DeliveryMethodID] AS [DeliveryMethodID],
[Limit1].[DeliveryCityID] AS [DeliveryCityID],
[Limit1].[PostalCityID] AS [PostalCityID],
[Limit1].[CreditLimit] AS [CreditLimit],
[Limit1].[AccountOpenedDate] AS [AccountOpenedDate],
[Limit1].[StandardDiscountPercentage] AS [StandardDiscountPercentage],
[Limit1].[IsStatementSent] AS [IsStatementSent],
[Limit1].[IsOnCreditHold] AS [IsOnCreditHold],
[Limit1].[PaymentDays] AS [PaymentDays],
[Limit1].[PhoneNumber] AS [PhoneNumber],
[Limit1].[FaxNumber] AS [FaxNumber],
[Limit1].[DeliveryRun] AS [DeliveryRun],
[Limit1].[RunPosition] AS [RunPosition],
[Limit1].[WebsiteURL] AS [WebsiteURL],
[Limit1].[DeliveryAddressLine1] AS [DeliveryAddressLine1],
[Limit1].[DeliveryAddressLine2] AS [DeliveryAddressLine2],
[Limit1].[DeliveryPostalCode] AS [DeliveryPostalCode],
[Limit1].[PostalAddressLine1] AS [PostalAddressLine1],
[Limit1].[PostalAddressLine2] AS [PostalAddressLine2],
[Limit1].[PostalPostalCode] AS [PostalPostalCode],
[Limit1].[LastEditedBy1] AS [LastEditedBy],
[Limit1].[ValidFrom1] AS [ValidFrom],
[Limit1].[ValidTo1] AS [ValidTo],
[Limit1].[BuyingGroupID2] AS [BuyingGroupID1],
[Extent3].[OrderID] AS [OrderID],
[Extent3].[CustomerID] AS [CustomerID1],
[Extent3].[SalespersonPersonID] AS [SalespersonPersonID],
[Extent3].[PickedByPersonID] AS [PickedByPersonID],
[Extent3].[ContactPersonID] AS [ContactPersonID],
[Extent3].[BackorderOrderID] AS [BackorderOrderID],
[Extent3].[OrderDate] AS [OrderDate],
[Extent3].[ExpectedDeliveryDate] AS [ExpectedDeliveryDate],
[Extent3].[CustomerPurchaseOrderNumber] AS [CustomerPurchaseOrderNumber],
[Extent3].[IsUndersupplyBackordered] AS [IsUndersupplyBackordered],
[Extent3].[Comments] AS [Comments],
[Extent3].[DeliveryInstructions] AS [DeliveryInstructions],
[Extent3].[InternalComments] AS [InternalComments],
[Extent3].[PickingCompletedWhen] AS [PickingCompletedWhen],
[Extent3].[LastEditedBy] AS [LastEditedBy1],
[Extent3].[LastEditedWhen] AS [LastEditedWhen],
CASE WHEN ([Extent3].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
FROM (SELECT TOP (1) [Extent1].[CustomerID] AS [CustomerID],
[Extent1].[CustomerName] AS [CustomerName], [Extent1].[BillToCustomerID] AS
[BillToCustomerID], [Extent1].[CustomerCategoryID] AS [CustomerCategoryID],
[Extent1].[BuyingGroupID] AS [BuyingGroupID1], [Extent1].[PrimaryContactPersonID] AS
[PrimaryContactPersonID], [Extent1].[AlternateContactPersonID] AS
[AlternateContactPersonID], [Extent1].[DeliveryMethodID] AS [DeliveryMethodID],
[Extent1].[DeliveryCityID] AS [DeliveryCityID], [Extent1].[PostalCityID] AS [PostalCityID],
[Extent1].[CreditLimit] AS [CreditLimit], [Extent1].[AccountOpenedDate] AS
[AccountOpenedDate], [Extent1].[StandardDiscountPercentage] AS
[StandardDiscountPercentage], [Extent1].[IsStatementSent] AS [IsStatementSent],
[Extent1].[IsOnCreditHold] AS [IsOnCreditHold], [Extent1].[PaymentDays] AS [PaymentDays],
[Extent1].[PhoneNumber] AS [PhoneNumber], [Extent1].[FaxNumber] AS [FaxNumber],
[Extent1].[DeliveryRun] AS [DeliveryRun], [Extent1].[RunPosition] AS [RunPosition],
[Extent1].[WebsiteURL] AS [WebsiteURL], [Extent1].[DeliveryAddressLine1] AS
[DeliveryAddressLine1], [Extent1].[DeliveryAddressLine2] AS [DeliveryAddressLine2],
[Extent1].[DeliveryPostalCode] AS [DeliveryPostalCode], [Extent1].[PostalAddressLine1] AS
[PostalAddressLine1], [Extent1].[PostalAddressLine2] AS [PostalAddressLine2],
[Extent1].[PostalPostalCode] AS [PostalPostalCode], [Extent1].[LastEditedBy] AS
[LastEditedBy1], [Extent1].[ValidFrom] AS [ValidFrom1], [Extent1].[ValidTo] AS [ValidTo1],
[Extent2].[BuyingGroupID] AS [BuyingGroupID2]
FROM [Sales].[Customers] AS [Extent1]
INNER JOIN [Sales].[BuyingGroups] AS [Extent2] ON [Extent1].[BuyingGroupID] =
[Extent2].[BuyingGroupID]
WHERE N'Tailspin Toys' = [Extent2].[BuyingGroupName] ) AS [Limit1]
LEFT OUTER JOIN [Sales].[Orders] AS [Extent3] ON [Limit1].[CustomerID] =
[Extent3].[CustomerID]
) AS [Project1]
ORDER BY [Project1].[BuyingGroupID1] ASC, [Project1].[CustomerID] ASC, [Project1].[C1]
ASC
- 23. LAZY LOAD
using (var context = new WideWorldImporters())
{
var customers = context.Customers
.Where(c => c.BuyingGroup
.BuyingGroupName == "Tailspin Toys");
var chosenCustomer = customers.First();
Console.WriteLine("Customer Id: {0} has {1} orders",
chosenCustomer?.CustomerId,
chosenCustomer?.Orders.Count);
}
- 24. SELECT TOP (1)
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[CustomerName] AS [CustomerName],
[Extent1].[BillToCustomerID] AS [BillToCustomerID],
[Extent1].[CustomerCategoryID] AS [CustomerCategoryID],
[Extent1].[BuyingGroupID] AS [BuyingGroupID],
[Extent1].[PrimaryContactPersonID] AS [PrimaryContactPersonID],
[Extent1].[AlternateContactPersonID] AS [AlternateContactPersonID],
[Extent1].[DeliveryMethodID] AS [DeliveryMethodID],
[Extent1].[DeliveryCityID] AS [DeliveryCityID],
[Extent1].[PostalCityID] AS [PostalCityID],
[Extent1].[CreditLimit] AS [CreditLimit],
[Extent1].[AccountOpenedDate] AS [AccountOpenedDate],
[Extent1].[StandardDiscountPercentage] AS [StandardDiscountPercentage],
[Extent1].[IsStatementSent] AS [IsStatementSent],
[Extent1].[IsOnCreditHold] AS [IsOnCreditHold],
[Extent1].[PaymentDays] AS [PaymentDays],
[Extent1].[PhoneNumber] AS [PhoneNumber],
[Extent1].[FaxNumber] AS [FaxNumber],
[Extent1].[DeliveryRun] AS [DeliveryRun],
[Extent1].[RunPosition] AS [RunPosition],
[Extent1].[WebsiteURL] AS [WebsiteURL],
[Extent1].[DeliveryAddressLine1] AS [DeliveryAddressLine1],
[Extent1].[DeliveryAddressLine2] AS [DeliveryAddressLine2],
[Extent1].[DeliveryPostalCode] AS [DeliveryPostalCode],
[Extent1].[PostalAddressLine1] AS [PostalAddressLine1],
[Extent1].[PostalAddressLine2] AS [PostalAddressLine2],
[Extent1].[PostalPostalCode] AS [PostalPostalCode],
[Extent1].[LastEditedBy] AS [LastEditedBy],
[Extent1].[ValidFrom] AS [ValidFrom],
[Extent1].[ValidTo] AS [ValidTo]
FROM [Sales].[Customers] AS [Extent1]
INNER JOIN [Sales].[BuyingGroups] AS [Extent2] ON [Extent1].[BuyingGroupID] =
[Extent2].[BuyingGroupID]
WHERE N'Tailspin Toys' = [Extent2].[BuyingGroupName]
exec sp_executesql N'SELECT
[Extent1].[OrderID] AS [OrderID],
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[SalespersonPersonID] AS [SalespersonPersonID],
[Extent1].[PickedByPersonID] AS [PickedByPersonID],
[Extent1].[ContactPersonID] AS [ContactPersonID],
[Extent1].[BackorderOrderID] AS [BackorderOrderID],
[Extent1].[OrderDate] AS [OrderDate],
[Extent1].[ExpectedDeliveryDate] AS [ExpectedDeliveryDate],
[Extent1].[CustomerPurchaseOrderNumber] AS
[CustomerPurchaseOrderNumber],
[Extent1].[IsUndersupplyBackordered] AS
[IsUndersupplyBackordered],
[Extent1].[Comments] AS [Comments],
[Extent1].[DeliveryInstructions] AS [DeliveryInstructions],
[Extent1].[InternalComments] AS [InternalComments],
[Extent1].[PickingCompletedWhen] AS [PickingCompletedWhen],
[Extent1].[LastEditedBy] AS [LastEditedBy],
[Extent1].[LastEditedWhen] AS [LastEditedWhen]
FROM [Sales].[Orders] AS [Extent1]
WHERE [Extent1].[CustomerID] =
@EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1
- 25. DATA MODIFICATION
context.Customers
.Where(t => t.Name == “Petya”)
.Update(t => new Customer { Name = “Petya” });
PM> Install-Package EntityFramework.Extended
context.BulkInsert(customers);
PM> Install-Package Z.EntityFramework.Extensions
- 27. OOP
public class WideWorldImporters : DbContext
{
public WideWorldImporters(string conString)
: base(conString) { }
public WideWorldImporters()
: this("WideWorldImporters") { }
// Some DbSets
}
using (var context = new WideWorldImport
ers("TestConString"))
{
context.Orders.ToList();
}
- 28. IF ORM IS SO BAD, WHAT’S SOLUTION?
Micro-ORM
• Handcraft SQL
• Handle mappings
• Less code (less functionality)
- 29. DAPPER
• Created for Stack Overflow
• Very. VERY fast
• Works with POCO and dynamics
using (var connection = new SqlConnection(conString))
{
var users = connection.Query<User>("select * from Users")
.ToList();
}
using (var connection = new SqlConnection(conString))
{
IEnumerable<dynamic> users = connection.Query ("select * from Users")
.ToList();
}
- 30. DAPPER. MAPPING
var sql = @"select * from Posts p left join Users u on u.Id = p.OwnerId order by
p.Id";
var data = connection.Query<Post, User, Post>(
sql,
(post, user) => { post.Owner = user; return post;});
var posts = data.ToList();
- 31. DAPPER. MULTI RESULTS
var sql = @" select * from Customers where CustomerId = @id
select * from Orders where CustomerId = @id
select * from Returns where CustomerId = @id";
using (var multi = connection.QueryMultiple(sql, new {id=selectedId}))
{
var customer = multi.Read<Customer>().Single();
var orders = multi.Read<Order>().ToList();
var returns = multi.Read<Return>().ToList();
}
- 32. DAPPER. PROCEDURES
var user = cnn.Query<User>(
"sp_GetUser",
new {Id = 1},
commandType: CommandType.StoredProcedure)
.SingleOrDefault();
- 33. DAPPER. OUTPUT PARAMETERS
var p = new DynamicParameters();
p.Add("@a", 11);
p.Add("@b", dbType: DbType.Int32, direction: ParameterDirection.Output);
p.Add("@c", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue);
con.Execute(
"spMagicProc",
p,
commandType: CommandType.StoredProcedure);
int b = p.Get<int>("@b");
int c = p.Get<int>("@c");
- 34. PETAPOCO
Fast like Dapper
Works with strictly undecorated POCOs, or attributed almost-POCOs
var user1 = db.Single<User>(1);
var user2 = db.Single<User>("WHERE Name = @0", “Vasya");
- 35. PETAPOCO
var u = new User();
u.Name = “Vasya”;
db.Insert(u);
// Update it
u.Name = “Kolya";
db.Update(u);
// Delete it
db.Delete(u);
db.Delete<User>(
"WHERE Name LIKE @0",
“V%");
db.Update<User>(
"SET Name=@0 WHERE Id>@1",
“XXX",
10);
- 36. SIMPLE DATA
Fun project of @markrendle
Seems to be dead
But still is fun
return Database.Open()
.Users.FindAllByName(name)
.FirstOrDefault();
- 37. PERFORMANCE
SELECT MAPPING OVER 500
ITERATIONS - POCO SERIALIZATION
Method Duration
Hand coded (using a SqlDataReader) 47ms
Dapper ExecuteMapperQuery 49ms
PetaPoco 52ms
NHibernate SQL 104ms
Linq 2 SQL ExecuteQuery 181ms
Entity framework ExecuteStoreQuery 631ms
SELECT MAPPING OVER 500
ITERATIONS - DYNAMIC
SERIALIZATION
Method Duration
Dapper ExecuteMapperQuery (dynamic) 48ms
Simple.Data 95ms
- 38. CONCLUSIONS
• ORM good or bad? Depends.
• Micro-ORM is good alternative in cases when ORM is bad.
• Knowing internal implementations or behavior can increase performance of using
of the frameworks.
• You should always understand at least one layer below what you are coding.