ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java图书管理系统实战:完整MySQL数据库脚本与JDBC事务设计

Java图书管理系统实战:完整MySQL数据库脚本与JDBC事务设计 简介基于Java与MySQL开发的完整图书管理系统资源包面向Java初学者、课程设计及毕业设计人群。系统按角色划分权限普通读者可查询图书与个人借阅情况管理员可完成用户管理、新书入库等操作主体包含系统管理、进书管理、图书入库管理和查询四大模块覆盖馆藏登记、编目、检索、借阅等日常核心流程。包内共270个文件压缩后约1.59MB。含46个Java源文件与200个class文件既可直接运行项目也可导入开发环境进行二次修改另有library.sql完整数据库脚本、工程配置文件及若干界面图片方便快速搭建调试。资源附有管理员登录账号和数据库连接配置说明降低上手门槛。目前已有6306人学习下载适合需要一套能跑、能改、能演示的图书管理系统参考答案。通过它可掌握用户权限控制、图书信息登记与编目、多条件查询等典型功能的实现思路并能在其基础上扩展预约、续借、统计等功能。1. 一个 Java 图书管理系统最值钱的其实是那份“完整数据库脚本”很多人看到“java实现的完整图书管理系统mysql完整数据库脚本”这个标题第一反应是课程设计第二反应是“又是 CRUD 练手项目”。但你要是真去做一遍就会发现借书、还书、图书检索这些功能本身并不难难的是把 MySQL 表结构设计得能撑住业务、把数据库脚本写得能一次跑通、把 Java 和 SQL 之间的数据类型与事务边界理顺。我在带新人时经常拿它当第一份实战任务一个人从空库开始建表、连库、写借还逻辑跑完基本就能覆盖后端开发日常最常用的那组技能。适合谁想补 Java 后端基本功的新人、需要做课程设计的学生、以及准备面试前想复习 JDBC 事务和索引的工程师。2. 先把“完整数据库脚本”做扎实6 张核心表加一套可重复执行的 SQL2.1 先从实体关系说起为什么图书管理系统至少需要 6 张表图书管理系统的业务模型并不复杂但很多人直接从一张 books 表开干结果做到借阅记录时被迫返工。先想清楚实体关系管理员需要登录读者要办借阅证图书要有分类一次借阅行为要记录“谁、哪本书、什么时候借、什么时候还、是否续借”。这里最核心的关系是读者与图书之间的多对多一个读者可以借多本书一本书在不同时间可以被多个读者借出所以必须拆出一张独立的借阅记录表用来承接这层关系。我一般会把表拆成这样管理员表、图书分类表、图书表、读者表、借阅记录表再加上一个可选的逾期费用表或操作日志表。标题里说的“完整数据库脚本”指的不仅是创建这几张表还应该包含外键约束、索引、初始管理员账号、测试图书数据和部分样例借阅记录。否则一套空表业务跑不起来测验也不方便。建表顺序有讲究必须先建父表再建子表。比如图书表依赖分类表借阅记录表依赖图书表和读者表。如果你把脚本一股脑写在一个文件里MySQL 执行到外键时会检查引用的表是否存在顺序错了直接报Cannot add foreign key constraint。为了避免这种问题我会在文件里把依赖关系从前往后排好并且把建库语句放在最前面。2.2 一份能直接落地的建库建表脚本字段、索引与外键一次到位下面这份 SQL 是一个最小但完整的数据库脚本骨架。你可以直接复制到本机 MySQL 执行也可以在此基础上扩充。我用的是 MySQL 5.7 和 8.0 都兼容的写法。-- 建库用 utf8mb4 而不是 utf8否则生僻字和 emoji 会丢 CREATE DATABASE IF NOT EXISTS library_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE library_system; -- 管理员表 CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(32) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 建议存 MD5 或加盐后的密文, real_name VARCHAR(32) DEFAULT NULL COMMENT 真实姓名, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员表; -- 图书分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 分类ID, name VARCHAR(50) NOT NULL UNIQUE COMMENT 分类名, sort_order INT DEFAULT 0 COMMENT 排序越小越靠前 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书分类; -- 图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 图书ID, category_id INT NOT NULL COMMENT 所属分类, isbn VARCHAR(20) DEFAULT NULL COMMENT ISBN 编码, title VARCHAR(128) NOT NULL COMMENT 书名, author VARCHAR(64) DEFAULT NULL COMMENT 作者, publisher VARCHAR(128) DEFAULT NULL COMMENT 出版社, total_stock INT NOT NULL DEFAULT 0 COMMENT 总库存, available_stock INT NOT NULL DEFAULT 0 COMMENT 可借库存, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_title (title), CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; -- 读者表 CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 读者ID, card_no VARCHAR(32) NOT NULL UNIQUE COMMENT 借阅证号, name VARCHAR(64) NOT NULL COMMENT 读者姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, status TINYINT DEFAULT 1 COMMENT 1正常 0挂失, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者表; -- 借阅记录表承接读者和图书的多对多关系 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 记录ID, book_id INT NOT NULL COMMENT 图书ID, reader_id INT NOT NULL COMMENT 读者ID, borrow_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 借出时间, due_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 应还时间, return_time DATETIME DEFAULT NULL COMMENT 实际归还时间, renew_count TINYINT DEFAULT 0 COMMENT 续借次数, fine_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT 罚款金额, status TINYINT DEFAULT 0 COMMENT 状态0借出 1已还 2逾期, INDEX idx_reader (reader_id), INDEX idx_book (book_id), INDEX idx_status (status), CONSTRAINT fk_br_book FOREIGN KEY (book_id) REFERENCES book(id), CONSTRAINT fk_br_reader FOREIGN KEY (reader_id) REFERENCES reader(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表; -- 初始数据密码为 123456 的 MD5 值仅用于演示 INSERT INTO admin(username, password, real_name) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 系统管理员); INSERT INTO category(name, sort_order) VALUES (文学, 1), (计算机, 2), (历史, 3); INSERT INTO book(category_id, isbn, title, author, publisher, total_stock, available_stock) VALUES (1, 9787020002207, 红楼梦, 曹雪芹, 人民文学出版社, 5, 5), (2, 9787111213826, Java编程思想, Bruce Eckel, 机械工业出版社, 3, 3), (3, 9787101000124, 史记, 司马迁, 中华书局, 2, 2);这段脚本的逻辑很直白建库、指定字符集、建表、加索引和外键、灌初始数据。几个关键参数说明一下。ENGINEInnoDB必须写因为 InnoDB 才支持外键和事务MyISAM 不支持后期做借书还书时会非常被动。utf8mb4_general_ci是排序规则检索时不区分大小写适合中文场景。外键命名前缀fk_便于后面删除和排查索引命名前缀idx_是为了在有多个索引时能一眼看出来是哪个字段。你可能会问available_stock为什么不加CHECK (available_stock 0)。因为在 MySQL 5.7 里 CHECK 约束会被解析但不会真正生效8.0.16 之后才严格执行。所以我在后面的 Java 代码里用条件更新来兜底这是更可靠的做法。2.3 外键、索引与字符集脚本里最容易翻车的三个参数先说外键。外键必须要求字段类型完全一致比如book.category_id是 INT父表category.id也必须是 INT不能是 BIGINT 或 INT UNSIGNED否则导入脚本时 MySQL 会报一个让你摸不着头脑的Cannot add foreign key constraint。另外外键列最好都建索引MySQL 在创建外键时会自动为父表列建索引但子表列不一定会所以我在脚本里手动加了idx_category、idx_reader、idx_book。再说字符集。很多老旧脚本还在用utf8但 MySQL 的utf8实际最多只支持 3 字节遇到“”这类生僻字或 emoji 就会报错。现在的共识是直接上utf8mb4字符集和排序规则在建库时定好建表时也顺手带上DEFAULT CHARSETutf8mb4。这样能避免一大半的中文乱码问题。最后是导入方式。这份脚本要整体执行不是复制一句话跑一句话。如果你在命令行手工复制粘贴大段 SQL一旦中间有语法错误MySQL 客户端会中断你也不知道哪些表建好了、哪些没建好。所以要么用source命令导入整个文件要么用数据库工具一次性执行整个脚本。2.4 导入脚本的两条路径命令行和可视化工具至少要会一条在 Windows 或 Linux 上最直接的导入方式是利用source命令mysql -uroot -p -e source /path/to/library_schema.sql如果你已经建好库也可以用重定向方式mysql -uroot -p library_system library_schema.sql这两条命令有一个区别source是 MySQL 客户端的命令它会逐条读取文件里的 SQL 并执行报错时你能看到具体在哪一行重定向方式是 shell 把文件内容喂给 mysql 客户端遇到错误时也能看到 SQL 错误编号但排查行号不那么直观。新手我更推荐source。如果你用的是 IDEA 自带的 Database 工具或者 Navicat导入逻辑一样先创建连接选中library_system库再执行整个脚本文件。唯一要注意的是可视化工具里“脚本是否包含建库语句”决定了你怎么导入。如果脚本里有CREATE DATABASE你直接对着连接的根节点执行即可如果没有你就必须先手动建好库再在目标库上跑表结构。标题里强调“完整数据库脚本”我个人的标准是脚本里必须带上建库语句和初始数据这样换个新环境删库重建一条命令全搞定这才是真正的“后悔药”。导入完成后用一条 SQL 验证一下表是否齐全SHOW TABLES;正常会看到admin、category、book、reader、borrow_record这五张表。如果表都在初始数据也插进去了数据库这一半就算完成了。3. Java 端借书还书事务链路从 JDBC 连接到 Service 层事务边界3.1 选型先说清楚Servlet JSP JDBC还是纯 JDBC 控制台标题没写用的什么前端框架这很正常。常见的做法是两类一是纯 Java 控制台或 Swing 界面配合 JDBC适合强调 Java SE 基础二是 Servlet JSP JDBC 的 Web 版本这也是大多数课设和面试项目的标准形态。我更推荐 Web 版本因为你能顺带把请求参数、会话、异常处理都过一遍面试时讲起来素材更多。不管哪种形态核心都不在页面而在 JDBC 连接和事务。所以我先写一个通用的数据库连接工具类后面所有的 DAO 和 Service 都基于它import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbUtil { private static final String URL jdbc:mysql://localhost:3306/library_system ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动未找到请检查依赖, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码里有三个参数需要格外注意。characterEncodingutf8对应数据库的utf8mb4保证中文在 JDBC 传输过程中不乱码。serverTimezoneAsia/Shanghai解决 MySQL 8.0 下的时区报错不写的话连接时可能出现 “The server time zone value” 异常。useSSLfalse是在本地开发时省去 SSL 握手生产环境不能照抄这个参数。关于驱动类名MySQL 5.7 及以前常用com.mysql.jdbc.DriverMySQL 8.0 之后必须用com.mysql.cj.jdbc.Driver。如果你用的是 mysql-connector-java 8.x继续写老驱动类名也能兼容但控制台会打警告建议一步到位写新的。3.2 借书业务的事务代码为什么不能把连接关在 try-with-resources 里借书、还书、续借这三个动作是典型的跨表事务。以借书为例业务上要做两件事往borrow_record插一条记录同时把book.available_stock减一。这两步必须在一个事务里完成否则会出现“记录有了但库存没减”或者“库存减了但记录丢了”的数据不一致。我在 Service 层的实现如下public void borrowBook(int bookId, int readerId) throws Exception { String checkSql SELECT available_stock FROM book WHERE id ? FOR UPDATE; String insertSql INSERT INTO borrow_record(book_id, reader_id, due_time) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY)); String updateSql UPDATE book SET available_stock available_stock - 1 WHERE id ? AND available_stock 0; Connection conn null; try { conn DbUtil.getConnection(); conn.setAutoCommit(false); PreparedStatement checkPs conn.prepareStatement(checkSql); checkPs.setInt(1, bookId); ResultSet rs checkPs.executeQuery(); if (!rs.next() || rs.getInt(available_stock) 0) { throw new RuntimeException(库存不足无法借出); } PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setInt(1, bookId); insertPs.setInt(2, readerId); insertPs.executeUpdate(); PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setInt(1, bookId); int rows updatePs.executeUpdate(); if (rows ! 1) { throw new RuntimeException(库存扣减失败可能已被并发借出); } conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }这段代码有三个设计点值得展开。第一我手动把autoCommit关掉让两条 SQL 同生共死任何一个失败都能回滚。第二SELECT ... FOR UPDATE是对图书行加锁阻止两个请求同时读到同一个库存数值这是解决“超借”的关键。第三UPDATE语句里带了available_stock 0条件就算并发穿透了第一层检查数据库层面也会挡住负数库存。有同学会问为什么不用 try-with-resources 自动关连接因为 try-with-resources 会在代码块结束时自动关闭连接而我们的commit/rollback必须在连接存活时执行。如果按 try-with-resources 写事务控制就失控了。正确姿势就是手动管理Connection的声明周期把它放在最外层finally 里再关。实际项目中这类事务操作还可以用Transactional注解Spring 容器环境但注解不改变底层的逻辑面试时被问到事务传播行为你仍然要能说出“连接、事务边界、回滚时机”这三件事。3.3 图书检索的 DAO 写法LEFT JOIN 和 LIMIT 是标配借书之前总得先查到哪些书能借所以 DAO 层有一个高频方法叫searchBooks。下面这段代码是一个完整的 JDBC 查询链路包含了 PreparedStatement、参数绑定、ResultSet 到实体对象的映射public ListBook searchBooks(String keyword) throws SQLException { String sql SELECT b.id, b.title, b.author, b.available_stock, c.name AS category_name FROM book b LEFT JOIN category c ON b.category_id c.id WHERE b.title LIKE ? OR b.author LIKE ? ORDER BY b.id DESC LIMIT 20; ListBook list 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()) { Book book new Book(); book.setId(rs.getInt(id)); book.setTitle(rs.getString(title)); book.setAuthor(rs.getString(author)); book.setAvailableStock(rs.getInt(available_stock)); book.setCategoryName(rs.getString(category_name)); list.add(book); } } } return list; }这个查询用到了LEFT JOIN因为有的图书可能没有分类或者分类被删除了这时候我们仍然想让它显示在结果集里。LIKE %关键字%会导致索引失效但如果数据库里只有几百本测试数据全表扫描也没问题不用为了几万行数据纠结。真正要注意的是LIMIT 20它防止一次查询把内存打爆尤其是你在做 Web 接口时别人手动构造一个空字符串你可能把整张表都捞出来了。PreparedStatement 的另一个作用是预防 SQL 注入。这里的参数全部是通过setString绑定的而不是直接拼字符串。哪怕是图书系统这种内部系统也千万不要用字符串拼接 SQL这是红线。4. 数据库脚本落地时的 5 个避坑记录从乱码到外键约束失败4.1 整库导入报错Unknown database library_system现象在 MySQL 里执行完整脚本前面的建表语句跑完了导入借阅记录表时报Unknown database library_system。原因很可能是你拿到的脚本里只有CREATE TABLE没有CREATE DATABASE而当前连接默认的库是另一个名字或者压根没选中库。解决先手动建库或者找一个真正完整的脚本文件如果脚本里已经带了CREATE DATABASE和USE语句导入时不要把命令后面的库名写错。我自己的习惯是脚本第一行永远是CREATE DATABASE IF NOT EXISTSIF NOT EXISTS很关键它允许你反复执行脚本而不报错。如果用可视化工具导入前也要留意光标选中的连接位置别把一个带USE的脚本对着服务器根节点执行这样容易把库建在错误位置。4.2 中文乱码为什么表里全是问号现象用命令行source导入脚本后SELECT * FROM book查出来中文全是???但用 Navicat 看又是正常的。原因有两个层面表结构字符集如果设成了latin1那神仙也救不回来还有一种可能是命令行客户端连接时的字符集和表字符集不一致导入时数据被错误转码。解决建表时统一用utf8mb4导入命令加上字符集参数mysql --default-character-setutf8mb4 -uroot -p library_schema.sql如果是在 IDEA 里执行可以在 Database 控制台的连接属性里加上characterEncodingutf8。事前定好字符集比事后转换容易得多这是我从乱的表里捞中文数据的血泪经验。4.3 外键约束失败Cannot add or update a child row现象往borrow_record插入一条记录报Cannot add or update a child row: a foreign key constraint fails。原因插入的book_id在book表里不存在或者reader_id在reader表里不存在。这种情况在手工测试时特别常见比如你先填了一张书 ID10但图书表最大 ID 才到 9。解决插入前先查这两张主表确认 ID 真实存在。也可以用一条 SQL 批量检查无效数据SELECT br.* FROM borrow_record br LEFT JOIN book b ON br.book_id b.id LEFT JOIN reader r ON br.reader_id r.id WHERE b.id IS NULL OR r.id IS NULL;如果业务上允许临时跳过外键检查可以使用SET FOREIGN_KEY_CHECKS0但这是一个非常危险的开关。我建议只在初始化测试数据时用正常业务不要开。4.4 脚本导入后少了一堆对象没有存储过程、触发器或测试数据现象从同事那拷来一份full.sql导完之后表结构齐全但翻遍整个库找不到任何存储过程、触发器连初始数据都只有半截。原因他导出时用的是只导出结构的选项或者使用了mysqldump -d这样的参数-d代表 only data? 这里容易混淆真正的只导出结构参数是--no-data而只导出数据不导出结构的是--no-create-info。很多导出工具默认只导结构所以要拿到动作齐全的脚本导出时要把存储过程、触发器、事件、数据都勾上。在命令行下完整导出一个库的推荐写法是mysqldump -uroot -p --databases library_system \ --routines --triggers --events \ --set-gtid-purgedOFF \ library_full.sql其中--routines导出存储过程和函数--triggers导出触发器--events导出事件。这个参数组合是项目交接时我最常用的一套。--set-gtid-purgedOFF是为了在本地环境导入时避免 GTID 相关报错。4.5 Java 连接 MySQL 失败Communications link failure 和 Access denied现象Java 代码启动时报Communications link failure或者直接抛Access denied for user rootlocalhost。原因有两类。链接失败通常是端口不对、MySQL 没启动、连接串里多写了空格Access denied 是密码错误或用户权限问题。在 MySQL 8.0 下还有一个隐蔽坑默认认证插件是caching_sha2_password如果你的 JDBC 驱动版本太老会报Public Key Retrieval is not allowed。解决先检查 MySQL 是否启动并监听 3306 端口Linux 用netstat -tunlp | grep 3306Windows 用任务管理器看服务。然后确认驱动版本mysql-connector-java 至少 8.0 以上。URL 里再加一个参数allowPublicKeyRetrievaltrue这能解决本地开发时除名认证插件的连接问题。密码和用户权限我的排查顺序是先命令行连再 JDBC 连命令能连 JDBC 不能连就检查驱动和 URL命令都不能连就去查服务状态和密码。5. 把项目从“能跑”升级到“能讲”3 个验证数据正确性的技巧项目跑起来不难但面试或答辩时你总不能只说“我点了借书按钮库存减了”。我从自己带人的经验里总结了三个验证技巧按顺序做一遍你对这套系统的掌控力会明显不一样。第一个技巧是写一条统计 SQL验证借阅数据的完整性。比如统计每个未还书读者的数量SELECT r.card_no, r.name, COUNT(br.id) AS unreturn_count FROM reader r LEFT JOIN borrow_record br ON r.id br.reader_id AND br.return_time IS NULL GROUP BY r.id, r.card_no, r.name HAVING unreturn_count 0 ORDER BY unreturn_count DESC;这条 SQL 能帮你发现脏数据如果一个读者的未还数量比他在borrow_record里借出的记录还多说明数据逻辑有问题。我一般会把它作为一个固定脚本放到项目里每次手动压测之前跑一遍。第二个技巧是手工模拟并发超借。开两个命令行窗口同时执行同一个“借书”请求或者用简单的 JMeter 脚本对同一个bookId发并发请求。跑完之后立刻查看available_stock是否为负数。正常情况下因为有SELECT ... FOR UPDATE和available_stock 0双重保护库存永远不会变成负数。如果变成负数说明你的事务边界写错了大概率是UPDATE没加条件或者把connection在事务中途关掉了。第三个技巧是面试讲解的准备。Java 面试题里关于这块的经典问法是这样的你说说借书这功能怎么防止两个人同时借走同一本书标准答法是先在可重复读隔离级别下用行锁FOR UPDATE锁定图书行然后插入借阅记录再执行带条件的库存扣减整个过程在一个事务里任何一个失败都会回滚。如果你还能顺口说出available_stock 0是兜底方案、为什么 MySQL 5.7 不信任 CHECK 约束面试官基本不会再追问下去。我做完这个项目之后的最大感受是所谓“完整”不是功能多而是你得知道自己每一步在干什么。数据库脚本为什么这么写、事务边界为什么画在这里、外键为什么会被阻塞这些问题比表面跑通更能检验功夫。项目迭代多轮之后我反而会每隔一段时间把一个新环境从建库到跑通完整来一遍用最笨的办法确认脚本没有腐化。希望帮到你让这份“图书管理系统”从一份能跑的项目变成你真正能讲清楚的技术底气。本文还有配套的精品资源点击获取
返回列表