C# WinForm 数据库增删改查(防SQL注入)+WinForm + SqlHelper 数据库增删改查

核心功能:多条件查询、添加、编辑、删除学生数据,全程防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 语句管理 SqlConnectionSqlCommandSqlDataAdapter 等对象,自动释放资源,避免连接泄露。

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>
相关推荐
AI多Agent协作实战派2 小时前
AI多Agent协作系统实战(二十三):Agent读HEARTBEAT.md不读AGENTS.md——openclaw的文件加载之谜
前端·数据库·人工智能·uni-app
Xin_ye100863 小时前
第三章:内存泄漏的常见“案发现场”
c#·wpf
jun_bai3 小时前
postgresql数据库免安装版安装到windows系统
数据库·postgresql
weixin_538601973 小时前
智能体测开Day33
数据库·oracle
番茄炒鸡蛋加糖3 小时前
Redis--Lua 脚本原子性与滑动窗口限流
数据库·redis·lua
瀚高PG实验室3 小时前
DELETE 条件字段缺少索引导致 SuperSync 同步严重延迟
数据库·postgresql·瀚高数据库
笃行3504 小时前
优化器凭什么改写你的 SQL:等价变换、条件调度与主流数据库的取舍
数据库
KaMeidebaby4 小时前
卡梅德生物技术快报|bli亲和力检测gst:告别批量跑胶:BLI实时酶切监测技术加速GST融合蛋白下游流程优化
前端·网络·数据库·人工智能·算法
千维百策6664 小时前
运用 SRE 原则降低生产事故影响:CRE 实战经验与可靠性优化方法
网络·数据库·人工智能
笃行3504 小时前
能跑不等于正确:生产环境不规范 SQL 的隐藏风险与编码规范
数据库