
不搞花架子直接说项目本身。图书管理系统是Java入门到进阶阶段出现频率最高的练手项目同时也是很多计算机专业课程设计和毕业设计的首选题目。这个项目题目里有两个关键词一是完整代码实现二是连接MYSQL数据库。前者说明需要的是能直接运行、能看逻辑、能复现的整套代码而不是零散的功能片段后者说明整个系统的数据落点都在MySQL里所有增删改查都要经过真实的数据库操作。这篇文章就围绕这两件事展开把表结构设计、JDBC连接、核心业务逻辑、常见坑位全部过一遍适合正在做课设、准备Java面试或者想搞清楚数据库连接细节的同学参考。1. 项目整体设计与需求拆解1.1 这个系统到底在解决什么问题图书馆的日常运营非常依赖借还书登记、图书库存盘点、读者借阅记录查询这些操作。传统手写登记的方式效率低、容易出错管理员想查一本书在哪、谁借走了、什么时候该还都极其痛苦。图书管理系统做的事情本质上就是为管理员提供一套数字化工具让图书信息和借阅记录全部落库通过程序完成信息的增删改查。从用户视角来看最基础的功能需求包括几个方面管理员登录系统身份校验要可靠。图书信息的录入、修改、删除和按条件查询。借书操作要能校验库存和借阅状态。还书操作要能更新库存和借阅记录。借阅记录的查询方便追踪每一本书的去向。标题里强调“连接MYSQL数据库”意味着这些数据不能优雅地躺在内存里——程序一关就全没了而是要永久的、结构化的存在MySQL里。这样才能保证重启程序后图书数据还在借书记录也在系统才有真正的使用价值。1.2 技术选型为什么走JDBC直连而不是直接上框架很多人会问现在都用Spring Boot MyBatis为什么还要用纯JDBC手写连接数据库我用这个项目回答你因为跳过了JDBC直接学框架你根本不知道数据库连接背后发生了什么。JDBC是Java连接数据库的标准接口MyBatis底层也是封装了JDBC。你把JDBC这套流程——加载驱动、获取连接、创建语句、执行SQL、处理结果集、释放资源——走一遍框架一上手就能看懂它到底在帮你做什么。这个项目我建议采用“纯Java控制台 JDBC MySQL”的经典组合而不是上来就套JSP、Servlet或者Spring Boot。原因有三控制台交互让注意力集中在SQL语句和业务逻辑上不需要分心处理页面跳转、表单提交这些Web层的东西。JDBC连接数据库的过程清晰暴露连接串怎么配、驱动怎么加载、PreparedStatement怎么用全部看得见摸得着。代码量适中适合一个人从零实现还能在面试时把项目讲清楚。这套组合做出来的代码结构后续想迁移到Spring Boot版本时DAO层和实体类几乎可以原封不动搬过去改动成本很低。1.3 数据库表结构设计三张表撑起整个系统数据库设计是这个项目的根基。我见过不少同学代码写了一半发现表结构不合理回头改表又改代码非常痛苦。设计阶段多花十分钟后面能省下半天时间。整个系统用三张表就够用户表usersid主键自增用户唯一标识。username用户名登录时使用。password密码落库时存MD5散列值不存明文。role角色标识管理员和普通读者的权限区分。real_name真实姓名用于显示借阅人信息。图书表booksid主键自增图书ID。book_name书名。author作者。publisher出版社。price价格用DECIMAL(10,2)类型避免浮点误差。stock库存数量整数类型。category分类方便按类别筛选。借阅记录表borrow_recordsid主键自增。book_id关联图书表主键。user_id关联用户表主键。borrow_date借书日期。return_date应还日期。status借阅状态1表示借出中2表示已归还。外键关系上borrow_records表的book_id和user_id分别指向books和users的主键。从表设计上保证数据可追溯。图书库存的扣减在借书时同步更新还书时同步恢复这个业务逻辑放在后面讲。2. 核心代码实现与关键细节2.1 数据库连接工具类JDBC连接的全过程不管写什么功能模块第一步永远是拿到数据库连接。把所有连接逻辑抽到一个工具类里是项目初期就该做好的事。来找JDBC连接的经典五步加载驱动类。定义数据库连接地址URL。DriverManager获取Connection对象。通过Connection创建Statement或者PreparedStatement。释放资源。工具类代码我直接给出一个项目里实测可用的版本import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { // 数据库连接地址 private static final String URL jdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USERNAME root; private static final String PASSWORD 123456; private static Connection conn null; // 静态代码块只加载一次驱动 static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } // 获取连接 public static Connection getConnection() throws SQLException { if (conn null || conn.isClosed()) { conn DriverManager.getConnection(URL, USERNAME, PASSWORD); } return conn; } // 释放资源关闭ResultSet、Statement、Connection public static void close(ResultSet rs, Statement stmt, Connection conn) { try { if (rs ! null) rs.close(); if (stmt ! null) stmt.close(); if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这里有几个细节值得说透驱动类名。MySQL 5.x之前用的是com.mysql.jdbc.DriverMySQL 8.x后驱动类改成了com.mysql.cj.jdbc.Driver。用错会直接报ClassNotFoundException这是高频报错点。连接串参数。serverTimezoneAsia/Shanghai解决的是时区问题MySQL 8及以上版本服务器跟本机时区不一致会报错。characterEncodingutf8解决中文乱码。useSSLfalse是关闭SSL校验本地开发不需要加密通道不关可能会报SSL连接错误。静态代码块 vs 每次获取连接时加载驱动。Class.forName加载驱动只需要做一次放进static代码块里是标准写法不用每次getConnection都重复加载。驱动注册到DriverManager之后就常驻内存了。2.2 用户登录模块密码校验与SQL注入防护登录功能几乎每个系统都有但我要在这单独拉出来讲是因为登录模块最容易暴露安全意识问题。我见过不少项目源码里用字符串拼接方式执行SQL比如String sql SELECT * FROM users WHERE username username AND password password ;这种写法非常危险。用户在用户名输入框输入admin OR 11SQL语句会变成SELECT * FROM users WHERE username admin OR 11 AND password OR条件让整条WHERE语句恒为真直接绕过密码校验登录成功。这就是最经典的SQL注入攻击几乎所有数据库安全类面试题都会问它。正确的做法是用PreparedStatement预编译public User login(String username, String password) { String sql SELECT id, username, password, role, real_name FROM users WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, md5(password)); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setRole(rs.getString(role)); user.setRealName(rs.getString(real_name)); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; }注意两个细节。第一PreparedStatement的参数用?占位setString传值时由MySQL驱动做参数转义用户输入的单引号会被当作普通字符处理注入语句就失效了。第二密码存的是MD5散列值md5方法如下private static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(MD5加密出错, e); } }密码散列化之后的好处是即使数据库文件泄露别人看到的也是一串固定长度的32位十六进制字符拿不到明文密码。这让登录模块多了一层安全保障。2.3 图书管理核心业务借书流程和事务边界图书管理系统的核心业务流是借书和还书。借书不是简单插入一条记录就完事它涉及多张表的数据联动。以借书为例完整流程是校验用户存在且角色为读者。校验图书存在且库存大于0。扣减图书表的库存。插入一条借阅记录。这四个步骤里任何一步失败整个操作都必须回滚。比如库存扣减成功了但借阅记录插入失败就会出现库存跟实际不相符的情况——书没借出去库存没了。这是典型的事务场景。public synchronized boolean borrowBook(int bookId, int userId) { Connection conn null; PreparedStatement psUpdateStock null; PreparedStatement psInsertRecord null; try { conn DBUtil.getConnection(); // 关闭自动提交开启事务 conn.setAutoCommit(false); // 查询库存并锁定记录 String checkSql SELECT stock FROM books WHERE id ? FOR UPDATE; try (PreparedStatement psCheck conn.prepareStatement(checkSql)) { psCheck.setInt(1, bookId); try (ResultSet rs psCheck.executeQuery()) { if (rs.next()) { int stock rs.getInt(stock); if (stock 0) { throw new RuntimeException(库存不足借阅失败); } } else { throw new RuntimeException(图书不存在); } } } // 扣减库存 String updateSql UPDATE books SET stock stock - 1 WHERE id ?; psUpdateStock conn.prepareStatement(updateSql); psUpdateStock.setInt(1, bookId); psUpdateStock.executeUpdate(); // 插入借阅记录 String insertSql INSERT INTO borrow_records(book_id, user_id, borrow_date, return_date, status) VALUES(?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 1); psInsertRecord conn.prepareStatement(insertSql); psInsertRecord.setInt(1, bookId); psInsertRecord.setInt(2, userId); psInsertRecord.executeUpdate(); // 提交事务 conn.commit(); return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException ex) { ex.printStackTrace(); } } }这段代码有几个非常关键的细节。conn.setAutoCommit(false)关闭自动提交模式后后续的SQL不会立刻生效要等commit才真正写入数据库。rollback是回滚让之前执行的SQL全部作废。关于行锁SELECT ... FOR UPDATE它把查询到的图书记录锁住避免多线程并发借书时大家都读到stock1然后同时扣减变成负数。我自己实际测试过去掉FOR UPDATE的情况下用两个线程同时借同一本书且库存只剩1本两个请求都可能成功最终库存变成-1。加了行锁之后第二个请求会阻塞等待第一个事务提交提交后重新读取库存发现已经是0直接抛异常这才是正确行为。2.4 还书流程别忽略状态校验还书逻辑比借书简单但有一个很容易踩的坑不校验借阅记录的状态就直接更新。public boolean returnBook(int recordId) { String sql UPDATE borrow_records SET status 2 WHERE id ? AND status 1; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, recordId); int rows ps.executeUpdate(); if (rows 0) { // 恢复库存 String updateStockSql UPDATE books b JOIN borrow_records r ON b.id r.book_id SET b.stock b.stock 1 WHERE r.id ?; try (PreparedStatement ps2 conn.prepareStatement(updateStockSql)) { ps2.setInt(1, recordId); ps2.executeUpdate(); } return true; } } catch (SQLException e) { e.printStackTrace(); } return false; }SQL里带AND status 1确保只有借出中的记录才能执行归还。如果记录已经是已归还状态UPDATE影响行数为0不会出现重复恢复库存的情况。3. 完整实操从建库建表到跑通项目3.1 环境准备MySQL 8 IDEA 驱动动手之前先把环境准备好。我以本机开发为例说明每一步需要做什么。第一步安装MySQL 8。Windows环境下直接去MySQL官网下载MySQL Installer只安装Server和Command Line Client。安装过程中的用户名密码记好默认root用户需要设置一个自己的密码。安装完成后在命令行用mysql -u root -p验证能登录。第二步安装Navicat或者直接用MySQL自带的Workbench。Navicat胜在操作直观建库建表看数据都方便有条件的话可以用它快速核对数据是否写入成功。第三步IDEA创建Java项目引入连接驱动。两种方式任选下载mysql-connector-java的jar包放在项目lib目录下右键选择Add as Library。这个方式不需要额外的构建工具。Maven项目在pom.xml里添加依赖dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency我用的是MySQL 8.0系列所以驱动版本选8.0.33跟服务器兼容很稳。如果你的MySQL是5.7及以下版本驱动选5.1.x系列更省事。3.2 建库建表与初始化数据打开Navicat新建数据库library_db字符集选utf8mb4。用以下几个建表语句直接执行CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(32) NOT NULL, role VARCHAR(10) NOT NULL DEFAULT user, real_name VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE books ( id INT AUTO_INCREMENT PRIMARY KEY, book_name VARCHAR(100) NOT NULL, author VARCHAR(50) DEFAULT NULL, publisher VARCHAR(100) DEFAULT NULL, price DECIMAL(10,2) DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, category VARCHAR(50) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_records ( id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, user_id INT NOT NULL, borrow_date DATETIME NOT NULL, return_date DATETIME DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1, CONSTRAINT fk_book FOREIGN KEY (book_id) REFERENCES books(id), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;初始化数据也一并写入方便程序启动后有东西可操作INSERT INTO users(id, username, password, role, real_name) VALUES (NULL, admin, MD5(123456), admin, 系统管理员), (NULL, zhangsan, MD5(abc123), user, 张三); INSERT INTO books(book_name, author, publisher, price, stock, category) VALUES (Java核心技术卷I, 凯·S·霍斯特曼, 机械工业出版社, 119.00, 5, 计算机), (深入理解Java虚拟机, 周志明, 机械工业出版社, 129.00, 3, 计算机), (MySQL必知必会, Ben Forta, 人民邮电出版社, 59.00, 8, 数据库), (活着, 余华, 作家出版社, 35.00, 10, 文学);有一点需要提前说清楚MD5(123456)这个函数在MySQL 8中默认是可用的生成的密文存进去之后Java代码里用md5方法对用户输入的密码加密再比对两边算法一致才能匹配。我自己踩过初始化数据里直接写明文密码的坑。初期偷懒把password字段存成123456登录验证时也拿明文去比较数据库里全是裸奔的用户密码。后来项目做完统一改用MD5才把这个问题处理干净。建议从项目第一版开始就养成不落明文密码的习惯。3.3 项目结构搭建分包划分与实体类设计代码不是堆在一个类里就能叫完整项目。合理的包结构让代码后期可维护也更容易讲明白系统架构。我建议用以下分包方式com.library ├── entity/ # 实体类 │ ├── User.java │ ├── Book.java │ └── BorrowRecord.java ├── util/ # 工具类 │ └── DBUtil.java ├── dao/ # 数据访问层 │ ├── UserDAO.java │ ├── BookDAO.java │ └── BorrowDAO.java ├── service/ # 业务逻辑层 │ ├── UserService.java │ ├── BookService.java │ └── BorrowService.java └── ui/ # 界面展示与交互 └── MainMenu.java分层的好处是界面层只负责接收输入和展示结果业务层处理业务规则DAO层只能跟数据库打交道。举个实际场景界面层需要显示“借书成功”但它不应该直接写SQL而是调用service层的方法service内部组织多个DAO方法协同完成借书流程。这样改动界面不影响数据库代码替换数据库连接方式也不影响界面。实体类以Book为例public class Book { private int id; private String bookName; private String author; private String publisher; private BigDecimal price; private int stock; private String category; // 省略getter和setter方法 }价格字段用BigDecimal而不是doubledouble在数据库和Java之间来回转换的时候浮点误差会让人抓狂。数据库DECIMAL对应Java的BigDecimal这是标准对应关系。3.4 图书列表查询分页与排序实操图书列表的展示不只是一条SELECT那么简单。系统里的图书数量一旦上到几十本控制台一屏打不完前端展示的体验就很差。分页是必须有的能力。public ListBook queryBooksByPage(int pageNum, int pageSize, String keyword) { ListBook bookList new ArrayList(); int offset (pageNum - 1) * pageSize; String sql SELECT id, book_name, author, publisher, price, stock, category FROM books; // 关键字搜索增强体验 if (keyword ! null !keyword.isEmpty()) { sql WHERE book_name LIKE ?; } sql ORDER BY id LIMIT ? OFFSET ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { int paramIndex 1; if (keyword ! null !keyword.isEmpty()) { ps.setString(paramIndex, % keyword %); } ps.setInt(paramIndex, pageSize); ps.setInt(paramIndex, offset); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setBookName(rs.getString(book_name)); book.setAuthor(rs.getString(author)); book.setPublisher(rs.getString(publisher)); book.setPrice(rs.getBigDecimal(price)); book.setStock(rs.getInt(stock)); book.setCategory(rs.getString(category)); bookList.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return bookList; }ORDER BY id LIMIT ? OFFSET ?是MySQL分页的标准姿势。offset的算法是(pageNum - 1) * pageSize第一页偏移0条第二页偏移pageSize条很容易理解。LIKE关键字搜索时预编译的?参数值直接拼上%即可搜书名、作者、分类都通用。关于LIKE查询有个性能细节如果数据量大LIKE %关键字%这种前置模糊匹配是走不了索引的全表扫描。但图书管理系统这种量级的数据完全够用不需要过度优化。如果想优化可以引入Elasticsearch或者MySQL全文索引那是后话。排序方面同样用ORDER BY实现热门借阅排名可以这样查String sql SELECT b.id, b.book_name, COUNT(r.id) AS borrow_count FROM books b LEFT JOIN borrow_records r ON b.id r.book_id GROUP BY b.id, b.book_name ORDER BY borrow_count DESC;LEFT JOIN保证一本都没被借过的图书也能出现在列表里借书次数为0。COUNT(r.id)统计每本书的借阅记录条数按降序排列得到热门图书排行。这个SQL在面试里也经常被问到JOIN和GROUP BY的组合使用。4. 常见问题与排查技巧实录4.1 数据库连接失败驱动、时区、权限三类大坑连接数据库是整套系统最容易出问题的地方而问题基本集中在三个方向第一类是ClassNotFoundException报错内容是ClassNotFoundException: com.mysql.jdbc.Driver。这个100%是驱动类名或者驱动jar包的问题。MySQL 8.x的驱动类名是com.mysql.cj.jdbc.Driver你还在用com.mysql.jdbc.Driver就会报这个错。另外确认引用的jar包版本高版本驱动确认类名写对了没。第二类是时区错误报错一般是The server time zone value йʱ is unrecognized。原因是MySQL 8服务器默认使用系统时区而JDBC连接串没有指定时区两端对不上。解决方式是在URL追加serverTimezoneAsia/Shanghai。第三类是Public Key Retrieval报错MySQL 8的默认认证方式是caching_sha2_password客户端连接时需要从服务端获取公钥做加密JDBC默认行为不允许这个操作。URL里加useSSLfalseallowPublicKeyRetrievaltrue即可解决。我自己在项目联调阶段把这三类错误轮着遇了一遍总结成一条经验连接串务必一次性写完整不要遇到一个错改一处太折腾。4.2 中文乱码三层设置缺一不可中文乱码问题出现得相当频繁而且一乱就是一大片。排查要按三层来检查第一层数据库。建库和建表时指定CHARSETutf8mb4就是告诉MySQL这个数据库要存unicode字符中文完全没问题。忘记指定的可以用ALTER TABLE命令补ALTER TABLE books CONVERT TO CHARACTER SET utf8mb4;第二层JDBC连接串。URL必须带characterEncodingutf8注意这里跟数据库里character_set_database的配置匹配起来。第三层控制台输出。Windows下IDEA的控制台默认编码可能不是UTF-8在IDEA的Settings里把Global Encoding、Project Encoding、Console的编码都设为UTF-8。命令行运行时在编译和运行命令加上-Dfile.encodingUTF-8参数。这三个地方只要有一个不对柜台就会显示一排烦人的问号。连续探查这个问题的经验教训是先确认数据库存进去的中文是不是正常的用Navicat直接看表。库里是乱的就是数据库建表和连接串的问题库是正常的但程序显示乱就是控制台和JVM编码的问题。4.3 业务逻辑Bug排查事务没提交、库存负数、并发冲突数据库连接层打通之后业务层的坑就轮到在运行时慢慢暴露了。我整理一下最常遇到的几个不执行commit就断言数据写进去了。忘了setAutoCommit(false)之后一定要手动commit或者写代码的时候根本没意识到事务已经开启数据在另一个连接里死活看不到更新。排查方式确认是否有事务开启查到commit或rollback了吗。笔记本还剩0本借书还能成功。这是库存校验缺失导致的。解决方式在前面borrowBook方法里讲过校验和扣减必须在同一事务里用行锁保护绝不能在SELECT和UPDATE之间留出空档。对了损坏数据还可能来自重复借同一本书。如果借阅表里已经有一条status1的同图书同用户记录新借书请求不应该再成功。在插入新借阅记录前查询该用户该图书是否存在借出中的记录即可。4.4 资源泄漏连接不关系统迟早卡到死这个问题虽然常见但在新手代码里几乎是批量出现。写完Connection不去close获取了ResultSet不去close程序跑不了几个来回就报Too many connections。MySQL默认连接上限是100多个连接不释放就会把连接池占满。我建议把所有资源关闭统一放到finally代码块里或者直接使用JDK 7引入的try-with-resources语法。try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ... } catch (SQLException e) { e.printStackTrace(); }用了try-with-resources之后Connection、Statement、ResultSet只要实现了AutoCloseable接口就会在try块结束后自动关闭代码少而且不容易遗漏。如果用的是我前面写的DBUtilclose方法也要在finally里确保调用。真实项目中连接不关闭属于那种“今天不出事、明天不出事、上线第三天就出事”的问题。你测试的时候数据量小加上连接偶尔被GC回收感受不到严重性。等系统真的被多个人同时用起来连接数飙升服务直接就瘫了。5. 项目扩展方向与面试讲解建议这个项目做完能跑通只是第一步。真正让项目有价值的是你能够扩展它并且在面试时把它讲成亮点。我给四个扩展方向的思路。第一个方向是加上图形界面。控制台版本的逻辑代码是完整的只是交互方式比较简陋。可以用Java Swing或JavaFX做界面把MainMenu替换成登录窗口和主窗口BookDAO、UserDAO这些类不需要大改。这样就变成了一个更像“系统”的东西。第二个方向是升级Web版本。把项目改造成Spring Boot MyBatis Vue的前后端分离版本数据库表结构几乎原封不动DAO层代码翻译成MyBatis的Mapper接口。这个升级过程你自然就理解了框架到底帮你做了什么。第三个方向是补充还书逾期和罚款逻辑。在BorrowRecord里加一个shouldReturnDate的字段还书时用当前日期跟应还日期比较超期天数乘以每日罚款金额算出一个逾期费用记录这样业务面就比普通的课设项目完整很多。第四个方向是权限体系的强化。读者只能看到自己的借阅记录管理员可以看全部借书也有容量限制这些都用role字段和SQL条件就能实现。给面试讲项目的时候不要停留在“我写了一个图书增删改查的系统”这种层面。重点讲三件事数据库表怎么设计的表间关系是什么JDBC连接MySQL踩过哪些坑怎么定位和解决的借书的库存扣减和借阅记录的写入为什么放在一个事务里。把这些问题想透了比堆砌功能列表管用得多。我个人在带项目时最常跟新手说的一句话是能跑通只是代码的及格线知道自己为什么能跑通才算是学会。这个项目做完你至少应该能回答为什么用PreparedStatement而不是Statement为什么借书要用事务为什么库存要加行锁为什么密码要加密存储。能在纸上画出三张表的关系能说出连接MySQL的URL各个参数的含义。这些才是这个项目真正的收获。最后再分享一个我在调试阶段的小习惯把SQL语句先在Navicat里跑一遍确认结果集返回正确了再回到Java代码里联调。数据库查询这层走通了代码层的排查范围瞬间缩小问题定位效率非常高。这个习惯我用在所有的数据库项目里实测下来很稳。