Entity Framework 性能
对 ADO.NET 和 Entity Framework 性能的比较。
引言
在设计软件时,性能通常是重中之重,随着新技术和新技术的出现,评估它们是否对解决方案有用非常重要。
本文档并非旨在深入探讨 Entity Framework。它仅仅呈现了使用 Entity Framework 的各种方法的发现及其性能特征,以便做出决策并为解决方案选择最佳的技术和方法。
测试环境
文章中运行测试时使用了以下环境
- Windows 7 x64, Intel® Core™2 Duo 2.64GHz, 8 GB RAM
- Windows Server 2008 R2 x64, AMD Phenom™ Quad-Core 1.8 GHz, 8 GB RAM
除非另有说明,所有测试均使用 .NET Framework 4.0 编译和运行。
数据库架构
这些测试的数据库架构旨在代表常见场景;存储客户相关信息。
测试夹具
该应用程序使用一个小型私有类来存储有关每个测试的信息
private class TestRun
{
public TestRun(string name, Action test)
{
Name = name;
Test = test;
}
public string Name { get; private set; }
public Action Test { get; private set; }
public double ExecuteTime { get; set; }
}
为每个要运行的测试创建了这些对象的集合。
Tests.Add(new TestRun("Clear",new Action(ADOTests.Clear)));
Tests.Add(new TestRun("ADO Insert", new Action(ADOTests.InsertCustomers)));
Tests.Add(new TestRun("ADO Get", new Action(ADOTests.GetCustomers)));
Tests.Add(new TestRun("Clear", new Action(ADOTests.Clear)));
Tests.Add(new TestRun("EF Batch Insert", new Action(EFTests.InsertCustomers_Batch)));
Tests.Add(new TestRun("Clear", new Action(ADOTests.Clear)));
Tests.Add(new TestRun("EF Single Insert", new Action(EFTests.InsertCustomers_Single)));
Tests.Add(new TestRun("EF Get", new Action(EFTests.GetCustomers)));
Tests.Add(new TestRun("EF Get Compiled", new Action(EFTests.GetCustomers_Compiled)));
Tests.Add(new TestRun("EF Get Compiled NoTracking",
new Action(EFTests.GetCustomers_CompiledNoTracking)));
Tests.Add(new TestRun("EF Get Execute", new Action(EFTests.GetCustomers_Execute)));
Tests.Add(new TestRun("EF Get NoTracking", new Action(EFTests.GetCustomers_NoTracking)));
每组测试运行五次以产生平均值。
for(int x = 0; x < 5; x++)
{
foreach(var test in Tests)
{
test.ExecuteTime = DoTest(test.Test,test.Name);
LogComplete(test);
}
LogToFile();
}
每个测试本身运行指定的次数,在本例中为 10 次。
private const int ITERATIONS = 10;
for(int x = 0; x < ITERATIONS; x++)
{
Console.WriteLine("{0} test: {1}", testName, x + 1);
Stopwatch.Reset();
Stopwatch.Start();
test();
Stopwatch.Stop();
executeTime += Stopwatch.Elapsed.TotalSeconds;
Console.WriteLine("Total Seconds for {0}: {1}", testName,
Stopwatch.Elapsed.TotalSeconds);
Console.WriteLine("---------------------------");
}
Clear
方法被当作一个特殊情况处理,因为它只需要运行一次。此方法仅用于清除先前测试或测试运行中输入的表中的任何行。
if(testName == "Clear")
{ test(); return 0;
}
ADO.NET 基线
ADO.NET 已经存在很多年了,是大多数开发人员熟悉的数据访问技术。第一组测试将使用这种熟悉的技术来建立比较基线。
对于 ADO.NET 测试,我将使用一些相当普通的存储过程,没有什么复杂的,正如下面的代码片段所示。其余的存储过程可以在本文档的下载中找到。
CREATE PROC InsertCustomer(@FName NVARCHAR(20),@LName NVARCHAR(30),
@Address_ID INT, @Email NVARCHAR(200) )
AS
BEGIN
SET NOCOUNT ON
INSERT INTO Customer(FName, LName, Address_ID, email)
VALUES (@FName, @LName, @Address_ID, @Email)
RETURN SCOPE_IDENTITY()
END
CREATE PROC GetFullCustomer
AS
BEGIN
SET NOCOUNT ON
SELECT Customer.ID, Fname, LName, Email,
A.ID AS Address_ID, A.Street, A.City, S.ID AS State_ID, S.Name AS State,
C.ID AS Country_ID, C.Name AS Country, A.PostalCode
FROM Customer
JOIN Address A ON A.ID = Address_ID
JOIN State S ON S.ID = A.State_ID
JOIN Country C ON C.ID = S.Country_ID
END
ADO.NET 和 Entity Framework 之间的一个区别是需要创建自己的实体对象来具体化检索或插入到数据库中的数据。这里有一些简单的 POCO 类,它们已被创建以方便此操作。同样,没有什么复杂的,但公平地代表了常见的业务场景。
public class Customer
{
public Customer(){ }
public Customer(int id, string fname, string lname,
Address address, string email)
{
ID = id;
FName = fname;
LName = lname;
Address = address;
Email = email;
}
public int ID { get; set; }
public string FName { get; set; }
public string LName { get; set; }
public Address Address { get; set; }
public string Email { get; set; }
public List<Phone> Phones { get; set; }
}
由于 ADO.NET 测试用作比较基线,并且它们使用了任何使用它构建数据访问类的人应该熟悉的技术,因此我们不会花费太多时间在这些方法上。完整的代码可在下载中查阅。
对于插入测试,创建了 1000 个 Customer
对象的集合,每个对象都有一个 Address
和五个 PhoneNumber
对象。迭代集合并将它们插入数据库。由于 Customer
引用 Address
,因此必须先插入 Address
以获取外键。然后插入 Customer
,然后是 Phone
,最后填充 Customer_Phone
。
public void InsertCustomer(List<ADOTest.Customer> customers)
{
int addressID = 0;
int customerID = 0;
using(SqlConnection conn = new SqlConnection(ConnectionString))
{
using(SqlCommand cmd = new SqlCommand("InsertCustomer", conn))
using(SqlCommand cmdAddr = new SqlCommand("InsertAddress", conn))
using(SqlCommand cmdPhone = new SqlCommand("InsertPhone", conn))
using(SqlCommand cmdCustomerPhone = new SqlCommand("InsertCustomerPhone", conn))
{
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@FName", System.Data.SqlDbType.NVarChar));
cmd.Parameters.Add(new SqlParameter("@LName", System.Data.SqlDbType.NVarChar));
cmd.Parameters.Add(new SqlParameter("@Address_ID", System.Data.SqlDbType.Int));
cmd.Parameters.Add(new SqlParameter("@Email", System.Data.SqlDbType.NVarChar));
cmd.Parameters.Add("@ReturnValue", System.Data.SqlDbType.Int).Direction = System.Data.ParameterDirection.ReturnValue;
cmdAddr.CommandType = System.Data.CommandType.StoredProcedure;
cmdAddr.Parameters.Add(new SqlParameter("@Street", System.Data.SqlDbType.NVarChar));
cmdAddr.Parameters.Add(new SqlParameter("@City", System.Data.SqlDbType.NVarChar));
cmdAddr.Parameters.Add(new SqlParameter("@State_ID", System.Data.SqlDbType.Int));
cmdAddr.Parameters.Add(new SqlParameter("@PostalCode", System.Data.SqlDbType.NVarChar));
cmdAddr.Parameters.Add("@ReturnValue", System.Data.SqlDbType.Int).Direction = System.Data.ParameterDirection.ReturnValue;
cmdPhone.CommandType = System.Data.CommandType.StoredProcedure;
cmdPhone.Parameters.Add(new SqlParameter("@Number", System.Data.SqlDbType.NVarChar));
cmdPhone.Parameters.Add(new SqlParameter("@Type_ID", System.Data.SqlDbType.Int));
cmdPhone.Parameters.Add("@ReturnValue", System.Data.SqlDbType.Int).Direction = System.Data.ParameterDirection.ReturnValue;
cmdCustomerPhone.CommandType = System.Data.CommandType.StoredProcedure;
cmdCustomerPhone.Parameters.Add(new SqlParameter("@Customer_ID", System.Data.SqlDbType.Int));
cmdCustomerPhone.Parameters.Add(new SqlParameter("@Phone_ID", System.Data.SqlDbType.Int));
foreach(var customer in customers)
{
if(conn.State != System.Data.ConnectionState.Open)
conn.Open();
addressID = InsertAddress(customer.Address, cmdAddr);
cmd.Parameters["@FName"].Value = customer.FName;
cmd.Parameters["@LName"].Value = customer.LName;
cmd.Parameters["@Address_ID"].Value = addressID;
cmd.Parameters["@Email"].Value = customer.Email;
cmd.ExecuteNonQuery();
customerID = (int)cmd.Parameters["@ReturnValue"].Value;
foreach(Phone phone in customer.Phones)
{
InsertCustomerPhone(customerID, phone, cmdPhone, cmdCustomerPhone);
}
}
}
}
}
对于检索测试,执行上面显示的 GetFullCustomer
存储过程,并迭代返回的 SqlDataReader
以具体化 Customer
对象。
public List<customer> GetAllCustomers()
{
List<customer> customers = new List<customer>();
using(SqlConnection conn = new SqlConnection(ConnectionString))
{
using(SqlCommand cmd = new SqlCommand("GetFullCustomer", conn))
using(SqlCommand cmd2 = new SqlCommand("GetPhonesForCustomer", conn))
{
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd2.CommandType = System.Data.CommandType.StoredProcedure;
conn.Open();
SqlDataReader dr = cmd.ExecuteReader();
while(dr.Read())
{
Country country = new Country(dr.GetInt32(9), dr.GetString(10));
State state = new State(dr.GetInt32(7), dr.GetString(8), country);
Address addr = new Address(dr.GetInt32(4), dr.GetString(5), dr.GetString(6),
state, dr.GetString(11));
addr.ID = dr.GetInt32(4);
Customer customer = new Customer(dr.GetInt32(0),
dr.GetString(1), dr.GetString(2), addr, dr.GetString(3));
customer.Phones = new List<phone>();
cmd2.Parameters.AddWithValue("@CustomerID", customer.ID);
SqlDataReader dr2 = cmd2.ExecuteReader();
while(dr2.Read())
{
customer.Phones.Add(new Phone(dr2.GetInt32(0), dr2.GetString(1),
new PhoneType(dr2.GetInt32(2), dr2.GetString(3))));
}
cmd2.Parameters.Clear();
dr2.Close();
customers.Add(customer);
}
}
}
return customers;
}
ADO.NET 结果
从这些结果可以看出,五次迭代测试产生非常一致的结果。我将使用这些结果作为后续 Entity Framework 测试的比较。当然,这些结果在不同的系统上会有所不同,您的结果可能与此不同。
Insert |
Get |
|
测试运行 1 |
6.36829512 |
0.28734904 |
测试运行 2 |
5.0966758 |
0.28829696 |
测试运行 3 |
5.12760286 |
0.28650676 |
测试运行 4 |
9.86927334 |
0.67176426 |
测试运行 5 |
5.1557739 |
0.28186196 |
Entity Framework 测试
本文档并非旨在深入探讨 Entity Framework;有很多其他资源可供参考,因此我不会深入研究。目标是比较 ADO.NET 和 Entity Framework 提供的各种方法。
Entity Framework 不使用为 ADO.NET 测试创建的 POCO 对象,而是映射数据库架构,并生成派生自 System.Data.Objects.DataClasses.EntityObject
的实体类,如下所示。
Entity Framework 插入和检索测试 1
Entity Framework 创建一个派生自 System.Data.Objects
的类,在本例中为 Entities
。ObjectContext
类维护到数据库的连接以及 ObjectStateManager
,后者用于维护 ObjectContext
中实体之间的状态和关系。
Entity Framework 使用的第一种插入方法通过 AddToCustomer
方法将所有客户添加到 ObjectContext
,然后通过调用 ObjectContext.SaveChanges
方法将它们插入数据库。SaveChanges
将作用于自上次调用以来已添加到 ObjectContext
或已修改的所有实体。由于所有 Customer
和相关实体都已在调用 SaveChanges
之前添加,因此它将处理所有实体。调用 SaveChanges
方法时,ObjectStateManager
用于确定需要执行的操作,在本例中为插入。如果附加到 ObjectContext
的任何实体已被修改,则会对这些实体执行更新,删除也是如此。
public void InsertCustomers_Batch(List<Customer> customers)
{
using(Entities context = new Entities())
{
foreach(Customer customer in customers)
{
context.AddToCustomer(customer);
}
context.SaveChanges();
}
}
AddToCustomer
方法是 Entity Framework 设计器生成的 ObjectContext
类添加的一个方法,它封装了 ObjectContext.AddObject
方法,专门用于 Customer
实体。
public void AddToCustomer(Customer customer)
{
base.AddObject("Customer", customer);
}
最终,会调用 ObjectContext.AddSingleObject
方法,实体会被分配一个 EnityKey
并附加到 RelationshipManager
,并通过 ObjectContext
类中的 ObjectStateManager
暴露。
internal void AddSingleObject(EntitySet entitySet, object entity, string argumentName)
{
RelationshipManager relationshipManager = EntityUtil.GetRelationshipManager(entity);
EntityKey key = FindEntityKey(entity, this);
if (key != null)
{
EntityUtil.ValidateEntitySetInKey(key, entitySet);
key.ValidateEntityKey(entitySet);
}
this.ObjectStateManager.AddEntry(entity, null, entitySet, argumentName, true);
if (relationshipManager != null)
{
relationshipManager.AttachContext(this, entitySet, MergeOption.AppendOnly);
}
}
RelationshipManager
会导致所有关联实体也被添加到 ObjectContext
会话中。
ObjectContext.SaveChanges
检查现有的 IEntityAdapter
实现,或者在找不到时创建一个,然后调用其 Update
方法。使用 SqlClient
提供程序,IEntityAdapter
当然是实现为 SqlDataAdapter
。
public int SaveChanges(bool acceptChangesDuringSave)
{
// code removed for clarity
if (this._adapter == null)
{
IServiceProvider providerFactory =
connection.ProviderFactory as IServiceProvider;
if (providerFactory != null)
{
this._adapter =
providerFactory.GetService(typeof(IEntityAdapter)) as IEnityAdapter;
}
if (this._adapter == null)
{
throw EntityUtil.InvalidDataAdapter();
}
}
// code removed for clarity
try
{
// code removed for clarity
objectStateEntriesCount = this._adapter.Update(this.ObjectStateManager);
// code removed for clarity
}
调用 SaveChanges
时,Entity Framework 会生成 SQL 语句来执行必要的_操作:插入、更新、删除。使用 SQL Profiler,我们可以看到为 AddToCustomer
操作创建并要执行的 SQL 语句。
在这里您会注意到,插入语句的顺序是 Address
行,然后是 Phone
行,然后是 Customer
行,最后是 Customer_Phone
行,正如上面 ADO.NET 测试中一样。
AddObject
和 SaveChanges
方法不生成 ObjectQuery
,因此我们无法使用 ToTraceString()
方法查看要执行的 SQL 语句。但是,我们可以使用 SQL Profiler 来查看调用 SaveChanges
时执行的语句。
exec sp_executesql N'insert [dbo].[Address]([Street], [City], [State_ID],[PostalCode])
values(@0, @1, @2, @3)
select[ID] from [dbo].[Address]
where @@ROWCOUNT > 0 and [ID] = scope_identity()',N'@0 nvarchar(3),
@1 nvarchar(7),@2 int,@3 nvarchar(5)',@0=N'686',@1=N'Anytown',@2=1,@3=N'12345'
exec sp_executesql N'insert [dbo].[Phone]([Number], [Type_ID])
values(@0, @1)
select[ID]
from[dbo].[Phone]
where @@ROWCOUNT > 0 and [ID] = scope_identity()',N'@0 nvarchar(1),@1 int',@0=N'3',@1=1
exec sp_executesql N'insert [dbo].[Customer]([FName], [LName], [Address_ID],[email])
values(@0, @1, @2, @3)
select[ID]
from[dbo].[Customer]
where @@ROWCOUNT > 0 and [ID] = scope_identity()',N'@0 nvarchar(2),
@1 nvarchar(7),@2 int,@3 nvarchar(12)',@0=N'EF',@1=N'Doe1000',@2=1073064,
@3=N'john@doe.com'
exec sp_executesql N'insert [dbo].[Customer_Phone]([Customer_ID], [Phone_ID])
values(@0, @1)',N'@0 int,@1 int',@0=1084372,@1=7711908
这里还有一点值得注意,当以批处理模式处理时,插入似乎没有顺序。使用 ADO.NET 或 Entity Framework 的单个插入,行会以 FIFO(先进先出)顺序从集合中插入。然而,在处理 Entity Framework 批处理插入时,它们似乎是随机顺序的。从代码可以看出,客户的姓氏附加了正在创建的实体的索引;相同的数字用于关联地址中的街道属性。
for(int x = 0; x < NumCustomers; x++)
{
EFTest.Customer customer = EFTest.Customer.CreateCustomer(0, "EF",
"Doe" + (x + 1));
customer.email = "john@doe.com";
customer.Address = CreateAddress(x + 1);
CreatePhones(NumPhones, customer);
Customers.Add(customer);
}
private EFTest.Address CreateAddress(int x)
{
EFTest.Address addr = EFTest.Address.CreateAddress(0, x.ToString(),
"Anytown", "12345");
addr.StateReference.EntityKey =
new System.Data.EntityKey("Entities.State", "ID", 1);
return addr;
}
然而,当插入到数据库时,我们可以看到 Customer
似乎是以 LIFO(后进先出)顺序添加的,而 Address
的插入顺序则有些随机。
检索实体
当使用 Entity Framework 从数据存储检索实体时,会构造一个 System.Data.Objects.ObjectQuery
。ObjectQuery
可以通过多种方式构造,我们将看到。第一个测试将使用方法表示法。ObjectQuery
不会立即查询数据库,并且可以存储以供将来使用。数据库在调用诸如 ToList()
之类的操作时被查询。
这里,创建了此模型的 ObjectContext
(Entities
)的实例,并用于形成客户实体的 ObjectQuery
。通过 LINQ to SQL 的 Include
方法添加了相关的实体关联。ObjectQuery
在调用 ToList()
时隐式执行,并将结果具体化为 Customer
实体的集合。
public List<Customer> GetAllCustomers()
{
using(Entities context = new Entities())
{
List<Customer> customers = context.Customer
.Include("Address")
.Include("Address.State")
.Include("Address.State.Country")
.Include("Phone")
.Include("Phone.PhoneType")
.ToList();
return customers;
}
}
与 AddToCustomer
方法一样,Entity Framework 会生成 SQL 语句;但是,查看生成的语句更容易。可以调用 ObjectQuery
的 ToTraceString()
方法来查看生成的 SQL 语句,如下面由上面的 ObjectQuery
创建的语句所示。
SELECT
[Project2].[ID] AS [ID],
[Project2].[FName] AS [FName],
[Project2].[LName] AS [LName],
[Project2].[email] AS [email],
[Project2].[ID1] AS [ID1],
[Project2].[Street] AS [Street],
[Project2].[City] AS [City],
[Project2].[PostalCode] AS [PostalCode],
[Project2].[ID2] AS [ID2],
[Project2].[Name] AS [Name],
[Project2].[Abbr] AS [Abbr],
[Project2].[ID3] AS [ID3],
[Project2].[ID4] AS [ID4],
[Project2].[Name1] AS [Name1],
[Project2].[C1] AS [C1],
[Project2].[C2] AS [C2],
[Project2].[C3] AS [C3],
[Project2].[C4] AS [C4],
[Project2].[ID5] AS [ID5],
[Project2].[Number] AS [Number],
[Project2].[Type_ID] AS [Type_ID]
FROM ( SELECT
[Extent1].[ID] AS [ID],
[Extent1].[FName] AS [FName],
[Extent1].[LName] AS [LName],
[Extent1].[email] AS [email],
[Extent2].[ID] AS [ID1],
[Extent2].[Street] AS [Street],
[Extent2].[City] AS [City],
[Extent2].[PostalCode] AS [PostalCode],
[Extent3].[ID] AS [ID2],
[Extent3].[Name] AS [Name],
[Extent3].[Abbr] AS [Abbr],
[Extent4].[ID] AS [ID3],
[Extent5].[ID] AS [ID4],
[Extent5].[Name] AS [Name1],
1 AS [C1],
1 AS [C2],
1 AS [C3],
[Project1].[ID] AS [ID5],
[Project1].[Number] AS [Number],
[Project1].[Type_ID] AS [Type_ID],
[Project1].[C1] AS [C4]
FROM [dbo].[Customer] AS [Extent1]
LEFT OUTER JOIN [dbo].[Address] AS [Extent2] ON [Extent1].[Address_ID] = [Extent2].[ID]
LEFT OUTER JOIN [dbo].[State] AS [Extent3] ON [Extent2].[State_ID] = [Extent3].[ID]
LEFT OUTER JOIN [dbo].[State] AS [Extent4] ON [Extent2].[State_ID] = [Extent4].[ID]
LEFT OUTER JOIN [dbo].[Country] AS [Extent5] ON [Extent4].[Country_ID] = [Extent5].[ID]
LEFT OUTER JOIN (SELECT
[Extent6].[Customer_ID] AS [Customer_ID],
[Extent7].[ID] AS [ID],
[Extent7].[Number] AS [Number],
[Extent7].[Type_ID] AS [Type_ID],
1 AS [C1]
FROM [dbo].[Customer_Phone] AS [Extent6]
INNER JOIN [dbo].[Phone] AS [Extent7] ON [Extent7].[ID] = [Extent6].[Phone_ID] )
AS [Project1] ON [Extent1].[ID] = [Project1].[Customer_ID]
) AS [Project2]
ORDER BY [Project2].[ID] ASC, [Project2].[ID1] ASC, [Project2].[ID2] ASC,
[Project2].[ID3] ASC, [Project2].[ID4] ASC, [Project2].[C4] ASC
结果
与使用 Entity Framework 相比,检索时间平均增加了约三秒。当您了解 Entity Framework 在具体化从数据库检索的数据时,它还将信息存储在用于维护状态的 ObjectStateManager
中,这当然会增加处理时间,从而可以解释这种差异。
让我们看看可以做些什么来缩短这些时间。
Entity Framework 插入测试 2
与其一次性将所有 Customer
和相关实体添加到 ObjectContext
和数据库,不如尝试单独添加它们。这类似于上面 ADO.NET 的方法。
public void InsertCustomers_Single(List<Customer> customers)
{
using(Entities context = new Entities())
{
foreach(Customer customer in customers)
{
context.AddToCustomer(customer);
context.SaveChanges();
}
}
}
结果
结果更接近 ADO.NET 插入测试的结果。差异可归因于 Entities Framework 生成 SQL 语句并使用系统存储过程 sp_executesql
,而不是使用 ADO.NET 调用重新编译的存储过程的方法。
检索不跟踪的实体
如上所述,Entity Framework 的状态管理功能确实会增加检索过程的时间。但是,在某些情况下并非总是需要状态管理。例如,当实体与 ObjectContext
断开连接时,就像在多层应用程序中跨层传输它时一样,所有状态信息都会丢失。在这种情况下,在具体化期间应用状态管理是无效的。
可以通过在 ObjectQuery
上设置 MergeOption
来控制这一点。默认设置为 AppendOnly
,这意味着只有新实体会被添加到 ObjectContext
;任何现有实体都不会被修改。通过使用 MergeOptions.NoTracking
,您可以禁用 ObjectQuery
的状态管理。
public List<Customer> GetAllCustomers_NoTracking()
{
using(Entities context = new Entities())
{
var customers = context.Customer
.Include("Address")
.Include("Address.State")
.Include("Address.State.Country")
.Include("Phone")
.Include("Phone.PhoneType");
customers.MergeOption = MergeOption.NoTracking;
return customers.ToList();
}
}
结果
我们在这里可以看到,状态管理为 ObjectQuery
的执行增加了大量时间。在没有跟踪的情况下,时间更接近 ADO.NTET 测试的时间,这正如预期的那样,因为 ADO.NET 也不包含状态管理。
检索已编译查询的实体
使用存储过程的一个好处是它们可以预编译而不是每次都编译,从而减少了执行所需的时间。类似于代码首次运行时 JIT 编译,Entity Framework 可以使用 CompledQuery
。正如名称和 JIT 引用所示,CompileQuery
在首次执行时被编译,然后在它仍在作用域内的情况下,后续执行将重用它。
这里,使用 Entity Expression 语法构造了一个 ObjectQuery
来执行与上一个测试相同的查询。
public static Func<Entities, IQueryable<Customer>> compiledQuery =
CompiledQuery.Compile((Entities ctx) => from c in ctx.Customer
.Include("Address")
.Include("Address.State")
.Include("Address.State.Country")
.Include("Phone")
.Include("Phone.PhoneType")
select c);
public List<Customer> GetAllCustomers_Compiled()
{
using(Entities context = new Entities())
{
return compiledQuery.Invoke(context).ToList();
}
}
结果
第一组结果显示了在空数据库上运行上述测试所需的时间,即没有执行任何插入。您可以轻松地看到查询首次编译和执行所需的时间,以及后续执行时间的减少。
空数据 – 编译查询 | |
测试运行 1 | 0.06587061 |
测试运行 2 | 0.00436616 |
测试运行 3 | 0.00411366 |
测试运行 4 | 0.00692281 |
测试运行 5 | 0.00413088 |
在实际应用程序中,我们还可以看到首次编译查询和后续调用的时间缩短。
EF 获取已编译 | |
迭代 1 | 3.4879755 |
迭代 2 | 3.0695143 |
迭代 3 | 2.7637209 |
迭代 4 | 2.6596132 |
迭代 5 | 2.695845 |
然而,当查看整体平均值时,与非编译查询相比,差异很小,这有点误导。
EF 获取 | EF 获取已编译 | |
测试运行 1 | 3.1006031 | 2.92106378 |
测试运行 2 | 2.5866506 | 2.547442 |
测试运行 3 | 2.6016029 | 2.5337393 |
测试运行 4 | 2.68804502 | 2.62037566 |
测试运行 5 | 2.5479289 | 2.50857524 |
检索已编译查询且不跟踪的实体
上述两种方法(CompiledQuery
和 NoTracking
)可以结合使用。
public List<Customer> GetAllCustomers_CompiledNoTracking()
{
using(Entities context = new Entities())
{
var query = compiledQuery(context);
((ObjectQuery)query).MergeOption = MergeOption.NoTracking;
return query.ToList();
}
}
结果
EF 获取已编译无跟踪 | |
测试运行 1 | 2.00207252 |
测试运行 2 | 1.90965804 |
测试运行 3 | 1.87372962 |
测试运行 4 | 1.90671946 |
测试运行 5 | 1.88587232 |
关注点
此代码的第一个版本是使用 .NET Framework 3.5 SP1 运行的,并且在已编译和未编译的查询之间没有显示太大差异。但是,在更新时,我使用了 .NET Framework 4.0,并发现了非常好的改进。
在服务器上运行测试的结果也高于预期,但这可能归因于低端服务器。当然,更好的硬件可能会带来不同的结果。
结论
同样,这些测试的结果将根据硬件、环境和其他因素而有所不同,并非旨在提供确定性信息,仅供参考。
历史
2009 年 10 月 31 日 - 首次发布
2011 年 10 月 25 日 - 修正了 GetAllCustomers 中的代码,移除了不必要的循环
修正了 InsertCustomer 中的代码,以避免因创建 SqlParameters 而导致结果失真