Dapper - 极速映射器






4.90/5 (7投票s)
在ASP.NET 7应用程序中引入Dapper
引言
Dapper是一款流行的SQL数据库映射器。通常,它将表行中的列字段映射到C#类实例中的属性,反之亦然。在数据管理的“三层模型”中,Dapper位于中间层,它从上层的视图或表示层获取数据,并以数据库可以处理的格式将其呈现给下层的数据层。同样,它从数据库获取数据,并以视图可以消费的格式将其呈现给上层。
为什么使用Dapper?
它易于使用、轻量级(47K)且速度快——大约是Entity Framework速度的两倍。其作者是StackOverflow的团队,他们维护着Dapper并用它来访问他们庞大的数据库。Dapper并非旨在取代Entity Framework;Entity Framework是一个出色的对象关系映射器(ORM),拥有你可能想要的所有功能,但许多用户在控制它时遇到困难,导致走了不必要的路线,并携带了永远不会打开的行李。有了Dapper,你就在驾驶座上。你掌握着它的方向,并决定它到达目的地后做什么。
实现
Dapper作为开源项目可在GitHub上找到,也可以作为Nuget包使用。它并非独立存在,而是作为一组扩展方法实现的,这些方法实际上扩展了IDbConnection
接口。这类似于system.Linq
扩展IEnumerable<T>
的方式。Dapper查询的典型格式是,将一个SQL语句作为string
传递,并附带语句所需的任何参数,这些参数以匿名类型的成员形式表示。大致如下所示。
int id = 7;
string sql = @"Select *from dbo.Employees where EmployeeID=@Id;";
using IDbConnection connection = _connectionCreator.CreateConnection();
var employee = await connection.QueryFirstOrDefaultAsync<Employee>(sql,new{Id=id});
这里,SQL语句从Employees
表中的行中选择所有列字段,其中EmployeeID
列等于输入参数Id
。“@
”符号标识了语句所需的输入参数。QueryFirstOrDefaultAsync<T>
泛型方法被使用,因为EmployeeID
是主键,所以只有一个记录需要查找。匿名类型的成员名称与SQL语句引用的输入参数名称匹配,并设置为局部变量id
的值。Dapper将查询的输出映射到type T
的一个实例。在此示例中,type T
被定义为Employee
类。这就是基本设置,还有其他Query
方法用于从数据库请求数据,以及Execute
方法用于向数据库传递命令,如Insert
、Delete
和Update
。通常,同一方法的异步和同步版本都存在。 Learn Dapper网站和GitHub上的DapperReadme.md文件提供了许多Dapper的优秀示例。
为ASP.NET应用程序中的Dapper配置
管理数据库连接
数据库连接在使用前需要打开,并在查询完成后立即关闭和释放。这意味着每个Dapper扩展方法都伴随着一定量的连接管理工作。还需要引用一个数据库服务器特定的IDbConnection
实例。因此,这里可能会出现大量代码重复,并且每次进行数据库调用的方法都会依赖于所选的IDbConnection
类型。解决此问题的一种方法是将Dapper方法封装在IDatabaseContext
类中,这样使用该类的任何东西都不需要了解Dapper或数据库连接。下面是一个调用DapperExecuteAsync
方法的IDatabaseContext
方法的示例。
public async Task<T> QueryFirstOrDefaultAsync<T>(
string sql,
object? parameters = null,
CommandType? commandType = null)
{
using IDbConnection connection = _connectionCreator.CreateConnection();
var result = await connection.QueryFirstOrDefaultAsync<T>(sql, parameters,
commandType: commandType);
return result;
}
ConnectionCreator.CreateConnection
方法是一个工厂方法,它返回一个Connection
的新实例。
public class MsSqlConnectionCreator : IConnectionCreator
{
protected ServerOptions _serverOptions;
public MsSqlConnectionCreator(IOptions<ServerOptions> serverOptionsSnapshot)
{
_serverOptions = serverOptionsSnapshot.Value;
}
public IDbConnection CreateConnection()
{
var connectionString = _serverOptions.MsSql;
return new SqlConnection(connectionString);
}
}
DatabaseContext
通过注入到其构造函数中的IOptions<T>
泛型类型的实例从数据库连接字符串中获取数据库连接。
public SqlServerContext(IOptions<ServerOptions> serverOptionsSnapshot)
{
_serverOptions = serverOptionsSnapshot.Value;
_connectionString = _serverOptions.MsSql;
}
这种技术被认为优于选择传递ConfigurationManager并使用该管理器读取连接的替代方案。它的优点是将ConfigurationManager
的作用域限制在Program.cs和Startup.cs中的其他应用程序构建器范围内。appsettings.json中ConnectionStrings节的成员值在运行时绑定到ServerOptions
单例类。
"ConnectionStrings": {
"MsSql": "Data Source=(localdb)\\ProjectModels;
Initial Catalog=Northwind;Integrated Security=True",
"MySql": "Server=127.0.0.1;user ID=root; Database=northwind; Password=Pa$$w0rd"
},
....
ServerOptions
类有两个属性,它们与ConnectionStrings节中的名称相匹配。
public class ServerOptions
{
//ConnectionStrings is not mapped,
//it's used to avoid a magic string when referencing
// the appsettings "ConnectionStrings" section
public const string ConnectionStrings = "ConnectionStrings";
public string MsSql { get; set; } = string.Empty;
public string MySql { get; set; } = string.Empty;
}
绑定在Program.cs中设置。
var section = builder.Configuration.GetSection(“ConnectionStrings”);
builder.Services.Configure<ServerOptions>(section);
管理SQL语句
拥有一个实现IDataAccess
接口的类可以解决很多问题,但表示层运行的服务仍然需要发出SQL语句。SQL语句本质上是数据库服务器的数据管理指令,理想情况下应该位于数据库内部。实现这一目标的方法是使用存储过程。存储过程是预编译的SQL语句,驻留在数据库中。在SQL Server对象资源管理器中,它们位于数据库的可编程性文件夹下。以下显示了如何从Northwind示例数据库中调用CustOrderHist
存储过程。
string customerID = "ANTON";//input param
var results = await _databaseContext.QueryAsync<ProductSummary>
(_storedProcedureId.CustomerOrderHistory,
new { CustomerID = customerID },
commandType: CommandType.StoredProcedure);
commandType
参数是通过使用命名参数初始化的。命名参数避免了为排在所需参数之前的参数输入大量null
值。过程名称作为自定义StoredProcedureId
类的属性传递。
public string CustomerOrderHistory { get; } = "dbo.CustOrderHist";
Northwind
的Orders
表中有超过800条记录,但Dapper可以在瞬间完成此过程。
示例应用程序
示例应用程序是一个ASP.NET 7控制台应用程序,它使用Northwind
数据库。它需要在appsettings.json文件中更新连接字符串,以指向数据库服务器实例。示例将运行TSQL或MySQL语句,具体取决于所选的SQL Server。它们并非旨在成为定论,大多数都是不言自明的,但有几个可能需要一些澄清。
示例 1
此查询涉及两个类。Order
类模拟Orders
表,Employee
类模拟Employees
表。Order
类还有一个引用Employees
表主键的外键,此外,它还有一个引用Employee
类实例的属性。
public class Order
{
public int OrderID { get; set; }
public string? CustomerID { get; set; }
public int EmployeeID { get; set; }
public DateTime OrderDate { get; set; }
public int ShipperID { get; set; }
public Employee? Employee { get; set; }
}
SQL语句在匹配的EmployeeIDs
上连接这两个表,并按Employee
的LastName
然后是FirstName
对查询进行排序。
string sql = @"select o.EmployeeID,o.OrderId,e.EmployeeID,e.FirstName,e.LastName
from dbo.Orders o
inner join dbo.Employees e
on o.EmployeeID = e.EmployeeID
order by e.LastName, e.FirstName";
Dapper将此查询的结果映射到Order
项的集合。
var employeeOrders= await _databaseContext.QueryAsync<Order, Employee>(sql,
(order, employee) =>
{
order.Employee = employee;
return order;
}, splitOn: "EmployeeID");
为了让Dapper能够高效地映射,需要提供一个函数,该函数将Order
类的Employee
属性设置为Employee
实例,并返回Order
类的实例。还需要告诉Dapper,在查询返回的所有列中,与Orders
表相关的列在哪里结束,而与Employees
表相关的列在哪里开始。假设是在遇到名称为“ID
”的ToUpper()
值的列名时。但是,在这种情况下,相关的标识列名是EmployeeID
,因此splitOn
参数需要设置为该名称。
示例 2
此示例包含一个通常会导致异常的SQL语句,因为它有一个集合作为输入参数,而集合参数通常是不允许的。Dapper通过将集合分解为一系列单个参数,以SQL解析器可以接受的格式来规避此限制。该语句将集合引用为@countries
。局部变量countryArray
通过匿名类型传递给QueryAsync
方法,该匿名类型有一个成员,其名称必须与引用的参数名称相同,并且设置为局部数组实例。该查询获取数组中命名的四个国家/地区中每个国家/地区提供的产品总数。
string[] countryArray = new[] { "France", "Germany", "UK", "USA" };
string sql = @"Select Suppliers.Country As SuppliersCountry,COUNT(*) as ProductCount
From Suppliers join Products on Suppliers.SupplierID=Products.SupplierID
where Suppliers.Country in @countries
Group by Suppliers.Country
Order by ProductCount Desc;";
var results = await _dba.QueryAsync<(string SuppliersCountry, int ProductCount)>
(sql, new { countries = countryArray });
如果您是SQL新手,上面的语句可能看起来有点令人生畏。一个有用的技巧是从后往前阅读它。因此,从最后一个子句开始,输出按ProductCount
降序排序。它仅在Suppliers.Country
存在于输入参数集合中时,才按Suppliers.Country
进行分组。数据是通过将Suppliers
表中的每一行与Products
表中的匹配行进行连接而获得的,连接条件是Suppliers.SupplierID
等于Products.SupplierID
。选定的行和列的输出将Suppliers.Country
作为SuppliersCountry
列的成员,并将组中所有项目的计数作为ProductCount
列的成员。在SQL中,分组表示为键/值对,其中值是从聚合函数派生的,该函数使用组的成员项集合作为输入参数。这将在几行代码中压缩大量功能。SQL是编程语言中的科伦坡;它比看起来更聪明。
结论
使用Dapper扩展方法作为数据库映射器,是使用**Entity Framework**提供的类型的有用且高效的替代方法。尤其是在不需要**Entity Framework**的高级功能的情况下。
学到的教训
务必await
Dapper的所有async
方法。简单地调用方法并返回Task
以便另一个方法可以await
它并不是一个好主意。尽管这种技术避免了使用async
关键字的开销,但它也会导致`null`引用异常。方法是从`using`语句中调用的,并且一旦返回未完成的Task
,`using`语句就会确保连接关闭,导致Dapper处于“脱节”状态。我是吃了亏才学到的。
历史
- 2023年1月3日:初版