
摘要
本文以一个真实可跑的示例讲解如何使用 ADO.NET 的 DataSet + SqlDataAdapter 向 MyBlog 数据库的 Users 表增加一条记录。文章以贴近日常交流的口吻展开:先给出适用场景、再给出完整且可运行的页面/后台代码,逐步解析关键模块(连接、DataAdapter、InsertCommand、Fill、NewRow、Update 等),最后给出示例测试结果、复杂度分析与实战注意点。本文适合想理解“离线编辑(在内存 DataTable 上操作)再一次性同步到数据库”这种编程模式的 ASP.NET 初中级开发者。
场景描述(为什么要这样做?)
假设你在维护一个小型博客系统 MyBlog,表 Users 存储用户注册信息。现在你做一个后台管理页面,允许管理员通过一个表单增加用户(或批量修改后一次性提交)。有两种常见实现方式:
- 直接执行
INSERTSQL(每次提交一条 SQL)——代码简单,适合单条写入场景。 - 使用
DataSet/DataAdapter:先Fill把表的数据读到内存的DataTable,在内存中添加/修改/删除行,最后一次性用DataAdapter.Update()把变更同步到数据库。优点是可以在内存中做事务式、批量的变更、便于做验证或在 UI 上编辑后再一起提交;缺点是占用内存并且适合数据量不大的场景。
本文用第二种方式做演示:页面先把 Users 当前数据加载进 DataSet(或一个空的 DataTable 结构),在内存中 NewRow() 并赋值,最后 sda.Update(ds, "Users") 将变更写回数据库。
适用场景举例:
- 后台管理员批量导入用户(先在页面里或文件里准备好多条,再一次性提交)。
- 在一个管理界面里允许编辑多行后统一提交(它能更好处理校验、回滚、一次性提交)。
- 需要在客户端/服务端做复杂校验后再写入数据库的情况。
题解答案(功能概述与要点)
功能:在 AdapterIns.aspx 页面实现“添加用户”功能。用户在页面填好账号(LoginId)、密码(LoginPwd)、姓名(Name)、QQ、邮箱(Mail),点提交后,后端用 SqlDataAdapter 读取 Users 表结构(与可选的现有数据),在内存中创建新 DataRow 并赋值,然后调用 sda.Update() 把新增行写入数据库。
关键点:
- 使用参数化 SQL 避免 SQL 注入;
- 通过
SqlDataAdapter的InsertCommand指定插入 SQL 及参数映射(参数名与 DataColumn 名字对应); - 使用
Fill()获取DataTable结构(可以只获取 schema,避免拉取大量数据); - 在
DataTable.NewRow()上赋值,Rows.Add()后调用Update(); - 异常处理与资源释放(
using或try-finally); - 可选使用
SqlCommandBuilder让DataAdapter自动生成Insert/Update/Delete命令(当使用简单SELECT * from Users并且表有主键时可用)。
题解代码(完整可运行示例)
下面给出一个简化但可直接运行的 ASP.NET WebForms 示例。请根据你的环境修改连接字符串(服务器名、数据库名、认证方式)。
数据库表结构(示例)
CREATE TABLE Users (
Id INT IDENTITY(1,1) PRIMARY KEY,
LoginId NVARCHAR(50) NOT NULL,
LoginPwd NVARCHAR(50) NOT NULL,
Name NVARCHAR(50) NULL,
QQ NVARCHAR(20) NULL,
Mail NVARCHAR(50) NULL
);
AdapterIns.aspx(前端表单)
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="AdapterIns.aspx.cs" Inherits="AdapterIns" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>添加用户 - 后台管理</title>
</head>
<body>
<form id="form1" runat="server">
<div style="width:400px;margin:20px;">
<h3>添加用户</h3>
<label>LoginId:</label><asp:TextBox ID="txtUid" runat="server" /><br /><br />
<label>LoginPwd:</label><asp:TextBox ID="txtPwd" runat="server" TextMode="Password"/><br /><br />
<label>Name:</label><asp:TextBox ID="txtName" runat="server" /><br /><br />
<label>QQ:</label><asp:TextBox ID="txtQQ" runat="server" /><br /><br />
<label>Mail:</label><asp:TextBox ID="txtMail" runat="server" /><br /><br />
<asp:Button ID="btnAdd" runat="server" Text="提交" OnClick="btnAdd_Click" />
<br /><br />
<asp:Label ID="lblMsg" runat="server" />
</div>
</form>
</body>
</html>
AdapterIns.aspx.cs(后端逻辑)
重点:代码经过清理与修正,使用
using自动释放资源、参数化查询、防止部分常见错误,并在注释里解释每一部分。
using System;
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;
public partial class AdapterIns : System.Web.UI.Page
{
protected void btnAdd_Click(object sender, EventArgs e)
{
// 从 Web.Config 读取连接字符串(推荐),示例中也可以直接在字符串里填写
string connStr = WebConfigurationManager.ConnectionStrings["MyBlogConn"]?.ConnectionString
?? "Server=YOUR_SERVER;Database=MyBlog;Integrated Security=true;";
// 新建 DataSet(在内存中操作)
DataSet ds = new DataSet();
try
{
using (SqlConnection sqlConn = new SqlConnection(connStr))
{
sqlConn.Open();
// 1) 建立 DataAdapter,Select 会被用来获取表结构(及可选的数据)
SqlDataAdapter sda = new SqlDataAdapter("SELECT * FROM Users WHERE 1=0", sqlConn);
// 注意:WHERE 1=0 只获取表结构,不拉取数据。适用于只想插入新行而不想加载全部行。
// 2) 设置 InsertCommand(使用参数化 SQL)
SqlCommand insertCmd = new SqlCommand(
"INSERT INTO Users(LoginId, LoginPwd, Name, QQ, Mail) VALUES (@LoginId, @LoginPwd, @Name, @QQ, @Mail); SELECT SCOPE_IDENTITY();",
sqlConn);
// 参数与 DataColumn 名称绑定(第二个参数是 SqlDbType,可根据实际类型调整)
insertCmd.Parameters.Add("@LoginId", SqlDbType.NVarChar, 50, "LoginId");
insertCmd.Parameters.Add("@LoginPwd", SqlDbType.NVarChar, 50, "LoginPwd");
insertCmd.Parameters.Add("@Name", SqlDbType.NVarChar, 50, "Name");
insertCmd.Parameters.Add("@QQ", SqlDbType.NVarChar, 20, "QQ");
insertCmd.Parameters.Add("@Mail", SqlDbType.NVarChar, 50, "Mail");
// 可选:如果希望 Update() 时能返回自增主键到 DataRow,可在 InsertCommand 中执行 SELECT SCOPE_IDENTITY()
// 并把 ReturnedValues 设置在适当位置(本例简化,不把返回主键写回 DataRow)
sda.InsertCommand = insertCmd;
// 3) Fill,只需表结构,指定表名 "Users"
sda.Fill(ds, "Users");
// 4) 获取 DataTable 引用
DataTable dt = ds.Tables["Users"];
// 5) 在 DataTable 中创建新行并赋值
DataRow dr = dt.NewRow();
dr["LoginId"] = txtUid.Text.Trim();
dr["LoginPwd"] = txtPwd.Text.Trim();
dr["Name"] = txtName.Text.Trim();
dr["QQ"] = txtQQ.Text.Trim();
dr["Mail"] = txtMail.Text.Trim();
// 可以在这里做业务层/数据格式校验,比如邮箱格式、账号是否重复(可再查询数据库或在内存中检查)
if (string.IsNullOrEmpty(dr["LoginId"].ToString()) || string.IsNullOrEmpty(dr["LoginPwd"].ToString()))
{
lblMsg.Text = "用户名和密码不能为空。";
return;
}
// 将新行加入表
dt.Rows.Add(dr);
// 6) 最后将 DataSet 的变更提交到数据库
// Update 会遍历 DataSet 找出新增/修改/删除的行并分别执行 InsertCommand/UpdateCommand/DeleteCommand
int rowsAffected = sda.Update(ds, "Users");
lblMsg.Text = $"提交成功,影响行数:{rowsAffected}";
}
}
catch (SqlException ex)
{
// 处理数据库异常,实际项目里可以记录日志
lblMsg.Text = "数据库操作出错:" + ex.Message;
}
catch (Exception ex)
{
lblMsg.Text = "发生错误:" + ex.Message;
}
}
}
提示:
- 我在
SELECT * FROM Users WHERE 1=0中使用WHERE 1=0的目的是仅获取表的 schema(列信息)和DataTable结构,而不下载任何已有数据,这样更节省网络/内存,特别是当你只是想插入新记录而不是编辑现有记录时。- 如果你确实需要在页面显示或编辑现有行,就把
WHERE 1=0换成SELECT * FROM Users(注意数据量)。
题解代码分析(逐行/模块解释)
下面按步骤详细解析示例中每个重要模块,帮助你真正理解每一行的作用与背后的原理。
连接字符串(connStr)
- 推荐把连接字符串放在
Web.config的<connectionStrings>节点,通过WebConfigurationManager.ConnectionStrings[...]读取,这样更安全、可维护。 - 示例
Integrated Security=true表示使用 Windows 身份验证;若你用 SQL Server 登录,请改成User Id=xxx;Password=yyy;。
风险与建议:
- 切勿在代码里硬编码生产数据库密码;
- 使用最小权限原则(数据库用户仅授予必要权限)。
SqlDataAdapter 的用途
-
SqlDataAdapter是通向数据库和内存DataSet/DataTable的桥梁。它有 4 个重要属性(命令):SelectCommand:用于填充DataTable/DataSet。InsertCommand:用于插入新记录。UpdateCommand:用于更新已有记录。DeleteCommand:用于删除记录。
在我们的示例里:
- 我们先用
SELECT ... WHERE 1=0只获取表结构; - 手动创建
InsertCommand并设置参数,这样DataAdapter.Update()就知道当数据表里有RowState==Added的行时,如何执行插入。
参数化 SQL 和参数映射
insertCmd.Parameters.Add("@LoginId", SqlDbType.NVarChar, 50, "LoginId");
这行做了两件事:
- 定义了 SQL 参数(类型、长度),防止 SQL 注入并保证类型匹配;
- 第 4 个参数
"LoginId"表示该参数会从DataRow["LoginId"]取得值(当执行Update()时自动绑定)。
如果你不使用第四个参数,你也可以在执行前手动设置参数值(比如 insertCmd.Parameters["@LoginId"].Value = someValue;),但使用列映射在 Update() 场景下非常方便。
Fill() 获取表结构或数据
sda.Fill(ds, "Users")会在ds.Tables["Users"]创建 DataTable 并填充数据(或仅填充 schema,如果使用WHERE 1=0)。Fill会自动把表的列名、类型、可空性等复制到DataTable,如果表有主键也会加载(注意:若 SELECT 未包含主键列,DataTable 里可能没有主键信息——这会影响后续Update的行为)。
DataTable.NewRow() 与 Rows.Add()
NewRow()返回一个DataRow,其列结构已和DataTable匹配。- 给 DataRow 指定列值后用
Rows.Add(dr)把它置为Added状态。 sda.Update(ds, "Users")会查看DataRow.RowState,对Added行执行InsertCommand,并在成功后把RowState设置为Unchanged。
Update() 的工作机制
-
Update()会遍历DataSet中目标DataTable的行:- 对
Added行执行InsertCommand; - 对
Modified行执行UpdateCommand; - 对
Deleted行执行DeleteCommand。
- 对
-
每执行一条命令,
DataAdapter会尝试把结果映射回DataRow(例如自增主键可以通过额外配置返回并写入 DataRow)。
错误处理与事务(增强建议)
-
在示例中我们用了
try/catch捕获异常。实际生产环境建议:- 使用数据库事务(
SqlTransaction)以实现更强的一致性(尤其是批量多条语句时)。 - 在
InsertCommand执行期间将SqlCommand.Transaction = transaction,并在全部成功后transaction.Commit(),出现异常时transaction.Rollback()。
- 使用数据库事务(
-
SqlCommandBuilder:当你用sda.SelectCommand = new SqlCommand("SELECT * FROM Users", sqlConn);并且表有主键时,可以创建SqlCommandBuilder cb = new SqlCommandBuilder(sda);它会自动为sda生成Insert/Update/Delete命令。但在很多场景我们更喜欢手动写命令以便更好地控制 SQL。
示例测试及结果(如何验证)
测试准备
-
在 SQL Server 中创建
MyBlog数据库并执行上文的CREATE TABLE Users脚本。 -
在
Web.config添加连接字符串(示例):<connectionStrings> <add name="MyBlogConn" connectionString="Server=.\SQLEXPRESS;Database=MyBlog;Integrated Security=true;" /> </connectionStrings> -
将
AdapterIns.aspx与AdapterIns.aspx.cs放到你的 WebForms 项目中并运行(IIS Express / Visual Studio)。
测试步骤
-
打开页面,输入:
- LoginId:
alice - LoginPwd:
pass123 - Name:
Alice Zhang - QQ:
1234567 - Mail:
alice@example.com
- LoginId:
-
点击 “提交”。
-
页面
lblMsg应显示提交成功,影响行数:1(或类似提示)。
数据库验证
在 SQL Server Management Studio 执行:
SELECT * FROM Users WHERE LoginId = 'alice';
应看到刚插入的一行,Id 为自增值。
常见问题及排查
- 影响行数为 0:说明
Update()没有检测到Added的行,可能是你忘了dt.Rows.Add(dr)或dr没真正被添加到DataTable。 - 连接失败:检查连接字符串、SQL Server 实例是否允许远程连接、认证方式等。
- 参数映射错误:确保
Parameters.Add(..., "LoginId")的第四个参数名跟 DataColumn 一致(区分大小写通常不敏感,但最好一致)。
时间复杂度与空间复杂度分析
这里的复杂度是针对内存/网络和大致操作成本的估计,不是精确的算法复杂度计算。
时间复杂度
-
Fill(ds, "Users"):- 如果
SELECT返回n行,则Fill的时间大致是 O(n)(网络传输 + 行解析 + DataRow 填充)。 - 我们建议用
WHERE 1=0或限制返回行数(例如只读 schema 或 top 1000),以避免拉取大量数据。
- 如果
-
NewRow与Rows.Add:O(1) 单行操作。 -
Update(ds, "Users"):Update会针对每一条Added/Modified/Deleted行执行对应命令。若新增m行,则约 O(m)(针对每行一次插入请求),其中每次插入还包括网络和数据库执行时间。
-
总结:
- 对于典型单条插入,整体时间复杂度接近 O(1)(常量操作)。
- 若批量插入
m条行,整体大致 O(m)。
空间复杂度
DataSet/DataTable内存占用与Fill获取的行数成正比,若n行则内存约 O(n)(每行占一定字节数)。当你只需要插入新行时,使用WHERE 1=0只取 schema,空间复杂度近似 O(m)(仅需存放新增的 m 行)。- 因此,若数据量较大(数万/百万行),不建议使用
DataSet整表加载;更推荐分批插入或直接使用批量插入(SqlBulkCopy)或直接INSERT语句。
总结(实战建议与常见拓展)
何时使用 DataSet + DataAdapter?
- 需要在内存中做复杂编辑、回滚或统一提交多条变更的场景;
- UI 需要呈现表格并且用户可以就地编辑多行后再一次
Save。
优化建议
- 只取 schema:如果只是插入新数据,使用
SELECT * FROM Users WHERE 1=0避免拉取数据。 - 使用事务:当批量插入多条记录时,使用
SqlTransaction以保证原子性。 - 批量插入:大量写入时使用
SqlBulkCopy会更高效。 - 参数化 SQL:避免字符串拼接以防 SQL 注入。
- 避免 DataSet 泄露:不要把整个 DataSet 当作 ViewState 存放在页面,会导致页面变大。
常见扩展
- 添加服务器端验证(邮箱格式、用户名唯一性);
- 在
InsertCommand中返回新插入的自增主键并同步回DataRow(可用于后续操作); - 使用
SqlCommandBuilder自动生成Update/Delete命令(但可维护性较差,建议手写 SQL)。


被折叠的 条评论
为什么被折叠?



