核心功能:多条件查询、添加、编辑、删除学生数据,全程防SQL注入、自动释放数据库连接
核心考点:SQLParameter参数化查询、using自动释放资源、DataGridView数据绑定、窗体传值、ADO.NET核心操作
适配环境:WinForm + SQL Server,连接字符串配置在App.config中
一、前期准备:配置文件与核心实体类
1. App.config 配置文件(数据库连接核心)
代码中通过配置文件读取数据库连接字符串,这是项目中常用的方式,便于后期修改连接信息,无需重新编译代码。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<!-- name属性对应代码中读取的名称,connectionString为数据库连接信息 -->
<add name="connectionString"
connectionString="Data Source=你的数据库服务器;Initial Catalog=你的数据库名称;Integrated Security=True;Connect Timeout=30"
providerName="System.Data.SqlClient"/>
</connectionStrings>
</configuration>
核心知识点:
-
Data Source:数据库服务器地址,本地可写.或localhost。 -
Initial Catalog:要连接的数据库名称,需提前创建。 -
Integrated Security=True:使用 Windows 身份验证,无需输入账号密码;若使用 SQL 身份验证,需添加User ID=账号;Password=密码;。 -
Connect Timeout=30:连接超时时间,单位为秒,避免长时间等待。
2. Sex 实体类(下拉框数据绑定)
该类用于封装性别选项,为 WinForm 下拉框(ComboBox)提供数据源,实现"显示名称"和"实际值"的分离。
namespace 数据库操作
{
// 内部类,仅在当前命名空间可用
internal class Sex
{
// 性别名称(下拉框显示的文本,如"男""女")
public string Name { get; set; }
// 性别值(数据库存储的实际值,如0代表男,1代表女)
public int Value { get; set; }
}
}
核心知识点:
-
internal修饰符:表示该类仅在当前程序集(项目)中可见,避免外部引用。 -
属性封装:通过自动属性封装数据,遵循面向对象的封装原则,便于数据的获取和设置。
-
下拉框绑定基础:后续通过
DisplayMember(显示文本)和ValueMember(实际值)绑定该类的属性。
二、主窗体(Form1):核心功能实现
Form1 是程序的主界面,实现学生信息的查询、删除,以及跳转添加/编辑窗体的功能,核心涉及数据库查询、数据绑定、事件处理等知识点。
1. 窗体初始化:读取数据库连接字符串
using System;
using System.Collections.Generic;
using System.Configuration; // 读取配置文件需引用该命名空间
using System.Data;
using System.Data.SqlClient; // SQL Server 数据库操作核心命名空间
using System.Windows.Forms;
namespace 数据库操作
{
public partial class Form1 : Form
{
// 定义全局变量,存储数据库连接字符串,供整个窗体使用
public string connectionString;
// 窗体构造函数,窗体创建时执行
public Form1()
{
// 初始化窗体控件(自动生成的代码,由WinForm设计器创建)
InitializeComponent();
// 从配置文件读取连接字符串
var connCfg = ConfigurationManager.ConnectionStrings["connectionString"];
// 检查是否读取到连接字符串,若未读取到,提示并退出程序
if (connCfg == null)
{
MessageBox.Show("App.config未找到连接字符串!");
Application.Exit(); // 退出整个应用程序
}
// 将读取到的连接字符串赋值给全局变量
connectionString = connCfg.ConnectionString;
}
}
}
核心知识点:
-
ConfigurationManager:位于System.Configuration命名空间,用于读取配置文件中的内容,需在项目中添加对该程序集的引用。 -
InitializeComponent():自动生成的方法,用于初始化窗体上的所有控件(如按钮、文本框、DataGridView),不可手动修改。 -
Application.Exit():退出整个 WinForm 应用程序,适用于程序初始化失败的场景。
2. 搜索按钮事件:触发数据查询
// 搜索按钮点击事件,用户点击"搜索"时执行
private void btnSearch_Click(object sender, EventArgs e)
{
// 调用优化后的查询方法,绑定数据到DataGridView
BindGridView2();
}
核心知识点:
-
事件处理:WinForm 控件的事件通过"双击控件"或"属性面板-事件"绑定,
sender代表触发事件的控件(此处为搜索按钮),e为事件参数。 -
方法调用:将查询逻辑封装为独立方法(
BindGridView2),实现代码复用,符合"单一职责原则"。
3. 基础查询方法(BindGridView):理解 SQL 注入风险
该方法是基础的查询实现,重点用于演示 SQL 注入的风险及基础的参数化查询解决思路,实际开发中推荐使用优化后的方法。
/// <summary>
/// 基础查询方法,演示SQL注入风险及基础参数化查询
/// </summary>
public void BindGridView()
{
// 1. 创建数据库连接对象,传入连接字符串
SqlConnection conn = new SqlConnection(connectionString);
// 打开数据库连接
conn.Open();
// 2. 定义参数数组,指定参数类型和长度,避免SQL注入
SqlParameter[] sqlParas = new SqlParameter[]
{
// @Name:参数名,SqlDbType.VarChar:参数类型,30:参数长度
new SqlParameter("@Name",SqlDbType.VarChar,30),
new SqlParameter("@AgeStart",SqlDbType.Int),
new SqlParameter("@AgeEnd",SqlDbType.Int),
new SqlParameter("@Sex",SqlDbType.Bit),
new SqlParameter("@BirthdayStart",SqlDbType.DateTime),
new SqlParameter("@BirthdayEnd",SqlDbType.DateTime),
};
// 3. 为参数赋值,获取控件中的用户输入
sqlParas[0].Value = $"%{txtStuName.Text}%"; // 模糊查询,前后加%
sqlParas[1].Value = nudAgeStart.Value; // 数值选择器的最小值
sqlParas[2].Value = nudAgeEnd.Value; // 数值选择器的最大值
sqlParas[3].Value = true; // 临时默认性别值
// 格式化日期,拼接时间,确保查询范围准确
sqlParas[4].Value = dtpBirthdayStart.Value.ToString("yyyy-MM-dd") + " 00:00:00";
sqlParas[5].Value = dtpBirthdayEnd.Value.ToString("yyyy-MM-dd") + " 23:59:59";
// 4. 拼接SQL语句,where 1=1 确保无查询条件时SQL语句合法
string sql = "select * from stufirst where 1=1";
// 判断姓名输入框是否有值,有值则添加姓名查询条件
if (!string.IsNullOrEmpty(txtStuName.Text))
{
sql += "and Name like @Name"; // 注意:此处缺少空格,优化后已修正
}
// 判断最小年龄是否大于0,有值则添加年龄查询条件
if(nudAgeStart.Value > 0)
{
sql += "and Age > @AgeStart";
}
if (nudAgeEnd.Value > 0)
{
sql += "and Age < @AgeEnd ";
}
if(!string.IsNullOrWhiteSpace(dtpBirthdayStart.Text))
{
sql += "and Brithday >= @BirthdayStart";
}
if (!string.IsNullOrWhiteSpace(dtpBirthdayEnd.Text))
{
sql += "and Brithday <= @BirthdayEnd";
}
// 5. 创建SQL命令对象,传入SQL语句和连接对象
SqlCommand cmd = new SqlCommand(sql, conn);
// 6. 添加参数并执行查询
cmd.Parameters.Clear(); // 清空原有参数,避免重复添加
cmd.Parameters.AddRange(sqlParas); // 添加参数数组
// ExecuteReader():执行查询,返回SqlDataReader对象,用于读取数据
SqlDataReader reader = cmd.ExecuteReader();
// 7. 加载数据并绑定到DataGridView
DataTable dt = new DataTable(); // 创建数据表对象,存储查询结果
dt.Load(reader); // 从SqlDataReader加载数据到DataTable
dataGridView1.DataSource = dt; // 将DataTable绑定到DataGridView,自动显示数据
// 8. 关闭数据库连接,释放资源
conn.Close();
}
核心知识点:
-
SqlConnection:数据库连接对象,负责与 SQL Server 建立连接,使用完必须关闭,否则会造成连接泄露。 -
SQL 注入风险:若直接拼接用户输入(如
sql += "and Name like '"+txtStuName.Text+"'"),用户输入1=1--时,SQL 语句会变成select * from stufirst where 1=1--...,--是 SQL 注释符,后续条件会被忽略,导致查询所有数据,造成信息泄露。 -
SqlParameter:参数化查询核心,将用户输入作为参数传递,而非直接拼接进 SQL 语句,数据库会将参数作为数据处理,而非 SQL 语句的一部分,彻底避免 SQL 注入。 -
SqlCommand:SQL 命令对象,负责执行 SQL 语句或存储过程,可通过Parameters属性添加参数。 -
SqlDataReader:只读、向前的数据流,用于读取查询结果,效率较高,但需保持数据库连接打开,读取完成后可通过dt.Load(reader)转换为 DataTable。 -
DataTable:内存中的数据表,可独立于数据库连接存在,是 DataGridView 常用的数据源。
4. 优化后的查询方法(BindGridView2):推荐使用
该方法在基础查询的基础上优化,解决了基础方法的不足(如缺少空格、手动管理连接、异常处理缺失等),是实际开发中的标准写法。
/// <summary>
/// 优化后的查询方法,防止SQL注入,自动管理连接,增加异常处理
/// </summary>
public void BindGridView2()
{
try // 异常捕获,避免程序崩溃
{
// 初始化SQL语句,where 1=1 确保无查询条件时合法
string sql = "select * from stufirst where 1=1";
// 使用List存储参数,比数组更灵活,可动态添加
List<SqlParameter> sqlParas = new List<SqlParameter>();
// 拼接SQL条件,每个条件前加空格,避免语法错误
// 姓名模糊查询
if (!string.IsNullOrEmpty(txtStuName.Text))
{
sql += " and Name like @Name"; // 加空格,避免和前一句拼接出错
sqlParas.Add(new SqlParameter("@Name", $"%{txtStuName.Text}%")); // 动态添加参数
}
// 最小年龄查询
if (nudAgeStart.Value > 0)
{
sql += " and Age > @AgeStart";
sqlParas.Add(new SqlParameter("@AgeStart", nudAgeStart.Value));
}
// 最大年龄查询
if (nudAgeEnd.Value > 0)
{
sql += " and Age < @AgeEnd ";
sqlParas.Add(new SqlParameter("@AgeEnd", nudAgeEnd.Value));
}
// 性别查询,排除"全部"选项(Value=-1)
if (cbbSex.SelectedValue.ToString() != "-1")
{
sql += " and Sex = @Sex";
sqlParas.Add(new SqlParameter("@Sex", cbbSex.SelectedValue));
}
// 开始日期查询,使用Date属性获取日期部分,避免时间干扰
if (!string.IsNullOrWhiteSpace(dtpBirthdayStart.Text))
{
sql += " and Brithday >= @BirthdayStart ";
sqlParas.Add(new SqlParameter("@BirthdayStart", dtpBirthdayStart.Value.Date));
}
// 结束日期查询,处理为次日0点前,避免漏查当天数据
if (!string.IsNullOrWhiteSpace(dtpBirthdayEnd.Text))
{
sql += " and Brithday < @BirthdayEnd";
// 日期+1天再减1秒,确保包含结束日期当天的所有数据
sqlParas.Add(new SqlParameter("@BirthdayEnd", dtpBirthdayEnd.Value.Date.AddDays(1).AddSeconds(-1)));
}
// 使用using语句管理连接,自动释放资源,无需手动Close
using (SqlConnection conn = new SqlConnection(connectionString))
{
// 创建命令对象并添加参数
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddRange(sqlParas.ToArray()); // 将List转换为数组添加
// 使用SqlDataAdapter填充数据集,简化数据读取
SqlDataAdapter ada = new SqlDataAdapter(cmd); // 数据适配器,自动管理连接的打开和关闭
DataSet ds = new DataSet(); // 数据集,可存储多个数据表
ada.Fill(ds, "StudentInfo"); // 将查询结果填充到数据集,命名为StudentInfo
// 获取查询结果表
DataTable dt = ds.Tables["StudentInfo"];
// 判断是否查询到数据,无数据则提示并清空DataGridView
if (dt == null || dt.Rows.Count == 0)
{
MessageBox.Show("未查询到匹配学生数据");
dataGridView1.DataSource = null;
return;
}
// 修改DataGridView表头显示文本,提升用户体验
dt.Columns["Id"].Caption = "学号";
dt.Columns["Name"].Caption = "姓名";
dt.Columns["Age"].Caption = "年龄";
dt.Columns["Sex"].Caption = "性别";
dt.Columns["BrithDay"].Caption = "出生年月";
// 绑定数据到DataGridView
dataGridView1.DataSource = dt;
}
}
catch (Exception ex) // 捕获所有异常
{
// 提示异常信息,便于定位问题
MessageBox.Show("数据库操作失败:\r\n" + ex.ToString());
}
}
核心知识点:
-
using语句:用于管理实现了IDisposable接口的对象(如 SqlConnection、SqlCommand),代码块执行完毕后,自动调用Dispose()方法释放资源,无需手动关闭连接,避免连接泄露。 -
List<SqlParameter>:相比数组,List 支持动态添加参数,更灵活,适合根据不同条件动态拼接参数的场景。 -
SqlDataAdapter:数据适配器,核心方法Fill()用于将查询结果填充到 DataSet 或 DataTable,自动打开和关闭数据库连接,无需手动操作。 -
DataSet:数据集,可存储多个 DataTable,相当于内存中的小型数据库,适合多表查询场景。 -
日期处理优化:使用
dtpBirthdayStart.Value.Date获取日期的"年月日"部分,避免时间(时分秒)干扰查询;结束日期处理为AddDays(1).AddSeconds(-1),确保包含结束日期当天的所有数据(如查询到2024-07-11 23:59:59)。 -
异常处理:
try-catch块捕获所有异常,避免程序因数据库错误(如连接失败、SQL 语法错误)崩溃,同时通过ex.ToString()打印完整异常信息,便于排查问题。 -
DataGridView 表头修改:通过
dt.Columns["列名"].Caption设置表头显示文本,不修改数据库列名,提升用户体验。
5. 日期选择器事件:设置日期显示格式
// 开始日期选择器值改变事件
private void dtpBirthdayStart_ValueChanged(object sender, EventArgs e)
{
// 设置日期显示格式为长格式(如"2024年7月11日")
dtpBirthdayStart.Format = DateTimePickerFormat.Long;
}
// 结束日期选择器值改变事件
private void dtpBirthdayEnd_ValueChanged(object sender, EventArgs e)
{
dtpBirthdayEnd.Format = DateTimePickerFormat.Long;
}
核心知识点:
-
DateTimePicker:日期选择器控件,Value属性获取用户选择的日期时间,Format属性设置显示格式(Long:长格式,Short:短格式,如"2024/7/11")。 -
ValueChanged事件:当用户选择的日期改变时触发,此处用于动态设置显示格式,提升用户体验。
6. 窗体加载事件:初始化下拉框与初始查询
// 主窗体加载事件,窗体显示时执行
private void Form1_Load(object sender, EventArgs e)
{
// 为性别下拉框绑定数据源
cbbSex.DataSource = new List<Sex>()
{
new Sex(){Name = "全部",Value = -1},
new Sex(){Name = "男",Value = 0},
new Sex(){Name = "女",Value = 1},
};
// 设置下拉框显示的文本为Sex类的Name属性
cbbSex.DisplayMember = "Name";
// 设置下拉框对应的实际值为Sex类的Value属性
cbbSex.ValueMember = "Value";
// 窗体加载时执行初始查询,显示所有学生数据
BindGridView2();
}
核心知识点:
-
ComboBox.DataSource:为下拉框设置数据源,可绑定集合、DataTable 等。 -
DisplayMember:设置下拉框中显示的文本对应的属性名(此处为 Sex.Name)。 -
ValueMember:设置下拉框选中项对应的实际值的属性名(此处为 Sex.Value),后续可通过cbbSex.SelectedValue获取选中的实际值。 -
Form1_Load事件:窗体加载完成后触发,用于初始化控件状态、加载初始数据。
7. 添加按钮事件:打开添加窗体
// 添加按钮点击事件
private void btnAdd_Click(object sender, EventArgs e)
{
// 创建添加窗体对象,默认是添加模式(isEdit=false)
Form2 f2 = new Form2();
// 以对话框形式打开窗体,等待用户操作完成后返回结果
if (f2.ShowDialog() == DialogResult.OK)
{
// 用户点击"添加"并成功后,刷新数据
BindGridView2();
}
}
核心知识点:
-
ShowDialog():以对话框形式打开窗体,阻塞当前窗体的操作,直到对话框关闭,返回值为DialogResult枚举(如 OK、Cancel)。 -
DialogResult.OK:表示用户在子窗体中完成了有效操作(如点击"添加""保存"),此时刷新主窗体数据,确保数据同步。
8. 编辑按钮事件:打开编辑窗体
// 编辑按钮点击事件
private void btnEdit_Click(object sender, EventArgs e)
{
// 检查是否选择了要编辑的行
if (dataGridView1.SelectedRows.Count == 0)
{
MessageBox.Show("请先选择一行");
return; // 未选择则直接返回,不执行后续代码
}
// 获取选中行的Id列值(学生ID),转换为int类型
int editId = (int)dataGridView1.SelectedRows[0].Cells["Id"].Value;
// 创建编辑窗体对象,传入编辑标识(true)和学生ID
Form2 f2 = new Form2(true, editId);
// 以对话框形式打开,编辑成功后刷新数据
if (f2.ShowDialog() == DialogResult.OK)
{
BindGridView2();
}
}
核心知识点:
-
DataGridView.SelectedRows:获取 DataGridView 中用户选中的行集合,SelectedRows.Count表示选中的行数。 -
Cells["Id"].Value:获取选中行中"Id"列的单元格值,需根据实际数据类型转换(此处为 int)。 -
窗体传参:通过 Form2 的构造函数传入
isEdit(是否为编辑模式)和editId(要编辑的学生ID),实现子窗体根据参数初始化数据。
9. 删除按钮事件:删除学生数据
// 删除按钮点击事件
private void btnRemove_Click(object sender, EventArgs e)
{
// 检查是否选择了要删除的行
if (dataGridView1.SelectedRows.Count == 0)
{
MessageBox.Show("请先选择一行");
return;
}
// 获取选中行的学生ID
int delId = (int)dataGridView1.SelectedRows[0].Cells["Id"].Value;
// 弹出确认对话框,提示用户删除不可恢复
DialogResult dia = MessageBox.Show($"确认删除学号为{delId}的学生数据?删除后不可恢复!", "删除确认", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (dia != DialogResult.Yes)
{
return; // 用户取消删除,直接返回
}
try
{
// using语句管理连接,自动释放资源
using(SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open(); // 打开连接
// 定义删除SQL语句,参数化查询
string sql = "delete from stufirst where Id = @Id";
SqlCommand cmd = new SqlCommand(sql,conn);
// 添加参数,防止SQL注入
cmd.Parameters.AddWithValue("@Id", delId);
// ExecuteNonQuery():执行增删改操作,返回受影响的行数
int affectRow = cmd.ExecuteNonQuery();
// 根据受影响行数判断是否删除成功
if (affectRow >0)
{
MessageBox.Show("删除成功");
BindGridView2(); // 刷新数据
}
else
{
MessageBox.Show("删除失败,未找到对应数据");
}
}
}
catch (Exception ex) // 捕获删除异常
{
MessageBox.Show("删除操作失败:\r\n" + ex.ToString());
}
}
核心知识点:
-
MessageBox.Show():弹出提示框,可设置标题、按钮类型(MessageBoxButtons.YesNo)、图标(MessageBoxIcon.Warning),提升用户体验,避免误操作。 -
ExecuteNonQuery():执行非查询类 SQL 语句(增、删、改),返回受影响的行数,可通过该返回值判断操作是否成功。 -
cmd.Parameters.AddWithValue("@Id", delId):简化的参数添加方式,自动推断参数类型,适合快速开发,也可使用new SqlParameter("@Id", SqlDbType.Int){Value=delId}明确指定参数类型,更安全。
三、添加/编辑窗体(Form2):复用窗体实现增删改
Form2 是复用窗体,通过构造函数参数区分"添加"和"编辑"模式,减少代码冗余,核心实现学生信息的添加和修改。
1. 窗体构造函数:接收参数,区分操作模式
public partial class Form2 : Form
{
// 公共变量,标识是否为编辑模式
public bool isEdit;
// 公共变量,存储编辑模式下的学生ID
public int editId;
// 构造函数,可接收两个参数,默认是添加模式(isEdit=false,editId=0)
public Form2(bool isEdit = false, int editId = 0)
{
InitializeComponent(); // 初始化控件
// 将传入的参数赋值给全局变量,供整个窗体使用
this.isEdit = isEdit;
this.editId = editId;
}
// 全局变量,存储数据库连接字符串
public string connectionString = ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString;
}
核心知识点:
-
可选参数:构造函数中
bool isEdit = false表示该参数为可选参数,若调用时不传入,默认值为 false(添加模式)。 -
this关键字:用于区分全局变量和局部变量(此处this.isEdit表示全局变量,isEdit表示构造函数的参数)。
2. 窗体加载事件:初始化下拉框,编辑模式加载数据
// Form2加载事件
private void Form2_Load(object sender, EventArgs e)
{
// 初始化性别下拉框,与Form1一致
cbbSex.DataSource = new List<Sex>()
{
new Sex(){Name="全部",Value=-1 },
new Sex(){Name="男",Value=0 },
new Sex(){Name="女",Value=1 },
};
cbbSex.DisplayMember = "Name";
cbbSex.ValueMember = "Value";
// 如果是编辑模式,根据学生ID查询数据并填充到控件
if (isEdit)
{
// using语句管理数据库连接
using(SqlConnection conn = new SqlConnection(connectionString))
{
// 检查连接状态,未打开则打开
if (conn.State != ConnectionState.Open)
{
conn.Open();
}
// 定义查询参数和SQL语句
SqlParameter paras = new SqlParameter("@Id",SqlDbType.Int);
paras.Value = editId; // 传入要编辑的学生ID
string sql = "select * from stufirst where Id =@Id"; // 根据ID查询单条数据
// 创建命令对象并执行查询
SqlCommand cmd = new SqlCommand(sql,conn);
cmd.Parameters.Clear();
cmd.Parameters.Add(paras);
SqlDataReader reader = cmd.ExecuteReader();
// 读取查询结果,填充到控件
if (reader.Read()) // Read():读取一行数据,返回true表示有数据
{
txtStuName.Text = reader["Name"].ToString(); // 姓名填充到文本框
nudAge.Value = (int)reader["Age"]; // 年龄填充到数值选择器
// 处理性别字段,兼容数据库中字段为空的情况
if (reader.IsDBNull(3)) // 判断第4列(Sex列)是否为null(索引从0开始)
{
cbbSex.SelectedValue = -1; // 为空则选择"全部"
}
else
{
// 数据库中Sex为bool类型,转换为下拉框对应的Value(1:女,0:男)
cbbSex.SelectedValue = (bool)reader["Sex"] ? 1 : 0;
}
// 处理出生日期字段,兼容空值
if (reader["BrithDay"] == DBNull.Value)
{
dtpBirthday.Value = DateTime.Now; // 为空则默认当前日期
}
else
{
dtpBirthday.Value = (DateTime)reader["BrithDay"]; // 填充选择的日期
}
}
// 修改窗体标题和按钮文本,区分编辑模式
this.Text = "编辑学生信息";
btnAdd.Text = "保存修改";
}
}
else
{
// 添加模式,修改窗体标题和按钮文本
this.Text = "添加学生信息";
btnAdd.Text = "添加";
}
}
核心知识点:
-
reader.Read():SqlDataReader 的核心方法,读取查询结果的下一行数据,返回 bool 值,true 表示读取到数据,false 表示无数据(适合查询单条数据的场景)。 -
reader.IsDBNull(3):判断指定索引的列(此处为第4列,Sex列)是否为数据库空值(DBNull),避免转换数据时出现空引用异常。 -
reader["列名"]:通过列名获取对应单元格的值,需根据实际数据类型转换(如 string、int、DateTime)。 -
单条数据查询:根据学生ID查询(
where Id =@Id),确保只返回一条数据,适合编辑场景。
3. 添加/保存按钮事件:执行添加或编辑操作
// 添加/保存按钮点击事件
private void btnAdd_Click(object sender, EventArgs e)
{
// 防呆验证:检查姓名是否为空,避免空数据插入数据库
if (string.IsNullOrEmpty(txtStuName.Text.Trim())) // Trim():去除前后空格
{
MessageBox.Show("学生姓名不能为空,请输入!");
txtStuName.Focus(); // 将光标定位到姓名文本框,方便用户输入
return;
}
// 获取控件中的数据
string name = txtStuName.Text.Trim(); // 姓名,去除前后空格
int age = (int)nudAge.Value; // 年龄
int sexValue = (int)cbbSex.SelectedValue; // 获取性别下拉框的实际值
bool sex = sexValue == 1; // 转换为bool类型(1:女,0:男)
DateTime birthday = dtpBirthday.Value; // 出生日期
// 定义SQL语句和参数集合
string sql = "";
List<SqlParameter> paramList = new List<SqlParameter>();
if (isEdit)
{
// 编辑模式:更新数据的SQL语句
sql = @"Update stufirst set Name = @Name,Age=@Age,Sex=@Sex,BrithDay=@BrithDay where Id =@Id";
paramList.Add(new SqlParameter("@Id", editId)); // 添加学生ID参数,指定更新哪条数据
}
else
{
// 添加模式:插入新数据的SQL语句
sql = "Insert into stufirst (Name,Age,Sex,BrithDay) Values (@Name,@Age,@Sex,@BrithDay)";
}
// 添加公共参数(姓名、年龄、性别、出生日期),两种模式都需要
paramList.Add(new SqlParameter("@Name", name));
paramList.Add(new SqlParameter("@Age", age));
paramList.Add(new SqlParameter("@Sex", sex));
paramList.Add(new SqlParameter("@BrithDay", birthday));
try
{
// using语句管理数据库连接
using(SqlConnection conn = new SqlConnection(connectionString))
{
if(conn.State !=ConnectionState.Open) conn.Open(); // 打开连接
// 创建命令对象并执行操作
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.Clear();
cmd.Parameters.AddRange(paramList.ToArray()); // 添加参数集合
int rows = cmd.ExecuteNonQuery(); // 执行增删改,返回受影响的行数
// 提示操作结果
if (rows >0)
{
MessageBox.Show(isEdit ? "学生信息修改成功!" : "学生信息添加成功!");
}
else
{
MessageBox.Show(isEdit ? "学生信息修改失败,未找到对应数据!" : "学生信息添加失败!");
}
}
// 设置对话框结果为OK,关闭窗体并返回主窗体
this.DialogResult = DialogResult.OK;
this.Close(); // 手动关闭窗体
}
catch (Exception ex) // 捕获异常
{
MessageBox.Show($"{(isEdit ? "修改" : "添加")}操作失败:\r\n" + ex.ToString());
}
}
核心知识点:
-
防呆验证:通过
string.IsNullOrEmpty(txtStuName.Text.Trim())检查姓名是否为空,避免插入无效数据,提升数据完整性;txtStuName.Focus()将光标定位到该文本框,方便用户输入。 -
SQL 语句区分:编辑模式使用
Update语句,通过where Id =@Id指定更新的行;添加模式使用Insert语句,插入新数据到表中。 -
参数复用:两种模式都需要姓名、年龄、性别、出生日期参数,提取为公共代码,减少冗余。
-
this.Close():手动关闭当前窗体,适用于操作完成后关闭子窗体的场景。
四、核心知识点总结
1. 数据库操作核心流程
所有数据库操作(增、删、改、查)的核心流程:创建连接 → 定义 SQL 语句和参数 → 创建命令对象 → 执行命令 → 处理结果 → 释放资源。
2. 防止 SQL 注入的关键
始终使用参数化查询(SqlParameter),禁止直接拼接用户输入到 SQL 语句中,这是数据库操作的安全底线。
3. 资源管理的最佳实践
使用 using 语句管理 SqlConnection、SqlCommand、SqlDataAdapter 等对象,自动释放资源,避免连接泄露。
4. WinForm 核心技巧
通过控件的事件绑定实现交互逻辑,通过 DataSource 实现数据绑定,复用窗体减少代码冗余,通过 ShowDialog() 实现窗体间的交互和数据同步。
5. 异常处理原则
所有数据库操作都要添加 try-catch 块,捕获异常并提示详细信息,便于排查问题,同时避免程序崩溃。
WinForm + SqlHelper 数据库增删改查
核心功能:基于SqlHelper封装类实现数据库查询、新增、修改、物理删除、模糊查询;结合DataGridView展示数据,适配期末实操考点
前置条件:已引用Microsoft.ApplicationBlocks.Data.En(SqlHelper所在程序集)、配置好app.config连接字符串
说明:代码按「命名空间与引用」「全局变量」「构造函数」「数据绑定」「增删改操作」「按钮事件」拆分,保留原功能,适配期末复习查看。
模块1:命名空间与程序集引用
核心作用:引入所需类库,支持数据库操作、WinForm界面、配置文件读取等功能。
using Microsoft.ApplicationBlocks.Data.En; // 引入SqlHelper所在程序集
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration; // 用于读取配置文件
using System.Data; // 用于DataSet、DataTable等数据类型
using System.Data.SqlClient; // 用于SQL Server数据库操作
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; // WinForm界面相关
模块2:命名空间与窗体类定义
核心作用:定义项目命名空间和窗体主类,承载所有功能逻辑。
namespace _1sqlHelper
{
public partial class Form1 : Form
{
// 后续模块代码依次插入此处
}
}
模块3:全局变量定义
核心作用:定义数据库连接字符串,从配置文件读取,避免硬编码。
// 全局变量:数据库连接字符串
// 从app.config配置文件中读取,name对应配置文件中的节点名
public string connectionString = ConfigurationManager.ConnectionStrings["connetionString"].ConnectionString;
模块4:构造函数(Form1)
核心作用:初始化窗体,添加逻辑删除、物理删除说明,以及原生ADO.NET操作示例(注释形式)。
public Form1()
{
InitializeComponent(); // 初始化窗体控件
#region 逻辑删除与物理删除说明(期末考点)
// 逻辑删除:并非真正删除数据库数据,而是通过状态字段控制展示
// 实现方式:在表中新增bool/int类型的Status字段(1=有效,0=已删除)
// 查询语句示例:select * from Students where Status = 1(仅查询有效数据)
// 操作语句示例:update Students set Status = 0 where Id = @id(标记为已删除)
// 物理删除:直接从数据库中删除数据,不可恢复
// 操作语句示例:delete from Students where Id = @id
#endregion
#region 原生ADO.NET操作步骤(对比SqlHelper,理解封装原理)
/*
//1.创建数据库连接对象并打开连接
SqlConnection connection = new SqlConnection(connectionString);
connection.Open();
//2.组织SQL参数,防止SQL注入(重点考点)
SqlParameter[] paras = new SqlParameter[]
{
new SqlParameter("@a","123"), // 字符串参数
new SqlParameter("@b",456), // 数字参数
};
//3.创建SQL指令执行对象
SqlCommand cmd = new SqlCommand();
cmd.Parameters.Clear(); // 清空原有参数,避免重复
cmd.Parameters.AddRange(paras); // 添加参数集合
//4.执行指令,通过DataAdapter填充DataSet
SqlDataAdapter ada = new SqlDataAdapter(cmd);
DataSet dataSet = new DataSet();
ada.Fill(dataSet); // 将查询结果填充到DataSet中
//5.将数据绑定到DataGridView展示
dataGridView1.DataSource = dataSet.Tables[0];
//6.关闭数据库连接,释放资源(必须执行,防止连接池耗尽)
connection.Close();
*/
#endregion
}
模块5:窗体加载事件(Form1_Load)
核心作用:窗体打开时,自动调用数据绑定方法,加载初始数据到DataGridView。
///
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_Load(object sender, EventArgs e)
{
BindGradView(); // 调用数据绑定方法,加载初始数据
}
模块6:数据绑定核心方法(BindGradView)
核心作用:实现模糊查询,将数据库数据查询后绑定到DataGridView,支持根据文本框内容筛选。
///
public void BindGradView()
{
// 1. 构建SQL参数:文本框内容作为模糊查询条件,拼接%实现通配
SqlParameter[] paras = new SqlParameter[]
{
new SqlParameter("@name",$"%{textBox1.Text}%")
};
// 2. 构建SQL查询语句,1=1用于拼接后续条件,避免语法错误
string sql = "select * from Students where 1=1";
// 3. 判断文本框是否有内容,有则添加模糊查询条件
if (!string.IsNullOrEmpty(textBox1.Text))
{
sql += " and StuName like @name "; // 注意空格,避免拼接后语法错误
}
#region SqlHelper.ExecuteDataset 说明(重点)
// 功能:执行查询语句,返回DataSet数据集(包含多张表)
// 参数1:数据库连接字符串
// 参数2:指令类型(CommandType.Text表示普通SQL语句,StoredProcedure表示存储过程)
// 参数3:待执行的SQL字符串
// 参数4:SQL参数数组(防止SQL注入,规范写法)
#endregion
DataSet set = SqlHelper.ExecuteDataset(connectionString, CommandType.Text, sql, paras);
// 4. 将查询结果绑定到DataGridView,Tables[0]表示DataSet中的第一张表
dataGridView1.DataSource = set.Tables[0];
}
模块7:新增数据按钮事件(button1_Click)
核心作用:实现学生数据新增,调用SqlHelper执行插入语句,成功后刷新数据。
///
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
// 1. 组织新增参数,与表中字段一一对应
SqlParameter[] paras = new SqlParameter[]
{
new SqlParameter("@name","张三"), // 学生姓名
new SqlParameter("@age",30), // 学生年龄
new SqlParameter("@sex",1), // 性别(1=男,0=女)
new SqlParameter("@birthday",DateTime.Now) // 出生日期,取当前时间
};
// 2. 构建新增SQL语句,参数化写法,避免SQL注入
string sql = "insert into Students (StuName,StuAge,StuSex,Birthday) values (@name,@age,@sex,@birthday)";
#region SqlHelper.ExecuteNonQuery 说明(重点)
// 功能:执行增、删、改语句,返回受影响的行数
// 返回值:int类型,1表示执行成功(新增1条数据),0表示无数据受影响
#endregion
int row = SqlHelper.ExecuteNonQuery(connectionString, CommandType.Text, sql, paras);
// 3. 判断新增是否成功,成功则重新绑定数据,刷新界面
if (row == 1)
{
BindGradView(); // 刷新DataGridView数据
MessageBox.Show("新增成功!"); // 可选:添加提示框,提升用户体验
}
else
{
MessageBox.Show("新增失败!");
}
}
模块8:修改数据按钮事件(button3_Click)
核心作用:修改选中行的学生数据,通过主键Id精准定位,成功后刷新数据。
///
/// <param name="sender"></param>
/// <param name="e"></param>
private void button3_Click(object sender, EventArgs e)
{
// 1. 校验是否选中数据行,避免空指针异常
if (dataGridView1.SelectedRows.Count == 0)
{
MessageBox.Show("请选择要修改的行!");
return;
}
// 2. 获取选中行的Id(主键),用于精准修改
int id = (int)dataGridView1.SelectedRows[0].Cells["Id"].Value;
// 3. 组织修改参数
SqlParameter[] paras = new SqlParameter[]
{
new SqlParameter("@name","赵四1"), // 修改后的姓名
new SqlParameter("@age",50), // 修改后的年龄
new SqlParameter("@sex",0), // 修改后的性别
new SqlParameter("@birthday",DateTime.Now), // 修改后的出生日期
new SqlParameter("@id",id) // 筛选条件:主键Id
};
// 4. 构建修改SQL语句,通过Id精准定位要修改的记录
string sql = "update Students set StuName=@name ,StuAge =@age ,StuSex=@sex ,Birthday = @birthday where Id = @id";
// 5. 执行修改操作,获取受影响行数
int row = SqlHelper.ExecuteNonQuery(connectionString, CommandType.Text, sql, paras);
// 6. 判断修改是否成功,成功则刷新数据
if (row == 1)
{
BindGradView();
MessageBox.Show("修改成功!");
}
else
{
MessageBox.Show("修改失败!");
}
}
模块9:刷新数据按钮事件(button4_Click)
核心作用:清空查询文本框,重新绑定数据,实现界面数据刷新。
///
/// <param name="sender"></param>
/// <param name="e"></param>
private void button4_Click(object sender, EventArgs e)
{
// 清空文本框,重新绑定数据,实现刷新效果
textBox1.Text = "";
BindGradView();
}
模块10:物理删除按钮事件(button5_Click)
核心作用:删除选中行的学生数据(物理删除,从数据库中移除),成功后刷新数据。
///
/// <param name="sender"></param>
/// <param name="e"></param>
private void button5_Click(object sender, EventArgs e)
{
// 1. 校验是否选中数据行
if (dataGridView1.SelectedRows.Count == 0)
{
MessageBox.Show("请选择要删除的行!");
return;
}
// 2. 获取选中行的Id(主键)
int id = (int)dataGridView1.SelectedRows[0].Cells["Id"].Value;
// 3. 组织删除参数
SqlParameter[] paras = new SqlParameter[]
{
new SqlParameter("@id",id) // 筛选条件:主键Id
};
// 4. 构建物理删除SQL语句
string sql = "delete Students where Id = @id";
// 5. 执行删除操作,获取受影响行数
int row = SqlHelper.ExecuteNonQuery(connectionString, CommandType.Text, sql, paras);
// 6. 判断删除是否成功,成功则刷新数据
if (row == 1)
{
BindGradView();
MessageBox.Show("删除成功!");
}
else
{
MessageBox.Show("删除失败!");
}
}
模块11:预留按钮事件(button2_Click,可拓展逻辑删除)
核心作用:预留按钮,可拓展逻辑删除功能,此处补充完整逻辑删除代码。
///
/// <param name="sender"></param>
/// <param name="e"></param>
private void button2_Click(object sender, EventArgs e)
{
// 1. 校验选中行
if (dataGridView1.SelectedRows.Count == 0)
{
MessageBox.Show("请选择要删除的行!");
return;
}
// 2. 获取选中行Id
int id = (int)dataGridView1.SelectedRows[0].Cells["Id"].Value;
// 3. 组织参数
SqlParameter[] paras = new SqlParameter[]
{
new SqlParameter("@id",id)
};
// 4. 构建逻辑删除SQL(修改Status字段为0,需确保表中有Status字段)
string sql = "update Students set Status = 0 where Id = @id";
// 5. 执行操作
int row = SqlHelper.ExecuteNonQuery(connectionString, CommandType.Text, sql, paras);
// 6. 刷新数据
if (row == 1)
{
BindGradView();
MessageBox.Show("逻辑删除成功!");
}
else
{
MessageBox.Show("逻辑删除失败!");
}
}
核心考点与代码优化说明
1. SqlHelper核心方法(必考)
-
ExecuteDataset:执行查询语句,返回DataSet,适用于多表查询、批量数据展示
-
ExecuteNonQuery:执行增删改语句,返回受影响行数,判断操作是否成功的依据
2. 关键优化点(避免报错)
-
添加选中行校验,防止未选中数据时出现空指针异常
-
SQL语句拼接时保留空格,避免语法错误(如`1=1and`)
-
添加操作结果提示框,提升程序交互性
-
预留逻辑删除功能,适配期末拓展考点
3. 防SQL注入(重点考点)
所有SQL操作均使用参数化查询(SqlParameter),避免直接拼接字符串,防止SQL注入攻击,这是考试和实际开发的规范写法。
4. app.config连接字符串配置示例(补充)
<configuration>
<connectionStrings>
<add name="connetionString" connectionString="server=服务器IP,端口;database=StudentDataBase;uid=sa;pwd=密码"/>
</connectionStrings>
</configuration>