在.NET项目里用Dapper访问数据库时,很多人习惯写单行SQL。但遇到数据库初始化、版本迁移或批量更新,往往要把一整段脚本放在.sql文件里。Dapper本身基于IDbConnection扩展,并不直接提供“跑脚本文件”的方法,需要我们自己读文件、拆语句、管事务。

一、为什么Dapper不能直接执行脚本文件
Dapper的Execute方法本质上是对ADO.NET的封装,它把传入的字符串作为一条命令发给数据库驱动。大多数数据库(如SQL Server)在一条命令里不允许同时出现多个独立语句,除非用分号隔开且驱动支持。而常见的.sql脚本里会有GO这种批处理分隔符,它不是SQL语法,而是客户端工具(如SSMS)用来切分批次的标记,数据库驱动并不认识。
如果我们把含GO的整段文本直接丢给Execute,就会抛出“GO附近有语法错误”。因此,执行脚本文件的核心任务,是把文件内容按批处理规则拆成多条可独立执行的命令,再逐条或整体提交。
二、读取并拆分SQL脚本文件
最简单可靠的方式是按行读取,遇到单独一行的GO(忽略大小写与空格)就切断。下面示例用C#实现了一个基础拆分器,同时过滤空行和单行注释,避免把无意义内容发给数据库。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
public static class SqlScriptSplitter
{
// 按GO分隔符拆分脚本,返回多个批次命令
public static List<string> SplitByGo(string filePath)
{
var batches = new List<string>();
var current = new List<string>();
foreach (var line in File.ReadAllLines(filePath))
{
// 去掉首尾空格后判断是否为GO批处理标记
var trimmed = line.Trim();
if (trimmed.Equals("GO", StringComparison.OrdinalIgnoreCase))
{
if (current.Count > 0)
{
batches.Add(string.Join(Environment.NewLine, current));
current.Clear();
}
}
else
{
// 跳过空行和--开头的注释行
if (!string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith("--"))
{
current.Add(line);
}
}
}
if (current.Count > 0)
{
batches.Add(string.Join(Environment.NewLine, current));
}
return batches.Where(b => !string.IsNullOrWhiteSpace(b)).ToList();
}
}
上面的代码把脚本拆成了字符串列表,每个元素是一条可执行的批处理命令。如果脚本中没有GO,整文件会被当作一个批次。对于MySQL等使用分号分隔的数据库,可以把拆分逻辑改成按分号切,但要注意字符串内的分号不能误切,生产环境建议用成熟库如MariaDbBatch。
这种方式的优点是简单直观,不依赖第三方SQL解析器;缺点是无法处理跨行注释/* */和复杂嵌套。若你的脚本含有这类内容,可在读取阶段先用正则剔除块注释,再走行拆分逻辑。
三、用Dapper在事务中批量执行
拆出批次后,借助Dapper的Execute配合IDbTransaction,就能保证要么全成功要么全回滚。以下示例展示从文件读取到执行的全过程:
using System;
using System.Data.SqlClient;
using Dapper;
class Program
{
static void Main()
{
var sqlFile = "init.sql";
var connectionString = "Server=127.0.0.1;Database=test;User Id=sa;Password=123;";
var batches = SqlScriptSplitter.SplitByGo(sqlFile);
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
try
{
foreach (var batch in batches)
{
// Dapper执行单条批处理命令
conn.Execute(batch, transaction: tx);
}
tx.Commit();
Console.WriteLine("脚本执行完成");
}
catch (Exception ex)
{
tx.Rollback();
Console.WriteLine("执行失败:" + ex.Message);
}
}
}
}
}
这里把拆分和执行分开,逻辑清晰。事务包裹了所有批次,任何一条失败都会整体回滚,适合初始化场景。如果脚本只是日常批量更新且允许部分成功,也可以去掉事务,逐条执行并记录错误,但初始化类操作强烈建议用事务。
需要注意的是,conn.Execute的第二个参数通常用来传匿名对象做参数化,本例直接传transaction命名参数,语法上没问题。若批次里含有参数化占位符,则要在循环内构造对应对象,而不能把不同结构的参数混在一起。
四、与逐条Execute的对比
有些开发者图省事,会在Execute里用分号拼一大串SQL,而非读文件。下面用表格列出两种思路的差异:
| 方式 | 可维护性 | 错误处理 | 适用场景 |
|---|---|---|---|
| 读取.sql文件并拆分 | 高,脚本独立于代码 | 事务内统一回滚 | 迁移、初始化、复杂批处理 |
| 代码内拼多语句 | 低,SQL散落在C#中 | 需手动截段捕获 | 简单固定小批量 |
从长期看,把SQL脚本作为独立资源文件(如放在项目Sql目录,设置生成操作为嵌入资源或复制至输出目录)更利于版本管理。Dapper只负责执行,不负责存逻辑,这种职责划分也让测试更方便:你可以单独对脚本做语法检查。
另外,在Web应用里执行大脚本要留意连接超时。可在连接字符串加Connection Timeout=300,或在执行前conn.Execute("SET LOCK_TIMEOUT 300000"),避免默认几十秒超时被中断。
五、常见误区与小结
一个典型误区是认为Dapper有类似ExecuteScript的内置方法。实际上官方只提供基础Execute,脚本处理能力要自己写。另一个误区是忽略GO只属于客户端工具,把它当SQL发给数据库。只要记住“读文件、按批拆、事务包”这三步,就能用Dapper稳妥地跑任意SQL脚本文件。
实践中建议把拆分器做成通用工具类,支持GO与分号双模式,并在执行前打印批次数,方便排查。这样无论是本地控制台还是服务端启动迁移,都能复用同一套逻辑,减少数据库操作带来的意外停机。