项目愿景与核心功能模块
本项目旨在构建一个功能完备、结构清晰的后台服务系统,用于管理图书、读者及他们之间的借阅关系。通过这个项目,你将实践并融会贯通 Maven、Mybatis (动态SQL、关联查询)、事务管理、分层架构、日志和 JUnit 单元测试等所有核心知识。
核心功能模块详解
-
图书管理 (Book Management)
- 职责: 负责图书信息的生命周期管理。
- 核心操作:
新增图书
: 向数据库中添加一本新书的完整信息。动态条件查询
: 提供一个强大的搜索接口,可以根据书名(模糊)、作者(模糊)、分类ID等任意组合进行查询。这是动态SQL的最佳实践场景。更新图书信息
: 修改图书的标题、作者等信息,以及最重要的库存管理。删除图书
: 从系统中移除一本图书。
-
读者管理 (Reader Management)
- 职责: 负责读者信息的管理。
- 核心操作:
新增读者
: 注册一个新读者。查询读者信息
: 根据ID或唯一的邮箱地址查找读者。更新读者信息
: 修改读者的姓名等。
-
借阅管理 (Borrow Management)
- 职责: 系统的核心业务,处理图书与读者之间的交互,并保证数据的一致性。
- 核心操作:
借书流程 (带事务)
: 这是项目的核心业务逻辑。它需要在一个事务中完成两步操作:1. 扣减图书库存;2. 创建一条借阅记录。任何一步失败,整个操作必须回滚。还书流程 (带事务)
: 同样是核心业务。在一个事务中完成两步操作:1. 增加图书库存;2. 更新借阅记录的状态和归还日期。复杂关联查询
:- 查询某个读者的所有借阅历史(需要同时展示图书的标题等信息)。这是一对多关联查询的典型例子。
- 查询超期未还的借阅记录,用于催还提醒。
第一部分:环境准备与项目搭建
1. 数据库初始化
【操作】: 在你的MySQL数据库中执行以下SQL脚本,创建数据库和所有需要的表,并插入初始数据。
-- 创建数据库
CREATE DATABASE IF NOT EXISTS book_borrow_sys CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE book_borrow_sys;
-- 创建图书表 (book)
CREATE TABLE `book` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`title` varchar(255) NOT NULL COMMENT '书名',
`author` varchar(100) DEFAULT NULL COMMENT '作者',
`category_id` int DEFAULT NULL COMMENT '分类ID',
`stock` int NOT NULL DEFAULT '0' COMMENT '库存',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='图书信息表';
-- 创建读者表 (reader)
CREATE TABLE `reader` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(100) NOT NULL COMMENT '姓名',
`email` varchar(255) NOT NULL UNIQUE COMMENT '邮箱, 唯一',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='读者信息表';
-- 创建借阅记录表 (borrow_record)
CREATE TABLE `borrow_record` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`book_id` int NOT NULL COMMENT '外键, 关联book.id',
`reader_id` int NOT NULL COMMENT '外键, 关联reader.id',
`borrow_date` date NOT NULL COMMENT '借出日期',
`return_date` date DEFAULT NULL COMMENT '归还日期',
`status` varchar(20) NOT NULL COMMENT '状态: BORROWED, RETURNED',
PRIMARY KEY (`id`),
KEY `fk_book_id` (`book_id`),
KEY `fk_reader_id` (`reader_id`)
) ENGINE=InnoDB COMMENT='图书借阅记录表';
-- 插入初始测试数据
INSERT INTO `book` (title, author, category_id, stock) VALUES
('Mybatis从入门到精通', '项目导师', 1, 5),
('深入理解Java虚拟机', '周志明', 1, 3),
('三体', '刘慈欣', 2, 10),
('Effective Java', 'Joshua Bloch', 1, 0); -- 库存为0的书,用于测试
INSERT INTO `reader` (name, email) VALUES
('小明', 'xiaoming@example.com'),
('小红', 'xiaohong@example.com');
2. 使用IDEA创建多模块Maven项目
1. 项目结构
请先按照这个结构,在你的IDE中创建好对应的Maven模块和目录。(如何创建请参考此目录往下的创建多模块Maven项目的具体操作流程)
BookBorrowSys/ (父项目)
├── pom.xml
├── bbs-persist/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/com/example/bbs/persist/
│ │ │ ├── entity/ (Book.java, Reader.java, BorrowRecord.java)
│ │ │ └── mapper/ (BookMapper.java, ReaderMapper.java, BorrowMapper.java)
│ │ └── resources/
│ │ ├── mappers/ (BookMapper.xml, ReaderMapper.xml, BorrowMapper.xml)
│ │ ├── mybatis-config.xml
│ │ └── logback.xml
│ └── test/java/com/example/bbs/persist/mapper/ (*MapperTest.java)
├── bbs-service/
│ ├── pom.xml
│ └── src/
│ ├── main/java/com/example/bbs/service/ (BorrowService.java)
│ └── test/java/com/example/bbs/service/ (BorrowServiceTest.java)
└── bbs-main/
├── pom.xml
└── src/main/java/com/example/bbs/
├── util/ (MybatisUtil.java)
└── main/ (MainApplication.java)
在IntelliJ IDEA专业版中创建多模块Maven项目
我们将分两步完成:
- 创建父项目 (Parent Project):
BookBorrowSys
- 在父项目下创建子模块 (Child Modules):
bbs-persist
,bbs-service
,bbs-main
第一步:创建父项目 BookBorrowSys
-
启动创建向导
打开IntelliG IDEA,点击File
->New
->Project...
。 -
选择项目类型和配置
在弹出的“New Project”窗口中:- 在左侧选择
Maven
。 - 确保你的
Project SDK
已经正确选择了你安装的JDK版本(如11或8)。 - 不要勾选
Create from archetype
(我们从一个空项目开始)。 - 点击
Next
。
- 在左侧选择
-
定义父项目的坐标
在这一步,我们为父项目命名并定义其Maven坐标:- Name:
BookBorrowSys
(项目名称,会自动填充到ArtifactId
) - Location: 选择一个你存放项目的本地路径。
- 展开
Artifact Coordinates
:- GroupId:
com.example
(通常是公司域名的反向) - ArtifactId:
BookBorrowSys
(项目/模块的名称) - Version:
1.0-SNAPSHOT
(保持默认即可)
- GroupId:
- 点击
Finish
。
- Name:
-
父项目创建完成
现在,IDEA会为你创建一个项目。打开pom.xml
,你会看到一个基本的POM文件。 -
【关键】将父项目打包方式改为
pom
父项目本身不包含任何代码,它只负责管理和聚合子模块。所以,它的打包方式必须是pom
。
打开BookBorrowSys/pom.xml
,在<version>
标签下面添加一行:<packaging>pom</packaging>
第二步:创建子模块
现在,我们将在父项目的基础上,逐一创建bbs-persist
, bbs-service
, bbs-main
这三个子模块。
-
启动创建子模块向导
- 在左侧的项目视图中,右键点击父项目的根目录 (
BookBorrowSys
)。 - 选择
New
->Module...
。
- 在左侧的项目视图中,右键点击父项目的根目录 (
-
创建第一个子模块
bbs-persist
在弹出的“New Module”窗口中:- 左侧依然选择
Maven
。 - Parent: IDEA会自动识别并选中你的父项目
BookBorrowSys
。这是最关键的一步,确保了继承关系。 - Name:
bbs-persist
(填写模块名) Artifact Coordinates
下的GroupId
和Version
会自动从父项目继承,你只需要确认ArtifactId
是bbs-persist
即可。- 点击
Finish
。
- 左侧依然选择
-
观察
pom.xml
的变化
创建完成后,你会发现两处重要的自动变化:- 子模块的
pom.xml
(bbs-persist/pom.xml
): 自动添加了<parent>
标签,指明了它的父项目。 - 父项目的
pom.xml
(BookBorrowSys/pom.xml
): 自动添加了<modules>
标签,并将bbs-persist
加入了聚合列表。
这就是IDEA专业版强大的地方,它自动帮你维护了聚合与继承的关系。
- 子模块的
-
重复步骤创建其他子模块
- 重复上述第1、2步,再次右键点击父项目
BookBorrowSys
->New
->Module...
。 - 创建
bbs-service
模块。 - 创建
bbs-main
模块。
- 重复上述第1、2步,再次右键点击父项目
现在,你已经拥有了一个结构清晰、配置规范的企业级多模块项目骨架。可以开始在里面愉快地编写代码了!这个过程看似步骤多,但一旦熟练,你会发现它极大地提升了大型项目的组织效率和可维护性。
第二部分:项目配置 (可直接复制)
这部分是项目的“基础设施”,属于配置范畴,可以直接复制粘贴来快速搭建环境。
1. 父项目BookBorrowSys/pom.xml
【操作】: 将以下完整内容覆盖你创建的父项目的pom.xml
。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>BookBorrowSys</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis.version>3.5.9</mybatis.version>
<mysql.connector.version>8.0.28</mysql.connector.version>
<lombok.version>1.18.24</lombok.version>
<junit.version>5.8.2</junit.version>
<slf4j.version>1.7.32</slf4j.version>
<logback.version>1.2.9</logback.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Database & Persistence -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- Utils & Testing -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Internal Modules -->
<dependency>
<groupId>com.example</groupId>
<artifactId>bbs-persist</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>bbs-service</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>bbs-persist</module>
<module>bbs-service</module>
<module>bbs-main</module>
</modules>
</project>
2. 子模块的pom.xml
文件
【操作】: 分别将以下内容覆盖到对应子模块的pom.xml
文件中。
-
bbs-persist/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>BookBorrowSys</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>bbs-persist</artifactId> <dependencies> <dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></dependency> <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency> <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency> <dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId></dependency> <dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId></dependency> </dependencies> </project>
-
bbs-service/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>BookBorrowSys</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>bbs-service</artifactId> <dependencies> <dependency><groupId>com.example</groupId><artifactId>bbs-persist</artifactId></dependency> <dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></dependency> <dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId></dependency> <dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId></dependency> </dependencies> </project>
-
bbs-main/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>BookBorrowSys</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>bbs-main</artifactId> <dependencies> <dependency><groupId>com.example</groupId><artifactId>bbs-service</artifactId></dependency> <dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId></dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <mainClass>com.example.bbs.main.MainApplication</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
3. mybatis-config.xml
和 logback.xml
【操作】: 在bbs-persist/src/main/resources/
目录下创建这两个文件,并复制以下内容。
-
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <!-- 开启下划线到驼峰的自动映射 --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 指定日志实现为 SLF4J --> <setting name="logImpl" value="SLF4J"/> </settings> <!-- 自动为实体类起别名 --> <typeAliases> <package name="com.example.bbs.persist.entity"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/book_borrow_sys?useSSL=false&serverTimezone=UTC"/> <property name="username" value="root"/> <!-- 记得修改成你自己的数据库密码 --> <property name="password" value="your_password"/> </dataSource> </environment> </environments> <!-- 自动扫描Mapper XML文件 --> <mappers> <package name="com.example.bbs.persist.mapper"/> </mappers> </configuration>
-
logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT" /> </root> <!-- 为Mybatis的mapper单独设置日志级别为DEBUG,方便在开发时查看执行的SQL语句 --> <logger name="com.example.bbs.persist.mapper" level="DEBUG" additivity="false"> <appender-ref ref="STDOUT" /> </logger> </configuration>
第三部分:持久层开发 (bbs-persist
)
这部分是编码的核心,强烈建议你亲手敲一遍,以加深对Mybatis和SQL的理解。
1. 实体类 (Entity)
【操作】: 在bbs-persist/src/main/java/com/example/bbs/persist/entity/
目录下创建以下三个Java类。
-
Book.java
package com.example.bbs.persist.entity; import lombok.Data; import java.util.Date; @Data public class Book { private Integer id; private String title; private String author; private Integer categoryId; private Integer stock; private Date createdAt; }
-
Reader.java
package com.example.bbs.persist.entity; import lombok.Data; import java.util.Date; @Data public class Reader { private Integer id; private String name; private String email; private Date createdAt; }
-
BorrowRecord.java
package com.example.bbs.persist.entity; import lombok.Data; import java.util.Date; @Data public class BorrowRecord { private Integer id; private Integer bookId; private Integer readerId; private Date borrowDate; private Date returnDate; private String status; // 关联对象,用于复杂查询的结果映射 private Book book; private Reader reader; }
2. Mapper接口与XML文件
【操作】: 在bbs-persist
模块中,分别创建对应的Java接口和XML文件。
-
BookMapper.java
(.../mapper/
)package com.example.bbs.persist.mapper; import com.example.bbs.persist.entity.Book; import org.apache.ibatis.annotations.Param; import java.util.List; public interface BookMapper { int insert(Book book); Book findById(Integer id); List<Book> findByCondition(Book book); int update(Book book); int deleteById(Integer id); int updateStock(@Param("id") Integer id, @Param("amount") int amount); }
-
BookMapper.xml
(.../resources/mappers/
)<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.bbs.persist.mapper.BookMapper"> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO book (title, author, category_id, stock) VALUES (#{title}, #{author}, #{categoryId}, #{stock}) </insert> <select id="findById" resultType="Book"> SELECT * FROM book WHERE id = #{id} </select> <select id="findByCondition" parameterType="Book" resultType="Book"> SELECT * FROM book <where> <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="author != null and author != ''"> AND author LIKE CONCAT('%', #{author}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> </select> <update id="update" parameterType="Book"> UPDATE book <set> <if test="title != null and title != ''">title = #{title},</if> <if test="author != null and author != ''">author = #{author},</if> <if test="categoryId != null">category_id = #{categoryId},</if> <if test="stock != null">stock = #{stock},</if> </set> WHERE id = #{id} </update> <delete id="deleteById"> DELETE FROM book WHERE id = #{id} </delete> <update id="updateStock"> UPDATE book SET stock = stock + #{amount} WHERE id = #{id} AND stock + #{amount} >= 0 </update> </mapper>
-
ReaderMapper.java
&ReaderMapper.xml
:// ReaderMapper.java package com.example.bbs.persist.mapper; import com.example.bbs.persist.entity.Reader; public interface ReaderMapper { int insert(Reader reader); Reader findById(Integer id); Reader findByEmail(String email); }
<!-- ReaderMapper.xml --> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.bbs.persist.mapper.ReaderMapper"> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO reader (name, email) VALUES (#{name}, #{email}) </insert> <select id="findById" resultType="Reader"> SELECT * FROM reader WHERE id = #{id} </select> <select id="findByEmail" resultType="Reader"> SELECT * FROM reader WHERE email = #{email} </select> </mapper>
-
BorrowMapper.java
&BorrowMapper.xml
:// BorrowMapper.java package com.example.bbs.persist.mapper; import com.example.bbs.persist.entity.BorrowRecord; import org.apache.ibatis.annotations.Param; import java.util.Date; import java.util.List; public interface BorrowMapper { int insert(BorrowRecord record); BorrowRecord findById(Integer id); int update(BorrowRecord record); List<BorrowRecord> findRecordsByReaderId(Integer readerId); List<BorrowRecord> findOverdueRecords(@Param("currentDate") Date currentDate); }
<!-- BorrowMapper.xml --> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.bbs.persist.mapper.BorrowMapper"> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO borrow_record (book_id, reader_id, borrow_date, status) VALUES (#{bookId}, #{readerId}, #{borrowDate}, #{status}) </insert> <select id="findById" resultType="BorrowRecord"> SELECT * FROM borrow_record WHERE id = #{id} </select> <update id="update" parameterType="BorrowRecord"> UPDATE borrow_record <set> <if test="returnDate != null">return_date = #{returnDate},</if> <if test="status != null and status != ''">status = #{status},</if> </set> WHERE id = #{id} </update> <resultMap id="BorrowRecordWithDetailsResultMap" type="BorrowRecord"> <id property="id" column="borrow_id"/> <result property="borrowDate" column="borrow_date"/> <result property="returnDate" column="return_date"/> <result property="status" column="status"/> <association property="book" javaType="Book"> <id property="id" column="book_id"/> <result property="title" column="book_title"/> <result property="author" column="book_author"/> </association> <association property="reader" javaType="Reader"> <id property="id" column="reader_id"/> <result property="name" column="reader_name"/> </association> </resultMap> <select id="findRecordsByReaderId" resultMap="BorrowRecordWithDetailsResultMap"> SELECT br.id as borrow_id, br.borrow_date, br.return_date, br.status, b.id as book_id, b.title as book_title, b.author as book_author, r.id as reader_id, r.name as reader_name FROM borrow_record br JOIN book b ON br.book_id = b.id JOIN reader r ON br.reader_id = r.id WHERE br.reader_id = #{readerId} ORDER BY br.borrow_date DESC </select> <select id="findOverdueRecords" resultMap="BorrowRecordWithDetailsResultMap"> SELECT br.id as borrow_id, br.borrow_date, br.return_date, br.status, b.id as book_id, b.title as book_title, b.author as book_author, r.id as reader_id, r.name as reader_name FROM borrow_record br JOIN book b ON br.book_id = b.id JOIN reader r ON br.reader_id = r.id WHERE br.status = 'BORROWED' AND DATEDIFF(#{currentDate}, br.borrow_date) > 30 </select> </mapper>
第四部分:业务层与主程序开发
1. MybatisUtil.java
(工具类)
【操作】: 在bbs-main/src/main/java/com/example/bbs/util/
目录下创建。
package com.example.bbs.util;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MybatisUtil {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
// 在实际应用中,这里应该有更健壮的异常处理
throw new RuntimeException("Could not initialize SqlSessionFactory", e);
}
}
public static SqlSessionFactory getSqlSessionFactory() {
return sqlSessionFactory;
}
}
2. BorrowService.java
(核心业务)
【操作】: 在bbs-service/src/main/java/com/example/bbs/service/
目录下创建。
package com.example.bbs.service;
import com.example.bbs.persist.entity.Book;
import com.example.bbs.persist.entity.BorrowRecord;
import com.example.bbs.persist.mapper.BookMapper;
import com.example.bbs.persist.mapper.BorrowMapper;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.List;
public class BorrowService {
private static final Logger log = LoggerFactory.getLogger(BorrowService.class);
private final SqlSessionFactory sqlSessionFactory;
public BorrowService(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
public boolean borrowBook(int readerId, int bookId) {
log.info("开始处理借书业务:读者ID={}, 图书ID={}", readerId, bookId);
// 使用try-with-resources确保SqlSession被关闭,手动管理事务
try (SqlSession session = sqlSessionFactory.openSession(false)) {
try {
BookMapper bookMapper = session.getMapper(BookMapper.class);
BorrowMapper borrowMapper = session.getMapper(BorrowMapper.class);
// 1. 检查库存
Book book = bookMapper.findById(bookId);
if (book == null) {
log.error("借书失败:图书不存在。图书ID: {}", bookId);
return false;
}
if (book.getStock() <= 0) {
log.warn("借书失败:图书 [{}] 库存不足。", book.getTitle());
return false;
}
// 2. 扣减库存 (amount为-1)
int updatedStockRows = bookMapper.updateStock(bookId, -1);
if (updatedStockRows == 0) {
throw new RuntimeException("更新库存失败,可能在并发情况下库存已不足。");
}
log.debug("图书库存更新成功,图书ID: {},现有库存: {}", bookId, book.getStock() - 1);
// 3. 创建借阅记录
BorrowRecord record = new BorrowRecord();
record.setBookId(bookId);
record.setReaderId(readerId);
record.setBorrowDate(new Date());
record.setStatus("BORROWED");
borrowMapper.insert(record);
log.debug("借阅记录创建成功,新记录ID: {}", record.getId());
// 4. 所有操作成功,提交事务
session.commit();
log.info("借书业务成功完成。读者ID: {}, 图书: {}", readerId, book.getTitle());
return true;
} catch (Exception e) {
// 任何一步失败,回滚所有数据库操作
session.rollback();
log.error("借书业务发生异常,事务已回滚。", e);
return false;
}
}
}
public boolean returnBook(int borrowRecordId) {
log.info("开始处理还书业务:借阅记录ID={}", borrowRecordId);
try (SqlSession session = sqlSessionFactory.openSession(false)) {
try {
BorrowMapper borrowMapper = session.getMapper(BorrowMapper.class);
BookMapper bookMapper = session.getMapper(BookMapper.class);
// 1. 查找借阅记录
BorrowRecord record = borrowMapper.findById(borrowRecordId);
if (record == null) {
log.error("还书失败:借阅记录不存在。记录ID: {}", borrowRecordId);
return false;
}
if ("RETURNED".equals(record.getStatus())) {
log.warn("还书失败:该书已归还。记录ID: {}", borrowRecordId);
return false;
}
// 2. 增加图书库存 (amount为+1)
bookMapper.updateStock(record.getBookId(), 1);
log.debug("图书库存已归还,图书ID: {}", record.getBookId());
// 3. 更新借阅记录状态
BorrowRecord recordToUpdate = new BorrowRecord();
recordToUpdate.setId(borrowRecordId);
recordToUpdate.setStatus("RETURNED");
recordToUpdate.setReturnDate(new Date());
borrowMapper.update(recordToUpdate);
log.debug("借阅记录状态已更新为RETURNED,记录ID: {}", borrowRecordId);
// 4. 提交事务
session.commit();
log.info("还书业务成功完成。记录ID: {}", borrowRecordId);
return true;
} catch (Exception e) {
session.rollback();
log.error("还书业务发生异常,事务已回滚。", e);
return false;
}
}
}
}
3. MainApplication.java
(主程序)
【操作】: 在bbs-main/src/main/java/com/example/bbs/main/
目录下创建。
package com.example.bbs.main;
import com.example.bbs.persist.entity.Book;
import com.example.bbs.persist.entity.BorrowRecord;
import com.example.bbs.persist.mapper.BookMapper;
import com.example.bbs.persist.mapper.BorrowMapper;
import com.example.bbs.service.BorrowService;
import com.example.bbs.util.MybatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import java.util.List;
public class MainApplication {
public static void main(String[] args) {
SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
BorrowService borrowService = new BorrowService(sqlSessionFactory);
System.out.println("\n========= 业务场景模拟开始 =========\n");
// --- 场景一: 小明借阅 '深入理解Java虚拟机' ---
System.out.println("--- 场景一: 小明(ID=1)借阅 '深入理解Java虚拟机'(ID=2) ---");
borrowService.borrowBook(1, 2);
// --- 场景二: 小红尝试借阅库存为0的 'Effective Java' ---
System.out.println("\n--- 场景二: 小红(ID=2)尝试借阅 'Effective Java'(ID=4), 此书库存为0 ---");
borrowService.borrowBook(2, 4);
// --- 场景三: 小明归还他借的第一本书 ---
// 我们需要先查到小明借的第一本书的记录ID
try (SqlSession session = sqlSessionFactory.openSession()) {
BorrowMapper borrowMapper = session.getMapper(BorrowMapper.class);
List<BorrowRecord> xiaomingRecords = borrowMapper.findRecordsByReaderId(1);
if (!xiaomingRecords.isEmpty() && "BORROWED".equals(xiaomingRecords.get(0).getStatus())) {
int recordIdToReturn = xiaomingRecords.get(0).getId();
System.out.printf("\n--- 场景三: 小明归还图书, 借阅记录ID: %d ---\n", recordIdToReturn);
borrowService.returnBook(recordIdToReturn);
}
}
// --- 场景四: 动态查询图书 ---
System.out.println("\n--- 场景四: 动态查询书名包含'入门', 作者是'项目导师'的图书 ---");
try (SqlSession session = sqlSessionFactory.openSession()) {
BookMapper bookMapper = session.getMapper(BookMapper.class);
Book queryCondition = new Book();
queryCondition.setTitle("入门");
queryCondition.setAuthor("项目导师");
List<Book> books = bookMapper.findByCondition(queryCondition);
System.out.println("查询结果:");
books.forEach(System.out::println);
}
System.out.println("\n========= 业务场景模拟结束 =========\n");
}
}
第五部分:项目构建与运行
【操作】:
- 构建: 在IDEA的Maven面板中,找到父项目
BookBorrowSys
,展开Lifecycle
,双击clean
,然后再双击install
。或者在项目根目录打开终端,执行mvn clean install
。 - 运行: 找到
bbs-main
模块下的MainApplication.java
,右键点击,选择Run 'MainApplication.main()'
。 - 观察控制台: 查看控制台打印的日志和输出,验证业务流程是否按预期执行。你将能清晰地看到Mybatis打印的SQL语句、SLF4J输出的业务日志以及最终的业务结果。
总结
至此,你已拥有了一份从数据库到最终应用程序的完整、可运行的项目代码和操作指南。这个项目不仅仅是代码的堆砌,它是一个有机的整体,展现了:
- 分层架构的思想。
- Maven如何组织和管理复杂项目。
- Mybatis如何处理各种数据库操作,特别是强大的动态SQL和关联查询。
- 事务在核心业务中的重要性。
- 日志如何帮助我们追踪和调试程序。
请务必亲手完成这个项目,它将是你技术成长道路上一个坚实的里程碑。