65.9K
CodeProject 正在变化。 阅读更多。
Home

Entity Framework 性能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (30投票s)

2009年10月31日

CPOL

11分钟阅读

viewsIcon

222542

downloadIcon

2005

对 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 的类,在本例中为 EntitiesObjectContext 类维护到数据库的连接以及 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 测试中一样。

AddObjectSaveChanges 方法不生成 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.ObjectQueryObjectQuery 可以通过多种方式构造,我们将看到。第一个测试将使用方法表示法。ObjectQuery 不会立即查询数据库,并且可以存储以供将来使用。数据库在调用诸如 ToList() 之类的操作时被查询。

这里,创建了此模型的 ObjectContextEntities)的实例,并用于形成客户实体的 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 语句;但是,查看生成的语句更容易。可以调用 ObjectQueryToTraceString() 方法来查看生成的 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

检索已编译查询且不跟踪的实体

上述两种方法(CompiledQueryNoTracking)可以结合使用。

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 而导致结果失真

© . All rights reserved.