Dotnet-Dapper的用法

Dapper是一个轻量级的ORM(对象关系映射)框架,专为.NET设计。它通过扩展IDbConnection接口,使开发者能够方便地执行SQL查询,并将查询结果映射到对象模型中。类似于ADO.NET

Dapper Query - 了解如何查询数据库并返回列表

Dapper基础:CURD

查询数据

Dapper使用Query方法执行SQL查询并返回结果集。

匿名查询

只用了Query(sql),而不是Query<T>()

cs 复制代码
//查询前10条,返回第一条
string sql = "SELECT TOP 10 * FROM OrderDetails";

using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{    
    var orderDetail = connection.Query(sql).FirstOrDefault();

    FiddleHelper.WriteTable(orderDetail);
}

强类型查询

使用Query<T>()

cs 复制代码
public class Student{
    public int Id { get; set; }
    public string Email { get; set; }
}

using (IDbConnection db = new SqlConnection(connectionString))
{
    string sql = "SELECT Id, Email FROMFROM Students WHERE Email = @Email";
    var students = db.Query<Student>(sql,new { Email = "test@xxx.com" }).ToList();
}

查询多映射

Invoice(发票)和InvoiceDetail(发票细节)

cs 复制代码
// 发票主表实体
public class Invoice
{
    public int ID { get; set; } // 主键
    public string InvoiceNo { get; set; } // 发票编号
    public DateTime InvoiceDate { get; set; } // 发票日期
    // 其他发票字段...

    // 关键修正:一对多关联 → 用集合存储多条明细(原代码是单个对象,错误)
    public List<InvoiceDetail> InvoiceDetails { get; set; } = new List<InvoiceDetail>(); // 初始化避免空引用

    // 必须重写 Equals 和 GetHashCode(用于 Distinct() 去重,基于主键 InvoiceID)
    public override bool Equals(object obj)
    {
        return obj is Invoice invoice && InvoiceID == invoice.InvoiceID;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(InvoiceID);
    }
}

// 发票明细表实体
public class InvoiceDetail
{
    public int DetailID { get; set; } // 明细主键(关键!原代码可能缺少,导致 splitOn 映射错位)
    public int InvoiceID { get; set; } // 外键(关联发票表)
    public string ProductName { get; set; } // 产品名称
    public decimal Quantity { get; set; } // 数量
    public decimal UnitPrice { get; set; } // 单价
    // 其他明细字段...
}
cs 复制代码
string sql 
= "SELECT * FROM Invoice AS A INNER JOIN InvoiceDetail AS B ON A.InvoiceID = B.InvoiceID;";

using (var connection = new SqlConnection(Connstr))
{
    connection.Open();
    //Dapper 映射:T1=Invoice(主表),T2=InvoiceDetail(从表),TReturn=Invoice
    var invoices = connection.Query<Invoice, InvoiceDetail, Invoice>(
            sql,
            (invoice, invoiceDetail) =>
            {
                invoice.InvoiceDetail = invoiceDetail;
                return invoice;
            },
            splitOn: "DetailID")
        .Distinct()
        .ToList();
}

splitOn: "InvoiceID"用于分隔查询字段,赋值,从这个把查询出来的字段,左边的赋值给主表,右边的赋值给从表,例如

cs 复制代码
splitOn:"Aid"

A.id A.name A.password   |   B.Aid B.bname B.bpasswrod .

最好不要select *,而是select 具体字段。

如果分割线选的不对可能出错例如,

cs 复制代码
SELECT
    A.Id, A.InvoiceNo, A.InvoiceDate,
    B.DetailId, -- 新增字段,放在 B.InvoiceId 前面
    B.InvoiceId, B.ProductName...

此时 splitOn: "InvoiceId" 会找第一个 InvoiceId,主表没有DetailId这个属性,映射直接报错。

查询多映射(1对多)

cs 复制代码
string sql = "SELECT TOP 10 * FROM Orders AS A INNER JOIN OrderDetails AS B ON A.OrderID = B.OrderID;";

using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{            
    var orderDictionary = new Dictionary<int, Order>();


    var list = connection.Query<Order, OrderDetail, Order>(
    sql,
    (order, orderDetail) =>
    {
        Order orderEntry;

        if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry))
        {
        orderEntry = order;
        orderEntry.OrderDetails = new List<OrderDetail>();
        orderDictionary.Add(orderEntry.OrderID, orderEntry);
        }

        orderEntry.OrderDetails.Add(orderDetail);
        return orderEntry;
    },
    splitOn: "OrderID")
    .Distinct()
    .ToList();

    Console.WriteLine(list.Count);

    FiddleHelper.WriteTable(list);
    FiddleHelper.WriteTable(list.First().OrderDetails);
}

返回的[0x123,0x123,0x134].Distinct()==[0x123,0x134].

查询单个项

QueryFirst

QuerySingle

QueryFirstOrDefault,

QuerySingleOrDefault

查询到无值,一个值,多个值的结果:

他们也支持QueryFirst(匿名),QueryFirst<T>(强类型)两种方式。

批处理 SQL 返回多个结果

QueryMultiple

以下示例演示如何在一次往返数据库执行两个 SQL 语句。在第一个查询中,我们从表中获取所有发票,在第二个查询中,我们从表中获取所有发票项目

cs 复制代码
string sql = "SELECT * FROM Invoices WHERE InvoiceID = @InvoiceID; SELECT * FROM InvoiceItems WHERE InvoiceID = @InvoiceID;";

using (var connection = My.ConnectionFactory())
{
    connection.Open();

    using (var multi = connection.QueryMultiple(sql, new {InvoiceID = 1}))
    {
        var invoice = multi.Read<Invoice>().First();
        var invoiceItems = multi.Read<InvoiceItem>().ToList();
    }
}

一次查询两个多结果的结果集

cs 复制代码
{
        List<Order> orders = new List<Order>();
        List<OrderDetail> allDetails = new List<OrderDetail>();

        // 需求:查询所有状态为"已付款"的订单 + 对应的所有明细(两个都是多结果集)
        string sql = @"
            -- 结果集1:多结果(所有已付款的订单)
            SELECT OrderID, OrderNo, OrderDate, Status 
            FROM Orders 
            WHERE Status = @Status; -- 条件:已付款

            -- 结果集2:多结果(这些订单对应的所有明细)
            SELECT DetailID, OrderID, ProductName, Quantity, UnitPrice 
            FROM OrderDetails 
            WHERE OrderID IN (SELECT OrderID FROM Orders WHERE Status = @Status); -- 关联主表条件
        ";

        try
        {
            using (var connection = CreateConnection())
            {
                await connection.OpenAsync();

                // 执行多结果集查询(两个结果集都是多数据)
                using (var multi = await connection.QueryMultipleAsync(sql, new { Status = "已付款" }))
                {
                    // 1. 读取第一个多结果集:所有已付款的订单(转为列表)
                    orders = (await multi.ReadAsync<Order>()).ToList();

                    // 2. 读取第二个多结果集:所有关联的明细(转为列表)
                    allDetails = (await multi.ReadAsync<OrderDetail>()).ToList();
                }
            }

            // 3. 核心:在内存中关联主表和从表(按 OrderID 分组匹配)
            // 先将明细按 OrderID 分组(key=OrderID,value=该订单的所有明细)
            var detailsGrouped = allDetails.GroupBy(d => d.OrderID).ToDictionary(g => g.Key, g => g.ToList());

            // 遍历每个订单,将对应的明细赋值给 OrderDetails 集合
            foreach (var order in orders)
            {
                // 若该订单有对应的明细,赋值;否则保持空集合(初始化时已 new List<>)
                if (detailsGrouped.TryGetValue(order.OrderID, out var orderDetails))
                {
                    order.OrderDetails = orderDetails;
                }
            }

            // 输出结果验证
            Console.WriteLine($"已付款订单总数:{orders.Count}");
            foreach (var order in orders)
            {
                Console.WriteLine($"订单编号:{order.OrderNo},明细数量:{order.OrderDetails.Count}");
                foreach (var detail in order.OrderDetails)
                {
                    Console.WriteLine($"  - 商品:{detail.ProductName},数量:{detail.Quantity}");
                }
            }
        }
        catch (SqlException ex)
        {
            Console.WriteLine($"数据库错误:{ex.Message}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"系统错误:{ex.Message}");
        }
    }

QueryUnbufferedAsync()

异步流式查询,防止一次性读入大数据占据内存过大,该函数可以一条一条,将内存占用从1G减到1K。底层应该使用了数据库游标

支持匿名,强类型,一对一,一对多

cs 复制代码
public class OrderDetail
{
    public int OrderDetailID { get; set; }
    public int OrderID { get; set; }
    public int ProductID { get; set; }
    public int Quantity { get; set; }
}

string sql = "SELECT TOP 10 * FROM OrderDetails";

using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
    await foreach (var orderDetail in connection.QueryUnbufferedAsync<OrderDetail>(sql))
    {
        Console.WriteLine($"{orderDetail.OrderDetailID} - {orderDetail.Quantity}");
    }
}

一对一映射

cs 复制代码
string sql = "SELECT * FROM Invoice AS A INNER JOIN InvoiceDetail AS B ON A.InvoiceID = B.InvoiceID;";

using (var connection = My.ConnectionFactory())
{
    var invoices = new List<Invoice>();

    await foreach (var invoice in connection.QueryUnbufferedAsync<Invoice, InvoiceDetail, Invoice>(
        sql,
        (inv, detail) =>
        {
            inv.InvoiceDetail = detail;
            return inv;
        },
        splitOn: "InvoiceID"))
    {
        invoices.Add(invoice);
    }
}

Execute

它可以执行一次或多次命令并返回受影响的行数。此方法通常用于执行:

存储过程

INSERT 语句

UPDATE 语句

DELETE 语句

执行存储过程
cs 复制代码
var sql = "usp_UpdateTable";

using (var connection = new SqlConnection(connectionString))
{
    var rowsAffected = connection.Execute(sql, new { id = 1, value1 = "ABC", value2 = "DEF" }, commandType: CommandType.StoredProcedure);
}

多次执行存储过程

cs 复制代码
string sql = "Invoice_Insert";

using (var connection = My.ConnectionFactory())
{
    var affectedRows = connection.Execute(sql,
        new[]
        {
            new {Kind = InvoiceKind.WebInvoice, Code = "Many_Insert_1"},
            new {Kind = InvoiceKind.WebInvoice, Code = "Many_Insert_2"},
            new {Kind = InvoiceKind.StoreInvoice, Code = "Many_Insert_3"}
        },
        commandType: CommandType.StoredProcedure
    );

    My.Result.Show(affectedRows);
}

在上面的示例中,名为"Invoice_Insert"的存储过程将被调用三次。

插入

多条记录插入

cs 复制代码
string sql = "INSERT INTO Customers (CustomerName) Values (@CustomerName);";

using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
    connection.Open();

    var affectedRows = connection.Execute(sql,
    new[]
    {
    new {CustomerName = "John"},
    new {CustomerName = "Andy"},
    new {CustomerName = "Allan"}
    }
);

Console.WriteLine(affectedRows);
更新

在下面的示例中,它将更新两条等于"1"和"4"的记录

cs 复制代码
string sql = "UPDATE Categories SET Description = @Description WHERE CategoryID = @CategoryID;";

using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{    
    var affectedRows = connection.Execute(sql,
    new[]
    {
    new {CategoryID = 1, Description = "Soft drinks, coffees, teas, beers, mixed drinks, and ales"},
    new {CategoryID = 4, Description = "Cheeses and butters etc."}
    }
);

Console.WriteLine(affectedRows);
删除
cs 复制代码
string sql = "DELETE FROM OrderDetails WHERE OrderDetailID = @OrderDetailID";

using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{            
    var affectedRows = connection.Execute(sql, 
        new[]
    {
    new {OrderDetailID = 1},
    new {OrderDetailID = 2},
    new {OrderDetailID = 3}
    }
);

Console.WriteLine(affectedRows);

ExecuteReader

cs 复制代码
using(var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServer()))
{
    var reader = connection.ExecuteReader("SELECT * FROM Customers WHERE CreatedDate > @CreatedDate;", new { createdDate} );

    DataTable table = new DataTable();
    table.Load(reader);
            
    FiddleHelper.WriteTable(table);
}

ExcuteScalar

它允许您执行 SQL 语句或存储过程,并在结果集中第一行的第一列返回标量值。

如果结果集包含多个列或行,则它仅采用第一行的第一列,所有其他值将被忽略。

如果结果集为空,它将返回引用。Null

与 or 等聚合函数一起使用非常有用。Count(*),Sum()

cs 复制代码
//得到客户总数
using(var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServer()))
{
    int rowCount = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM Customers");

    Console.WriteLine(rowCount);
}
//得到客户id为1的人的名字
using(var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServer()))
{
    var name = connection.ExecuteScalar<string>("SELECT Name FROM Customers WHERE CustomerID = @CustomerID;", new { CustomerID = 1});

    Console.WriteLine(name);
}

多类型结果

cs 复制代码
string sql = "SELECT * FROM Invoice;";

using (var connection = My.ConnectionFactory())
{
    connection.Open();

    var invoices = new List<Invoice>();

    using (var reader = connection.ExecuteReader(sql))
    {
        var storeInvoiceParser = reader.GetRowParser<StoreInvoice>();得到转化器
        var webInvoiceParser = reader.GetRowParser<WebInvoice>();

        while (reader.Read())
        {
            Invoice invoice;

            switch ((InvoiceKind) reader.GetInt32(reader.GetOrdinal("Kind")))int转成枚举
            {
                case InvoiceKind.StoreInvoice:
                    invoice = storeInvoiceParser(reader);转化
                    break;
                case InvoiceKind.WebInvoice:
                    invoice = webInvoiceParser(reader);
                    break;
                default:
                    throw new Exception(ExceptionMessage.GeneralException);
            }

            invoices.Add(invoice);
        }
    }
}

Dapper缓冲,就是提前ToList()

事务

cs 复制代码
string sql = "INSERT INTO Customers (CustomerName) Values (@CustomerName);";

using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
    connection.Open();
	
    using (var transaction = connection.BeginTransaction())
    {
        try
        {
            var affectedRows = connection.Execute(sql, new { CustomerName = "Mark" }, transaction: transaction);
			
            transaction.Commit(); // ✅ Commit when everything is fine
			
            Console.WriteLine(affectedRows);
        }
        catch
        {
            transaction.Rollback(); // ✅ Rollback if something goes wrong
            throw;
        }
    }
}

Dapper Plus

PM> Install-Package Z.Dapper.Plus

1实体映射配置(DapperPlusManager)

cs 复制代码
// 配置 Supplier 实体:映射到数据库表 Suppliers,主键是 SupplierID(Identity 表示"身份字段",通常是自增主键)
DapperPlusManager.Entity<Supplier>().Table("Suppliers").Identity(x => x.SupplierID);

目录

Dapper基础:CURD

查询数据

匿名查询

强类型查询

查询多映射

查询多映射(1对多)

查询单个项

[批处理 SQL 返回多个结果](#批处理 SQL 返回多个结果)

QueryUnbufferedAsync()

Execute

执行存储过程

插入

更新

删除

ExecuteReader

ExcuteScalar

事务

[Dapper Plus](#Dapper Plus)

批量插入主从表

一对多

实体具有标识属性,但您希望强制它插入特定值

插入而不返回标识值


cs 复制代码
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{    
    // 核心链式操作:批量插入供应商 → 关联外键 → 批量插入产品
    connection.BulkInsert(suppliers) // 第一步:批量插入所有供应商
              .ThenForEach(x => x.Products.ForEach(p => p.SupplierID = x.SupplierID)) // 第二步:给产品设置外键(关联供应商ID)
              .ThenBulkInsert(x => x.Products); // 第三步:批量插入所有产品
}

插入一对一

cs 复制代码
// @nuget: Z.Dapper.Plus
using Z.Dapper.Plus;
  
DapperPlusManager.Entity<Supplier>().Table("Suppliers").Identity(x => x.SupplierID);
DapperPlusManager.Entity<Product>().Table("Products").Identity(x => x.ProductID);

using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{    
    connection.BulkInsert(suppliers).ThenForEach(x => x.Product.SupplierID = x.SupplierID).ThenBulkInsert(x => x.Product);
}

一对多

cs 复制代码
// @nuget: Z.Dapper.Plus
using Z.Dapper.Plus;
   
DapperPlusManager.Entity<Supplier>().Table("Suppliers").Identity(x => x.SupplierID); 
DapperPlusManager.Entity<Product>().Table("Products").Identity(x => x.ProductID);     

using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{    
    connection.BulkInsert(suppliers).ThenForEach(x => x.Products.ForEach(y => y.SupplierID =  x.SupplierID)).ThenBulkInsert(x => x.Products);
}

实体具有标识属性,但您希望强制它插入特定值

  • 场景 1:数据库表主键是「非自增列」(如 UUID、业务自定义主键),需要手动指定 CustomerID 值插入;
  • 场景 2:数据库表主键是「自增列」,但需要批量导入历史数据(历史数据已有固定主键值,不能重新生成);
  • 场景 3:数据同步(从其他系统同步客户数据,主键值需保持一致,不能由当前数据库生成)。
cs 复制代码
 底层原理(以 SQL Server 为例)
Dapper Plus 执行时的底层 SQL 逻辑:


sql
-- 1. 开启手动插入标识列(允许手动传入 CustomerID)
SET IDENTITY_INSERT Customers ON;

-- 2. 批量插入数据(包含 CustomerID 字段)
BULK INSERT Customers (CustomerID, CustomerName, Email, CreateTime)
FROM ...; -- 数据来源(Dapper Plus 封装的内存数据)

-- 3. 关闭手动插入标识列(恢复默认行为)
SET IDENTITY_INSERT Customers OFF;

用法:

connection.UseBulkOptions(options => options.InsertKeepIdentity = true).BulkInsert(Entity);

例子:

cs 复制代码
// @nuget: Z.Dapper.Plus
using Z.Dapper.Plus;

DapperPlusManager.Entity<Customer>().Table("Customers"); 
        
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
    var validCustomers = customers.Where(c => c.CustomerID > 0).ToList(); // 过滤无效主键
    connection.UseBulkOptions(options => options.InsertKeepIdentity = true).BulkInsert(validCustomers);
}

字段映射必须正确(避免主键值插入失败)

cs 复制代码
DapperPlusManager.Entity<Customer>()
                 .Table("Customers")
                 .Identity(x => x.CustID) // 实体主键
                 .Map(x => x.CustID, "CustomerID"); // 映射到表主键字段
                .Map(x => x.CustomerName, "CustomerName")
                 .Map(x => x.Email, "Email")
                 .Map(x => x.CreateTime, "CreateTime");也可以用[Column] 特性(数据注解) 
直接在实体类的属性上标记数据库字段名,无需在 DapperPlusManager 中通过 .Map() 显式配置
cs 复制代码
数据库主键需支持手动插入
坑 1:若数据库表主键是「自增列」且未开启 IDENTITY_INSERT(Dapper Plus 会自动处理,但需确保当前用户有该权限),会抛 "不能为标识列插入显式值" 的错误;
坑 2:若实体中的 CustomerID 值重复(如两个客户的 CustomerID 都是 1001),插入时会抛 "主键约束冲突" 的错误;
解决:
确保实体中的 CustomerID 唯一(批量插入前校验去重);
确保数据库用户有 ALTER TABLE 或 IDENTITY_INSERT 权限(否则无法切换标识列开关)。

插入而不返回标识值

意思就是数据库生成的主键id回填到你实体类中。

配置 options.AutoMapOutputDirection = false,可以提升性能。

cs 复制代码
// @nuget: Z.Dapper.Plus
using Z.Dapper.Plus;

DapperPlusManager.Entity<Customer>().Table("Customers"); 
        
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
    connection.UseBulkOptions(options => options.AutoMapOutputDirection = false).BulkInsert(customers);
            
    FiddleHelper.WriteTable("1 - Customers (from list)", customers);
}
相关推荐
能摆一天是一天2 小时前
JAVA Function
java
SimonKing2 小时前
百度统计、Google Analytics平替开源网站分析工具:Umami
java·后端·程序员
Juchecar2 小时前
设计模式不是Java专属,其他语言的使用方法
java·python·设计模式
马克学长2 小时前
SSM基于Java的医疗器械销售系统oy281(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·开发语言·用户管理·ssm 框架·医疗器械销售系统
Seven973 小时前
MyBatis 常见面试题
java·mybatis
lqj_本人3 小时前
Rust与Go:现代系统编程语言的深度对比
开发语言·golang·rust
我命由我123453 小时前
Android WebView - loadUrl 方法的长度限制
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
前端架构师-老李3 小时前
Maven安装以及环境变量配置(macOS)
java·macos·maven
星释3 小时前
Rust 练习册 :Macros与宏系统
开发语言·后端·rust