一、前言
二、设计思路
三、核心代码分析
(一) 在项目中建立专属文件夹
(二) 在文件夹内实现数据操作的基础准备
(四) 添加测试调用入口
(五) 运行测试
五、写在后面
六、源码下载
首先,鉴于本文所展现的ORM耗时测试已成为了博友的吐嘈点,我想我有必要声明一点:我发布这个测试框架,相当于一个活动,目的是收集各种数据访问解决方案的实现示例,并在性能,易用性,代码量上做一个综合的对比,让大家更好的了解各个解决方案的优缺点,选择的时候更明确。时间的对比只是其中一个方面,并不是对比的全部。
最近又掀起了数据访问组件&ORM的性能比拼了,但基本都是各说各的好,没有一个统一的标准与平台来进行对比。
郭某不才,花了点时间写了个数据访问性能测试框架,主要是想各个ORM在一个统一的环境下完成相同的事,来进行一下公平的对比,看看哪家的性能更好、更易用、完成相同的业务代码量更少。
特别说明,本框架测试环境如下:VS2010+SP1或以上,SQL2005或以上,.NET 4.0,能运行以上环境的操作系统
本框架的代码已发布到 codeplex 上,同学们可以通过VS自带的团队项目管理功能(TFS)或SVN 随时获取最新代码。
项目地址:https://datatester.codeplex.com
TFS获取地址:https://tfs.codeplex.com:443/tfs/TFS17,(似乎只有团队成员可以用TFS方式)需要登录,并在项目的 SOURCE CODE 标签的 Connect 连接处获取VS端的登录用户名,密码为自己的账号密码。
SVN获取地址:https://datatester.svn.codeplex.com/svn
这个测试框架主要是设计一个通用的测试基类,把要做的事规定好,并把实现细节开放出来供给各家ORM自己去实现,为公平起见,规定如下:
根据这个设计思想,设计了单个实体与多个实体的添加,查询,修改,删除操作,为了体现各个ORM的易用性,还添加了一个比较复杂的查询操作。以上这些操作都是在基类中定义了对应的 protected abstract 的方法,需要在具体的实现类中进行实现。并且唯一的前提条件只有基类中的的一个只读的数据库连接字符串ConnectionString,如果该连接串不满足要求,也可以在实现类中进行重写。
本次测试使用到的数据库结构如下(偷下懒,直接上EF4.0的反向数据库功能生成的 edmx 图上)
由图可以看出,实体关系如下:
在本框架中,规定的要实现的业务如下:
复杂查询将查询已完成的订单信息的集合,并以如下视图模型作为结果装载
视图模型定义如下:
1 namespace DataTestFramework.ViewModels 2 { 3 /// <summary> 4 /// 视图模型——订单信息 5 /// </summary> 6 public class OrderView 7 { 8 public int OrderId { get; set; } 9 10 public DateTime OrderDate { get; set; }11 12 public decimal SumMoney { get; set; }13 14 public bool Finished { get; set; }15 16 /// <summary>17 /// 订单关联的客户名称18 /// </summary>19 public string CustomerName { get; set; }20 21 /// <summary>22 /// 当前订单的所有订单明细所关联的产品名称,多个以逗号分隔23 /// </summary>24 public string ProductNames { get; set; }25 }26 }
借助VS2012更新了Update1后的新功能 Code Map,测试基类的结构展现如下:
上图展示了测试基类TesterBase的调用结构:
测试基类 TesterBase 的具体代码如下:
1 namespace DataTestFramework.Infrastructure 2 { 3 /// <summary> 4 /// 测试类基类 5 /// </summary> 6 public abstract class TesterBase 7 { 8 private const string _connectionString = "Data Source=.; Integrated Security=True;" + 9 " Initial Catalog=DataTestFramework; Pooling=True; MultipleActiveResultSets=True;"; 10 11 /// <summary> 12 /// 获取 数据库连接字符串 13 /// </summary> 14 protected virtual string ConnectionString 15 { 16 get { return _connectionString; } 17 } 18 19 #region 受保护方法 20 21 #region 单个操作 22 23 /// <summary> 24 /// 添加单个客户信息 25 /// </summary> 26 /// <param name="customer">待添加的客户信息</param> 27 protected abstract void CreateCustomer(Customer customer); 28 29 /// <summary> 30 /// 获取指定名称的客户信息 31 /// </summary> 32 /// <param name="customerName">带Token的客户名称</param> 33 /// <returns>指定名称的客户信息,不存在时返回null</returns> 34 protected abstract Customer RetrieveCustomer(string customerName); 35 36 /// <summary> 37 /// 更新指定客户信息的 PostalCode="100000",Tel="13800138000" 38 /// </summary> 39 /// <param name="customer">待更新的客户信息</param> 40 protected abstract void UpdateCustomer(Customer customer); 41 42 /// <summary> 43 /// 删除指定客户信息 44 /// </summary> 45 /// <param name="customerId">客户编号</param> 46 protected abstract void DeleteCustomer(int customerId); 47 48 #endregion 49 50 #region 批量操作 51 52 /// <summary> 53 /// 批量添加客户信息 54 /// </summary> 55 /// <param name="customers">待添加的客户信息集合</param> 56 protected abstract void CreateCustomers(IEnumerable<Customer> customers); 57 58 /// <summary> 59 /// 反向(先倒序排序)获取指定数量的客户信息 60 /// </summary> 61 /// <param name="count">要获取的数量</param> 62 /// <returns>获取的客户信息集合</returns> 63 protected abstract IEnumerable<Customer> RetrieveCustomers(int count); 64 65 /// <summary> 66 /// 更新指定的多个客户信息,在Address后面加上当前客户的CustomerName信息 67 /// </summary> 68 /// <param name="customers">待更新的客户信息</param> 69 protected abstract void UpdateCustomers(IEnumerable<Customer> customers); 70 71 /// <summary> 72 /// 批量删除指定编号的客户信息 73 /// </summary> 74 /// <param name="customerIds">待删除的客户编号集合</param> 75 protected abstract void DeleteCustomers(IEnumerable<int> customerIds); 76 77 #endregion 78 79 #region 复杂查询 80 81 /// <summary> 82 /// 查询所有已完成(Finished == true)的订单,构建订单视图模型 83 /// </summary> 84 /// <returns>满足条件的订单视图模型集合</returns> 85 protected abstract IEnumerable<OrderView> RetrieveOrderViews(); 86 87 #endregion 88 89 #endregion 90 91 #region 私有方法 92 93 /// <summary> 94 /// 单个实体操作测试 95 /// </summary> 96 /// <returns>是否继续</returns> 97 private bool SingleCrudTest() 98 { 99 string token = DateTime.Now.ToString("hhmmssfff");100 Stopwatch watch = new Stopwatch();101 Customer customer = new Customer102 {103 CustomerName = "郭明锋@中国" + token,104 ContactName = "郭明锋",105 Address = "北京,北京",106 PostalCode = "100001",107 Tel = "13800138001"108 };109 110 //单个客户添加111 watch.Restart();112 CreateCustomer(customer);113 watch.Stop();114 Console.WriteLine("单个实体添加成功,耗时:{0}", watch.Elapsed);115 116 //查询最后添加的客户信息117 watch.Restart();118 Customer lastCustomer = RetrieveCustomer(customer.CustomerName);119 watch.Stop();120 if (lastCustomer != null && lastCustomer.CustomerName == customer.CustomerName)121 {122 Console.WriteLine("单个实体查询成功,耗时:{0}", watch.Elapsed);123 }124 else125 {126 Console.WriteLine("单个实体查询失败,耗时:{0},测试终止。", watch.Elapsed);127 return false;128 }129 130 //更新上一步查询出来的客户信息131 watch.Restart();132 UpdateCustomer(lastCustomer);133 watch.Stop();134 lastCustomer = RetrieveCustomer(customer.CustomerName);135 if (lastCustomer != null && lastCustomer.PostalCode == "100000" && lastCustomer.Tel == "13800138000")136 {137 Console.WriteLine("单个实体更新成功,耗时:{0}", watch.Elapsed);138 }139 else140 {141 Console.WriteLine("单个实体更新失败,耗时:{0},测试终止。", watch.Elapsed);142 return false;143 }144 145 //删除本次添加的单个客户信息146 watch.Restart();147 DeleteCustomer(lastCustomer.CustomerId);148 watch.Stop();149 lastCustomer = RetrieveCustomer(customer.CustomerName);150 if (lastCustomer == null)151 {152 Console.WriteLine("单个实体删除成功,耗时:{0}", watch.Elapsed);153 }154 else155 {156 Console.WriteLine("单个实体删除成功,耗时:{0},测试终止", watch.Elapsed);157 return false;158 }159 return true;160 }161 162 /// <summary>163 /// 批量实体操作测试164 /// </summary>165 /// <returns>是否继续</returns>166 private bool MultipleCrudTest()167 {168 string token = DateTime.Now.ToString("hhmmssfff");169 Stopwatch watch = new Stopwatch();170 Console.WriteLine("要开始批量测试,请输入批量大小:");171 int count;172 bool flag = int.TryParse(Console.ReadLine(), out count);173 while (!flag)174 {175 Console.WriteLine("要开始批量测试,请输入批量大小:");176 flag = int.TryParse(Console.ReadLine(), out count);177 }178 List<Customer> customers = new List<Customer>();179 for (int index = 0; index < count; index++)180 {181 customers.Add(new Customer182 {183 CustomerName = string.Format("郭明锋@中国{0}{1}", token, index),184 ContactName = "郭明锋",185 Address = "北京,北京",186 PostalCode = "100001",187 Tel = "13800138001"188 });189 }190 Console.WriteLine("开始进行批量测试,测试数量为:{0}", count);191 192 //批量客户添加193 watch.Restart();194 CreateCustomers(customers);195 watch.Stop();196 Console.WriteLine("批量实体添加成功,耗时:{0}", watch.Elapsed);197 198 //查询最后添加的客户信息199 watch.Restart();200 List<Customer> lastCustomers = RetrieveCustomers(count).ToList();201 watch.Stop();202 //以前后客户名称集合取差集来判断是否一致203 if (!lastCustomers.Select(m => m.CustomerName).Except(customers.Select(m => m.CustomerName)).Any())204 {205 Console.WriteLine("批量实体查询成功,耗时:{0}", watch.Elapsed);206 }207 else208 {209 Console.WriteLine("批量实体查询失败,耗时:{0},测试终止。", watch.Elapsed);210 return false;211 }212 213 //批量更新客户信息214 watch.Restart();215 UpdateCustomers(lastCustomers);216 lastCustomers = RetrieveCustomers(count).ToList();217 if (!lastCustomers.Select(m => m.Address).Intersect(customers.Select(m => m.Address)).Any())218 {219 Console.WriteLine("批量实体更新成功,耗时:{0}", watch.Elapsed);220 }221 else222 {223 Console.WriteLine("批量实体更新失败,耗时:{0},测试终止。", watch.Elapsed);224 return false;225 }226 227 //批量删除本次添加的客户信息228 List<int> customerIds = lastCustomers.Select(m => m.CustomerId).ToList();229 watch.Restart();230 DeleteCustomers(customerIds);231 watch.Stop();232 lastCustomers = RetrieveCustomers(count).ToList();233 if (!lastCustomers.Select(m => m.CustomerName).Intersect(customers.Select(m => m.CustomerName)).Any())234 {235 Console.WriteLine("批量实体删除成功,耗时:{0}", watch.Elapsed);236 }237 else238 {239 Console.WriteLine("批量实体删除失败,耗时:{0},测试终止。", watch.Elapsed);240 return false;241 }242 return true;243 }244 245 private void RetrieveComplex()246 {247 Stopwatch watch = new Stopwatch();248 watch.Restart();249 List<OrderView> orderViews = RetrieveOrderViews().ToList();250 watch.Stop();251 Console.WriteLine("复杂查询执行成功,耗时{1},共获取{0}个订单视图信息。", watch.Elapsed, orderViews.Count);252 }253 254 #endregion255 256 #region 公共方法257 258 /// <summary>259 /// 开始测试工作,主要对 Customer 表进行操作,工作顺序为增、查、改、删,进行单个操作,批量操作与复杂查询260 /// </summary>261 public void Work()262 {263 bool isContinue = SingleCrudTest();264 if (!isContinue)265 {266 return;267 }268 isContinue = MultipleCrudTest();269 if (!isContinue)270 {271 return;272 }273 RetrieveComplex();274 }275 276 #endregion277 }278 }
下面,我就以EntityFramework 4.4 来演示一下怎样使用这个测试框架。可能有同学会说,EF6都出来了,为什么还使用4.4版本?原因一、.net4.0只支持到4.4版本,原因二、windows 2003 只支持到 .net 4.0,原因三、我正在使用的是4.4版本。如果你觉得4.4版本 out 了,可以自己去实现一个 EF6的测试示例,呵呵。废话不多说,下面我们来实现 EntityFramework 4.4 的测试示例。
为了更好的管理各个数据访问方案的实现代码,也为防止项目结构混乱,各个方案的代码应在自己的文件夹内实现。在这里,创建一个名为EntityFramework的文件夹
首先要进行相应数据访问方案的基础准备,比如ado.net方案,可能需要一个SqlHelper的辅助操作类,又或者其他ORM,需要进行数据映射,生成数据实体等等。对于EntityFramework,只需要实现一个数据上下文类即可:
1 namespace DataTestFramework.EntityFramework 2 { 3 public class EFDbContext : DbContext 4 { 5 public EFDbContext(string connectionStringOrName) 6 : base(connectionStringOrName) { } 7 8 public DbSet<Customer> Customers { get; set; } 9 10 public DbSet<Category> Categories { get; set; }11 12 public DbSet<Product> Products { get; set; }13 14 public DbSet<Order> Orders { get; set; }15 16 public DbSet<OrderDetail> OrderDetails { get; set; }17 18 protected override void OnModelCreating(DbModelBuilder modelBuilder)19 {20 modelBuilder.Entity<Product>().HasRequired(m => m.Category).WithMany(n => n.Products).HasForeignKey(m => m.CategoryId);21 modelBuilder.Entity<Order>().HasRequired(m => m.Customer).WithMany(n => n.Orders).HasForeignKey(m => m.CustomerId);22 modelBuilder.Entity<OrderDetail>().HasRequired(m => m.Product).WithMany(n => n.OrderDetails).HasForeignKey(m => m.ProductId);23 modelBuilder.Entity<OrderDetail>().HasRequired(m => m.Order).WithMany(n => n.OrderDetails).HasForeignKey(m => m.OrderId);24 }25 }26 }
特别说明:项目中已经添加了测试所需的POCO实体类,如果这些类不符合具体的数据访问方案的要求(比如实体类是代码生成器来生成的),可以自行在自己的文件夹中定义需要的实体类,在数据访问实现中再把操作结果转换为系统定义的POCO实体类,以进行数据操作结果的验证。
测试基类 TesterBase 中已定义了需要实现的测试用例,需要继承这个基类,实现相应数据访问方案的具体操作实现。对于 EntityFramework,实现如下:
1 namespace DataTestFramework.EntityFramework 2 { 3 /// <summary> 4 /// EntityFramework测试类 5 /// </summary> 6 public class EntityFrameworkTester : TesterBase 7 { 8 private const int _pageSize = 300; 9 10 #region 单个操作 11 12 /// <summary> 13 /// 添加单个客户信息 14 /// </summary> 15 /// <param name="customer">待添加的客户信息</param> 16 protected override void CreateCustomer(Customer customer) 17 { 18 using (EFDbContext db = new EFDbContext(ConnectionString)) 19 { 20 db.Customers.Add(customer); 21 db.SaveChanges(); 22 } 23 } 24 25 /// <summary> 26 /// 获取指定名称的客户信息 27 /// </summary> 28 /// <param name="customerName">带Token的客户名称</param> 29 /// <returns>指定名称的客户信息,不存在时返回null</returns> 30 protected override Customer RetrieveCustomer(string customerName) 31 { 32 using (EFDbContext db = new EFDbContext(ConnectionString)) 33 { 34 return db.Customers.SingleOrDefault(m => m.CustomerName == customerName); 35 } 36 } 37 38 /// <summary> 39 /// 更新指定客户信息的 PostalCode="100000",Tel="13800138000" 40 /// </summary> 41 /// <param name="customer">待更新的客户信息</param> 42 protected override void UpdateCustomer(Customer customer) 43 { 44 customer.PostalCode = "100000"; 45 customer.Tel = "13800138000"; 46 using (EFDbContext db = new EFDbContext(ConnectionString)) 47 { 48 if (db.Entry(customer).State == EntityState.Detached) 49 { 50 db.Customers.Attach(customer); 51 db.Entry(customer).State = EntityState.Modified; 52 } 53 db.SaveChanges(); 54 } 55 } 56 57 /// <summary> 58 /// 删除指定客户信息 59 /// </summary> 60 /// <param name="customerId">客户编号</param> 61 protected override void DeleteCustomer(int customerId) 62 { 63 using (EFDbContext db = new EFDbContext(ConnectionString)) 64 { 65 Customer customer = db.Customers.SingleOrDefault(m => m.CustomerId == customerId); 66 if (customer == null) 67 { 68 return; 69 } 70 db.Customers.Remove(customer); 71 db.SaveChanges(); 72 } 73 } 74 75 #endregion 76 77 #region 批量操作 78 79 /// <summary> 80 /// 批量添加客户信息 81 /// </summary> 82 /// <param name="customers">待添加的客户信息集合</param> 83 protected override void CreateCustomers(IEnumerable<Customer> customers) 84 { 85 using (EFDbContext db = new EFDbContext(ConnectionString)) 86 { 87 try 88 { 89 db.Configuration.AutoDetectChangesEnabled = false; 90 List<Customer> customerList = customers as List<Customer> ?? customers.ToList(); 91 int pageCount = customerList.Count / _pageSize; 92 pageCount = customerList.Count % _pageSize > 0 ? pageCount + 1 : pageCount; 93 for (int index = 0; index < pageCount; index++) 94 { 95 List<Customer> pageData = customerList.Skip(index * _pageSize).Take(_pageSize).ToList(); 96 foreach (Customer customer in pageData) 97 { 98 db.Customers.Add(customer); 99 }100 db.SaveChanges();101 }102 }103 finally104 {105 db.Configuration.AutoDetectChangesEnabled = true;106 }107 }108 }109 110 /// <summary>111 /// 反向(先倒序排序)获取指定数量的客户信息112 /// </summary>113 /// <param name="count">要获取的数量</param>114 /// <returns>获取的客户信息集合</returns>115 protected override IEnumerable<Customer> RetrieveCustomers(int count)116 {117 using (EFDbContext db = new EFDbContext(ConnectionString))118 {119 return db.Customers.OrderByDescending(m => m.CustomerId).Take(count).ToList();120 }121 }122 123 /// <summary>124 /// 更新指定的多个客户信息,在Address后面加上当前客户的CustomerName信息125 /// </summary>126 /// <param name="customers">待更新的客户信息</param>127 protected override void UpdateCustomers(IEnumerable<Customer> customers)128 {129 using (EFDbContext db = new EFDbContext(ConnectionString))130 {131 List<int> ids = customers.Select(m => m.CustomerId).ToList();132 int pageCount = ids.Count / _pageSize;133 pageCount = ids.Count % _pageSize > 0 ? pageCount + 1 : pageCount;134 for (int index = 0; index < pageCount; index++)135 {136 List<int> pageIds = ids.Skip(index * _pageSize).Take(_pageSize).ToList();137 db.Customers.Update(m => pageIds.Contains(m.CustomerId), n => new Customer138 {139 Address = n.Address + n.CustomerName140 });141 }142 }143 }144 145 /// <summary>146 /// 批量删除指定编号的客户信息147 /// </summary>148 /// <param name="customerIds">待删除的客户编号集合</param>149 protected override void DeleteCustomers(IEnumerable<int> customerIds)150 {151 List<int> ids = customerIds as List<int> ?? customerIds.ToList();152 using (EFDbContext db = new EFDbContext(ConnectionString))153 {154 int pageCount = ids.Count / _pageSize;155 pageCount = ids.Count % _pageSize > 0 ? pageCount + 1 : pageCount;156 for (int index = 0; index < pageCount; index++)157 {158 List<int> pageIds = ids.Skip(index * _pageSize).Take(_pageSize).ToList();159 db.Customers.Delete(m => pageIds.Contains(m.CustomerId));160 }161 }162 }163 164 #endregion165 166 #region 复杂查询167 168 /// <summary>169 /// 查询所有已完成(Finished == true)的订单,构建订单视图模型170 /// </summary>171 /// <returns>满足条件的订单视图模型集合</returns>172 protected override IEnumerable<OrderView> RetrieveOrderViews()173 {174 using (EFDbContext db = new EFDbContext(ConnectionString))175 {176 var orders = db.Orders.Where(m => m.Finished).Select(m => new177 {178 m.OrderId,179 m.OrderDate,180 m.SumMoney,181 m.Finished,182 Customer = new { m.Customer.CustomerName },183 ProductNames = m.OrderDetails.Select(n => n.Product.ProductName)184 }).ToList();185 return orders.Select(order => new OrderView186 {187 OrderId = order.OrderId,188 OrderDate = order.OrderDate,189 SumMoney = order.SumMoney,190 Finished = order.Finished,191 CustomerName = order.Customer.CustomerName,192 ProductNames = order.ProductNames.ExpandAndToString(",")193 }).ToList();194 }195 }196 197 #endregion198 }199 }
对于批量修改,删除操作,我使用了 EntityFramework.Extended.dll 程序集(可以在Nuget中获取)来进行实现,该程序集对于每次批量操作只生成一条sql语句,而不会像EntityFramework提供的原生方法那样批量N条数据就要生成N条sql语句。并且使用了逐部分处理的方式,以免进行大量数据处理时一次提交太多的数据导致性能低下。
复杂查询的业务,对于EntityFramework来说,实现是相当简单的,完全是要什么取什么,使用IQueryable<T>的扩展方法Select来按需获取,然后用匿名类来装载查询结果,再根据这个查询结果构造视图模型的列表集合,具体实现如上面代码中 166行-197行所示。
添加EntityFrameworkTester类之后的 Code Map如下所示:
在Program类中,定义了很多的Method方法供调用,请不要修改Program类的Main方法,只需要在 HelpInfo 方法中添加使用哪个序号的Method进行调用的信息,然后在相应序号的Method方法中,如这里EntityFramework使用 Method01方法来作为入口,则进行如下修改:
1 private static void HelpInfo()2 {3 Console.WriteLine("=============帮-助-信-息============");4 Console.WriteLine("h.帮助信息");5 Console.WriteLine("0.退出程序");6 Console.WriteLine("1.性能测试——EF");7 Console.WriteLine("=============帮-助-信-息============");8 }
1 private static void Method01()2 {3 Console.WriteLine("=============EntityFramework测试开始============");4 EntityFrameworkTester tester = new EntityFrameworkTester();5 tester.Work();6 Console.WriteLine("=============EntityFramework测试结束============");7 }
至此,EntityFramework使用此测试框架的代码已添加完毕,运行测试,忽略第一次运行的结果,分别执行5000条与10000条的批量数据操作,结果如下:
现在代码示例中只有本人添加的一个EntityFramework示例,其他的方案比如ado.net及其他ORM期待高手来添加,请大家下载并编写自己的数据访问方案的实现,可在评论处留下载链接,也可直接发给我,我整理之后将在本系列后续中详解实现过程,并进行综合的对比。
另外,如果当前框架不能满足部分数据访问方案的要求导致无法添加测试的话,也希望能提出来,我再进行更新。
本框架的代码已发布到 codeplex 上,同学们可以通过VS自带的团队项目管理功能(TFS)或SVN 随时获取最新代码。
联系客服