基于Java与SQL-Server的图书管理系统课程设计:数据库约束、事务与并发控制实战
简介本资源为基于JAVA与SQL Server的图书管理系统课程设计完整文档面向计算机相关专业学生及需要完成数据库课程设计的学习者帮助解决图书馆借阅者与工作人员查找书目困难、管理效率低下的问题。文档围绕Java语言程序设计、SQL Server数据库管理、数据库设计需求分析、E-R图、逻辑与物理结构设计、系统前后端开发与测试、JAVA与SQL Server集成等核心知识点展开并包含课程设计报告的编写规范与评估标准。压缩包内共1个doc文件约672KB内容涵盖课程设计目的、进度安排、参考文献、中英文摘要及成绩评定表等完整结构可直接作为课程设计报告模板与开发思路参考。目前已有4270人学习下载适合需要系统梳理数据库设计流程、掌握JAVA连接SQL Server实现图书管理功能的读者借鉴使用。1. 图书管理系统课程设计为什么“能跑”和“能过”是两回事每年一到学期中后段做「基于JAVA和SQL-Server图书管理系统课程设计」的人就会扎堆。多数人第一反应是去网上找一份现成源码改个标题、换个配色然后祈祷答辩老师不细看。但真正做过一轮的人都知道这类系统最要命的不是功能多而是数据一致性和业务闭环——借书时库存没扣、还书时罚款算错、并发借同一本书直接超卖这些才是答辩现场被追问到哑口无言的地方。这篇笔记面向的是正在做课程设计的学生以及需要快速交付一个可演示、可讲解、可扩展的图书管理系统的开发者。我会把整个方案拆成数据库设计、Java后端实现、事务与并发控制、常见翻车点四个层面每一步都给出可复现的代码和参数说明。你不需要有很深的框架经验只要会基本的Java和SQL就能跟着把一套能经得起追问的系统搭出来。核心思路只有一句话先把数据库约束写死再让Java层做业务编排最后用事务兜底。2. 数据库先立住SQL-Server建表与约束的硬核细节2.1 为什么建议把业务规则下沉到数据库层很多人做课程设计时习惯把校验全写在Java里比如“库存大于0才能借”。但课程设计答辩时老师经常会问一句“如果我不走你的程序直接连数据库插一条借阅记录你的系统能拦住吗”这时候如果数据库没有任何约束你就只能尴尬地说“正常不会这么操作”。把关键规则下沉到SQL-Server层好处有三个第一任何入口的写入都会被约束拦截第二Java层的代码可以更专注于流程编排第三答辩时你可以直接演示“绕过程序直接改库会被拒绝”这是一个很加分的点。具体来说图书管理系统里适合放在数据库层的规则包括库存不能为负、同一用户对同一本书不能有两条未归还记录、借阅日期不能晚于应还日期、罚款金额不能为负。这些用CHECK约束和UNIQUE约束就能覆盖大部分场景。2.2 核心表结构与建表脚本下面这套表结构是我在多个课程设计中反复打磨过的版本字段不多但够用关键是约束写得比较完整。数据库名用LibraryDB排序规则选Chinese_PRC_CI_AS避免中文乱码。-- 创建数据库如果不存在 IF NOT EXISTS (SELECT name FROM sys.databases WHERE name NLibraryDB) BEGIN CREATE DATABASE LibraryDB COLLATE Chinese_PRC_CI_AS; END GO USE LibraryDB; GO -- 图书表库存字段带CHECK约束防止负数 CREATE TABLE Books ( BookID INT IDENTITY(1,1) PRIMARY KEY, ISBN VARCHAR(20) NOT NULL UNIQUE, Title NVARCHAR(100) NOT NULL, Author NVARCHAR(50) NOT NULL, Publisher NVARCHAR(80) NULL, TotalStock INT NOT NULL DEFAULT 0 CHECK (TotalStock 0), Available INT NOT NULL DEFAULT 0 CHECK (Available 0), -- 可用数量不能超过总库存 CONSTRAINT CK_Books_Available CHECK (Available TotalStock) ); GO -- 读者表学生和教师用Type区分 CREATE TABLE Readers ( ReaderID INT IDENTITY(1,1) PRIMARY KEY, CardNo VARCHAR(20) NOT NULL UNIQUE, Name NVARCHAR(30) NOT NULL, ReaderType TINYINT NOT NULL DEFAULT 1 CHECK (ReaderType IN (1,2)), -- 1学生 2教师 MaxBorrow INT NOT NULL DEFAULT 5, Dept NVARCHAR(50) NULL, Status TINYINT NOT NULL DEFAULT 1 CHECK (Status IN (0,1)) -- 1正常 0冻结 ); GO -- 借阅记录表这是业务核心约束最多 CREATE TABLE BorrowRecords ( RecordID INT IDENTITY(1,1) PRIMARY KEY, BookID INT NOT NULL, ReaderID INT NOT NULL, BorrowDate DATETIME NOT NULL DEFAULT GETDATE(), DueDate DATETIME NOT NULL, ReturnDate DATETIME NULL, Fine DECIMAL(8,2) NOT NULL DEFAULT 0 CHECK (Fine 0), Status TINYINT NOT NULL DEFAULT 1 CHECK (Status IN (1,2,3)), -- 1借出 2已还 3逾期未还 CONSTRAINT FK_Borrow_Book FOREIGN KEY (BookID) REFERENCES Books(BookID), CONSTRAINT FK_Borrow_Reader FOREIGN KEY (ReaderID) REFERENCES Readers(ReaderID), -- 应还日期必须晚于借出日期 CONSTRAINT CK_Borrow_Date CHECK (DueDate BorrowDate) ); GO -- 关键索引按读者查未还记录、按图书查借阅历史 CREATE INDEX IX_Borrow_Reader_Status ON BorrowRecords(ReaderID, Status); CREATE INDEX IX_Borrow_Book_Status ON BorrowRecords(BookID, Status); GO这段脚本里有几个点值得展开说。Available TotalStock这个约束看起来简单但它能防止“还书时把可用数量加超”这种低级错误。BorrowRecords表上的两个索引不是随便加的IX_Borrow_Reader_Status服务于“查某个读者当前借了几本书”IX_Borrow_Book_Status服务于“查某本书当前被借出几本”这两个查询在借书和还书流程里都会高频出现。2.3 用触发器还是用存储过程课程设计里的取舍课程设计里常见的做法是用触发器自动更新库存。比如在BorrowRecords上建一个AFTER INSERT触发器插入借阅记录后自动把Books.Available减一。这样做的好处是Java层代码简单坏处是调试困难一旦触发器逻辑写错问题会藏得很深。我的建议是课程设计阶段优先用存储过程显式控制库存变更触发器只用来做日志记录。原因很直接——存储过程的逻辑是可见的、可单步调试的答辩时你也能讲清楚每一步在做什么。触发器适合做“事后记录”比如每次借阅后往一张OperationLog表里插一条日志这个不影响主流程出问题也好排查。下面是一个借书存储过程的示例把“检查库存、检查读者状态、插入记录、扣减库存”四步放在一个事务里CREATE PROCEDURE sp_BorrowBook BookID INT, ReaderID INT, Days INT 30, Result INT OUTPUT, Message NVARCHAR(100) OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- 1. 检查读者状态 IF NOT EXISTS (SELECT 1 FROM Readers WHERE ReaderIDReaderID AND Status1) BEGIN SET Result -1; SET Message N读者不存在或已被冻结; ROLLBACK TRANSACTION; RETURN; END -- 2. 检查当前借阅数量是否超限 DECLARE CurrentBorrow INT, MaxBorrow INT; SELECT CurrentBorrow COUNT(*) FROM BorrowRecords WHERE ReaderIDReaderID AND Status IN (1,3); SELECT MaxBorrow MaxBorrow FROM Readers WHERE ReaderIDReaderID; IF CurrentBorrow MaxBorrow BEGIN SET Result -2; SET Message N已达到最大借阅数量; ROLLBACK TRANSACTION; RETURN; END -- 3. 检查库存用UPDLOCK防止并发超借 DECLARE Avail INT; SELECT Avail Available FROM Books WITH (UPDLOCK) WHERE BookIDBookID; IF Avail IS NULL OR Avail 0 BEGIN SET Result -3; SET Message N图书库存不足; ROLLBACK TRANSACTION; RETURN; END -- 4. 插入借阅记录并扣减库存 INSERT INTO BorrowRecords(BookID, ReaderID, BorrowDate, DueDate, Status) VALUES(BookID, ReaderID, GETDATE(), DATEADD(DAY, Days, GETDATE()), 1); UPDATE Books SET Available Available - 1 WHERE BookIDBookID; COMMIT TRANSACTION; SET Result 0; SET Message N借阅成功; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRANSACTION; SET Result -99; SET Message ERROR_MESSAGE(); END CATCH END GO这里最关键的一行是SELECT Avail Available FROM Books WITH (UPDLOCK)。UPDLOCK会在读取时对行加更新锁防止两个并发请求同时读到相同的库存值然后都执行扣减。没有这个锁两个用户同时借同一本只剩一本的书就会出现库存变成-1的情况。这个坑我在早期做课程设计时踩过答辩演示时刚好被老师撞见场面相当尴尬。3. Java后端落地JDBC连接、DAO分层与事务控制3.1 为什么课程设计用JDBC比用框架更稳很多教程一上来就推Spring Boot MyBatis但对于课程设计来说框架的配置成本和版本兼容问题往往比业务逻辑本身还耗时。我一般建议如果课程设计周期在两周以内用原生JDBC 手写DAO层。这样做的好处是每一行代码你都能讲清楚答辩时老师问“事务是怎么控制的”你可以直接指到Connection.setAutoCommit(false)那一行而不是含糊地说“框架帮我们管了”。当然JDBC的样板代码多所以需要做一层简单的封装。下面这个DBUtil类负责连接管理和资源释放是整个后端的地基。import java.sql.*; public class DBUtil { // SQL-Server默认端口1433数据库名LibraryDB private static final String URL jdbc:sqlserver://localhost:1433;databaseNameLibraryDB;encryptfalse; private static final String USER sa; private static final String PASSWORD YourStrongPassword; static { try { Class.forName(com.microsoft.sqlserver.jdbc.SQLServerDriver); } catch (ClassNotFoundException e) { throw new RuntimeException(SQL-Server驱动未找到, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 统一释放资源顺序不能错ResultSet - Statement - Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }连接字符串里的encryptfalse是为了避免本地开发时证书验证的麻烦。如果你用的SQL-Server版本较新默认会要求加密连接加上这个参数可以省去配置证书的步骤。生产环境当然不能这么写但课程设计阶段以能跑通为优先。3.2 借书业务的Java实现事务边界要画在哪里事务边界画在哪里是课程设计里最能体现水平的一个细节。常见错误是把setAutoCommit(false)放在DAO方法内部然后每个DAO方法各自提交。这样做的后果是借书流程里“插入记录”和“扣减库存”两个操作不在同一个事务里如果扣减库存失败借阅记录已经写进去了数据就脏了。正确的做法是在Service层开启事务把多个DAO操作包在同一个Connection里。下面这个BorrowService演示了完整的借书流程import java.sql.*; public class BorrowService { public String borrowBook(int bookId, int readerId, int days) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务边界在Service层 // 1. 检查读者状态和借阅上限 ReaderDAO readerDAO new ReaderDAO(); Reader reader readerDAO.findById(conn, readerId); if (reader null || reader.getStatus() ! 1) { conn.rollback(); return 读者不存在或已被冻结; } int current readerDAO.countActiveBorrows(conn, readerId); if (current reader.getMaxBorrow()) { conn.rollback(); return 已达到最大借阅数量; } // 2. 检查库存并锁定行 BookDAO bookDAO new BookDAO(); Book book bookDAO.findByIdForUpdate(conn, bookId); if (book null || book.getAvailable() 0) { conn.rollback(); return 图书库存不足; } // 3. 插入借阅记录 BorrowDAO borrowDAO new BorrowDAO(); borrowDAO.insert(conn, bookId, readerId, days); // 4. 扣减库存 bookDAO.decreaseAvailable(conn, bookId); conn.commit(); return 借阅成功; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} return 系统异常 e.getMessage(); } finally { DBUtil.close(conn, null, null); } } }这段代码里有三个关键点。第一conn.setAutoCommit(false)在Service层调用所有DAO方法接收同一个conn参数保证在同一个事务里。第二findByIdForUpdate方法内部执行的SQL带WITH (UPDLOCK)和前面存储过程里的锁策略一致。第三任何一步失败都调用rollback()并且返回具体的错误信息方便前端展示。对应的BookDAO.findByIdForUpdate实现如下public Book findByIdForUpdate(Connection conn, int bookId) throws SQLException { String sql SELECT BookID, Title, Available FROM Books WITH (UPDLOCK) WHERE BookID ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, bookId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Book b new Book(); b.setBookId(rs.getInt(BookID)); b.setTitle(rs.getString(Title)); b.setAvailable(rs.getInt(Available)); return b; } } } return null; }注意WITH (UPDLOCK)必须和事务配合使用如果autoCommit是true锁会在语句执行后立即释放起不到防并发的作用。这也是为什么我一直强调事务边界要放在Service层——锁的有效范围取决于事务的范围。3.3 还书与罚款计算日期处理里的隐藏坑还书流程比借书多了一个罚款计算。规则通常是逾期每天罚0.2元上限20元。看起来简单但日期处理有几个坑第一DueDate和ReturnDate都是DATETIME类型直接相减得到的是天数差但可能带小数第二如果当天还书不应该算逾期第三罚款金额要保留两位小数。下面是一个经过验证的罚款计算方法import java.sql.Timestamp; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; public class FineCalculator { private static final double FINE_PER_DAY 0.2; private static final double MAX_FINE 20.0; public static double calculate(Timestamp dueDate, Timestamp returnDate) { LocalDateTime due dueDate.toLocalDateTime(); LocalDateTime ret returnDate.toLocalDateTime(); // 按自然日计算当天还书不算逾期 long overdueDays ChronoUnit.DAYS.between(due.toLocalDate(), ret.toLocalDate()); if (overdueDays 0) { return 0.0; } double fine overdueDays * FINE_PER_DAY; fine Math.min(fine, MAX_FINE); // 保留两位小数避免浮点误差 return Math.round(fine * 100.0) / 100.0; } }这里用ChronoUnit.DAYS.between对LocalDate做计算而不是直接对LocalDateTime做减法目的是忽略具体时刻只按自然日算。比如应还日期是1月1日23:59实际还书是1月2日00:01按自然日算逾期1天这是合理的。如果用LocalDateTime直接减得到的是2分钟会被算成0天反而不符合业务预期。4. 并发借书与库存超卖三个必须验证的测试场景4.1 用JMeter或手写线程模拟并发借书课程设计答辩时如果老师问“你这个系统支持多人同时借书吗”光说“支持”是不够的最好能现场演示。我一般会准备一个简单的并发测试类用CountDownLatch模拟10个线程同时借同一本只剩3本库存的书预期结果是3个成功、7个失败。import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; public class ConcurrentBorrowTest { public static void main(String[] args) throws InterruptedException { int threadCount 10; CountDownLatch startGate new CountDownLatch(1); CountDownLatch endGate new CountDownLatch(threadCount); AtomicInteger success new AtomicInteger(0); AtomicInteger fail new AtomicInteger(0); BorrowService service new BorrowService(); for (int i 0; i threadCount; i) { new Thread(() - { try { startGate.await(); // 所有线程在此等待同时起跑 String result service.borrowBook(1, 1, 30); if (借阅成功.equals(result)) { success.incrementAndGet(); } else { fail.incrementAndGet(); } } catch (InterruptedException ignored) { } finally { endGate.countDown(); } }).start(); } startGate.countDown(); // 发令 endGate.await(); System.out.println(成功 success.get() 失败 fail.get()); } }测试前先把Books表里BookID1的Available改成3然后运行这个测试类。如果输出是“成功3失败7”说明锁策略生效了。如果成功数大于3说明UPDLOCK没起作用需要检查事务是否真的开启了。4.2 库存扣减为负的排查路径如果并发测试出现了库存为负的情况按以下顺序排查第一确认BookDAO.findByIdForUpdate里的SQL确实带了WITH (UPDLOCK)有时候复制粘贴会漏掉。第二确认Service层调用了conn.setAutoCommit(false)并且所有DAO方法用的是同一个conn对象。第三检查SQL-Server的隔离级别默认是READ COMMITTED配合UPDLOCK足够应对课程设计级别的并发。第四如果用了连接池确认连接池没有把autoCommit重置为true。提示SQL-Server的UPDLOCK在READ COMMITTED隔离级别下会持有锁直到事务结束这正是我们需要的行为。不要随意把隔离级别调到READ UNCOMMITTED那会让锁失效。4.3 还书时库存加超的另一种超卖除了借书时的超卖还书时也可能出问题如果同一个借阅记录被重复还书库存会被加两次。防止的方法是还书前先检查记录状态只有Status1或Status3的记录才能还书还书后把状态改成2。-- 还书存储过程的关键片段 UPDATE BorrowRecords SET ReturnDate GETDATE(), Status 2, Fine Fine WHERE RecordID RecordID AND Status IN (1,3); IF ROWCOUNT 0 BEGIN SET Result -1; SET Message N该记录已归还或不存在; ROLLBACK TRANSACTION; RETURN; END UPDATE Books SET Available Available 1 WHERE BookID BookID;WHERE子句里的Status IN (1,3)加上ROWCOUNT判断保证了只有第一次还书操作会生效。这个模式在课程设计里很实用答辩时也可以作为一个“防重复提交”的亮点来讲。5. 课程设计避坑从环境配置到答辩演示的五个血泪教训5.1 坑一SQL-Server驱动版本与JDK版本不匹配现象Java程序启动时报NoClassDefFoundError或ClassNotFoundException提示找不到com.microsoft.sqlserver.jdbc.SQLServerDriver。原因SQL-Server的JDBC驱动分多个版本mssql-jdbc-9.x需要JDK 8以上mssql-jdbc-12.x需要JDK 11以上。如果JDK是8却用了12.x的驱动就会报错。解决JDK 8用mssql-jdbc-9.4.1.jre8.jarJDK 11及以上用mssql-jdbc-12.4.2.jre11.jar。在IDEA里通过Project Structure的Libraries添加jar包不要只放在lib目录却不加入classpath。5.2 坑二中文乱码从数据库一路乱到前端现象图书标题里的中文在Java程序里显示正常但存进数据库变成问号或者从数据库读出来变成乱码。原因三个环节都可能出问题——数据库排序规则不是Chinese_PRC_CI_AS、JDBC连接字符串没指定字符集、Java源文件编码不是UTF-8。解决建库时指定COLLATE Chinese_PRC_CI_AS连接字符串加上;sendStringParametersAsUnicodetrueIDEA里把File Encoding全部设为UTF-8。如果已经建了库可以用ALTER DATABASE LibraryDB COLLATE Chinese_PRC_CI_AS修改但已有数据可能需要重新导入。5.3 坑三事务没回滚导致连接池耗尽现象程序运行一段时间后卡死所有数据库操作都超时重启后恢复但过一会又卡。原因某个异常分支里没有调用rollback()连接带着未提交的事务被归还到连接池下一个请求拿到这个连接后一直等锁。解决在catch块里无条件调用rollback()并且用try-finally保证连接一定被关闭。如果用了连接池在归还连接前检查autoCommit状态并重置为true。5.4 坑四答辩演示时数据被改乱现象演示借书功能时发现库存数量不对或者某个读者显示已借10本书但上限是5本。原因开发过程中手动改过数据库或者测试脚本没有清理数据。解决准备一个reset.sql脚本在答辩前执行一次把所有表清空并插入固定的演示数据。脚本里用TRUNCATE TABLE比DELETE更快但要注意外键约束先删子表再删主表。-- 答辩前重置演示数据 USE LibraryDB; GO DELETE FROM BorrowRecords; DELETE FROM Books; DELETE FROM Readers; DBCC CHECKIDENT (BorrowRecords, RESEED, 0); DBCC CHECKIDENT (Books, RESEED, 0); DBCC CHECKIDENT (Readers, RESEED, 0); INSERT INTO Readers(CardNo, Name, ReaderType, MaxBorrow, Dept, Status) VALUES (S001, N张三, 1, 5, N计算机系, 1), (S002, N李四, 1, 5, N计算机系, 1); INSERT INTO Books(ISBN, Title, Author, Publisher, TotalStock, Available) VALUES (978-7-111-11111-1, NJava编程思想, N某作者, N某出版社, 5, 5), (978-7-111-22222-2, N数据库系统概论, N某作者, N某出版社, 3, 3); GO5.5 坑五只测了正常流程没测边界条件现象答辩时老师让演示“借一本库存为0的书”程序直接抛异常或者插入了一条错误记录。原因开发时只测了库存充足的情况没有测库存为0、读者被冻结、借阅超限这些边界。解决在BorrowService里对每种失败情况返回不同的错误码和提示信息前端根据错误码展示对应的提示。测试时至少覆盖以下场景库存为0、读者不存在、读者被冻结、借阅已达上限、同一读者重复借同一本书如果业务不允许。6. 进阶技巧用视图和窗口函数做借阅统计报表课程设计如果只做到增删改查分数通常在中游。想往上走一步可以加一个“借阅统计”模块用SQL-Server的视图和窗口函数直接出报表Java层只负责展示。这样做的好处是统计逻辑集中在数据库层Java代码简洁而且答辩时能展示你对SQL的掌握程度。先建一个视图把借阅记录、图书信息、读者信息关联起来CREATE VIEW v_BorrowDetail AS SELECT br.RecordID, b.Title AS BookTitle, b.ISBN, r.Name AS ReaderName, r.CardNo, br.BorrowDate, br.DueDate, br.ReturnDate, br.Status, br.Fine, CASE WHEN br.Status 2 AND br.ReturnDate br.DueDate THEN N逾期归还 WHEN br.Status 2 THEN N正常归还 WHEN br.Status IN (1,3) AND GETDATE() br.DueDate THEN N已逾期 ELSE N借阅中 END AS BorrowState FROM BorrowRecords br JOIN Books b ON br.BookID b.BookID JOIN Readers r ON br.ReaderID r.ReaderID; GO然后写一个统计查询用窗口函数算出每本书的借阅次数排名以及每个读者的借阅总量-- 借阅热度排名每本书被借次数及排名 SELECT BookTitle, COUNT(*) AS BorrowCount, RANK() OVER (ORDER BY COUNT(*) DESC) AS RankNo FROM v_BorrowDetail GROUP BY BookTitle; -- 读者借阅统计包含当前未还数量和累计罚款 SELECT ReaderName, CardNo, COUNT(*) AS TotalBorrows, SUM(CASE WHEN Status IN (1,3) THEN 1 ELSE 0 END) AS CurrentBorrows, SUM(Fine) AS TotalFine FROM v_BorrowDetail GROUP BY ReaderName, CardNo ORDER BY TotalBorrows DESC;这两个查询可以直接在SQL-Server Management Studio里执行也可以封装成DAO方法供Java调用。RANK()函数在SQL-Server 2005以上都支持课程设计环境一般不会有兼容问题。如果你想让报表更直观可以在Java层用JTable或简单的HTML表格展示不需要引入图表库。还有一个实用技巧把逾期未还的读者自动冻结。可以写一个定时任务或者手动执行的存储过程每天检查一次v_BorrowDetail里BorrowState已逾期且逾期超过30天的记录把对应读者的Status改成0。这样答辩时你可以演示“逾期冻结”功能比单纯说“支持逾期罚款”更有说服力。CREATE PROCEDURE sp_FreezeOverdueReaders AS BEGIN UPDATE Readers SET Status 0 WHERE ReaderID IN ( SELECT DISTINCT ReaderID FROM BorrowRecords WHERE Status IN (1,3) AND DATEDIFF(DAY, DueDate, GETDATE()) 30 ); END GO这个存储过程可以手动执行也可以在Java里用ScheduledExecutorService每天调一次。课程设计里手动执行就够了但你可以把定时调用的代码也写上作为扩展点讲。最后说一个我自己的习惯每次改完数据库脚本一定先在SSMS里单独跑一遍确认没有语法错误再集成到Java里。早期我图省事直接在Java里改SQL字符串结果一个拼写错误排查了半小时。后来养成“SQL先在SSMS验证再复制到Java”的习惯效率反而高了很多。希望帮到你。本文还有配套的精品资源点击获取