尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Java控制台图书管理系统:从数据库设计到JDBC实战

Java控制台图书管理系统:从数据库设计到JDBC实战 1. 项目缘起与核心价值最近在整理硬盘翻出了大学时期写的第一个像模像样的项目——一个用Java和MySQL实现的控制台版图书管理系统。现在回头看代码虽然青涩但麻雀虽小五脏俱全它几乎涵盖了从数据库设计、业务逻辑处理到用户交互的所有基础环节。对于正在学习Java后端开发、尤其是对如何将Java与数据库结合感到迷茫的朋友来说这个项目是一个绝佳的练手模板。它没有花哨的界面所有操作都在黑框框控制台里完成这反而能让你把注意力完全集中在核心的业务流程、数据流转和代码结构上。无论是为了应对课程设计、毕业设计还是为了夯实Java SE和JDBC的基础这个项目都能提供一个清晰、完整的实践路径。今天我就把这个项目的完整实现思路、关键代码以及我当年踩过的坑重新梳理一遍希望能给需要的朋友一份可以直接“抄作业”的参考。2. 系统需求分析与功能模块设计在动手写代码之前明确我们要做什么是第一步。一个图书管理系统最核心的参与者是“管理员”和“图书”/“读者”信息。因此我们需要设计出与之对应的功能。2.1 核心功能清单基于最简单的业务场景我将其划分为以下几个模块图书信息管理这是系统的基石。需要能对图书进行增、删、改、查。每本书至少应包含图书编号唯一标识、书名、作者、出版社、库存数量、价格等基本信息。读者信息管理管理系统的使用者读者信息。功能同样包括增删改查。读者信息通常有读者证号唯一、姓名、性别、联系方式、可借阅数量上限、已借阅数量等。借阅与归还管理这是系统的核心业务流程。借阅读者凭证号借书。系统需要检查该书是否在库库存0、该读者是否已达借阅上限、该读者是否有超期未还的图书。检查通过后完成借阅操作图书库存减1读者已借数量加1并生成一条借阅记录。归还读者还书。系统根据图书编号和读者证号找到对应的借阅记录判断是否超期。归还成功后图书库存加1读者已借数量减1更新借阅记录的状态为“已归还”。借阅记录查询管理员可以按读者、按图书或按时间段查询所有的借阅历史记录方便追溯和管理。数据统计与报表简单的统计功能如图书总量、读者总量、当前在借图书数量、最受欢迎的图书排行等为管理提供数据支持。2.2 为什么选择控制台你可能会问现在都是Web和App的时代了为什么还要做控制台程序这正是这个项目的教学意义所在。聚焦核心逻辑剥离了前端页面渲染、网络请求处理等复杂层开发者可以100%专注于后端业务逻辑、数据库操作和程序结构的设计。这对于初学者理解MVC模型-视图-控制器思想非常有益——你可以把控制台的输入输出看作是“视图层”业务处理是“控制器层”而Java Bean和数据库则是“模型层”。降低入门门槛无需学习HTML、CSS、JavaScript或任何前端框架也无需配置复杂的Web服务器如Tomcat。只需要一个Java开发环境JDK和一个MySQL数据库就能跑起来。快速验证与调试所有操作和结果都通过System.out.println打印在控制台流程一目了然非常便于跟踪数据流向和调试逻辑错误。理解数据流转的本质无论前端多么华丽后端与数据库交互的本质就是这一套接收参数、处理业务、执行SQL、返回结果。控制台程序将这个本质暴露无遗。注意在实际商业开发中控制台程序通常用于批处理、后台任务或运维脚本。但作为学习项目它是理解后端开发精髓的完美起点。3. 数据库设计与建表语句数据库是系统的“记忆中枢”设计的好坏直接决定了程序的复杂度和性能。我们遵循数据库设计的三范式基础原则来规划表结构。3.1 核心表结构设计我们至少需要三张表book图书表、reader读者表、borrow_record借阅记录表。它们之间的关系是一个读者可以借阅多本书一本书可以被多个读者借阅在不同时间因此reader和book之间是多对多关系需要通过borrow_record这个“中间表”来建立关联。1. 图书表 (book)这张表存储所有图书的静态信息。CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 图书ID主键自增长, isbn VARCHAR(20) UNIQUE NOT NULL COMMENT 国际标准书号具有唯一性, name VARCHAR(100) NOT NULL COMMENT 图书名称, author VARCHAR(50) NOT NULL COMMENT 作者, publisher VARCHAR(50) COMMENT 出版社, price DECIMAL(10, 2) COMMENT 价格, total_quantity INT DEFAULT 0 COMMENT 总数量馆藏量, current_quantity INT DEFAULT 0 COMMENT 当前可借数量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书信息表;设计理由id使用自增主键作为系统内部唯一标识性能好且简单。isbn图书的唯一业务标识添加UNIQUE约束防止重复录入。total_quantity和current_quantity这是关键设计。total_quantity代表图书馆拥有该书的总册数是固定值。current_quantity代表当前在馆可借的册数会随着借阅和归还动态变化。这样设计避免了每次查询库存都要去关联borrow_record表做统计极大提升了查询性能。utf8mb4字符集确保能存储所有Emoji和生僻字避免乱码问题。2. 读者表 (reader)这张表存储读者信息。CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 读者ID主键, card_number VARCHAR(20) UNIQUE NOT NULL COMMENT 借书证号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) COMMENT 性别M/F, phone VARCHAR(20) COMMENT 联系电话, max_borrow INT DEFAULT 5 COMMENT 最大可借阅数量, current_borrowed INT DEFAULT 0 COMMENT 当前已借阅数量, register_date DATE COMMENT 注册日期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者信息表;设计理由card_number读者的唯一业务标识类似学号或工号。max_borrow和current_borrowed与图书表设计思路一致通过冗余字段来提升性能。current_borrowed需要在借阅和归还时进行原子性的增减操作。3. 借阅记录表 (borrow_record)这张表记录每一次借阅行为的历史。CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 记录ID, book_id INT NOT NULL COMMENT 图书ID外键关联book.id, reader_id INT NOT NULL COMMENT 读者ID外键关联reader.id, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 借阅时间, due_time DATETIME NOT NULL COMMENT 应还时间, return_time DATETIME COMMENT 实际归还时间, status TINYINT DEFAULT 1 COMMENT 状态1-借阅中2-已归还3-超期归还, INDEX idx_reader_book (reader_id, book_id), INDEX idx_borrow_time (borrow_time), CONSTRAINT fk_book FOREIGN KEY (book_id) REFERENCES book (id) ON DELETE RESTRICT, CONSTRAINT fk_reader FOREIGN KEY (reader_id) REFERENCES reader (id) ON DELETE RESTRICT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;设计理由id使用BIGINT因为借阅记录会随时间快速增长。due_time应还时间由借阅时间加上借阅周期如30天计算得出。这是一个非常重要的业务字段。return_time归还时才填入用于计算是否超期以及超期天数。status使用状态码而非文本节省空间且便于程序判断。外键约束FOREIGN KEY确保了数据的参照完整性。例如不能插入一条book_id不存在的借阅记录。ON DELETE RESTRICT表示如果图书或读者记录被删除时存在关联的借阅记录则禁止删除防止产生“孤儿数据”。索引idx_reader_book联合索引对于“查询某读者借了某本书”这类场景速度极快。idx_borrow_time索引方便按时间范围进行统计查询。3.2 初始化测试数据建表后插入一些测试数据方便后续开发调试。-- 插入图书 INSERT INTO book (isbn, name, author, publisher, price, total_quantity, current_quantity) VALUES (978-7-111-59927-5, Java核心技术 卷I, 凯·S·霍斯特曼, 机械工业出版社, 149.00, 10, 10), (978-7-121-42286-7, MySQL必知必会, 本·福达, 电子工业出版社, 69.00, 5, 5), (978-7-115-54746-5, 深入理解计算机系统, Randal E.Bryant, 人民邮电出版社, 139.00, 3, 3); -- 插入读者 INSERT INTO reader (card_number, name, gender, phone, max_borrow) VALUES (R20230001, 张三, M, 13800138000, 5), (R20230002, 李四, F, 13900139000, 5);4. Java项目环境搭建与核心工具类编写我们使用最经典的Java SE环境配合MySQL Connector/J驱动来操作数据库。项目结构力求清晰。4.1 项目结构与Maven依赖创建一个标准的Maven项目目录结构如下book-management-console/ ├── pom.xml └── src/ └── main/ ├── java/ │ └── com/ │ └── yourname/ │ └── bookms/ │ ├── Main.java // 程序入口控制台菜单 │ ├── dao/ // 数据访问层 │ │ ├── BaseDao.java │ │ ├── BookDao.java │ │ ├── ReaderDao.java │ │ └── BorrowRecordDao.java │ ├── entity/ // 实体类对应数据库表 │ │ ├── Book.java │ │ ├── Reader.java │ │ └── BorrowRecord.java │ ├── service/ // 业务逻辑层 │ │ ├── BookService.java │ │ ├── ReaderService.java │ │ └── BorrowService.java │ ├── util/ // 工具类 │ │ ├── DBUtil.java // 数据库连接工具 │ │ └── ConsoleUtil.java // 控制台输入输出工具 │ └── view/ // 视图层控制台交互 │ └── ConsoleView.java └── resources/ └── db.properties // 数据库配置文件pom.xml文件只需引入MySQL驱动依赖dependencies dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version !-- 请使用与你的MySQL版本匹配的驱动 -- /dependency /dependencies4.2 数据库连接工具类 (DBUtil.java)这是与数据库打交道的门户必须写得健壮。我们采用经典的“数据库连接池”思想简化版——不引入第三方池如HikariCP但封装获取和关闭连接的方法并读取配置文件。package com.yourname.bookms.util; import java.io.InputStream; import java.sql.*; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; private static String driver; // 静态代码块在类加载时读取配置文件 static { try { InputStream is DBUtil.class.getClassLoader().getResourceAsStream(db.properties); Properties props new Properties(); props.load(is); url props.getProperty(db.url); user props.getProperty(db.user); password props.getProperty(db.password); driver props.getProperty(db.driver); Class.forName(driver); // 注册驱动 } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(初始化数据库配置失败, e); } } // 获取数据库连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } // 关闭资源重载方法应对不同情况 public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } public static void close(Connection conn, Statement stmt) { close(conn, stmt, null); } }对应的resources/db.properties文件db.drivercom.mysql.cj.jdbc.Driver db.urljdbc:mysql://localhost:3306/book_management?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse db.userroot db.passwordyour_password重要提示serverTimezoneAsia/Shanghai必须设置否则可能遇到时区错误导致时间字段不对。useSSLfalse如果MySQL是本地开发环境且未配置SSL建议关闭避免连接警告。密码不要写在代码里配置文件也应妥善保管这是最基本的安全意识。4.3 实体类 (Entity) 编写实体类是数据库表在Java中的映射通常是一个简单的Java Bean。package com.yourname.bookms.entity; import java.math.BigDecimal; import java.util.Date; public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private BigDecimal price; private Integer totalQuantity; private Integer currentQuantity; private Date createTime; // 省略 Getter 和 Setter 方法以及 toString 方法 }Reader和BorrowRecord类依此类推。这里强烈建议使用Lombok库的Data注解来简化代码避免手动编写大量的getter/setter。如果你的项目引入了Lombok实体类将非常简洁。这也是相关热词中“java: you aren‘t using a compiler supported by lombok”所指向的问题——需要确保IDE安装了Lombok插件并开启了注解处理。4.4 控制台输入工具类 (ConsoleUtil.java)处理控制台输入是个细活需要处理各种异常比如用户输入的不是数字。package com.yourname.bookms.util; import java.util.Scanner; public class ConsoleUtil { private static Scanner scanner new Scanner(System.in); public static String readString(String prompt) { System.out.print(prompt); return scanner.nextLine().trim(); } public static int readInt(String prompt) { while (true) { System.out.print(prompt); String input scanner.nextLine().trim(); try { return Integer.parseInt(input); } catch (NumberFormatException e) { System.out.println(输入错误请输入一个整数); } } } public static BigDecimal readBigDecimal(String prompt) { while (true) { System.out.print(prompt); String input scanner.nextLine().trim(); try { return new BigDecimal(input); } catch (NumberFormatException e) { System.out.println(输入错误请输入一个有效的金额如 12.5); } } } // ... 可以继续封装其他类型的读取方法 }5. 数据访问层 (DAO) 设计与实现DAO层负责所有与数据库的直接交互。我们设计一个BaseDao来封装通用的增删改查操作其他具体的DAO继承它。5.1 基础DAO类 (BaseDao.java)这里使用JDBC原生API。为了简化我们只实现通用的更新和查询方法。在实际中你可能会使用DbUtils、Spring JdbcTemplate等工具来进一步简化。package com.yourname.bookms.dao; import com.yourname.bookms.util.DBUtil; import java.lang.reflect.Field; import java.sql.*; import java.util.ArrayList; import java.util.List; public abstract class BaseDaoT { // 通用的更新操作INSERT, UPDATE, DELETE public int update(String sql, Object... params) { Connection conn null; PreparedStatement pstmt null; try { conn DBUtil.getConnection(); pstmt conn.prepareStatement(sql); // 设置参数 for (int i 0; i params.length; i) { pstmt.setObject(i 1, params[i]); } return pstmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return -1; } finally { DBUtil.close(conn, pstmt); } } // 通用的查询单个对象例如根据ID查询 public T queryForObject(String sql, ClassT clazz, Object... params) { Connection conn null; PreparedStatement pstmt null; ResultSet rs null; try { conn DBUtil.getConnection(); pstmt conn.prepareStatement(sql); for (int i 0; i params.length; i) { pstmt.setObject(i 1, params[i]); } rs pstmt.executeQuery(); if (rs.next()) { return mapResultSetToEntity(rs, clazz); } return null; } catch (Exception e) { e.printStackTrace(); return null; } finally { DBUtil.close(conn, pstmt, rs); } } // 通用的查询列表 public ListT queryForList(String sql, ClassT clazz, Object... params) { ListT list new ArrayList(); Connection conn null; PreparedStatement pstmt null; ResultSet rs null; try { conn DBUtil.getConnection(); pstmt conn.prepareStatement(sql); for (int i 0; i params.length; i) { pstmt.setObject(i 1, params[i]); } rs pstmt.executeQuery(); while (rs.next()) { list.add(mapResultSetToEntity(rs, clazz)); } return list; } catch (Exception e) { e.printStackTrace(); return list; } finally { DBUtil.close(conn, pstmt, rs); } } // 一个简单的反射工具方法将ResultSet的一行映射到一个实体对象 // 注意这是一个简化版要求实体类属性名与数据库列名一致或遵循下划线转驼峰 private T mapResultSetToEntity(ResultSet rs, ClassT clazz) throws Exception { T obj clazz.newInstance(); ResultSetMetaData metaData rs.getMetaData(); int columnCount metaData.getColumnCount(); for (int i 1; i columnCount; i) { String columnName metaData.getColumnLabel(i); // 获取列名或别名 Object columnValue rs.getObject(i); // 将下划线命名转为驼峰例如 book_id - bookId String fieldName toCamelCase(columnName); try { Field field clazz.getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, columnValue); } catch (NoSuchFieldException e) { // 如果属性不存在跳过可能是查询结果中有额外列 // System.out.println(警告实体类 clazz.getName() 中没有属性 fieldName); } } return obj; } private String toCamelCase(String columnName) { // 简单的下划线转驼峰逻辑可根据需要完善 String[] parts columnName.split(_); StringBuilder sb new StringBuilder(parts[0]); for (int i 1; i parts.length; i) { sb.append(parts[i].substring(0, 1).toUpperCase()) .append(parts[i].substring(1).toLowerCase()); } return sb.toString(); } }5.2 图书DAO (BookDao.java)具体DAO继承BaseDao并实现自己特有的数据访问方法。package com.yourname.bookms.dao; import com.yourname.bookms.entity.Book; import java.util.List; public class BookDao extends BaseDaoBook { // 新增图书 public int addBook(Book book) { String sql INSERT INTO book (isbn, name, author, publisher, price, total_quantity, current_quantity) VALUES (?, ?, ?, ?, ?, ?, ?); return update(sql, book.getIsbn(), book.getName(), book.getAuthor(), book.getPublisher(), book.getPrice(), book.getTotalQuantity(), book.getCurrentQuantity()); } // 根据ID删除图书 public int deleteBookById(int id) { // 注意由于有外键约束如果该书有借阅记录删除会失败RESTRICT String sql DELETE FROM book WHERE id ?; return update(sql, id); } // 更新图书信息这里假设只更新基本信息不更新数量 public int updateBook(Book book) { String sql UPDATE book SET name?, author?, publisher?, price? WHERE id?; return update(sql, book.getName(), book.getAuthor(), book.getPublisher(), book.getPrice(), book.getId()); } // 根据ID查询图书 public Book getBookById(int id) { String sql SELECT * FROM book WHERE id ?; return queryForObject(sql, Book.class, id); } // 根据书名模糊查询 public ListBook getBooksByName(String name) { String sql SELECT * FROM book WHERE name LIKE ?; return queryForList(sql, Book.class, % name %); } // 查询所有图书 public ListBook getAllBooks() { String sql SELECT * FROM book ORDER BY id; return queryForList(sql, Book.class); } // 更新图书库存借阅或归还时调用 public int updateBookQuantity(int bookId, int delta) { // delta为正数表示归还增加库存为负数表示借阅减少库存 // 使用原子操作避免并发问题在简单控制台程序中并发问题不突出但这是一个好习惯 String sql UPDATE book SET current_quantity current_quantity ? WHERE id ? AND current_quantity ? 0; // 第三个参数用于检查确保库存不会减为负数 return update(sql, delta, bookId, delta); } }ReaderDao和BorrowRecordDao的实现逻辑类似需要根据各自的业务字段编写SQL。BorrowRecordDao会复杂一些因为它涉及多表关联查询例如查询借阅记录时需要同时查出图书名和读者名。6. 业务逻辑层 (Service) 实现Service层是业务规则的核心它调用DAO完成数据操作并在此过程中执行业务校验和逻辑处理。这是体现你编程思想的关键层。6.1 图书服务 (BookService.java)package com.yourname.bookms.service; import com.yourname.bookms.dao.BookDao; import com.yourname.bookms.entity.Book; import java.util.List; public class BookService { private BookDao bookDao new BookDao(); public boolean addBook(Book book) { // 业务校验ISBN不能重复 if (book.getIsbn() null || book.getIsbn().trim().isEmpty()) { System.out.println(ISBN不能为空); return false; } // 这里可以添加更复杂的校验比如调用DAO检查ISBN是否已存在 // 简化处理假设DAO的insert会因唯一约束失败我们捕获异常 book.setCurrentQuantity(book.getTotalQuantity()); // 新书当前库存等于总库存 int result bookDao.addBook(book); return result 0; } public boolean deleteBook(int id) { // 删除前可以检查是否有未归还的借阅记录 // 简化处理直接删除由数据库外键约束阻止 int result bookDao.deleteBookById(id); if (result 0) { System.out.println(删除图书成功); return true; } else { System.out.println(删除失败可能该图书不存在或有未归还的记录。); return false; } } public ListBook searchBooks(String keyword) { if (keyword null || keyword.trim().isEmpty()) { return bookDao.getAllBooks(); } return bookDao.getBooksByName(keyword.trim()); } // 借书时减少库存 public boolean borrowBook(int bookId) { int result bookDao.updateBookQuantity(bookId, -1); return result 0; } // 还书时增加库存 public boolean returnBook(int bookId) { int result bookDao.updateBookQuantity(bookId, 1); return result 0; } }6.2 借阅服务 (BorrowService.java) - 核心业务这是整个系统最复杂的业务逻辑所在它需要协调BookService、ReaderService和BorrowRecordDao。package com.yourname.bookms.service; import com.yourname.bookms.dao.BorrowRecordDao; import com.yourname.bookms.entity.BorrowRecord; import com.yourname.bookms.entity.Book; import com.yourname.bookms.entity.Reader; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; public class BorrowService { private BorrowRecordDao borrowRecordDao new BorrowRecordDao(); private BookService bookService new BookService(); private ReaderService readerService new ReaderService(); private static final int BORROW_DAYS 30; // 借阅周期30天 public boolean borrowBook(int readerId, int bookId) { // 1. 检查读者是否存在且状态正常 Reader reader readerService.getReaderById(readerId); if (reader null) { System.out.println(读者不存在); return false; } if (reader.getCurrentBorrowed() reader.getMaxBorrow()) { System.out.println(该读者借阅数量已达上限); return false; } // 2. 检查图书是否存在且有库存 Book book bookService.getBookById(bookId); // 假设BookService有这个方法 if (book null) { System.out.println(图书不存在); return false; } if (book.getCurrentQuantity() 0) { System.out.println(该图书已无库存); return false; } // 3. 检查该读者是否已借阅此书且未归还可选 // 这里省略实际可根据需求添加 // 4. 所有检查通过开始事务性操作模拟 // 在简单的控制台程序中我们无法使用数据库事务但操作顺序很重要。 // 先减少图书库存 boolean bookUpdateOk bookService.borrowBook(bookId); if (!bookUpdateOk) { System.out.println(更新图书库存失败); return false; } // 再增加读者已借数量 boolean readerUpdateOk readerService.increaseBorrowed(readerId); if (!readerUpdateOk) { // 如果读者更新失败需要回滚图书库存在无事务环境下这是一个难点 bookService.returnBook(bookId); // 尝试补偿 System.out.println(更新读者借阅数量失败操作已回滚); return false; } // 最后创建借阅记录 BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowTime(new Date()); // 计算应还时间 LocalDateTime dueTime LocalDateTime.now().plusDays(BORROW_DAYS); record.setDueTime(Date.from(dueTime.atZone(ZoneId.systemDefault()).toInstant())); record.setStatus(1); // 借阅中 int recordResult borrowRecordDao.addRecord(record); if (recordResult 0) { System.out.println(借阅成功应还日期 dueTime.toLocalDate()); return true; } else { // 记录创建失败也需要回滚前两步在实际项目中必须使用数据库事务 bookService.returnBook(bookId); readerService.decreaseBorrowed(readerId); System.out.println(创建借阅记录失败操作已回滚); return false; } } public boolean returnBook(int recordId) { // 1. 查询借阅记录 BorrowRecord record borrowRecordDao.getRecordById(recordId); if (record null || record.getStatus() ! 1) { System.out.println(借阅记录不存在或已归还); return false; } // 2. 更新记录状态和归还时间 record.setReturnTime(new Date()); Date dueTime record.getDueTime(); Date now new Date(); int newStatus 2; // 已归还 if (now.after(dueTime)) { newStatus 3; // 超期归还 System.out.println(该书已超期); // 这里可以计算超期天数并模拟罚款逻辑 } record.setStatus(newStatus); // 3. 事务性操作先更新记录再更新库存和读者信息 int updateRecordResult borrowRecordDao.updateRecord(record); if (updateRecordResult 0) { return false; } boolean bookOk bookService.returnBook(record.getBookId()); boolean readerOk readerService.decreaseBorrowed(record.getReaderId()); if (bookOk readerOk) { System.out.println(归还成功); return true; } else { // 同样面临回滚问题简化处理打印错误 System.out.println(归还过程中更新图书或读者信息失败数据可能不一致); return false; } } }踩坑实录与思考 上面的borrowBook和returnBook方法暴露了在没有数据库事务支持下的典型问题——数据一致性。我们通过顺序执行和“补偿操作”来模拟回滚但这并不完美。例如在borrowBook的最后一步创建记录失败我们尝试补偿前两步但如果补偿操作returnBook或decreaseBorrowed也失败了呢数据就永久不一致了。解决方案使用数据库事务这是最正确的方式。在DAO层获取连接后设置conn.setAutoCommit(false)在所有操作成功后conn.commit()失败则conn.rollback()。这要求同一个事务内的所有DAO操作使用同一个Connection对象。我们可以通过ThreadLocal或传递Connection参数来实现。使用Spring框架在Spring管理的项目中可以通过Transactional注解轻松声明事务边界框架会帮我们处理这一切。设计补偿接口对于复杂的分布式系统有Saga等分布式事务模式。在我们的简单项目中理解这个痛点并知道事务的重要性就是最大的收获。7. 控制台视图与主程序循环这是用户直接交互的部分负责展示菜单、接收输入、调用Service并显示结果。7.1 控制台视图 (ConsoleView.java)package com.yourname.bookms.view; import com.yourname.bookms.service.*; import com.yourname.bookms.entity.*; import com.yourname.bookms.util.ConsoleUtil; import java.util.List; public class ConsoleView { private BookService bookService new BookService(); private ReaderService readerService new ReaderService(); private BorrowService borrowService new BorrowService(); public void showMainMenu() { while (true) { System.out.println(\n 图书管理系统 ); System.out.println(1. 图书管理); System.out.println(2. 读者管理); System.out.println(3. 借阅管理); System.out.println(4. 归还管理); System.out.println(5. 查询借阅记录); System.out.println(0. 退出系统); System.out.println(); int choice ConsoleUtil.readInt(请选择操作); switch (choice) { case 1: showBookMenu(); break; case 2: showReaderMenu(); break; case 3: processBorrow(); break; case 4: processReturn(); break; case 5: queryBorrowRecords(); break; case 0: System.out.println(感谢使用再见); System.exit(0); default: System.out.println(输入有误请重新选择); } } } private void showBookMenu() { while (true) { System.out.println(\n--- 图书管理 ---); System.out.println(1. 新增图书); System.out.println(2. 删除图书); System.out.println(3. 修改图书); System.out.println(4. 查询图书); System.out.println(0. 返回上级); int choice ConsoleUtil.readInt(请选择); switch (choice) { case 1: addBook(); break; case 2: deleteBook(); break; case 3: updateBook(); break; case 4: searchBooks(); break; case 0: return; default: System.out.println(输入错误); } } } private void addBook() { System.out.println(\n--- 新增图书 ---); Book book new Book(); book.setIsbn(ConsoleUtil.readString(ISBN: )); book.setName(ConsoleUtil.readString(书名: )); book.setAuthor(ConsoleUtil.readString(作者: )); book.setPublisher(ConsoleUtil.readString(出版社: )); book.setPrice(ConsoleUtil.readBigDecimal(价格: )); book.setTotalQuantity(ConsoleUtil.readInt(总数量: )); if (bookService.addBook(book)) { System.out.println(新增图书成功); } else { System.out.println(新增图书失败请检查ISBN是否重复或信息是否完整。); } } private void searchBooks() { System.out.println(\n--- 查询图书 ---); String keyword ConsoleUtil.readString(输入书名关键字直接回车查看全部: ); ListBook books bookService.searchBooks(keyword); if (books.isEmpty()) { System.out.println(未找到相关图书。); } else { System.out.println(ID\tISBN\t\t书名\t\t作者\t库存\t价格); System.out.println(------------------------------------------------------------); for (Book b : books) { System.out.printf(%d\t%s\t%s\t%s\t%d\t%.2f\n, b.getId(), b.getIsbn(), b.getName(), b.getAuthor(), b.getCurrentQuantity(), b.getPrice()); } } } // ... 其他菜单方法deleteBook, updateBook, showReaderMenu等实现逻辑类似 // ... processBorrow, processReturn, queryBorrowRecords 需要调用对应的Service方法 }7.2 程序入口 (Main.java)package com.yourname.bookms; import com.yourname.bookms.view.ConsoleView; public class Main { public static void main(String[] args) { ConsoleView view new ConsoleView(); view.showMainMenu(); } }8. 项目总结、优化与扩展思路至此一个基础的控制台版图书管理系统就搭建完成了。你可以运行Main类在控制台里体验完整的图书借阅流程。这个项目虽然简单但它串联起了Java SE核心语法、面向对象设计、JDBC数据库编程、三层架构MVC等关键知识点。回顾核心收获数据库设计理解了实体关系一对多、多对多、主外键约束、索引优化以及通过冗余字段提升查询性能的思路。JDBC编程掌握了连接数据库、执行SQL、处理结果集、资源关闭的完整流程并封装了通用的DAO工具类。分层架构实践了DAO、Service、View的分层思想明确了各层的职责DAO管数据、Service管业务、View管交互代码结构清晰易于维护和扩展。业务逻辑实现了借阅归还这一核心业务流程并深入思考了数据一致性和事务处理这个关键问题。异常处理与用户体验在控制台交互中加入了输入校验和友好的提示信息。可以继续优化的方向引入数据库连接池使用HikariCP或Druid替代我们简单的DBUtil能显著提升数据库连接管理的效率和性能这也是企业级应用的标配。实现真正的事务改造DAO和Service层确保borrowBook和returnBook等核心操作在同一个数据库事务中执行保证ACID特性。使用MyBatis或JPA用这些ORM框架替代手写JDBC能极大减少样板代码让你更专注于业务逻辑。这是从“会用JDBC”到“会用主流持久层框架”的关键一步。日志记录使用Log4j2或SLF4J记录系统运行日志、操作日志和异常信息便于排查问题。单元测试为Service层的方法编写JUnit单元测试确保业务逻辑的正确性。扩展功能超期罚款在BorrowRecord表中增加fine_amount罚款金额字段在归还时计算。图书分类增加分类表支持按分类查询。预约功能读者可以预约已被借出的图书。数据导出将图书清单、借阅记录导出为Excel或PDF文件。升级为Web版本这是最自然的演进路径。你可以用Spring Boot快速搭建一个Web后端提供RESTful API然后开发一个简单的前端页面可以用Thymeleaf模板引擎或者Vue/React前后端分离。届时你现在写的Service层和DAO层代码几乎可以无缝迁移。这个项目就像一块坚实的基石理解了它你再去看那些热门的“Java面试八股文”里关于JDBC、事务、数据库锁、索引优化的问题就会有更直观和深刻的认识。希望这份详细的拆解能帮助你不仅完成一个项目更能理解项目背后的设计思想和工程原则。编程之路动手实践永远是最好的老师。
返回列表