ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C#与SQL Server 2008图书管理系统:数据库设计与并发控制实践

C#与SQL Server 2008图书管理系统:数据库设计与并发控制实践 简介这是一套基于C#与Visual Studio开发、搭配SQL Server 2008数据库的图书管理系统完整项目适合用于课程设计、毕业设计也可作为WinForms与数据库编程的入门实战参考。项目围绕读者信息、图书信息、借阅信息、用户信息四张核心表展开管理员可完成读者、图书、借阅记录的增删改查并能按读者查询未归还图书普通读者能浏览图书、查看本人借阅与自己未还书籍还可修改账户信息。借书、还书流程完整逾期未还罚金自动计算读者在线缴纳罚款即可销账业务逻辑比较贴近实际图书管理场景。压缩包共105个文件以42个cs源码文件为主体配20个resources与20个resx资源文件另含可直接运行的exe、SQL Server的mdf数据文件及ldf日志文件整包仅623KB适合快速下载和本地调试。目前已有2737人学习使用对理解C#窗体应用、SQL Server表设计与C#数据交互都有切实帮助。1. 图书管理系统这套C# Visual Studio SQL Server 2008组合现在还在学什么如果把“图书管理系统C#、Visual Studio、SQL Server 2008”丢给一个写了十年业务系统的工程师多半会先笑一声“太老了吧”然后沉默几秒。因为这套组合虽然看着像课设但它背后踩的每一个点——连接字符串、数据适配器、参数化查询、事务与并发——到今天还在以各种变体出现在企业系统里。SQL Server 2008 的兼容性确实老旧但用 C# 操作关系型数据库的核心流程并没有变建库建表、写数据访问层、绑定 UI、处理异常。这篇博文不评价这个项目是否“过时”而是顺着这条路把每一步怎么做、参数怎么设、坑在哪里讲清楚。准备接手老旧校园系统、正在赶毕业设计或者想用一个小项目把 ADO.NET 和 SQL Server 摸透的开发者都适合往下看。2. 先建 SQL Server 2008 的库表结构与存储过程再写 C#图书管理系统最忌讳一上来就拖控件。数据库结构没立住后面 C# 代码写得再漂亮也是空中楼阁。SQL Server 2008 的图形界面操作简单但脚本才是可复现、可交接、能进版本库的东西。这里按最小可用模型来设计三张表读者表、图书表、借阅表再配一个存储过程和几个必要索引。2.1 图书管理系统的核心表设计读者、图书、借阅怎么建模读者表保存读者编号、姓名和联系方式。图书表保存 ISBN、书名、作者、库存总量和当前可借数量。借阅表是关联表记录读者借了哪本书、什么时候借、什么时候还、实际归还日期。三张表之间的外键关系要显式声明不能只在业务逻辑里“默认有”。一个常见错误是把“是否在借”存在图书表里。这样每借一次就要改一次状态还书时再改回来遇到图书损坏、预约、续借等场景就非常别扭。正确做法是让借阅表的“实际归还日期”这一列为 NULL 时表示未还还书时写入日期。判断某本书是否可借用“库存总量减去未还记录数”实时计算而不是维护一个冗余字段。SQL Server 2008 支持这种即席统计量级在十万条以内毫无压力。2.2 用 SQL Server 2008 的查询分析器建库建表的可执行脚本打开 SQL Server Management Studio新建查询按下面脚本执行。库名用 LibraryDB兼容级别设置成 SQL Server 2008100避免后续部署到别的机器出现版本兼容问题。IF DB_ID(LibraryDB) IS NULL CREATE DATABASE LibraryDB; GO USE LibraryDB; GO IF OBJECT_ID(dbo.BorrowRecord, U) IS NOT NULL DROP TABLE dbo.BorrowRecord; IF OBJECT_ID(dbo.Book, U) IS NOT NULL DROP TABLE dbo.Book; IF OBJECT_ID(dbo.Reader, U) IS NOT NULL DROP TABLE dbo.Reader; GO CREATE TABLE dbo.Reader ( ReaderId INT IDENTITY(1,1) PRIMARY KEY, ReaderNo VARCHAR(20) NOT NULL UNIQUE, ReaderName NVARCHAR(50) NOT NULL, Phone VARCHAR(20) NULL, CreatedAt DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE dbo.Book ( BookId INT IDENTITY(1,1) PRIMARY KEY, ISBN VARCHAR(20) NOT NULL, BookName NVARCHAR(100) NOT NULL, Author NVARCHAR(50) NULL, TotalCount INT NOT NULL CHECK (TotalCount 0), CreatedAt DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE dbo.BorrowRecord ( BorrowId INT IDENTITY(1,1) PRIMARY KEY, ReaderId INT NOT NULL REFERENCES dbo.Reader(ReaderId), BookId INT NOT NULL REFERENCES dbo.Book(BookId), BorrowDate DATETIME NOT NULL DEFAULT GETDATE(), DueDate DATETIME NOT NULL, ReturnDate DATETIME NULL, CHECK (ReturnDate IS NULL OR ReturnDate BorrowDate) ); CREATE INDEX IX_BorrowRecord_Reader ON dbo.BorrowRecord(ReaderId, ReturnDate); CREATE INDEX IX_BorrowRecord_Book ON dbo.BorrowRecord(BookId, ReturnDate);这些语句的逻辑是先删后建保证脚本可以重复执行。IDENTITY(1,1) 让 ReaderId 和 BookId 自增不用在 C# 里手动生成主键。DueDate 是应还日期ReturnDate 为 NULL 表示还没归还CHECK 约束防止出现“还书时间早于借书时间”这种脏数据。索引加在 (ReaderId, ReturnDate) 和 (BookId, ReturnDate) 上是为了让“查某读者未还记录”和“查某本书在借数量”走索引覆盖避免全表扫描。SQL Server 2008 的查询优化器对这类复合索引识别得不错别画蛇添足再加单列索引。2.3 用存储过程查询可借状态减少 C# 端多表 JOIN很多初学者习惯在 C# 里先查图书表再查借阅表然后自己在内存里减库存。这种写法既慢又容易在并发时出错。把统计逻辑收进存储过程让 SQL Server 一次算完C# 端只接收结果集。CREATE PROCEDURE dbo.usp_SearchBooks Keyword NVARCHAR(100) AS BEGIN SET NOCOUNT ON; SELECT b.BookId, b.ISBN, b.BookName, b.Author, b.TotalCount, ISNULL(borrow.ActiveCount, 0) AS BorrowedCount, b.TotalCount - ISNULL(borrow.ActiveCount, 0) AS AvailableCount FROM dbo.Book b OUTER APPLY ( SELECT COUNT(*) AS ActiveCount FROM dbo.BorrowRecord br WHERE br.BookId b.BookId AND br.ReturnDate IS NULL ) borrow WHERE b.BookName LIKE % Keyword % OR b.ISBN LIKE % Keyword % ORDER BY b.BookId DESC; END GOOUTER APPLY 在 SQL Server 2008 里用得很多它让子查询可以引用外表 b 的列每次按 BookId 统计未还数量。LIKE % Keyword % 是前导通配会牺牲索引效率但这属于常见做法图书量不大时完全够用。C# 端调用这个存储过程时只需传一个 Keyword 参数参数化查询天然规避了 SQL 注入比拼接字符串靠谱得多。3. C# Visual Studio 连接 SQL Server 2008 的数据访问层写法表结构就位后进入 Visual Studio 里写 C# 代码。无论用 WinForms 还是 WPF数据访问层都可以抽成一个 DbHelper 类。下面这套写法基于 ADO.NET不依赖 Entity Framework也不需要 NuGet 装额外包对 SQL Server 2008 最友好。3.1 连接字符串与 SqlConnection 的关键参数连接字符串里的每项参数都有实际含义。Data Source 指定 SQL Server 实例Initial Catalog 指定数据库名Integrated SecurityTrue 表示用 Windows 身份验证走当前登录用户的 Windows 账号连接数据库。如果部署在服务器上而服务器与开发机不在同一个域通常改用 SQL Server 身份验证写 User ID 和 Password。string connectionString Data Source.\\SQLEXPRESS; Initial CatalogLibraryDB; Integrated SecurityTrue; MultipleActiveResultSetsTrue; Connect Timeout15;; using (SqlConnection conn new SqlConnection(connectionString)) { conn.Open(); // 业务操作 }Data Source 写成.\\SQLEXPRESS是命名实例的写法默认实例直接写DataSource.或localhost即可。MultipleActiveResultSetsTrue 允许在同一连接上同时打开多个 SqlDataReaderWinForms 里一边遍历数据一边做 UPDATE 时特别有用。Connect Timeout15 是连接超时秒数单位是秒不是毫秒。如果 SQL Server 服务没启动这里会在 15 秒后抛异常而不是卡死界面。3.2 封装 DbHelper参数化查询与 SqlDataAdapter 刷新DbHelper 至少需要两个方法ExecuteNonQuery 处理增删改ExecuteDataTable 处理查询并填充 DataTable。所有方法内部都用 using 包裹连接和命令确保释放连接回到 ADO.NET 连接池。public static class DbHelper { private static readonly string ConnStr ConfigurationManager.ConnectionStrings[LibraryDb].ConnectionString; public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(ConnStr)) { using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); return cmd.ExecuteNonQuery(); } } } public static DataTable ExecuteDataTable(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(ConnStr)) { using (SqlDataAdapter adapter new SqlDataAdapter(sql, conn)) { if (parameters ! null) { adapter.SelectCommand.Parameters.AddRange(parameters); } DataTable dt new DataTable(); adapter.Fill(dt); return dt; } } } }SqlDataAdapter 在内部自动处理连接打开和关闭Fill 方法执行 SelectCommand 并把结果填充进 DataTable。这里没有显式 conn.Open()因为 DataAdapter.Fill 会自动打开连接并再关闭。ExecuteNonQuery 则必须手动 Open因为命令执行需要活跃连接。SqlParameter 是参数化查询的核心数据库端以变量方式接收值C# 端传的是参数对象而不是拼进 SQL 字符串从根上规避了单引号逃逸带来的注入风险。调用时这样写SqlParameter[] ps { new SqlParameter(Keyword, SqlDbType.NVarChar, 100) { Value keyword } }; DataTable books DbHelper.ExecuteDataTable(dbo.usp_SearchBooks, ps); dataGridView1.DataSource books;注意 SqlParameter 里的 SqlDbType 要与存储过程参数类型一致NVarChar 对应 C# 的 string长度 100 要和表结构匹配。长度不匹配不会报错但会多做一次隐式转换极端情况下字符被截断。Value 赋值不允许为 null需要 DBNull.Value 时单独处理。3.3 WinForms 界面绑定与数据刷新节流DataGridView 绑定 DataTable 后每次刷新数据都重新 Fill 一个新 DataTable 再赋给 DataSource是最直接的做法。数据量在几千行时毫无问题但如果在 TextChanged 事件里每次按键都触发一次数据库查询SQL Server 2008 会频繁编译执行计划界面也会明显卡顿。这种现象很像 C# 循环数据采集和 UI 刷新卡顿的问题主线程被数据库操作阻塞UI 消息泵无法处理重绘消息。常见做法是给搜索框加一个延时定时器用户停止输入 500ms 后再发起查询。private System.Windows.Forms.Timer searchTimer; private void txtKeyword_TextChanged(object sender, EventArgs e) { searchTimer.Stop(); searchTimer.Start(); } private void searchTimer_Tick(object sender, EventArgs e) { searchTimer.Stop(); LoadBookList(txtKeyword.Text.Trim()); }Timer 的 Interval 设置为 500。用户每敲一个字符定时器重置只有停顿超过 500ms 才真正执行 LoadBookList。这样避免了对 SQL Server 2008 的高频请求同时让 DataGridView 有充足时间重绘。如果数据量超过一万行再加分页用 ROW_NUMBER() OVER (ORDER BY BookId) 做分页查询不要一次性全量拉到客户端。4. Visual Studio 和 SQL Server 2008 开发调试时的典型故障排查这套组合里有一半时间在写代码另一半时间在处理环境问题。SQL Server 2008 的服务、Visual Studio 的组件、连接字符串的环境差异每一样都能让程序崩在莫名其妙的地方。4.1 “由于出现错误,无法启动 Visual Studio”与 ServiceHub 问题Visual Studio 在加载项目时偶发“由于出现错误,无法启动 Visual Studio。Microsoft.ServiceHub.Client.Controller”的报错这在安装了多个 VS 版本或 VS 突然崩溃后最常见。ServiceHub 是 VS 用来协调后台服务进程的组件缓存损坏后会导致启动失败。处理步骤是先关掉所有 VS 实例删除项目目录下的隐藏文件夹.vs里面存放缓存和用户状态然后用命令行启动devenv /resetuserdata重置用户数据。如果还不行打开 Visual Studio Installer 选择“修复”。SQL Server 2008 或老版本项目大多是 32 位依赖修复 VS 时注意勾选“.NET 桌面开发”工作负载。4.2 SQL Server 2008 服务未启动与 TCP/IP 协议设置C# 程序连接本机 SQL Server 时报“在建立与服务器的连接时出错”首先别去改代码而是打开“SQL Server 配置管理器”。确认 SQL Server 服务处于“运行中”状态再检查“SQL Server 网络配置”下的 TCP/IP 协议是否启用。SQL Server 2008 的默认实例监听 1433 端口命名实例则使用动态端口防火墙放行时也要对应处理。启动失败的另一个常见原因是系统存在待重启文件操作导致安装程序在“设置 Windows Update 时失败”安装 SQL Server 2008 时会卡在“是否需要重启计算机”。这时候打开注册表编辑器定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager把 PendingFileRenameOperations 这个值删掉然后重新安装即可。这是 SQL Server 2008 时代比较经典的处理方法操作前注意备份注册表或先导出该项。4.3 数据库连接失败时的快速定位表排查连接问题时建议按下表逐行对照检查而不是反复重装软件。错误现象可能原因处理方式用户 sa 登录失败18456SQL Server 身份验证未启用或密码错误用 Windows 身份登录改服务器属性为“SQL Server 和 Windows 身份验证模式”重设 sa 密码无法打开数据库“LibraryDB”数据库未附加或文件路径不对挂载 .mdf 文件到实例或检查 Initial Catalog 名称请求失败或服务没有及时响应SQL Server 服务停止或连接超时服务设为自动启动调整 Connect TimeoutOLE DB 错误 80040e14SQL 语法不兼容 SQL Server 2008去掉高版本特性比如 STRING_AGG、TRIM 等函数用命令行验证连接也是一种办法不需要打开 SSMSsqlcmd -S .\SQLEXPRESS -d LibraryDB -E -Q SELECT TOP 1 BookName FROM dbo.Book-E 表示 Windows 身份验证。能跑通这条命令说明数据库服务、协议和身份验证都没问题接下来去检查 C# 代码和连接字符串。sqlcmd 是 SQL Server 自带的命令行工具SQL Server 2008 的安装目录里默认包含。5. 借阅并发冲突处理一个值得加入图书管理系统的存储过程技巧图书管理系统的业务逻辑看似简单真正到了多读者同时借同一本书时库存扣减很可能会被查漏。C# 端先查剩余数量再执行的写法两个请求可以同时读到库存为 1同时通过校验分别下借阅单库存变成负数。SQL Server 2008 支持事务和锁可以把这个统计和插入逻辑封装进一个存储过程C# 只调用它。5.1 用事务和行级锁保证可借数量不为负下面这个存储过程完成三个动作检查可借数、插入借阅记录、更新图书表的冗余可借数量。为了让检查与写入原子化使用 UPDLOCK 提示配合事务。CREATE PROCEDURE dbo.usp_BorrowBook ReaderId INT, BookId INT, DueDate DATETIME AS BEGIN BEGIN TRANSACTION; BEGIN TRY DECLARE AvailableCount INT; SELECT AvailableCount TotalCount - ( SELECT COUNT(*) FROM dbo.BorrowRecord WHERE BookId BookId AND ReturnDate IS NULL ) FROM dbo.Book WITH (UPDLOCK, ROWLOCK) WHERE BookId BookId; IF AvailableCount 0 BEGIN ROLLBACK TRANSACTION; RETURN -1; END INSERT INTO dbo.BorrowRecord (ReaderId, BookId, BorrowDate, DueDate) VALUES (ReaderId, BookId, GETDATE(), DueDate); COMMIT TRANSACTION; RETURN 0; END TRY BEGIN CATCH IF XACT_STATE() 0 ROLLBACK TRANSACTION; RETURN -2; END CATCH END GOWITH (UPDLOCK, ROWLOCK) 是关键。UPDLOCK 让查询在读取 Book 行时直接获取更新锁并持有到事务结束另一个并发会话必须等这个事务提交或回滚后才能读取同一行。ROWLOCK 表示锁粒度锁定在行上避免升级成表锁导致整个借阅过程串行化。C# 端调用这个存储过程时用 ExecuteNonQuery 执行返回值小于 0 时提示“该书已全部借出或操作失败”。5.2 验证并发与死锁验证方法是开两个 sqlcmd 窗口同时执行EXEC dbo.usp_BorrowBook 1, 1, 2026-06-01。正常情况下只有一个窗口返回 0另一个返回 -1。如果出现死锁错误信息里会包含“事务进程 ID与另一个进程已被死锁在 lock 资源上”这时检查锁粒度是否过大或者是不是反向更新了多行数据。死锁并不总是坏事只要死锁事务被 SQL Server 自动回滚C# 端捕获 SqlException 后重试即可。这套事务逻辑跑通之后图书管理系统的数据完整性基本就立住了。后续如果再扩展续借、超期罚款只需要在存储过程加参数和条件C# 端的调用方式完全不用改。本文还有配套的精品资源点击获取
返回列表