基于Java和SQL Server的图书馆管理系统数据库设计与实现 简介这是一套基于Java与SQL Server实现的简易图书馆管理系统主要面向计算机相关专业学生、数据库课程设计初学者以及需要快速完成课设或进行二次开发的开发者。系统围绕图书信息管理、借阅归还、读者管理等典型业务展开通过JDBC完成Java界面与SQL Server数据库的交互适合用来理解数据库连接、表结构设计与增删改查的基本流程。压缩包共127个文件包含74个class、25个java源文件另有SQL脚本、jar依赖库、png/jpg界面截图以及少量Excel报表结构完整清晰便于直接导入IDE运行并对照学习。资源包大小仅1.43MB轻量易用。已有2386人浏览学习是值得参考的课设范例。通过源码和脚本读者可以掌握从数据库建表、存储过程到Java窗口界面布局、事件响应的完整实现套路也能借助截图快速定位功能位置省去从零搭建的麻烦。1. 为什么拿图书馆管理系统做数据库课设一个看似简单、实际考验数据完整性设计的题目选题是课设的第一道坎。同样一门数据库课设有人交上去被导师当典型表扬有人被当场问得说不出话。差别基本不在功能多不多而在数据库这一层立不立得住。基于 Java 和 SQL Server 的简易图书馆管理系统是课设题目里最稳妥也最容易被低估的一个需求边界清楚业务逻辑完整能安排的加分点——触发器、存储过程、事务、视图——一个不少。而且它的演示效果直观答辩时不用花五分钟解释业务场景。我建议正在选课设题或者已经选了图书馆题但不知道从哪下手的同学按这条路径做先设计库表再打通 JDBC最后补业务代码和演示顺序反了就是翻车开局。2. 先把数据库立住SQL Server 建库建表、五张业务表与加分约束2.1 为什么选 SQL Server以及五张表怎么拆课设里选数据库常见的就是 SQL Server 和 MySQL 二选一。我倾向选 SQL Server理由不是性能而是它的管理工具对新手友好。SQL Server 自带的图形化管理工具能直接编表、看依赖关系、调试触发器这些操作对课设演示和答辩都有直观价值。更重要的是T-SQL 语法在触发器、存储过程和事务上有非常标准的写法网上能查到的资料量也足够。代价是连接配置比 MySQL 繁琐第 5 章会专门讲。表结构设计是整份课设的核心评价点。很多同学喜欢把所有字段塞进一张大表读者借了书就把书名写在读者信息表里一本书被借两次就要写两行重复的读者信息这是典型的第一范式问题。我通常按「谁、借了什么、什么时候还、超期罚多少」这条业务线拆表最后得到五张管理员表 Admin、读者表 Reader、图书表 Book、借阅表 Borrow、罚款表 Fine。管理员和读者分开是职责区分读者参与的业务动作管理员不能直接等同罚款单独建表是因为罚款是一个业务事件要关联到某一次借阅记录上还牵扯到是否已缴纳的状态。图书表里放了 TotalCount 和 AvailableCount 两个字段前者是馆藏总量后者是当前可借数量。按第三范式严格讲可用数量可以由总量减借出数算出来属于冗余但把它放进图书表可以避免每次借书都去扫描借阅记录统计数量而且可以由触发器自动维护。这一处冗余反而成了答辩时能讲清楚的设计亮点。2.2 建库建表脚本从创建数据库到外键约束建表脚本直接决定后续所有代码能不能稳定跑起来。下面的脚本覆盖了库、表、主键、外键、唯一约束和检查约束建议在图形化管理工具里直接执行-- 如果库不存在则创建避免重复执行时报错 IF DB_ID(NLibraryDB) IS NULL CREATE DATABASE LibraryDB; GO USE LibraryDB; GO -- 管理员表只保存登录账号和密码 CREATE TABLE Admin ( AdminID INT IDENTITY(1,1) PRIMARY KEY, AdminName NVARCHAR(50) NOT NULL, Password NVARCHAR(64) NOT NULL ); -- 读者表读者编号唯一注册日期由数据库默认生成 CREATE TABLE Reader ( ReaderID INT IDENTITY(1,1) PRIMARY KEY, ReaderNo NVARCHAR(20) NOT NULL UNIQUE, ReaderName NVARCHAR(50) NOT NULL, Phone NVARCHAR(20) NULL, RegDate DATE NOT NULL DEFAULT CAST(GETDATE() AS DATE) ); -- 图书表AvailableCount 表示当前可借数量 CREATE TABLE Book ( BookID INT IDENTITY(1,1) PRIMARY KEY, BookNo NVARCHAR(20) NOT NULL UNIQUE, BookName NVARCHAR(100) NOT NULL, Author NVARCHAR(50) NULL, Publisher NVARCHAR(50) NULL, TotalCount INT NOT NULL DEFAULT 0, AvailableCount INT NOT NULL DEFAULT 0 ); -- 借阅表ReturnDate 为 NULL 表示未归还 CREATE TABLE Borrow ( BorrowID INT IDENTITY(1,1) PRIMARY KEY, ReaderID INT NOT NULL, BookID INT NOT NULL, BorrowDate DATE NOT NULL DEFAULT CAST(GETDATE() AS DATE), DueDate DATE NOT NULL, ReturnDate DATE NULL, CONSTRAINT FK_Borrow_Reader FOREIGN KEY (ReaderID) REFERENCES Reader(ReaderID), CONSTRAINT FK_Borrow_Book FOREIGN KEY (BookID) REFERENCES Book(BookID), CONSTRAINT CK_Borrow_DueDate CHECK (DueDate BorrowDate) ); -- 罚款表Settled 标记是否已缴纳 CREATE TABLE Fine ( FineID INT IDENTITY(1,1) PRIMARY KEY, BorrowID INT NOT NULL, ReaderID INT NOT NULL, Amount DECIMAL(8,2) NOT NULL DEFAULT 0, Settled BIT NOT NULL DEFAULT 0, CONSTRAINT FK_Fine_Borrow FOREIGN KEY (BorrowID) REFERENCES Borrow(BorrowID), CONSTRAINT FK_Fine_Reader FOREIGN KEY (ReaderID) REFERENCES Reader(ReaderID) );几个容易忽略的点字符串字段全部用 NVARCHAR 而不是 VARCHAR因为 NVARCHAR 以 Unicode 存储Java 端传过来的中文不会出现乱码主键用 IDENTITY 自增Java 代码里完全不用也不该手动指定主键值ReaderNo 和 BookNo 加了 UNIQUE这对应现实里的读者证号和图书编号Java 侧插入重复时会直接报唯一约束错误比在代码里先查一遍再判断更可靠。DueDate 的 CHECK 约束保证还书期限一定晚于借书日期这条约束在演示时能拿出来说。2.3 视图、触发器与存储过程课设加分三件套光有表和约束系统只是一张空壳。课设拿高分通常要看三个东西视图简化查询、触发器自动维护数据、存储过程封装业务规则。借阅明细是每次查询都要用的数据涉及三张表关联与其在 Java 里写 JOIN不如建一个视图CREATE VIEW v_BorrowDetail AS SELECT b.BorrowID, r.ReaderNo, r.ReaderName, bk.BookNo, bk.BookName, b.BorrowDate, b.DueDate, b.ReturnDate FROM Borrow b INNER JOIN Reader r ON b.ReaderID r.ReaderID INNER JOIN Book bk ON b.BookID bk.BookID;库存数量的自动维护交给触发器。借出记录一插入图书表的 AvailableCount 就减一还书时 ReturnDate 从 NULL 变成非 NULLAvailableCount 加一。这样 Java 代码里永远不需要写 UPDATE Book 的语句也就少了一类状态不一致的隐患CREATE TRIGGER trg_Borrow_AfterInsert ON Borrow AFTER INSERT AS BEGIN SET NOCOUNT ON; UPDATE b SET AvailableCount b.AvailableCount - 1 FROM Book b INNER JOIN inserted i ON b.BookID i.BookID WHERE b.AvailableCount 0; END GO CREATE TRIGGER trg_Borrow_AfterUpdate ON Borrow AFTER UPDATE AS BEGIN SET NOCOUNT ON; -- 只在“从未还变已还”时触发加库存 UPDATE b SET AvailableCount b.AvailableCount 1 FROM Book b INNER JOIN inserted i ON b.BookID i.BookID INNER JOIN deleted d ON i.BorrowID d.BorrowID WHERE i.ReturnDate IS NOT NULL AND d.ReturnDate IS NULL; END GO存储过程把「借书」这个动作完整封装起来查读者、查图书、检查可借数量、写借阅记录全部在一个事务里完成。课设演示时直接在管理工具里调用一次存储过程比在 Java 里点按钮更有说服力因为你能让导师清楚地看到业务规则是数据库自己保证的CREATE PROCEDURE usp_BorrowBook ReaderNo NVARCHAR(20), BookNo NVARCHAR(20), Days INT 30 AS BEGIN BEGIN TRY BEGIN TRANSACTION; DECLARE ReaderID INT, BookID INT; SELECT ReaderID ReaderID FROM Reader WHERE ReaderNo ReaderNo; SELECT BookID BookID FROM Book WHERE BookNo BookNo; IF ReaderID IS NULL OR BookID IS NULL BEGIN RAISERROR(N读者或图书不存在, 16, 1); RETURN; END; IF (SELECT AvailableCount FROM Book WHERE BookID BookID) 0 BEGIN RAISERROR(N该书已无可借数量, 16, 1); RETURN; END; INSERT INTO Borrow (ReaderID, BookID, BorrowDate, DueDate) VALUES (ReaderID, BookID, GETDATE(), DATEADD(DAY, Days, GETDATE())); COMMIT TRANSACTION; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRANSACTION; THROW; END CATCH END GO注意存储过程里抛错用的是 RAISERROR 加 THROW 的组合RAISERROR 中断流程THROW 把错误抛给 Java 端。JDBC 里这个错误会变成一个 SQLException消息里直接能看到「该书已无可借数量」非常便于联调。第 4 章会接着用这个存储过程写借书功能。3. Java 联上 SQL ServerJDBC 驱动引入、最小连接代码与核心参数3.1 驱动 jar 与工程导入Java 连 SQL Server 不是 JDK 自带的需要一个名为 mssql-jdbc 的驱动 jar。我见过的很多失败案例卡点不在代码而在 jar 没真正进 classpath。最常见的做法有两种如果你的工程用了 Maven在 pom.xml 里引入 mssql-jdbc 对应的依赖选一个当前稳定版如果课设要求直接用开发工具跑把下载好的 jar 放到项目根目录下的 lib 文件夹然后在工具里对 jar 执行「添加为库」。驱动加载的类名是固定的 com.microsoft.sqlserver.jdbc.SQLServerDriver。注意大小写SQLServer 中间不能写成 SqlServer。早期 JDBC 规范还要求手动调用 Class.forName 注册驱动新版本驱动已经支持自动注册但课设代码里保留一行 Class.forName 也没有坏处反而更容易让导师看出你清楚驱动加载机制。另外一个容易被忽略的问题是驱动包和 SQL Server 的兼容性。驱动本身向下兼容做得不错但如果你用的是老版本 SQL Server而驱动太新握手时可能报协议相关错误。课设场景没有特殊需求选发布超过半年的稳定版本就比较稳妥不必追最新。3.2 最小连接代码与连接串参数连接参数的坑比驱动多。下面的工具类是我在课设里固定使用的模板先保证连接能建立再谈业务import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { public static Connection getConnection() throws SQLException { // 连接串参数说明 // databaseName 指定要连接的库不是 SQL Server 实例名 // encrypttrue 时驱动会对连接加密 // trustServerCertificatetrue 表示信任自签名证书仅限开发环境 String url jdbc:sqlserver://localhost:1433; databaseNameLibraryDB; encrypttrue; trustServerCertificatetrue; loginTimeout5; return DriverManager.getConnection(url, sa, your_password); } }连接串里最值得注意的参数是 encrypt 和 trustServerCertificate。新版 mssql-jdbc 驱动默认要求加密连接而本机开发环境装的大多是自签名证书如果只开 encrypt 不开 trustServerCertificate启动程序就会报 PKIX path building failed。很多同学第一次连 SQL Server 就卡在这一步网上答案杂七杂八实际上加上 trustServerCertificatetrue 就解决了。这个参数的含义是「信任服务器证书不校验证书链」只适合本地课设环境不能原样搬到生产环境。连接串里各参数的作用大致如下参数作用课设建议databaseName要连接的数据库名写第 2 章建好的 LibraryDBencrypt是否启用 TLS 加密保持 true新版驱动默认开启trustServerCertificate是否信任服务器自签名证书本地开发设 true否则会报 PKIXloginTimeout连接超时时间秒设 5 秒快速暴露环境问题integratedSecurity是否用系统账号登录用 sa 登录时设 false 或不写连接串还有一些容易踩的变体。localhost 改成远程主机 IP 就能连别的机器上的库如果安装时用的是命名实例需要在连接串里加 instanceNameSQLEXPRESS 这样的参数不能用 databaseName 代替databaseName 只是库名和实例名是两个概念。SQL Server 的端口不一定固定是 1433可以在连接串里写成 localhost:1434 覆盖默认端口。3.3 先写一个连接测试类再往后开发我强烈建议在写任何业务功能之前先跑一个最纯粹的连接测试。这样可以把环境问题和代码问题分开后面所有页面调不通时先回头检查这里public class ConnectTest { public static void main(String[] args) { try (Connection conn DBUtil.getConnection()) { System.out.println(连接成功当前是否关闭 conn.isClosed()); } catch (SQLException e) { System.err.println(连接失败 e.getMessage()); e.printStackTrace(); } } }这段代码用 try-with-resources 管理连接不管成功失败都自动释放。如果控制台在 5 秒内打出「连接成功」说明驱动、端口、认证、库名全部正确后面写业务就不会被环境问题反复打断。如果失败优先检查 1433 端口是否监听其次检查账号密码和认证模式这些在第 5 章都会展开。本地方便的验证方式还有一个打开命令行输入下面的命令看到 LISTENING 状态说明 SQL Server 确实在监听该端口netstat -ano | findstr 1433没有输出就说明 TCP/IP 协议没开这时不用改 Java 代码直接去改数据库服务配置。把这一步养成习惯能省掉大量玄学排查时间。4. 核心功能写进代码登录、图书查询、借阅归还与 JTable 展示4.1 登录模块PreparedStatement 与密码处理从这章开始所有代码默认依赖第 3 章的 DBUtil。第一个功能是管理员登录。登录直接对应 Admin 表SQL 写成 SELECT COUNT(*) 比把密码查出来再比对更简单也避免把密码读到内存里public boolean login(String adminName, String password) { // 用占位符 ? 代替直接拼接杜绝 SQL 注入 String sql SELECT COUNT(*) FROM Admin WHERE AdminName ? AND Password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, adminName); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { rs.next(); return rs.getInt(1) 0; } } catch (SQLException e) { e.printStackTrace(); return false; } }注意区分 Statement 和 PreparedStatement。Statement 适合执行没有条件的固定 SQL登录查询带了用户输入必须用 PreparedStatement 的占位符。把用户输入的密码直接拼到字符串里一旦输入框填了 OR 11SELECT 的结果就永远不为空登录直接被绕过。这是课设里最容易被扣分的点之一。顺带提一句密码存储。课设阶段很多同学直接明文存演示没问题但导师要是追问「密码怎么存」会有点被动。最低成本的改进是入库前做一次 SHA-256 哈希。做法是建表后先往 Admin 表写入哈希后的默认密码Java 登录时把输入做同样哈希再比对。不要用 MD5课设里能给出这个思路就足够体现安全意识了。4.2 图书查询与新增从结果集到实体对象图书管理模块是代码量最大的部分查询和新增是两个最典型的写法。查询用 LIKE 模糊匹配书名或作者输入的关键字通过参数传入public ListBook searchBooks(String keyword) { String sql SELECT BookID, BookNo, BookName, Author, Publisher, TotalCount, AvailableCount FROM Book WHERE BookName LIKE ? OR Author LIKE ?; ListBook books new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { books.add(new Book( rs.getInt(BookID), rs.getString(BookNo), rs.getString(BookName), rs.getString(Author), rs.getString(Publisher), rs.getInt(TotalCount), rs.getInt(AvailableCount))); } } } catch (SQLException e) { e.printStackTrace(); } return books; }这里用 ps.setString 传入带 % 的模糊条件而不是在 SQL 里手动拼是参数化查询的规范姿势。LIKE 有个小坑关键字里本身含 % 或 _ 会被当成通配符。课设数据量小不处理也能跑但如果想做得严谨传参前把关键字里的 % 和 _ 转义掉避免用户输入特殊符号时查到意料之外的结果。新增图书更为直白唯一要提醒的是两层数量的初始化public boolean addBook(Book book) { String sql INSERT INTO Book (BookNo, BookName, Author, Publisher, TotalCount, AvailableCount) VALUES (?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, book.getBookNo()); ps.setString(2, book.getBookName()); ps.setString(3, book.getAuthor()); ps.setString(4, book.getPublisher()); ps.setInt(5, book.getTotalCount()); ps.setInt(6, book.getTotalCount()); // 新增时可用数量与总量一致 return ps.executeUpdate() 1; } catch (SQLException e) { e.printStackTrace(); return false; } }作者和出版社在业务里允许为空。如果你在 Java 端把空字符串传进去数据库里存的就是空字符串而不是 NULL后面查「出版社为空」的报表就不准。更规范的做法是进入方法时先判断null 和空串统一转成 NULL。这也是课设里容易被导师抓到的小细节。4.3 借书与还书调用存储过程与触发器回补库存借书逻辑直接复用第 2 章的 usp_BorrowBook 存储过程。Java 端用 CallableStatement 调用参数顺序和存储过程定义保持一致public boolean borrowBook(String readerNo, String bookNo) { // 直接调用数据库存储过程业务规则由数据库保证 String sql { CALL usp_BorrowBook(?, ?, 30) }; try (Connection conn DBUtil.getConnection(); CallableStatement cs conn.prepareCall(sql)) { cs.setString(1, readerNo); cs.setString(2, bookNo); cs.execute(); return true; } catch (SQLException e) { // 存储过程里 RAISERROR 的信息会出现在 e.getMessage() 中 System.err.println(借书失败 e.getMessage()); return false; } }这里把借书规则放进存储过程而不是 Java 里是有意的设计取舍。在 Java 里写借书逻辑一般是先查库存、再判断、再插入、再更新库存四步之间只要有一行代码抛异常数据库的状态就可能停在中间。存储过程把四步放进了一个事务任何一步失败都会整体回滚。课设答辩时这个设计决策比「我写了很多功能」更容易被认可。还书更简单因为库存回补已经交给触发器了Java 只需要把 ReturnDate 置为当前日期public boolean returnBook(int borrowID) { // 库存加一由触发器 trg_Borrow_AfterUpdate 自动完成 String sql UPDATE Borrow SET ReturnDate GETDATE() WHERE BorrowID ? AND ReturnDate IS NULL; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, borrowID); return ps.executeUpdate() 1; } catch (SQLException e) { e.printStackTrace(); return false; } }UPDATE 影响行数为 0 时说明这条借阅记录不存在或已经还过。这里不应该当成异常抛出正常返回 false由调用方提示「该记录不存在或已归还」即可。还书逻辑里不需要手动写 UPDATE Book 的句子也不能写否则会跟触发器重复加库存这就是第 5 章要讲的库存错乱问题。4.4 把视图数据灌进 JTable避免手工拼接 JOIN借阅记录的列表展示正确做法是查第 2 章的 v_BorrowDetail 视图然后逐行往 JTable 的 DefaultTableModel 里塞public void loadBorrowTable(JTable table) { String sql SELECT * FROM v_BorrowDetail ORDER BY BorrowDate DESC; DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); // 先清空旧数据避免重复加载时叠加 try (Connection conn DBUtil.getConnection(); Statement st conn.createStatement(); ResultSet rs st.executeQuery(sql)) { while (rs.next()) { model.addRow(new Object[]{ rs.getInt(BorrowID), rs.getString(ReaderNo), rs.getString(ReaderName), rs.getString(BookNo), rs.getString(BookName), rs.getDate(BorrowDate), rs.getDate(DueDate), rs.getDate(ReturnDate) }); } } catch (SQLException e) { e.printStackTrace(); } }这段代码用 Statement 是安全的因为 SQL 里没有任何用户输入如果查询条件要接收界面上的关键字还是改回 PreparedStatement。视图的好处在这里体现得很明显Java 端不用写 JOIN不用关心三张表的外键关系只要按字段名取数据。setRowCount(0) 这行容易被遗漏很多同学的表格在多次刷新后出现重复行原因就是旧数据没清掉。关于 JTable 的列名如果你用默认的 DefaultTableModel 在界面里手动设置列名注意列顺序要和上面 addRow 的数组顺序完全一致否则表格内容会错位。这种错位不报错是典型的可视层 bug排起来很费眼。5. 联调避坑SQL Server 和 Java 配对的 5 个高频翻车现场以下五条是我在带课设时反复看到的翻车点按出现频率排序。每一条都有明确的报错特征照着现象对号入座即可。5.1 端口连不上、连接超时SQL Server 默认不开 TCP/IP现象程序抛 ConnectException提示 Connection refused 或 Connection timed out也有的是「通过端口 1433 连接到主机 localhost 失败」。这个错误和 Java 代码本身无关先不要改代码。原因SQL Server 安装后TCP/IP 协议默认是禁用的。JDBC 走的是 TCP 1433 端口协议没启用时端口根本不会监听连接直接被拒。解决在 SQL Server 配置管理器里展开「SQL Server 网络配置」找到对应实例的「协议」把 TCP/IP 启用然后重启 SQL Server 服务。重启之后用 netstat -ano | findstr 1433 确认端口处于 LISTENING 状态再跑程序。注意配置管理器要以管理员权限打开修改后不重启服务不会生效。如果装的是多个实例还要确认 JDBC 连的是哪个实例的端口实例不同端口可能不同。5.2 报 PKIX path building failed新版驱动默认校验证书现象连接报错里同时出现 javax.net.ssl.SSLHandshakeException 和 PKIX path building failed很多人误以为是驱动问题或证书问题折腾半天。原因新版 mssql-jdbc 驱动默认 encrypttrue驱动会对服务器证书做信任校验。本机开发环境用的通常是自签名证书不在 JVM 信任库里于是握手失败。解决开发环境最简单也最正确的做法是连接串加 trustServerCertificatetrue第 3 章的模板里已经写了。这个参数就是告诉驱动「我不校验证书链直接信任」。如果在生产环境应该把服务器证书导入 JVM 信任库而不是关掉校验但课设阶段用不着。如果你不想用这个参数另一个办法是把证书导出来 import 到 Java 的 cacerts 里操作成本高不建议课设投入时间。5.3 ClassNotFoundException / No suitable driverjar 没有进 classpath现象编译能过运行时第一行就抛 ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver或者抛 SQLException: No suitable driver found for jdbc:sqlserver://...。原因前者说明驱动类找不到通常是 jar 没导入工程或导入后开发工具不认后者说明连接串和驱动不匹配比如驱动 jar 版本过旧、连接串前缀写错、URL 里的分号被错误转义。解决重新确认 jar 在项目的 lib 目录下并且已经被添加为库。另外检查驱动类名大小写SQLServerDriver 不能写成 SqlServerDriver。连接串前缀一定是 jdbc:sqlserver:中间有冒号写成 jdbc:sqlserver:// 是另一种常见笔误。区分这两个错误有个简单办法ClassNotFoundException 是类加载层面的问题No suitable driver 是驱动注册后不认 URL 格式的问题前者查 jar后者查 URL。5.4 sa 登录失败或打不开 LibraryDB认证模式与账号状态现象抛 Login failed for user sa或者 Cannot open database LibraryDB requested by the login。这两种报错容易被混为一谈但原因完全不同。原因第一种是登录本身被拒。SQL Server 安装时选的是纯系统认证模式时sa 账号没有启用或者密码策略不允许简单密码账号状态显示禁用时任何连接都会被拒绝。第二种是账号能登录但没有权限访问目标库通常是图形工具里建的库和 Java 连接串写的库名不一致。解决用系统身份登录图形化管理工具在实例属性的安全性页里把认证模式改成混合认证模式然后在 sa 账号上重新设置符合复杂度的密码并确认状态页的登录已启用。改完必须重启 SQL Server 服务。如果报打不开 LibraryDB先用建库脚本确认库真的存在再看 sa 账号的默认数据库是不是被设成了别的库这两种情况都会报同样的错误文本。5.5 外键冲突、库存对不上触发器与业务逻辑的顺序问题现象借书时报外键约束冲突提示 INSERT 语句与 FOREIGN KEY 约束冲突或者还书之后图书表 AvailableCount 明显不对多减了一次或多加了一次。原因外键失败通常是你插入 Borrow 时ReaderID 或 BookID 在对应主表里不存在。这些 ID 应该由主表的自增值提供不能在前端硬编码。库存对不上则往往是因为还书时既写了 JDBC 的 UPDATE Book 语句又存在触发器自动加库存两边叠加自然出错。还有一种隐蔽情况有些人为了「保险」在触发器和存储过程里同时维护库存双份更新也会导致数量漂移。解决借书前先通过 ReaderNo 查 ReaderID通过 BookNo 查 BookID再调用存储过程由存储过程内部做存在性检查还书时严格遵守「Java 只更新借阅记录触发器负责库存回补」的约定。维护一个简单原则能用触发器完成的数据维护不要写第二遍。这条原则也是答辩时回答「为什么库存是准的」的核心论据。如果不小心已经把库存改乱了别在程序里打补丁直接执行 UPDATE Book SET AvailableCount TotalCount - (SELECT COUNT(*) FROM Borrow WHERE ReturnDate IS NULL AND BookID Book.BookID) 重新校准一次。6. 让课设从「能用」到「优秀」事务习惯、索引设计与答辩演示的一个技巧最后这部分不留大段代码只给你三个真正让我在课设里少吃亏的习惯。第一个习惯是借还相关操作一律走事务。哪怕触发器已经自动处理库存借书动作本身也包含了「检查数据存在、检查库存、插入记录」多个步骤任何一个环节失败都不该留下半条借阅记录。存储过程里的 BEGIN TRANSACTION 不是装饰品它是「全部成功才算成功」的保证。你可以在答辩现场故意调用一次不存在的图书号让导师看到报错后库里的库存和借阅记录都没有变化这一下比任何功能截图都直观。第二个习惯是给外键列加索引。Borrow 表经常按 ReaderID 或 BookID 关联查询SQL Server 不会自动给外键建索引。数据量上百条时感觉不到上万条时 JOIN 的代价会明显拉高。建索引的脚本很短加到建表脚本最后即可CREATE NONCLUSTERED INDEX IX_Borrow_ReaderID ON Borrow(ReaderID); CREATE NONCLUSTERED INDEX IX_Borrow_BookID ON Borrow(BookID);第三个技巧是准备一条逾期统计 SQL 作为演示收尾。图书馆系统的核心数据价值就在这里找出所有已超期未归还的借阅记录并算出逾期天数。这条 SQL 在答辩时一执行业务闭环就完整了SELECT BorrowID, ReaderID, BookID, DATEDIFF(DAY, DueDate, GETDATE()) AS OverdueDays FROM Borrow WHERE ReturnDate IS NULL AND DueDate GETDATE();我当年做课设最亏的就是到最后一天才开始补数据完整性设计导致触发器、事务全是后加的代码改得乱七八糟。如果重来一次我会先把建表脚本、存储过程和触发器调通再让 Java 代码只做界面和调用。这个顺序看上去慢实际上省掉了大量来回返工的时间。希望帮到你。本文还有配套的精品资源点击获取