数据访问方法比较 - 第一部分
这是关于 .NET 环境中数据访问方法比较的第一部分。
引言
这是关于 .NET 环境中数据访问方法比较的第一部分。在这一部分中,我将重点关注读取操作。我将比较 ADO.NET、Dapper 和 NHibernate。尽管比较是在 .NET 环境中的 SQL Server 上进行的,但其概念和结果应该适用于其他环境,例如 Java 和 Oracle。
背景
多年前,当我访问任何网站时,我非常高兴能坐在房间里获取世界各地的信息。我使用的计算机当然是任何标准下的极旧计算机。这些极旧的计算机早已被扔进了垃圾桶,我也升级了我的计算机。新计算机的内存至少增加了数百倍,硬盘空间增加了数百倍,CPU 速度也快了很多倍(摩尔定律)。旧计算机只有一个 CPU 核心,而我今天可以在沃尔玛购买的折扣笔记本电脑可以轻松拥有至少几个核心。互联网速度也多年来取得了令人印象深刻的增长。网络服务器和数据库服务器都像我的个人电脑一样得到了改进。固态硬盘在这些服务器上已不再罕见。这些都是令人兴奋的人类成就,我们都应该为此感到自豪。但是当我坐在现代计算机前通过大大改进的互联网访问一些网站时,我惊讶地发现,多年前提供相同功能的网站并没有变得快多少。至少没有快到足以与基础设施的改进相匹配。其中一些网站实际上是由一些世界级公司托管的,例如超大型银行、保险公司和无线电话服务提供商。
在我看来,网站没有达到其应有的速度有许多原因,其中包括但不限于以下几点:
- 过度且不受控制地使用 CSS、图像和 JavaScript。这些文件未正确缓存且未以高效方式加载,从而导致不必要的服务器往返。
- 异步或同步 AJAX 调用选择不当和过度使用,导致服务器往返次数过多。
- 僵化、无条件和过度使用不利于性能的设计模式。
- 当然,还有许多其他因素。
谈到网站速度,我们应该始终记住,一个简单的功能性企业级网站至少由两台服务器支持:Web 服务器和数据库服务器。在许多情况下,选择从 Web 服务器访问数据库服务器的方法可能会导致显著的性能差异。在本文中,我将通过运行示例向您展示这种差异有多大。如果您查看此链接,您会发现访问数据库的方法实在太多了。在本文中,我将重点关注 .NET 环境中最常用的方法,即ADO.NET、NHibernate 和Dapper。我将使用 MVC Web 应用程序向您展示如何使用它们并进行性能比较。
- 在 .NET 环境中,ADO.NET 是默认的数据访问方法。实际上,如果您只想访问 SQL Server 数据库,您需要的所有程序集都包含在 .NET Framework 中,并且在部署应用程序时无需携带任何额外的 DLL。在典型的使用 ADO.NET 的应用程序中,存储过程通常用于在数据库服务器中对某些编程逻辑进行分组,这提供了关注点分离和一些性能优势。它还提供了额外的安全性,以防止SQL 注入。如果应用程序仅通过存储过程访问数据库,则数据库管理员可以通过仅授予应用程序对这些存储过程的执行权限来最大化数据库的安全性,但拒绝访问其他数据库对象,例如表和视图。
- 通过 ADO.NET 返回的数据采用 DataReader 或 DataSet 的形式。在大多数情况下,这已足够。但在 MVC 应用程序中,人们通常倾向于以 视图模型 的形式将数据从控制器传递到视图,这为程序员提供了 智能感知 的优势。Dapper 被引入正是为了帮助我们进行这种传输。正如 Dapper 的创建者明确指出的那样,Dapper 的唯一目的是帮助程序员进行这种传输。Dapper 非常轻量级。它被实现为围绕
IDbConnection
接口的一组 扩展方法。由于 Dapper 只是一组扩展方法,您可以使用 ADO.NET 连接对象 提供的所有功能,但可以利用 Dapper 的 对象映射 功能。 - NHibernate 的设计目标与 Dapper 不同。它旨在完全覆盖 ADO.NET。它非常重量级。如果你想使用 NHibernate,你需要在你的应用程序中添加几个额外的 DLL,并根据你是否使用 Fluent NHibernate 在 XML 或 C# 类中设置你的映射。本文附带的示例使用 Fluent NHibernate。为了克服一些缺点,NHibernate 引入了许多扩展,包括但不限于 Entity Hilo 和 Hibernate 查询语言。在本文的第 2 部分中,我将花一些时间介绍 Entity Hilo,向你展示它是什么以及它能为我们带来什么。我将不涉及 Hibernate 查询语言,因为它是一个更复杂且多余的、基于成熟的 ANSI SQL 的版本。对于许多新程序员来说,设置 NHibernate 环境可能是一个挑战。在本文中,我将向你展示我在准备示例应用程序时发现的最简单的方法。我希望当你阅读本文时,该方法仍然有效。请记住,与数据库服务器通信的唯一方式是 SQL,而 ADO.NET 是 .NET 环境中连接数据库服务器的默认方法。连接数据库服务器时,NHibernate 确实使用了 ADO.NET,无论 NHibernate 文档是否告诉我们。
示例数据库
本文的第一部分有两个目的。它将向您展示如何在应用程序中使用这三种数据访问方法,并且还会比较简单读取操作的性能。本文中的示例使用 Visual Studio 2010 和 SQL Server 2008 R2。如果您想重复这些示例,您将需要一个 SQL Server,并且需要对其拥有管理权限。如果您在数据库服务器中运行以下脚本,您将拥有示例所需的所有数据库、表和存储过程。
SET NOCOUNT ON
USE [master]
GO
-- Create a login user
IF EXISTS
(SELECT * FROM sys.server_principals
WHERE name = N'TestUser')
DROP LOGIN [TestUser]
GO
EXEC sp_addlogin @loginame = 'TestUser', @passwd = 'Password123';
GO
-- Create the database
IF EXISTS
(SELECT name FROM sys.databases
WHERE name = N'SQLPerformanceRead')
BEGIN
ALTER DATABASE [SQLPerformanceRead] SET SINGLE_USER
WITH ROLLBACK IMMEDIATE
DROP DATABASE [SQLPerformanceRead]
END
GO
CREATE DATABASE SQLPerformanceRead
GO
USE [SQLPerformanceRead]
GO
-- Grant the required database access to the login
sp_grantdbaccess 'TestUser'
GO
sp_addrolemember 'db_datareader', 'TestUser'
GO
sp_addrolemember 'db_datawriter', 'TestUser'
GO
CREATE TABLE [dbo].[TStudent](
[ID] [int] NOT NULL,
[LastName] [varchar](50) NOT NULL,
[FirstName] [varchar](50) NOT NULL,
[EnrollmentTime] [datetime] NOT NULL,
CONSTRAINT [PK_TStudent] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
CREATE TABLE [dbo].[TCourse](
[ID] [int] NOT NULL,
[Name] [varchar](50) NOT NULL,
[CreditHours] [int] NOT NULL,
CONSTRAINT [PK_TCourse] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
CREATE TABLE [dbo].[TStudentScore](
[StudentID] [int] NOT NULL,
[CourseID] [int] NOT NULL,
[Score] [int] NOT NULL,
[LastUpdated] [datetime] NOT NULL,
CONSTRAINT [PK_TStudentScore] PRIMARY KEY CLUSTERED
(
[StudentID] ASC,
[CourseID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
ALTER TABLE [dbo].[TStudentScore]
WITH CHECK ADD CONSTRAINT [FK_TStudentScore_TScore]
FOREIGN KEY([CourseID])
REFERENCES [dbo].[TCourse] ([ID])
ALTER TABLE [dbo].[TStudentScore]
WITH CHECK ADD CONSTRAINT [FK_TStudentScore_TStudent]
FOREIGN KEY([StudentID])
REFERENCES [dbo].[TStudent] ([ID])
DECLARE @i AS INT
DECLARE @j AS INT
DECLARE @NoOfStudents AS INT
SET @NoOfStudents = 5000
-- Add the courses
INSERT INTO TCourse VALUES(1, 'English', 3)
INSERT INTO TCourse VALUES(2, 'Math', 5)
INSERT INTO TCourse VALUES(3, 'Biology', 4)
INSERT INTO TCourse VALUES(4, 'Music', 3)
INSERT INTO TCourse VALUES(5, 'Basketball', 2)
-- Add some students
SET @i = 1
WHILE @i <= @NoOfStudents
BEGIN
INSERT INTO TStudent VALUES
(@i, 'L Name No.' + CAST(@i AS VARCHAR(4)),
'F Name No.' + CAST(@i AS VARCHAR(4)),
DATEADD (y , -1 , GETDATE()))
SET @i = @i + 1
END
-- Add some scores for each student
SET @i = 1
WHILE @i <= @NoOfStudents
BEGIN
SET @j = 1
WHILE @j <= 5
BEGIN
INSERT INTO TStudentScore
VALUES(@i, @j, ROUND(40 + 60*RAND(), 0), GETDATE())
SET @j = @j + 1
END
SET @i = @i + 1
END
GO
CREATE PROCEDURE DBO.GetStudents
AS
BEGIN
SET NOCOUNT ON;
SELECT S.ID StudentId, s.LastName, s.FirstName,
AVG(SS.Score) AverageScore,
SUM(CASE WHEN SS.Score >= 60
THEN C.CreditHours ELSE 0 END) Credits
FROM TStudent S
LEFT JOIN TStudentScore SS ON S.ID = SS.StudentID
LEFT JOIN TCourse C ON SS.CourseID = C.ID
GROUP BY S.ID, s.LastName, s.FirstName
ORDER BY S.ID
END
GO
CREATE PROCEDURE DBO.GetStudentScores
@StudentId AS INT
AS
BEGIN
SET NOCOUNT ON;
SELECT SS.CourseID CourseId, C.Name CourseName,
C.CreditHours CreditHours, SS.Score
FROM TStudentScore SS
LEFT JOIN TCourse C ON SS.CourseID = C.ID
WHERE SS.StudentID = @StudentId
END
GO
GRANT EXECUTE ON OBJECT::[dbo].[GetStudents] TO TestUser
GRANT EXECUTE ON OBJECT::[dbo].[GetStudentScores] TO TestUser
GO
-- Bring the database on-line
ALTER DATABASE [SQLPerformanceRead] SET MULTI_USER
GO
PRINT 'SQLPerformanceRead database is created'
脚本成功运行后,我们将在 SQL Server 中拥有一个名为 [SQLPerformanceRead] 的数据库。
- [SQLPerformanceRead] 数据库包含三张表和两个存储过程。脚本还为这些表添加了适当的索引和外键约束。
- 除了创建表,脚本还会向表中添加一些数据。我们将使用表中的数据进行性能比较。
- 该脚本还创建了一个用户名/密码对“TestUser/Password123”,并授予其访问存储过程和表所需的权限。示例应用程序将使用此用户名访问数据库。
如果您运行存储过程 [GetStudents],您将得到以下结果,这将是应用程序在 Web 浏览器中读取和显示的数据。
[TStudent] 表存储了 5000 名学生的基本信息,[TCourse] 表存储了总共 5 门课程,包含课程名称和学分。[TStudentScore] 表连接了 [TStudent] 表和 [TCourse] 表,用于存储每个学生每门课程的成绩。[GetStudents] 存储过程列出了所有学生,汇总了每个学生的平均分,并计算了学生获得的总学分。对于每门课程,只有当学生成绩超过 60 分时,他/她才能获得学分。示例应用程序还会加载每个学生所修课程以及每门课程的成绩,如下图所示,通过运行存储过程 [GetStudentScores]。
示例 MVC 应用程序
示例 MVC 应用程序在 Visual Studio 2010 中开发。我非常清楚当前的 MVC 版本是 4。但我也知道并非所有人都在使用 MVC 4,所以此应用程序是使用 MVC 2 开发的,因此大多数人都可以轻松下载并运行该应用程序。
- “Controllers\HomeController.cs”文件实现了 MVC 应用程序的控制器。
- “ViewModels\StudentCoursesVm.cs”和“ViewModels\StudentSummariesVm.cs”文件实现了应用程序的视图模型。
- “Views\Home”文件夹中的文件是 MVC 视图。
数据库连接字符串保存在应用程序的“web.config”文件中的“appSettings
”部分。
"Data Source=localhost;Initial Catalog=SQLPerformanceRead;User Id=TestUser;Password=Password123"
在开始使用应用程序之前,我们需要启用应用程序以使用所有三种数据访问方法。使用这些数据访问方法的所有基本组件都实现在“DataAccessUtilities”文件夹中。
数据访问工具
每种数据访问方法在相应的文件夹中都实现了一些辅助类或初始设置实用程序。“DataAccessMethod.cs”文件定义了一些将在整个应用程序中使用的“enum”,用于标识用于从数据库加载数据的数据访问方法。
namespace SQLPerformanceRead.DataAccessUtilities
{
public enum DataAccessMethod
{ AdoNet, Dapper, NHibernateDefault, NHibernateImproved };
}
ADO.NET 实用程序
正如我们之前讨论过的,ADO.NET 是 .NET Framework 的一部分。如果只想连接 SQL Server 数据库,我们不需要任何特殊配置即可使用 ADO.NET。但为了方便后续编程,我在“ADONET\ADONetUtility.cs”文件中编写了一些辅助方法。
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
namespace SQLPerformanceRead.DataAccessUtilities.ADONET
{
public static class AdoNetUtility
{
private static readonly string ConnectionString;
static AdoNetUtility()
{
ConnectionString = ConfigurationManager.AppSettings["ConnectionString"];
}
private static SqlConnection GetConnection()
{
return new SqlConnection
{
ConnectionString = ConnectionString
};
}
public static void ExecuteCommand(SqlCommand cmd)
{
using (var connection = GetConnection())
{
cmd.Connection = connection;
connection.Open();
cmd.ExecuteNonQuery();
}
}
public static DataTable GetADataTable(SqlCommand cmd)
{
var aTable = new DataTable();
using (var connection = GetConnection())
{
cmd.Connection = connection;
cmd.CommandTimeout = 0;
connection.Open();
var adapter = new SqlDataAdapter { SelectCommand = cmd };
adapter.Fill(aTable);
}
return aTable;
}
public static SqlDataReader GetADataReader(SqlCommand cmd)
{
SqlDataReader aReader = null;
var connection = GetConnection();
cmd.Connection = connection;
cmd.CommandTimeout = 0;
connection.Open();
aReader = cmd.ExecuteReader();
return aReader;
}
}
}
本文的这一部分我们不会使用所有这些辅助方法,因为我们只关注读取操作。在文章的后续部分,我们将处理插入、更新和删除操作,届时我们将使用所有这些方法。
Dapper 实用程序
Dapper 使用起来非常简单。如果您访问 Dapper 网站,您会找到一个可以下载的单独文件。您可以将此文件编译成 DLL 并在您的项目中引用它。您也可以简单地将此文件添加到您的项目中。我选择将其添加到我的项目中的“Dapper\Dapper.cs”文件中,这样在应用程序部署时就不必担心太多 DLL。众所周知,Dapper 只是围绕 IDbConnection
接口的一组扩展方法。我们不需要进行任何特殊配置即可使用它。“Dapper\DapperUtility.cs”文件只实现了一个方法来返回数据库连接对象,以便我们在尝试使用 Dapper 查询数据库时节省一些打字工作。
using System.Configuration;
using System.Data.SqlClient;
namespace SQLPerformanceRead.DataAccessUtilities.Dapper
{
public class DapperUtility
{
private static readonly string ConnectionString;
static DapperUtility()
{
ConnectionString = ConfigurationManager.AppSettings["ConnectionString"];
}
public static SqlConnection GetConnection()
{
return new SqlConnection
{
ConnectionString = ConnectionString
};
}
}
}
NHibernate 实用程序
NHibernate 的设置并不容易。它确实需要应用程序中额外的 DLL。经过一番尝试,我发现获取 Fluent NHibernate 所有 DLL 的最简单方法是通过 Nuget。如果您已经在 Visual Studio 中设置了 Nuget,您可以为您的项目启动它。
如果您从未在项目中引用过任何 NHibernate 相关的 DLL,您可以简单地搜索并找到“FluentNHibernate”包并安装它。安装过程将下载所有用于 Fluent NHibernate 的 DLL,并在您的项目中添加引用。如果由于某种原因,您的项目已经引用了任何 NHibernate 相关的 DLL,您在安装包时可能会遇到问题。您很可能会遇到版本不匹配问题。Nuget 确实会自动下载包,但它有点“buggy”。它不保证下载您所需的最小 DLL 集。如果出现任何问题,您将不得不手动修复问题,这可能非常耗时且可能不是愉快的体验。有时 Nuget 甚至可能无法启动,解决问题的方法很大程度上是针对具体情况的。无论如何,本文不是关于 Nuget。至少我用它来获取创建此示例应用程序所需的 DLL,我希望您也能好运。在最坏的情况下,使用 Fluent NHibernate 的最小 DLL 集位于文章附加的 zip 文件中的“NHibernateDLLs”文件夹中。
在项目中引用 DLL 后,让我们看一下“NHibernate\NHibernateUtility.cs”文件。
using System.Configuration;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using SQLPerformanceRead.DataAccessLayer.NHibernate.DataModel;
namespace SQLPerformanceRead.DataAccessUtilities.NHibernate
{
public static class NHibernateUtility
{
private static ISessionFactory _sessionFactory;
private static string GetConnectionString()
{
return ConfigurationManager.AppSettings["ConnectionString"];
}
private static ISessionFactory SessionFactory
{
get
{
InitializeSessionFactory();
return _sessionFactory;
}
}
public static void InitializeSessionFactory()
{
if (_sessionFactory == null)
{
string conStr = GetConnectionString();
_sessionFactory = Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(conStr))
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<TStudent>())
.BuildSessionFactory();
}
}
public static ISession OpenSession()
{
return SessionFactory.OpenSession();
}
}
}
InitializeSessionFactory
方法初始化 NHibernate 会话工厂。OpenSession
方法返回的是 NHibernate 会话对象。我们将使用此会话对象对数据库执行查询。在大多数应用程序中,InitializeSessionFactory
方法只调用一次。对于这个 MVC 项目,它在 Global.asax 文件的 Application_Start
事件中调用。
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
// Initiate NHibernate Session Factory
NHibernateUtility.InitializeSessionFactory();
}
InitializeSessionFactory
方法将遍历应用程序使用的所有映射类和数据库对象。我们稍后将在本文中看到映射类。对于这个简单的三表示例应用程序来说,这不是问题。但对于任何中等规模的实际应用程序,这个过程确实需要一些时间,并且每次你对代码进行小修改并重新编译应用程序时都会发生。如果你使用 NHibernate,并且需要在开发应用程序时修改代码并调试它,请耐心等待,因为应用程序需要完成 InitializeSessionFactory
才能启动。
视图模型
应用程序的视图模型在“ViewModels\StudentSummariesVm.cs”和“ViewModels\StudentCoursesVm.cs”文件中实现。
using System;
using System.Collections.Generic;
namespace SQLPerformanceRead.ViewModels
{
public class StudentSummary
{
public int StudentId { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public int AverageScore { get; set; }
public int Credits { get; set; }
}
public class StudentSummariesVm
{
public List<StudentSummary> StudentSummaries { get; set; }
public TimeSpan LoadTime { get; set; }
}
}
using System;
using System.Collections.Generic;
namespace SQLPerformanceRead.ViewModels
{
public class StudentCourse
{
public int CourseId { get; set; }
public string CourseName { get; set; }
public int CreditHours { get; set; }
public int Score { get; set; }
}
public class StudentCoursesVm
{
public List<StudentCourse> StudentCourses { get; set; }
public TimeSpan LoadTime { get; set; }
}
}
每个视图模型都包含将在 Web 浏览器中呈现的数据列表,并且每个视图模型都有一个名为 `LoadTime` 的成员变量,用于记录查询数据库和创建视图模型所花费的总时间。我们将使用所有三种数据访问方法来创建视图模型,并比较每种方法所花费的时间。
数据访问层
数据访问层是应用程序通过每种数据访问方法查询数据库以创建视图模型的地方。它在 DataAccessLayer 文件夹中实现。
ADO.NET 数据访问方法
“ADONET\AdoNetDbAccess.cs”文件实现了使用 ADO.NET 访问数据库的方法。
using System;
using System.Data;
using System.Data.SqlClient;
using SQLPerformanceRead.DataAccessUtilities.ADONET;
using SQLPerformanceRead.ViewModels;
using System.Collections.Generic;
namespace SQLPerformanceRead.DataAccessLayer.ADONET
{
public class AdoNetDbAccess
{
public static StudentSummariesVm LoadStudentSummaries()
{
var summaries = new StudentSummariesVm();
var startTime = DateTime.Now;
var cmd = new SqlCommand
{
CommandType = CommandType.StoredProcedure,
CommandText = "GetStudents"
};
var studentSummaries = new List<StudentSummary>();
using (var aReader = AdoNetUtility.GetADataReader(cmd))
{
while (aReader.Read())
{
var summary = new StudentSummary
{
StudentId = Convert.ToInt32(aReader["StudentId"]),
LastName = aReader["LastName"].ToString(),
FirstName = aReader["FirstName"].ToString(),
AverageScore = Convert.ToInt32(aReader["AverageScore"]),
Credits = Convert.ToInt32(aReader["Credits"])
};
studentSummaries.Add(summary);
}
}
var timeSpent = DateTime.Now.Subtract(startTime);
summaries.StudentSummaries = studentSummaries;
summaries.LoadTime = timeSpent;
return summaries;
}
public static StudentCoursesVm LoadStudentCourses(int studentId)
{
var courses = new StudentCoursesVm();
var startTime = DateTime.Now;
var cmd = new SqlCommand
{
CommandType = CommandType.StoredProcedure,
CommandText = "GetStudentScores"
};
cmd.Parameters.Add("@StudentId", SqlDbType.Int).Value = studentId;
var studentCourses = new List<StudentCourse>();
using (var aReader = AdoNetUtility.GetADataReader(cmd))
{
while (aReader.Read())
{
var course = new StudentCourse
{
CourseId = Convert.ToInt32(aReader["CourseId"]),
CourseName = aReader["CourseName"].ToString(),
CreditHours = Convert.ToInt32(aReader["CreditHours"]),
Score = Convert.ToInt32(aReader["Score"])
};
studentCourses.Add(course);
}
}
var timeSpent = DateTime.Now.Subtract(startTime);
courses.StudentCourses = studentCourses;
courses.LoadTime = timeSpent;
return courses;
}
}
}
LoadStudentSummaries
方法创建 `StudentSummariesVm` 视图模型对象,而 LoadStudentCourses
方法创建 `StudentCoursesVm` 视图模型对象。这两个方法都只是调用之前创建的存储过程,并遍历 DataReader
来创建视图模型。整个过程所花费的时间保存在 `LoadTime` 变量中。
Dapper 数据访问方法
“Dapper\DapperDbAccess.cs”文件实现了使用 Dapper 访问数据库的方法。
using System;
using System.Collections.Generic;
using System.Data;
using SQLPerformanceRead.ViewModels;
using SQLPerformanceRead.DataAccessUtilities.Dapper;
using System.Linq;
namespace SQLPerformanceRead.DataAccessLayer.Dapper
{
public class DapperDbAccess
{
public static StudentSummariesVm LoadStudentSummaries()
{
var summaries = new StudentSummariesVm();
var startTime = DateTime.Now;
List<StudentSummary> studentSummaries;
using (var cn = DapperUtility.GetConnection())
{
cn.Open();
studentSummaries
= cn.Query<StudentSummary>("GetStudents",
commandType: CommandType.StoredProcedure).ToList();
}
var timeSpent = DateTime.Now.Subtract(startTime);
summaries.StudentSummaries = studentSummaries;
summaries.LoadTime = timeSpent;
return summaries;
}
public static StudentCoursesVm LoadStudentCourses(int studentId)
{
var courses = new StudentCoursesVm();
var startTime = DateTime.Now;
List<StudentCourse> studentCourses;
using (var cn = DapperUtility.GetConnection())
{
cn.Open();
studentCourses
= cn.Query<StudentCourse>("GetStudentScores",
new {StudentId = studentId},
commandType: CommandType.StoredProcedure).ToList();
}
var timeSpent = DateTime.Now.Subtract(startTime);
courses.StudentCourses = studentCourses;
courses.LoadTime = timeSpent;
return courses;
}
}
}
此文件与“ADONET\AdoNetDbAccess.cs”文件几乎相同,但更简单。Dapper 的泛型扩展方法“Query
”用于创建视图模型,为我们节省了一些编码工作。
Fluent NHibernate 数据访问方法
通过 NHibernate 访问数据库不像前两种方法那么简单。我们首先在“NHibernate\DataModel\DataModels.cs”文件中创建数据模型类。
using System;
using System.Collections.Generic;
namespace SQLPerformanceRead.DataAccessLayer.NHibernate.DataModel
{
public class TCourse
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual int CreditHours { get; set; }
}
public class TStudent
{
public virtual int Id { get; set; }
public virtual string LastName { get; set; }
public virtual string FirstName { get; set; }
public virtual DateTime EnrollmentTime { get; set; }
public virtual IList<TStudentScore> StudentScores { get; set; }
}
public class TStudentScore
{
public virtual int StudentId { get; set; }
public virtual int CourseId { get; set; }
public virtual int Score { get; set; }
public virtual DateTime LastUpdated { get; set; }
public virtual TCourse Course { get; set; }
public override bool Equals(object obj)
{
var other = obj as TStudentScore;
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return StudentId == other.StudentId &&
CourseId == other.CourseId;
}
public override int GetHashCode()
{
unchecked
{
int hash = GetType().GetHashCode();
hash = (hash * 31) ^ StudentId.GetHashCode();
hash = (hash * 31) ^ CourseId.GetHashCode();
return hash;
}
}
}
}
通常,每个类对应于数据库中的一个表。在引用关系的情况下,您需要在类中添加一个对象“List
”。TStudentScore
有点特殊,它有两个名为 Equals
和 GetHashCode
的方法。在使用 NHibernate 时,最好为数据库中的每个表添加一个“ID”。如果您不这样做,您将需要在相应的数据模型类中添加这两个方法。在我的例子中,[TStudentScore] 只是 [TStudent] 和 [TCourse] 表之间的关系表。为这个表添加一个“ID”确实毫无意义。如果我添加“ID”,我还需要在“ID”列上创建一个索引,这将耗费大量不必要的 SQL Server 资源。所以我选择在数据模型类中添加 Equals
和 GetHashCode
方法。在创建数据模型类时,您需要记住按照 NHibernate 的要求将每个成员变量声明为 virtual
。创建数据模型类和映射后,我们就可以使用它们通过 NHibernate 查询数据库。让我们首先看一下“NHibernate\NHibernateDbAccessDefault.cs”文件。
using FluentNHibernate.Mapping;
using SQLPerformanceRead.DataAccessLayer.NHibernate.DataModel;
namespace SQLPerformanceRead.DataAccessLayer.NHibernate.TableMapping
{
public sealed class TCourseMap : ClassMap<TCourse>
{
public TCourseMap()
{
Table("TCourse");
Id(x => x.Id).Column("ID");
Map(x => x.Name).Column("Name");
Map(x => x.CreditHours).Column("CreditHours");
}
}
public class TStudentMap : ClassMap<TStudent>
{
public TStudentMap()
{
Table("TStudent");
Id(x => x.Id).Column("ID");
Map(x => x.LastName).Column("LastName");
Map(x => x.FirstName).Column("FirstName");
Map(x => x.EnrollmentTime).Column("EnrollmentTime");
HasMany(x => x.StudentScores).KeyColumn("StudentId");
}
}
public sealed class TStudentScoreMap : ClassMap<TStudentScore>
{
public TStudentScoreMap()
{
Table("TStudentScore");
CompositeId().KeyProperty(x => x.StudentId, "StudentId")
.KeyProperty(x => x.CourseId, "CourseId");
Map(x => x.Score).Column("Score");
Map(x => x.LastUpdated).Column("LastUpdated");
References(x => x.Course).ForeignKey("CourseId").Column("CourseId");
}
}
}
NHibernateDbAccessDefault
类执行的操作与我们通过 ADO.NET 和 Dapper 执行的操作完全相同,只是数据库访问使用的是 NHibernate。由于 NHibernate 不提倡使用存储过程,因此此类直接通过映射访问数据库表。这就是我们授予用户“TestUser”表级权限的原因。您可能还会注意到聚合是在 C# 代码中完成的。我们实际上可以使用 Hibernate Query Language 让 SQL Server 执行聚合。由于 Hibernate Query Language 与 对象关系映射 概念的初衷冲突,而且它是 美国国家标准协会 已经建立的 ANSI SQL 更复杂和冗余的重复,我决定在本文中跳过它。但如果您感兴趣,可以从此链接中探索它。如果您仔细查看 NHibernateDbAccessDefault
类,您会发现通过 NHibernate 进行数据库查询的语法如下所示。
using System;
using System.Collections.Generic;
using SQLPerformanceRead.DataAccessLayer.NHibernate.DataModel;
using SQLPerformanceRead.DataAccessUtilities.NHibernate;
using SQLPerformanceRead.ViewModels;
using NHibernate.Linq;
using System.Linq;
namespace SQLPerformanceRead.DataAccessLayer.NHibernate
{
public class NHibernateDbAccessDefault
{
public static StudentSummariesVm LoadStudentSummaries()
{
var summaries = new StudentSummariesVm();
var startTime = DateTime.Now;
var studentSummaries = new List<StudentSummary>();
using (var session = NHibernateUtility.OpenSession())
{
var students = session.Query<TStudent>().ToList();
foreach (var student in students)
{
int count = 0;
int credits = 0;
double totalScore = 0;
foreach (var score in student.StudentScores)
{
count++;
totalScore = totalScore + score.Score;
if (score.Score >= 60)
{
credits = credits + score.Course.CreditHours;
}
}
int averageScore = Convert.ToInt32(totalScore / count);
var studentSummary = new StudentSummary
{
StudentId = student.Id,
LastName = student.LastName,
FirstName = student.FirstName,
AverageScore = averageScore,
Credits = credits
};
studentSummaries.Add(studentSummary);
}
}
var timeSpent = DateTime.Now.Subtract(startTime);
summaries.StudentSummaries = studentSummaries;
summaries.LoadTime = timeSpent;
return summaries;
}
public static StudentCoursesVm LoadStudentCourses(int studentId)
{
var courses = new StudentCoursesVm();
var startTime = DateTime.Now;
var studentCourses = new List<StudentCourse>();
using (var session = NHibernateUtility.OpenSession())
{
var studentScores = session.Query<TStudentScore>()
.Where(s => s.StudentId == studentId).ToList();
foreach (var studentScore in studentScores)
{
var studentCourse = new StudentCourse
{
CourseId = studentScore.CourseId,
CourseName = studentScore.Course.Name,
CreditHours = studentScore.Course.CreditHours,
Score = studentScore.Score
};
studentCourses.Add(studentCourse);
}
}
var timeSpent = DateTime.Now.Subtract(startTime);
courses.StudentCourses = studentCourses;
courses.LoadTime = timeSpent;
return courses;
}
}
}
尽管每个查询都只是一行代码,但执行时,这行代码可能会导致数千次往返数据库查询。稍后您会发现,与其他方法相比,NHibernate 完成相同任务的速度要慢得多。为了提高性能,我创建了另一个名为 NHibernateDbAccessImproved
的类。此类与 NHibernateDbAccessDefault
完全相同,只是查询采用以下语法:
var students = session.Query<TStudent>().ToList();
和
var studentScores = session.Query<TStudentScore>()
.Where(s => s.StudentId == studentId).ToList();
FetchMany
和 Fetch
强制 NHibernate 以急切加载的方式从数据库检索信息,从而减少了数据库往返次数。
var students = session.Query<TStudent>()
.FetchMany(s=>s.StudentScores)
.ThenFetch(s=>s.Course)
.ToList();
和
var studentScores = session.Query<TStudentScore>()
.Where(s => s.StudentId == studentId)
.Fetch(s=>s.Course)
.ToList();
FetchMany
和 Fetch
强制 NHibernate 以急切加载的方式从数据库检索信息,从而减少了数据库往返次数。
控制器
此 MVC 应用程序的控制器在“Controllers\HomeController.cs”文件中实现。
using System.Web.Mvc;
using SQLPerformanceRead.DataAccessLayer.ADONET;
using SQLPerformanceRead.DataAccessLayer.Dapper;
using SQLPerformanceRead.DataAccessLayer.NHibernate;
using SQLPerformanceRead.DataAccessUtilities;
using SQLPerformanceRead.ViewModels;
namespace SQLPerformanceRead.Controllers
{
public class HomeController : Controller
{
public ActionResult Index() { return View(); }
public ActionResult StudentSummaries(string method)
{
var summaries = new StudentSummariesVm();
if (method == DataAccessMethod.AdoNet.ToString())
{
summaries = AdoNetDbAccess.LoadStudentSummaries();
}
else if (method == DataAccessMethod.Dapper.ToString())
{
summaries = DapperDbAccess.LoadStudentSummaries();
}
else if (method == DataAccessMethod.NHibernateDefault.ToString())
{
summaries = NHibernateDbAccessDefault.LoadStudentSummaries();
}
else
{
summaries = NHibernateDbAccessImproved.LoadStudentSummaries();
}
return PartialView(summaries);
}
public ActionResult StudentCourses(int studentId, string method)
{
var courses = new StudentCoursesVm();
if (method == DataAccessMethod.AdoNet.ToString())
{
courses = AdoNetDbAccess.LoadStudentCourses(studentId);
}
else if (method == DataAccessMethod.Dapper.ToString())
{
courses = DapperDbAccess.LoadStudentCourses(studentId);
}
else if (method == DataAccessMethod.NHibernateDefault.ToString())
{
courses = NHibernateDbAccessDefault.LoadStudentCourses(studentId);
}
else
{
courses = NHibernateDbAccessImproved.LoadStudentCourses(studentId);
}
return PartialView(courses);
}
}
}
Index
动作方法仅加载应用程序的主页。StudentSummaries
和 StudentCourses
方法调用相应的数据访问方法来生成视图模型,并渲染相应的子视图,以供应用程序主页发出的 AJAX 调用 使用。
视图
应用程序的主页在“Views\Home\Index.aspx”文件中实现。
<div>
<label class="textLabel">Select a data access method</label>
<span>
<input type="radio" name="radAccessMethod"
value="<%=DataAccessMethod.AdoNet.ToString() %>" checked="checked" />
<label>ADO.Net</label>
<input type="radio" name="radAccessMethod"
value="<%=DataAccessMethod.Dapper.ToString() %>" />
<label>Dapper.Net</label>
<input type="radio" name="radAccessMethod"
value="<%=DataAccessMethod.NHibernateDefault.ToString() %>" />
<label>NHibernate Default</label>
<input type="radio" name="radAccessMethod"
value="<%=DataAccessMethod.NHibernateImproved.ToString() %>" />
<label>NHibernate Improved</label>
</span>
<label class="buttonlink" id="lblLoadStudents">Load Students</label>
<label class="buttonlink" id="lblClear">Clear</label>
</div>
<div id="divStudents"></div>
<div id="divStudentCourses"></div>
单选按钮允许我们选择要使用的数据访问方法。`lblLoadStudents` 和 `lblClear` 标签充当动作按钮。点击 `lblLoadStudents` 标签将使用所选的数据访问方法从数据库加载结果并将其显示在网页上。点击 `lblClear` 标签将清除结果。支持此页面的 JavaScript 如下:
<script type="text/javascript">
var MakeAjaxCall = function (url, successCallback) {
$.ajax({
cache: false,
type: "GET",
async: false,
url: url,
dataType: "text",
success: successCallback,
error: function (xhr) {
alert(xhr.responseText);
}
});
};
$(document).ready(function () {
$('#lblLoadStudents').click(function () {
var method = $('input[name=radAccessMethod]:checked').val();
var url = studentSummariesUrl + "?method=" + method;
MakeAjaxCall(url, function (htmlFragment) {
$('#divStudents').html(htmlFragment);
});
});
$('#lblClear').click(function () {
$('#divStudents').html('');
$("#divStudentCourses").dialog('close');
});
$('input[name=radAccessMethod]').change(function () {
$('#lblClear').click();
});
});
var ShowStudentCourses = function (studentId) {
var method = $('input[name=radAccessMethod]:checked').val();
var url = studentCoursesUrl + "?studentId=" + studentId
+ "&method=" + method;
MakeAjaxCall(url, function (htmlFragment) {
$('#divStudentCourses').html(htmlFragment);
var option = {
width: 500,
height: 300,
title: "Courses for student ID = " + studentId
};
$("#divStudentCourses").dialog(option);
});
};
</script>
两个子视图 StudentSummaries.ascx 和 StudentCourses.ascx 实现如下:
<%@ Control Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<SQLPerformanceRead
.ViewModels.StudentSummariesVm>" %>
<br/>
<label>Database Access Time: <span style="color: red"><%=Model.LoadTime %></span></label>
<div id="divStudentSummary" class="divTable">
<div class="tableHeader">
<span>StudentId</span>
<span>LastName</span>
<span>FirstName</span>
<span>AverageScore</span>
<span>Credits</span>
</div>
<%foreach (var summary in Model.StudentSummaries)
{ %>
<div>
<span><label class="buttonlink"
onclick="return ShowStudentCourses(<%= summary.StudentId %>) ">
<%=summary.StudentId %>
</label></span>
<span><%=summary.LastName %></span>
<span><%=summary.FirstName %></span>
<span><%=summary.AverageScore %></span>
<span><%=summary.Credits %></span>
</div>
<% } %>
</div>
<%@ Control Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<SQLPerformanceRead
.ViewModels.StudentCoursesVm>" %>
<br/>
<label>
Database Access Time: <span style="color: red"><%=Model.LoadTime %></span>
</label>
<div id="divStudentCourse" class="divTable">
<div class="tableHeader">
<span>CourseId</span>
<span>CourseName</span>
<span>CreditHours</span>
<span>Score</span>
</div>
<%foreach (var course in Model.StudentCourses)
{
var cssClass = (course.Score >= 60)?"succcess":"fail";
%>
<div class="<%=cssClass %>">
<span><%=course.CourseId %></span>
<span><%=course.CourseName %></span>
<span><%=course.CreditHours %></span>
<span><%=course.Score %></span>
</div>
<% } %>
</div>
运行应用程序
确保您的 SQL Server 正在运行且可访问,并确保 [SQLPerformanceRead] 数据库已成功创建,然后我们就可以运行应用程序了。
我们可以勾选其中一个单选按钮来表示所选的数据访问方法,然后点击“加载学生”标签。学生摘要将加载到网页上,您可以在页面上看到从数据库数据创建视图模型所花费的时间。如果您点击任何学生的“学生 ID”,该学生所修课程将加载到页面中,并且所花费的时间也会显示在页面上。
这个例子远非一个商业级的网络应用程序,我没有向用户提供任何反馈来指示 AJAX 调用将加载请求的信息。当您测试 NHibernate 方法时,您需要耐心等待,因为它加载数据需要很长时间,您可能会认为应用程序已经死机了。但如果一切顺利,并且您足够耐心,结果最终会返回。
比较结果
以下是在调试模式下对每种方法运行 10 次结果取平均值后的比较结果。
我们可以看到,直接 ADO.NET 与 Dapper 相比没有明显的差异,但 NHibernate 完成相同任务的速度明显更慢。由于改进后的 NHibernate 方法减少了对数据库服务器的往返次数,其性能优于 NHibernate 的默认行为。如果您设置了 SQL Profiler,您会发现改进后的 NHibernate 方法所执行的 SQL 查询数量大大减少了。将应用程序部署到 Web 服务器后进行的进一步比较产生了以下结果:
从结果中我们可以看到,所有方法通过部署到 Web 服务器都提高了性能。默认的 NHibernate 方法改进最大,但完成相同任务所需的时间仍然明显更长,尽管不到 100 倍。随着技术的发展,计算机硬件和网络资源越来越便宜,但如果您想拥有一个快 100 倍的基础设施来弥补 NHibernate 使用不当造成的缓慢性能,那仍然是极其昂贵的。一个有趣的观察是,Dapper 部署到 Web 服务器后实际上比直接 ADO.NET 表现更好。这可能是因为 Dapper 中有一些比我自己手写代码更好的实现。嗯,也可能仅仅是测量误差,或者我的测试计算机在不同时间运行条件不同,因为差异无论如何都不是非常显著。
可能的进一步比较
我们实际上可以通过更改数据库中学生的总数来进行另一次比较。如果您更改创建数据库的脚本并通过更改脚本中的以下代码行来在数据库中添加不同数量的学生:
SET @NoOfStudents = 5000
您可以将其设置为除 5000 之外的任何数字。重新创建数据库后,您可以检查不同数据访问方法在不同大小的数据集上的性能。我不会在这里展示我自己的比较细节。但我发现,数据集越大,NHibernate 的性能越差。我绝对鼓励您进行这个实验。这个实验将帮助您决定如果您的应用程序确实是高并发且任务关键型,应该使用哪种数据访问方法。如果您确实想进行这个实验,当您运行 SQL 脚本时,您的 Management Studio 中可能会看到一些错误消息,因为服务器可能仍然被测试应用程序连接。不用担心。SQL 脚本是可重入的,所以您可以根据需要运行任意多次,直到数据库被删除并重新创建。很抱歉我没有为您提供一个单独的脚本来在不重新创建数据库的情况下更改数据库中的数据。但我最近真的很忙于一些更重要的工作。
值得关注的点
- 这是 .NET 环境中数据访问方法比较的第一部分。
- 示例应用程序向您展示了如何使用 ADO.NET、Dapper 和 Fluent NHibernate 在 SQL Server 数据库上执行读取操作。如果您不熟悉如何使用这些方法,可以使用此示例入门。
- 使用示例应用程序对这些数据访问方法进行了性能比较。结果表明,ADO.NET 和 Dapper 从数据库数据生成 MVC 视图模型所需的时间大致相同,但 NHibernate 的性能明显较慢。
- 该示例还向您展示了 NHibernate 的性能可以通过正确设置 NHibernate 查询的急切或延迟加载行为来提高,但这并不能使 NHibernate 的性能超越 ADO.NET 和 Dapper。如果您在应用程序中使用 NHibernate,加载行为可能会显著影响您的性能,您应该花足够的时间来研究它。
- 比较结果显示,应用程序部署到 Web 服务器时,性能差异不如调试环境中那么大。作为程序员,您很可能会在调试模式下运行应用程序。如果您的数据访问方法在您的环境中对您的数据集运行速度慢近 1000 倍,您可能一天也做不了多少工作。
- 如果您的应用程序使用 NHibernate,它需要在应用程序启动时进行初始化。如果您的应用程序比较复杂,初始化将需要一些时间。这在生产环境中通常不是问题,因为它只在应用程序启动时初始化一次。但如果您是程序员,并且每天需要多次更改程序和重新编译代码,那么您每天都必须多次等待初始化运行,这绝对不利于您的生产力。
- 本次比较是在我的个人电脑上进行的。强烈建议您在更全面的环境中自行进行比较。如果您发现不同的趋势,我绝对想听听,并重新进行我的比较。
- 我最近有点忙。但正如所承诺的,我将在短期内发布文章的第 2 和第 3 部分。在第二部分中,我将重点关注插入操作并向您展示 NHibernate Hilo,并告诉您我的发现。
- 我已经在 Firefox、Chrome 和较高版本的 Internet Explorer 中测试了该应用程序。尽管该应用程序在所有平台上运行,但在 Internet Explorer 上运行较慢,因为我们在一页中显示了 5000 名学生的信息。如果您想自行运行该应用程序,我希望您能意识到这一点。该应用程序在我的计算机上的 Firefox 和 Chrome 上运行良好。
- 如果您对本文感兴趣,有时间的话可以看看第 2 部分和第 3 部分。
- 希望您喜欢我的帖子,也希望这篇文章能以某种方式帮助到您。
历史
- 首次修订 - 2013 年 3 月 23 日。