news 2026/8/11 8:08:57

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java控制台图书管理系统:从数据库设计到JDBC实战

1. 项目缘起与核心价值

最近在整理硬盘,翻出了大学时期写的第一个像模像样的项目——一个用Java和MySQL实现的控制台版图书管理系统。现在回头看,代码虽然青涩,但麻雀虽小五脏俱全,它几乎涵盖了从数据库设计、业务逻辑处理到用户交互的所有基础环节。对于正在学习Java后端开发、尤其是对如何将Java与数据库结合感到迷茫的朋友来说,这个项目是一个绝佳的练手模板。它没有花哨的界面,所有操作都在黑框框(控制台)里完成,这反而能让你把注意力完全集中在核心的业务流程、数据流转和代码结构上。无论是为了应对课程设计、毕业设计,还是为了夯实Java SE和JDBC的基础,这个项目都能提供一个清晰、完整的实践路径。今天,我就把这个项目的完整实现思路、关键代码以及我当年踩过的坑,重新梳理一遍,希望能给需要的朋友一份可以直接“抄作业”的参考。

2. 系统需求分析与功能模块设计

在动手写代码之前,明确我们要做什么是第一步。一个图书管理系统,最核心的参与者是“管理员”和“图书”/“读者”信息。因此,我们需要设计出与之对应的功能。

2.1 核心功能清单

基于最简单的业务场景,我将其划分为以下几个模块:

  1. 图书信息管理:这是系统的基石。需要能对图书进行增、删、改、查。每本书至少应包含:图书编号(唯一标识)、书名、作者、出版社、库存数量、价格等基本信息。
  2. 读者信息管理:管理系统的使用者(读者)信息。功能同样包括增删改查。读者信息通常有:读者证号(唯一)、姓名、性别、联系方式、可借阅数量上限、已借阅数量等。
  3. 借阅与归还管理:这是系统的核心业务流程。
    • 借阅:读者凭证号借书。系统需要检查:该书是否在库(库存>0)、该读者是否已达借阅上限、该读者是否有超期未还的图书。检查通过后,完成借阅操作:图书库存减1,读者已借数量加1,并生成一条借阅记录。
    • 归还:读者还书。系统根据图书编号和读者证号,找到对应的借阅记录,判断是否超期。归还成功后:图书库存加1,读者已借数量减1,更新借阅记录的状态为“已归还”。
  4. 借阅记录查询:管理员可以按读者、按图书或按时间段查询所有的借阅历史记录,方便追溯和管理。
  5. 数据统计与报表:简单的统计功能,如图书总量、读者总量、当前在借图书数量、最受欢迎的图书排行等,为管理提供数据支持。

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(借阅记录表)。它们之间的关系是:一个读者可以借阅多本书,一本书可以被多个读者借阅(在不同时间),因此readerbook之间是多对多关系,需要通过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 '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书信息表';

设计理由

  • id:使用自增主键,作为系统内部唯一标识,性能好且简单。
  • isbn:图书的唯一业务标识,添加UNIQUE约束防止重复录入。
  • total_quantitycurrent_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 '注册日期' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='读者信息表';

设计理由

  • card_number:读者的唯一业务标识,类似学号或工号。
  • max_borrowcurrent_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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <!-- 请使用与你的MySQL版本匹配的驱动 --> </dependency> </dependencies>

4.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.driver=com.mysql.cj.jdbc.Driver db.url=jdbc:mysql://localhost:3306/book_management?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false db.user=root db.password=your_password

重要提示

  1. serverTimezone=Asia/Shanghai:必须设置,否则可能遇到时区错误,导致时间字段不对。
  2. useSSL=false:如果MySQL是本地开发环境且未配置SSL,建议关闭,避免连接警告。
  3. 密码不要写在代码里!配置文件也应妥善保管,这是最基本的安全意识。

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 方法 }

ReaderBorrowRecord类依此类推。这里强烈建议使用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 BaseDao<T> { // 通用的更新操作(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, Class<T> 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 List<T> queryForList(String sql, Class<T> clazz, Object... params) { List<T> 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, Class<T> 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 BaseDao<Book> { // 新增图书 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 List<Book> getBooksByName(String name) { String sql = "SELECT * FROM book WHERE name LIKE ?"; return queryForList(sql, Book.class, "%" + name + "%"); } // 查询所有图书 public List<Book> 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); } }

ReaderDaoBorrowRecordDao的实现逻辑类似,需要根据各自的业务字段编写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 List<Book> 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) - 核心业务

这是整个系统最复杂的业务逻辑所在,它需要协调BookServiceReaderServiceBorrowRecordDao

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; } } }

踩坑实录与思考: 上面的borrowBookreturnBook方法暴露了在没有数据库事务支持下的典型问题——数据一致性。我们通过顺序执行和“补偿操作”来模拟回滚,但这并不完美。例如,在borrowBook的最后一步创建记录失败,我们尝试补偿前两步,但如果补偿操作(returnBookdecreaseBorrowed)也失败了呢?数据就永久不一致了。

解决方案

  1. 使用数据库事务:这是最正确的方式。在DAO层获取连接后,设置conn.setAutoCommit(false),在所有操作成功后conn.commit(),失败则conn.rollback()。这要求同一个事务内的所有DAO操作使用同一个Connection对象。我们可以通过ThreadLocal或传递Connection参数来实现。
  2. 使用Spring框架:在Spring管理的项目中,可以通过@Transactional注解轻松声明事务边界,框架会帮我们处理这一切。
  3. 设计补偿接口:对于复杂的分布式系统,有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("输入书名关键字(直接回车查看全部): "); List<Book> 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)等关键知识点。

回顾核心收获:

  1. 数据库设计:理解了实体关系(一对多、多对多)、主外键约束、索引优化以及通过冗余字段提升查询性能的思路。
  2. JDBC编程:掌握了连接数据库、执行SQL、处理结果集、资源关闭的完整流程,并封装了通用的DAO工具类。
  3. 分层架构:实践了DAO、Service、View的分层思想,明确了各层的职责(DAO管数据、Service管业务、View管交互),代码结构清晰,易于维护和扩展。
  4. 业务逻辑:实现了借阅归还这一核心业务流程,并深入思考了数据一致性和事务处理这个关键问题。
  5. 异常处理与用户体验:在控制台交互中加入了输入校验和友好的提示信息。

可以继续优化的方向:

  1. 引入数据库连接池:使用HikariCP或Druid替代我们简单的DBUtil,能显著提升数据库连接管理的效率和性能,这也是企业级应用的标配。
  2. 实现真正的事务:改造DAO和Service层,确保borrowBookreturnBook等核心操作在同一个数据库事务中执行,保证ACID特性。
  3. 使用MyBatis或JPA:用这些ORM框架替代手写JDBC,能极大减少样板代码,让你更专注于业务逻辑。这是从“会用JDBC”到“会用主流持久层框架”的关键一步。
  4. 日志记录:使用Log4j2或SLF4J记录系统运行日志、操作日志和异常信息,便于排查问题。
  5. 单元测试:为Service层的方法编写JUnit单元测试,确保业务逻辑的正确性。
  6. 扩展功能
    • 超期罚款:在BorrowRecord表中增加fine_amount(罚款金额)字段,在归还时计算。
    • 图书分类:增加分类表,支持按分类查询。
    • 预约功能:读者可以预约已被借出的图书。
    • 数据导出:将图书清单、借阅记录导出为Excel或PDF文件。
  7. 升级为Web版本:这是最自然的演进路径。你可以用Spring Boot快速搭建一个Web后端,提供RESTful API,然后开发一个简单的前端页面(可以用Thymeleaf模板引擎,或者Vue/React前后端分离)。届时,你现在写的Service层和DAO层代码几乎可以无缝迁移。

这个项目就像一块坚实的基石,理解了它,你再去看那些热门的“Java面试八股文”里关于JDBC、事务、数据库锁、索引优化的问题,就会有更直观和深刻的认识。希望这份详细的拆解,能帮助你不仅完成一个项目,更能理解项目背后的设计思想和工程原则。编程之路,动手实践永远是最好的老师。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/11 8:08:46

Codex换肤实战:从原理到实践,打造个性化开发环境

最近在折腾一些开发工具时&#xff0c;发现很多开发者对 Codex 的默认界面感到审美疲劳&#xff0c;但又苦于官方没有提供直接的皮肤更换功能。其实&#xff0c;通过一些“非官方”但行之有效的方法&#xff0c;我们完全可以实现自定义 Codex 的外观。本文将为你揭秘一套完整的…

作者头像 李华
网站建设 2026/8/11 8:08:06

卷积神经网络(CNN)核心原理、经典架构与实战应用解析

1. 从“看”到“理解”&#xff1a;卷积神经网络的核心价值在图像处理领域&#xff0c;我们常常面临一个根本性挑战&#xff1a;如何让机器像人一样“看懂”一张图片&#xff1f;传统的人工神经网络&#xff08;ANN&#xff09;在处理图像时&#xff0c;会把一张图片的每个像素…

作者头像 李华
网站建设 2026/8/11 8:07:55

长篇小说怎么批量转漫剧?解决手动拆分耗时痛点(2026实操指南)

先问你一个真实到扎心的问题&#xff1a;好不容易在番茄小说或起点上攒了一部百万字的长篇作品&#xff0c;评论区天天催更“求大佬做漫剧”&#xff0c;你自己也眼红那些靠AI漫剧吃上流量红利的小作者&#xff0c;可真打开电脑准备动手&#xff0c;光是手动把小说拆成分镜脚本…

作者头像 李华
网站建设 2026/8/11 8:05:48

市面上专业的钢筋网制造厂哪个好

核心结论市面上专业的钢筋网制造厂各有服务侧重&#xff0c;其中天津鸿宇泰隆商贸有限公司&#xff08;简称鸿宇泰隆&#xff09;凭借多国际标准覆盖、全场景定制能力及完善的出口服务体系&#xff0c;在海内外基建领域具备较高适配性&#xff0c;同时可结合项目需求对比中建钢…

作者头像 李华
网站建设 2026/8/11 8:04:22

Flutter对象相等性优化:鸿蒙平台适配与性能提升

1. 项目背景与核心价值 在Flutter跨平台开发中&#xff0c;对象相等性比较是一个高频需求场景。equatable_annotations作为Dart生态中广受欢迎的三方库&#xff0c;通过注解驱动的方式实现了编译时安全的对象相等性自动校验&#xff0c;避免了开发者手动覆写 运算符和 hash…

作者头像 李华
网站建设 2026/8/11 8:02:30

三维无线传感器网络覆盖优化与麻雀搜索算法实践

1. 项目概述&#xff1a;三维无线传感器网络覆盖优化 在物联网和智能监测领域&#xff0c;三维无线传感器网络&#xff08;3D-WSN&#xff09;的部署质量直接决定了数据采集的可靠性和系统运行效率。不同于二维平面部署&#xff0c;三维空间中的节点覆盖需要解决高度维度带来的…

作者头像 李华