简介:一份基于C#与SQL Server开发的图书管理系统课程设计项目,适合正在完成数据库或C#课程大作业的计算机专业学生,也适合希望了解WinForms与ADO.NET数据交互的初学者。资源共包含187个文件,压缩包仅2.62MB,核心为79个C#源码文件,涵盖界面逻辑、业务处理与数据访问层;另有数据库主文件(.mdf)与日志文件(.ldf),便于直接附加使用;同时包含.resx资源文件、.xsd数据集定义及项目配置文件,可快速还原Visual Studio开发环境。系统涵盖图书信息录入、查询、借还管理等典型功能,并涉及SqlConnection、SqlCommand及DataTable等关键对象的用法。已有692人学习下载,可作为课程设计参考模板,也可在此基础上扩展分类管理、权限控制等模块,对理解C#面向对象编程与SQL Server数据库设计均有实际帮助。
1. 拿到这套 C# 图书管理系统:先别急着双击 exe,你大概率会卡在数据库连接上
很多人在网上下载了“课程设计-基于C#的图书管理系统(源码+数据库).zip”,解压后第一反应是找到.sln或.exe双击运行,结果要么弹窗报错“建立与服务器的连接时出错”,要么登录界面永远转圈。这套系统是典型的 VS + SQL Server 课程设计项目,核心不在界面而在数据访问层,也就是项目里那个db.cs文件。你拿到手的第一件事不是编译,而是先确认自己的 SQL Server 版本、实例名和认证方式,然后把db.cs里的连接字符串改成你自己的环境配置,最后再把数据库文件附加进去。这套项目适合两类人:一类是做数据库课程设计或 C# 学期大作业的学生,另一类是刚接触 ADO.NET 想找一份完整代码做参照的开发者。它能帮你搞明白 WinForms 界面怎么绑定数据、SqlConnection 和 SqlCommand 怎么配合、表之间的主外键关系在 UI 层怎么体现,但前提是你愿意先花十分钟把环境搭对。
2. 把数据库附加到 SQL Server:在对象资源管理器里完成三步操作
2.1 找到数据库文件:.mdf 和 .ldf 都在项目目录里
解压源码包之后,你会看到一堆.cache文件、.csproj文件、app.config和db.cs,其中真正重要的是那两个数据库文件——BooksDB.mdf和BooksDB_log.ldf。有些版本打包时只给了.mdf,日志文件丢失也没关系,SQL Server 在附加时会自动重建日志,前提是mdf文件本身完整。这里要提醒一句:不要把mdf文件直接复制到C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA目录下然后尝试用CREATE DATABASE ... FOR ATTACH附加,这个目录的权限很容易出问题,尤其是当你用学校机房或公司电脑时,SQL Server 服务账户未必有写入权限。
最常见的做法是在 SQL Server Management Studio(SSMS)里右键“数据库”节点,选择“附加”,然后在弹出的对话框里点击“添加”按钮,选中BooksDB.mdf文件。SSMS 会自动识别同目录下的日志文件,如果日志文件缺失或损坏,它会弹警告但依然允许你附加。附加成功后,左侧数据库列表里会出现 BooksDB 节点,展开它你能看到表、视图、存储过程等对象,确认一下有几张表——一般至少包含图书表、读者表、借阅表和用户表,如果少表说明附加的是旧版本或备份不完整。
如果你更喜欢用命令行,也可以用sqlcmd。打开命令提示符,输入以下命令:
sqlcmd -S localhost -E -Q "EXEC sp_attach_db @dbname = N'BooksDB', @filename1 = N'D:\CourseDesign\BooksDB.mdf', @filename2 = N'D:\CourseDesign\BooksDB_log.ldf'"这条命令用 Windows 身份认证连接本机默认实例,然后把指定路径下的两个文件附加为 BooksDB 数据库。-E表示信任连接,也就是用当前 Windows 用户身份登录 SQL Server;@filename1和@filename2必须写成绝对路径,且路径中不能有中文,SQL Server 对中文路径的支持有时候会抽风。如果你不想动命令行,直接用 SSMS 图形界面就够了——很多搞课程设计的同学第一次接触数据库,图形界面更不容易出错。
2.2 检查 SQL Server 登录模式:Windows 认证还是混合认证
图书管理系统的db.cs里写好的连接字符串决定了系统用什么方式登录 SQL Server,而你 SQL Server 的认证模式必须能兼容它。最常见的连接字符串有两种写法:一种是Server=.;Database=BooksDB;Integrated Security=True;,这种是 Windows 身份认证,不需要用户名密码;另一种是Server=.;Database=BooksDB;User Id=sa;Password=123456;,这种用 SQL Server 账号登录。问题来了——如果你的 SQL Server 安装时只启用了 Windows 身份认证模式,那么第二条连接字符串无论如何都连不上,因为 SQL Server 根本不会接受sa账号的登录请求。
查看和修改认证模式的方法是:在 SSMS 中右键服务器实例,点“属性”,切到“安全性”选项卡,你会看到“服务器身份验证”下面有两个单选按钮——Windows 身份验证模式和 SQL Server 身份验证模式。很多课程设计的系统默认是用sa账号连接的,所以你需要勾选“SQL Server 身份验证模式”,然后点确定。改完之后 SSMS 会提示你重启服务才能生效,这一步别跳过,不重启的话设置不会立即起作用。重启方式是在左侧“对象资源管理器”里右键服务器实例,选择“重新启动”,或者在 Windows 服务管理器里找到MSSQLSERVER服务右键重启。
顺带检查一下sa账号是否被禁用。在 SSMS 安全性节点下展开“登录名”,右键sa选“属性”,在“状态”页里确认“登录”选项是“已启用”。因为 SQL Server 默认安装时sa账号是禁用的,很多教程只说“设置成混合认证模式”,却没说还要单独启用sa账号。如果你不想用sa,也可以在 SSMS 里新建一个登录名,授予它db_owner权限,然后把db.cs里的连接字符串改成这个新账号的用户名和密码。这样更安全,毕竟sa是数据库超级管理员,泄露了影响面太大。
2.3 实测连接:用 .NET 自带工具验证连接字符串
改完认证模式之后,不要急着运行整个系统,先用一个小工具验证连接字符串能不能通。在 VS 里随便新建一个控制台项目,贴入以下代码:
using System; using System.Data.SqlClient; class Program { static void Main() { // 注意这里的 Server 要替换成你自己的实例名 string connStr = "Server=.;Database=BooksDB;User Id=sa;Password=123456;"; using (SqlConnection conn = new SqlConnection(connStr)) { try { conn.Open(); Console.WriteLine("连接成功!当前数据库:" + conn.Database); } catch (Exception ex) { Console.WriteLine("连接失败:" + ex.Message); } } } }这段代码的核心逻辑就三步:构造SqlConnection对象、调用Open()方法建立连接、用完释放资源。Server=.里的点代表本机默认实例,如果你的 SQL Server 是命名实例,比如SQLEXPRESS,那这里要改成Server=.\SQLEXPRESS。Database=BooksDB指定初始数据库,如果你的库名不叫这个,对应改成你自己的库名。User Id和Password是 SQL Server 账号登录方式下的凭证。
如果这段代码报“找不到服务器或实例名”,你先确认 SQL Server 服务是否在运行,在 Windows 服务管理器里看MSSQLSERVER或MSSQL$SQLEXPRESS的状态,没启动的话右键启动。如果报“无法连接到因为它是不可见的”,大概率是 TCP/IP 协议没启用,打开 SQL Server 配置管理器,在“SQL Server 网络配置”里把 TCP/IP 协议状态改成已启用,然后重启 SQL Server 服务。这个坑在课程设计现场很常见——代码明明没问题,但就是连不上,最后发现是协议没开。从那以后我每次都要先跑一遍这段测试代码,确认连接通了才继续下一步。
3. 读懂数据库表结构:图书、读者、借阅三张表怎么关联
3.1 从 .mdf 里导出建表语句:看清主键和外键的关系
数据库附加上去之后,下一步是看表结构。这套系统的数据模型对课程设计来说比较标准——无非是图书表、读者表、借阅表和用户表。图书表一般命名为Books,包含BookID、BookName、Author、Publisher、ISBN、CategoryID、Stock等字段;读者表命名为Readers,包含ReaderID、ReaderName、Phone、Email等字段;借阅表BorrowRecords负责记录谁借了哪本书、借走日期、应还日期、实际还书日期。用户表通常是Users,存放系统登录账号和密码。
你可以在 SSMS 里右键某张表,选择“编写脚本为 → CREATE 到 → 新查询编辑器窗口”,这样就能看到完整的建表语句。以 Books 表为例,常见的建表语句长这样:
CREATE TABLE [dbo].[Books] ( [BookID] INT IDENTITY (1, 1) NOT NULL, [BookName] NVARCHAR (50) NOT NULL, [Author] NVARCHAR (50) NULL, [Publisher] NVARCHAR (50) NULL, [ISBN] NVARCHAR (20) NULL, [CategoryID] INT NULL, [Stock] INT DEFAULT ((0)) NOT NULL, PRIMARY KEY CLUSTERED ([BookID] ASC), FOREIGN KEY ([CategoryID]) REFERENCES [dbo].[Category] ([CategoryID]) );这里有几个值得注意的细节。IDENTITY (1, 1)表示BookID是自增列,从 1 开始每次加 1,插入数据时不需要显式赋值。PRIMARY KEY CLUSTERED是聚集主键,SQL Server 默认按这个字段排序存储数据。外键约束指向Category表的CategoryID字段,意思是图书表里的CategoryID必须在分类表里存在,否则插入会报外键冲突。DEFAULT ((0))给Stock字段设置默认值 0,防止插数据时该字段为空。
3.2 借阅记录表:借书还书业务的核心逻辑
借阅表是整个系统最核心的表,因为它同时关联了图书表和读者表。借书时插入一条记录,还书时更新这条记录的实还日期,如果超期还能根据应还日期和实还日期算出罚款金额。典型的借阅表结构如下:
CREATE TABLE [dbo].[BorrowRecords] ( [RecordID] INT IDENTITY (1, 1) NOT NULL, [BookID] INT NOT NULL, [ReaderID] INT NOT NULL, [BorrowDate] DATETIME DEFAULT (getdate()) NOT NULL, [DueDate] DATETIME NOT NULL, [ReturnDate] DATETIME NULL, PRIMARY KEY CLUSTERED ([RecordID] ASC), FOREIGN KEY ([BookID]) REFERENCES [dbo].[Books] ([BookID]), FOREIGN KEY ([ReaderID]) REFERENCES [dbo].[Readers] ([ReaderID]) );DueDate一般是在BorrowDate基础上加 30 天,这个由 C# 代码计算后传入,也可以在存储过程里用DATEADD(day, 30, GETDATE())自动生成。ReturnDate初始为 NULL,还书时用UPDATE语句把它更新成当前时间。这三张表的关系一看就明白——BorrowRecords是中间表,通过BookID和ReaderID把 Books 和 Readers 连起来,这就是典型的两表关联查询场景。
查询某本书当前被谁借走,SQL 语句要这样写:
SELECT r.ReaderName, b.BookName, br.BorrowDate, br.DueDate FROM BorrowRecords br INNER JOIN Readers r ON br.ReaderID = r.ReaderID INNER JOIN Books b ON br.BookID = b.BookID WHERE br.ReturnDate IS NULL AND b.BookID = 1;这条语句用了两次INNER JOIN,先把借阅记录关联到读者拿名字,再关联到图书拿书名,WHERE br.ReturnDate IS NULL只筛选没还的记录。理解了这个查询逻辑,你在 C# 端写SqlCommand时就不会发懵——无非是把 SQL 字符串塞进命令对象,然后把执行结果绑定到 DataGridView。
3.3 数据库连接字符串存在哪:db.cs 和 app.config 的优先级问题
这套系统里数据库连接字符串最可能出现在两个地方:db.cs和app.config(编译后是.exe.config)。db.cs里通常定义了一个公共类,静态字段保存连接字符串,或者提供一个GetConnection()方法返回SqlConnection实例。app.config则是 .NET 标准的配置方式,在connectionStrings节里用add元素声明。
常见的app.config配置长这样:
<configuration> <connectionStrings> <add name="LibraryDB" connectionString="Server=.;Database=BooksDB;User Id=sa;Password=123456;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>然后在db.cs里这样读取:
public static SqlConnection GetConnection() { string connStr = ConfigurationManager.ConnectionStrings["LibraryDB"].ConnectionString; return new SqlConnection(connStr); }这里有个优先级问题需要注意:代码里如果直接硬编码了连接字符串,那app.config里的配置不会生效,因为代码根本不读它。你要先搜索整个解决方案里SqlConnection出现的所有位置,确认是不是统一通过GetConnection()获取连接。如果有些地方直接new SqlConnection("Server=..."),那你必须改代码而不是改配置文件。很多同学改了app.config里的密码发现系统还是连不上,最后发现有的窗口代码自己硬编码了连接字符串,血泪教训。
4. 数据访问层实战:ADO.NET 五件套在图书管理里的典型用法
4.1 SqlConnection + SqlCommand + SqlDataReader:最轻量的查询方式
图书管理系统的数据访问层几乎全是 ADO.NET,这套技术栈虽然看起来老,但它能让你把 SQL 语句的执行过程看得很透彻。最简单的用法是SqlConnection打开连接,SqlCommand执行 SQL,SqlDataReader逐行读取结果。图书查询界面通常就是这种方式,比如在文本框里输入书名关键字,然后点击“查询”按钮,代码逻辑如下:
private void btnSearch_Click(object sender, EventArgs e) { string keyword = txtKeyword.Text.Trim(); if (keyword == "") { MessageBox.Show("请输入查询关键字"); return; } string sql = "SELECT BookID, BookName, Author, Publisher, ISBN, Stock FROM Books WHERE BookName LIKE @kw"; using (SqlConnection conn = GetConnection()) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@kw", "%" + keyword + "%"); conn.Open(); DataTable dt = new DataTable(); SqlDataAdapter adapter = new SqlDataAdapter(cmd); adapter.Fill(dt); dataGridView1.DataSource = dt; } }这段代码有两个地方值得关注。第一是用了@kw参数而不是直接拼接字符串,这样既防止 SQL 注入,也避免书名里有单引号时把 SQL 语句搞崩。第二是填数据用了SqlDataAdapter而不是SqlDataReader,因为DataGridView.DataSource需要一个DataTable对象,用SqlDataAdapter.Fill()一步到位。有些人用SqlDataReader循环读取再手动加到DataGridView.Rows,那是绕远路,而且还要自己处理列映射。
Parameters.AddWithValue这个方法有个小坑——如果keyword是空字符串,LIKE '%%'会匹配所有记录,所以代码里先用if拦了一下。还有一种更稳妥的做法是给AddWithValue传一个SqlDbType.NVarChar参数,避免做隐式转换,但课程设计项目里用AddWithValue就够用了,不用过度设计。
4.2 SqlParameter 防注入:别把输入直接拼进 SQL 字符串
很多早期的课程设计代码喜欢这样写:"SELECT * FROM Books WHERE BookName = '" + txtBookName.Text + "'",这种写法在单机版的图书管理系统里可能运行正常,但一旦系统被别人拿到,在文本框里输入' OR '1'='1这样的内容,SQL 语句就会变成SELECT * FROM Books WHERE BookName = '' OR '1'='1',把所有图书记录全部查出来。这是典型的 SQL 注入,虽然课程设计答辩时未必有人攻击你的系统,但代码评审老师一眼就能看出这是低级问题。
写SqlParameter的标准姿势是这样:
string sql = "INSERT INTO Books (BookName, Author, Publisher, ISBN, CategoryID, Stock) VALUES (@name, @author, @publisher, @isbn, @categoryId, @stock)"; using (SqlConnection conn = GetConnection()) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.Add("@name", SqlDbType.NVarChar, 50).Value = txtBookName.Text; cmd.Parameters.Add("@author", SqlDbType.NVarChar, 50).Value = txtAuthor.Text; cmd.Parameters.Add("@publisher", SqlDbType.NVarChar, 50).Value = txtPublisher.Text; cmd.Parameters.Add("@isbn", SqlDbType.NVarChar, 20).Value = txtISBN.Text; cmd.Parameters.Add("@categoryId", SqlDbType.Int).Value = int.Parse(cmbCategory.SelectedValue.ToString()); cmd.Parameters.Add("@stock", SqlDbType.Int).Value = int.Parse(txtStock.Text); conn.Open(); int rows = cmd.ExecuteNonQuery(); if (rows > 0) MessageBox.Show("图书添加成功"); else MessageBox.Show("图书添加失败"); }这里用cmd.Parameters.Add显式指定了参数类型和长度,好处是 SQL Server 能准确预估执行计划,而且数据长度超了会直接报错而不是静默截断。ExecuteNonQuery()适用于插入、更新、删除这类不返回结果集的操作,返回的是受影响的行数。int.Parse把文本框内容转成整型,但如果文本框里输入了非数字字符,这里会抛异常,严谨的做法是改用int.TryParse,但课程设计里用Parse也可以接受,毕竟业务场景很单纯。
4.3 DataSet 和 DataTable:给 DataGridView 绑数据时别用错对象
查询结果绑定数据网格DataGridView时,有两种常见写法。一种是用SqlDataAdapter.Fill(DataSet)然后设置dataGridView1.DataSource = ds.Tables[0];另一种是直接Fill(DataTable)然后dataGridView1.DataSource = dt。两种写法最终效果一样,但DataSet内部可以装多个DataTable,适合多表关联查询时分别存放在内存中供后续操作。对这个系统来说,单表查询用DataTable就够了,代码短,逻辑清晰。
private void LoadAllBooks() { string connStr = ConfigurationManager.ConnectionStrings["LibraryDB"].ConnectionString; string sql = @" SELECT b.BookID, b.BookName, b.Author, b.Publisher, b.ISBN, c.CategoryName, b.Stock FROM Books b LEFT JOIN Category c ON b.CategoryID = c.CategoryID"; DataTable dt = new DataTable(); using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) using (SqlDataAdapter adapter = new SqlDataAdapter(cmd)) { adapter.Fill(dt); dataGridView1.DataSource = dt; dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; } }LEFT JOIN的作用是即使图书的CategoryID在分类表里找不到对应记录,也能查出图书数据,CategoryName字段显示为空白而不是整行消失。AutoSizeColumnsMode.Fill让列自动填满网格宽度,避免出现大块空白区域。这段代码如果运行时报“DataGridView 中发生以下错误:DataGridView 出现未被处理的异常”,多半是数据源里某一列的值为 NULL 而DataGridView无法处理,常见解决方法是让 SQL 查询里用ISNULL(CategoryName, '未分类')把 NULL 替换成空字符串。
5. 避坑手册:编译错误、登录失败、数据不显示的三类典型问题
5.1 编译报错 CS1061:控件命名冲突导致找不到属性
现象:编译时出现CS1061: "DataGridView" 未包含 "DataSource" 的定义之类的错误,明明代码看起来完全没写错。原因:项目文件夹里有一个叫DataGridView.cs的类文件,或者using System.Windows.Forms;缺失导致编译器不认识这个控件类型。这个报错在 .NET Framework 的老项目里很常见——因为这套系统用 Visual Studio 的版本可能比较老,项目文件里自动生成的设计器代码和你手写的using指令之间出现了命名冲突。
解决:先检查代码文件顶部using列表里有没有System.Windows.Forms和System.Data,没有就补上;然后检查项目里是否新建了名称和系统类重名的文件,有就重命名。另一个常见原因是目标框架版本不匹配——DataGridView只在 .NET Framework 2.0 及以上才有,如果你把项目改成了 .NET Client Profile,部分控件会不可用。在项目属性里把目标框架改成 .NET Framework 4.0 或更高,顺手把ConfigurationManager的引用System.Configuration.dll加上,这两个问题经常一起出现。
5.2 运行时错误 26:远程主机强迫关闭了一个现有的连接
现象:程序在conn.Open()这行抛异常,消息类似“在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误。未找到或无法访问服务器。请验证实例名称是否正确并且 SQL Server 已配置为允许远程连接”。原因不止一个,但最常见两种情况:一是 SQL Server 服务没启动,二是 TCP/IP 协议被禁用。也有一种情况是连接字符串里的服务器名写成了其他机器的 IP 地址,而你明明在单机运行。
解决:先敲services.msc打开服务管理器,找到SQL Server (MSSQLSERVER)或SQL Server (SQLEXPRESS)看状态,没启动就右键启动,启动类型设为自动,防止下次开机又停掉。再打开 SQL Server 配置管理器,检查MSSQLSERVER 的协议下的 TCP/IP 是否已启用。如果这两步都做了还是报错,用telnet 127.0.0.1 1433测试端口是不是通的,不通说明防火墙把 1433 端口拦了,在 Windows 防火墙入站规则里放行 1433 端口。有些同学用的是 SQL Server Express 版本,它默认实例名是SQLEXPRESS,监听端口可能是动态的,此时连接字符串里写Server=.\SQLEXPRESS而不是Server=.。
5.3 登录失败 18456:sa 被禁用或密码策略问题
现象:程序运行到登录界面输入账号密码后,报错 “用户 'sa' 登录失败 (Error 18456)”,但在 SSMS 里用 Windows 身份认证能正常连接。原因:SQL Server 安装时默认禁用了sa账号,或者sa的密码为空但 SQL Server 启用了密码策略检查,不允许空密码。解决分三步——先用 Windows 身份认证登录 SSMS,展开安全性 → 登录名,双击sa,在“常规”页设置一个强密码(至少 8 位,包含字母数字和符号),在“状态”页把登录改为启用;然后右键服务器实例选“属性”,在“安全性”页勾选“SQL Server 和 Windows 身份验证模式”;最后重启 SQL Server 服务。
这里有个细节:如果你改了密码策略,sa账号设置的密码太简单,比如123456,SQL Server 依然会拒绝登录,虽然它报的错和账号禁用一模一样。所以一定要把密码设置得复杂一点,比如Sa123456!。课程设计答辩时老师经常会现场要求打开db.cs看连接字符串,如果你的密码明文写123456且sa启用,老师会指出安全隐患,虽然不会有真实攻击,但评分印象分会差一些。从那以后我每次提交课程设计前都会把db.cs里的连接字符串改成从app.config读取,源码里不留任何明文账号。
5.4 下拉框不显示数据:SelectedValue 和 DisplayMember 的匹配问题
现象:窗体加载后,分类下拉框ComboBox点击展开是空的,或者显示的是System.Data.DataRowView而不是具体的分类名称。原因:ComboBox的DataSource绑定了一个DataTable,但没有正确设置DisplayMember和ValueMember。DisplayMember是用户在下拉框里看到的文字,ValueMember是选中项对应的实际值,也就是插入图书时的CategoryID。
解决:在窗体Load事件里写如下代码:
DataTable dt = new DataTable(); using (SqlConnection conn = GetConnection()) using (SqlDataAdapter adapter = new SqlDataAdapter("SELECT CategoryID, CategoryName FROM Category", conn)) { adapter.Fill(dt); } cmbCategory.DataSource = dt; cmbCategory.DisplayMember = "CategoryName"; cmbCategory.ValueMember = "CategoryID";设置完这三行,下拉框会正常显示分类名称,cmbCategory.SelectedValue返回的是选中分类的CategoryID。有一个隐藏坑:如果你的Category表里CategoryID和CategoryName不是这两列名字,你需要把DisplayMember和ValueMember改成实际的列名,否则下拉框会显示成空。另外,如果在ValueMember设置之前就访问SelectedValue,会返回 null——因为绑定还没建立完成。你可以把绑定代码放在Load事件里,访问代码放在按钮点击事件里,顺序上一般不会出问题。
5.5 中文乱码:表字段用 varchar 而代码写入的是 Unicode
现象:图书管理系统里输入中文书名,数据库中显示正常,但在程序界面上显示成??或者乱码。原因:数据库表字段定义成了varchar而不是nvarchar。varchar按单字节存储,只能存 ASCII 字符和扩展字符集,中文用varchar存会丢失信息;nvarchar按双字节存储,能完整保存中文。很多课程设计的建表脚本里写的是varchar,因为默认模板,结果运行起来中文显示就翻车了。
解决:在 SSMS 里执行以下修改语句:
ALTER TABLE [dbo].[Books] ALTER COLUMN [BookName] NVARCHAR (50) NOT NULL; ALTER TABLE [dbo].[Books] ALTER COLUMN [Author] NVARCHAR (50) NULL; ALTER TABLE [dbo].[Readers] ALTER COLUMN [ReaderName] NVARCHAR (50) NOT NULL;把字段类型从varchar改成nvarchar之后,重新运行系统测试一下中文显示。这只是修改表结构,不影响已有数据,但如果已有数据已经是乱码,你需要先删除乱码记录再重新插入。从那以后我做数据库设计,凡是可能存中文的字段,一律用nvarchar,不管对方需求文档里怎么写的,这是被课程设计现场答辩时中文乱码逼出来的教训。
6. 把课程设计升级成能答辩的作品:加登录验证、分页查询和一点代码规范
6.1 给用户表加一条管理员记录:登录验证从 SQL 查询开始
很多课程设计系统在登录时只判断用户名密码是否匹配,不做角色区分。如果你想让系统看起来更完整,可以在Users表里加一个Role字段,然后在登录按钮事件里把角色一起查出来。这样登录后主窗体可以根据角色决定显示哪些按钮——管理员能看到“用户管理”,普通读者只有借书还书功能。
修改后的登录查询语句如下:
string sql = "SELECT UserID, UserName, Role FROM Users WHERE UserName = @name AND Password = @pwd"; using (SqlConnection conn = GetConnection()) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@name", txtUserName.Text.Trim()); cmd.Parameters.AddWithValue("@pwd", txtPassword.Text); conn.Open(); using (SqlDataReader reader = cmd.ExecuteReader()) { if (reader.Read()) { string role = reader["Role"].ToString(); if (role == "Admin") { // 打开管理员主窗体 AdminMainForm mainForm = new AdminMainForm(); mainForm.UserName = txtUserName.Text.Trim(); mainForm.Show(); this.Hide(); } else { ReaderMainForm readerForm = new ReaderMainForm(); readerForm.UserName = txtUserName.Text.Trim(); readerForm.Show(); this.Hide(); } } else { MessageBox.Show("用户名或密码错误"); } } }SqlDataReader.Read()每次调用只读取一行数据,如果查询结果有记录返回 true,没有则 false。reader["Role"]是索引器语法,返回 object 类型,用ToString()转成字符串。这样做的好处是登录校验和角色分发在同一个using块里完成,连接关闭后不会遗留未释放的资源。如果你不想用SqlDataReader,也可以用SqlCommand.ExecuteScalar()返回第一行第一列的值,但那样只能拿一个字段,拿不到角色信息,不够灵活。
6.2 DataGridView 分页:给自己的系统加个像样的列表页
课程设计的图书列表如果一次查出几千条记录,加载会卡顿,答辩时老师会问你“数据量大怎么办”。这时候哪怕你不做真分页,至少要用 SQL 的OFFSET FETCH或ROW_NUMBER()做一个假分页。SQL Server 2012 以上版本可以用OFFSET FETCH,写法干净利落:
private void LoadPage(int pageIndex, int pageSize) { string sql = @" SELECT BookID, BookName, Author, Publisher, ISBN, Stock FROM Books ORDER BY BookID OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY"; DataTable dt = new DataTable(); using (SqlConnection conn = GetConnection()) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@offset", (pageIndex - 1) * pageSize); cmd.Parameters.AddWithValue("@pageSize", pageSize); conn.Open(); SqlDataAdapter adapter = new SqlDataAdapter(cmd); adapter.Fill(dt); dataGridView1.DataSource = dt; } }OFFSET跳过前面若干行,FETCH NEXT取后面的若干行,两者配合完成分页。(pageIndex - 1) * pageSize是跳过多少行的公式,第 1 页跳过 0 行,第 2 页跳过 10 行。这里的参数类型要注意:SQL Server 的OFFSET和FETCH后面的值必须是整数,所以@offset和@pageSize用Add方法添加时指定SqlDbType.Int,不要用AddWithValue,因为AddWithValue有时候会把整数推断成 bigint 导致隐式转换问题。分页按钮的Click事件里调用LoadPage(currentPage, 10)即可,每次翻页刷新网格数据。
6.3 代码规范:把连接字符串和 SQL 语句从窗体里挪走
课程设计代码最容易被老师挑毛病的点是:窗体代码里到处是SqlConnection、SqlCommand、SELECT *。哪怕功能全部正确,老师也会觉得这不像一个规范的系统。解决办法是把数据访问逻辑抽到一个公共类里,比如BookDAL,窗体只调用它的方法。我给你一个最小重构示范——先建一个BookService类:
public class BookService { public DataTable GetAllBooks() { string sql = "SELECT BookID, BookName, Author, Publisher, ISBN, Stock FROM Books"; return SqlHelper.ExecuteDataTable(sql); } public bool AddBook(string name, string author, string publisher, string isbn, int categoryId, int stock) { string sql = @"INSERT INTO Books (BookName, Author, Publisher, ISBN, CategoryID, Stock) VALUES (@name, @author, @publisher, @isbn, @categoryId, @stock)"; SqlParameter[] parameters = { new SqlParameter("@name", SqlDbType.NVarChar, 50) { Value = name }, new SqlParameter("@author", SqlDbType.NVarChar, 50) { Value = author }, new SqlParameter("@publisher", SqlDbType.NVarChar, 50) { Value = publisher }, new SqlParameter("@isbn", SqlDbType.NVarChar, 20) { Value = isbn }, new SqlParameter("@categoryId", SqlDbType.Int) { Value = categoryId }, new SqlParameter("@stock", SqlDbType.Int) { Value = stock } }; return SqlHelper.ExecuteNonQuery(sql, parameters) > 0; } }窗体按钮事件里只需要一行代码:dataGridView1.DataSource = new BookService().GetAllBooks();。这样窗体代码不直接碰 SQL,所有数据访问逻辑集中在服务类里。如果你时间充裕,再抽一层SqlHelper静态类封装连接打开、命令执行、参数传递,那就是标准的三层架构雏形。答辩时老师问“你这个项目分层了吗”,你可以直接回答“界面层只负责展示,业务逻辑在 Service,数据访问封装在 SqlHelper”,这个回答比空泛地说“我用了分层”有说服力得多。
6.4 验收前最后一遍检查:用 SQL Server Profiler 确认系统执行的语句
我把系统要提交之前,会花五分钟用 SQL Server Profiler 抓一遍系统实际执行的 SQL 语句。打开 SSMS 里的“工具 → SQL Server Profiler”,新建跟踪,勾选SQL:BatchCompleted和RPC:Completed两类事件,然后回系统里操作几个功能——登录、查书、借书、还书、添加图书记录。回到 Profiler 看捕获的语句,主要确认三件事:有没有SELECT *查询、有没有字符串拼接进 SQL 语句、有没有连接泄漏(每个操作的SqlConnection是否及时关闭)。
这个习惯帮我发现过很多隐蔽问题。比如有一次我的图书管理系统在查询时把整个 DataTable 拿回来然后在 C# 端循环过滤,Profiler 里一看,SQL 语句是SELECT * FROM Books,根本没用WHERE过滤,数据量大了系统肯定慢。后来改成把查询条件拼进 SQL 语句,请求量降了一个数量级。从那以后我每次提交课程设计之前都强制自己走一遍 Profiler 检查,确认没有SELECT *、没有裸奔的 SQL 拼接才敢交。希望帮到你。
本文还有配套的精品资源,点击获取