前言:为什么我们需要持久层框架?
在Java企业级应用开发中,数据库访问一直是核心环节。传统JDBC编程需要开发者处理大量重复性工作,而MyBatis作为优秀的持久层框架,通过对象关系映射显著提升了开发效率。
在开始正式讲解技术之前,让我们先深入思考一个场景:假设你现在要开发一个电商系统,需要将用户的信息(比如用户名、密码)保存到数据库中。如果只用最基础的JDBC技术,你会面对怎样的开发体验?
第一阶段:问题锚定(解决什么痛点?)
1.1 企业开发中的痛点:JDBC的"七宗罪"
在早期的Java Web开发中,开发者直接使用JDBC操作数据库,面临着诸多问题。让我们通过一个完整的代码示例,真实感受一下这种痛苦。
1.1.1 JDBC基础操作示例
public class UserDAO {
// 增:添加用户
public void addUser(User user) {
Connection conn = null;
PreparedStatement ps = null;
try {
// 1. 加载驱动(每次都要写)
Class.forName("com.mysql.jdbc.Driver");
// 2. 获取连接(每次都要写)
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/shop?useSSL=false",
"root",
"123456"
);
// 3. 编写SQL(硬编码在Java中)
String sql = "insert into user(username, password, email, phone, create_time) values (?, ?, ?, ?, ?)";
ps = conn.prepareStatement(sql);
// 4. 设置参数(繁琐的手动设置)
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setString(3, user.getEmail());
ps.setString(4, user.getPhone());
ps.setTimestamp(5, new Timestamp(user.getCreateTime().getTime()));
// 5. 执行
ps.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 释放资源(最容易忘记的一步)
try {
if (ps != null) ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// 查:根据ID查询用户
public User getUserById(int id) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/shop?useSSL=false",
"root",
"123456"
);
String sql = "select * from user where id = ?";
ps = conn.prepareStatement(sql);
ps.setInt(1, id);
rs = ps.executeQuery();
// 7. 结果集映射(最繁琐的部分)
if (rs.next()) {
user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setEmail(rs.getString("email"));
user.setPhone(rs.getString("phone"));
user.setCreateTime(rs.getTimestamp("create_time"));
// 如果表有20个字段,这里就要写20行...
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 8. 释放ResultSet、Statement、Connection
try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (ps != null) ps.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
return user;
}
// 查:条件查询(动态SQL的噩梦)
public List<User> searchUsers(String username, String email, Integer age) {
// 噩梦开始:需要手动拼接SQL
StringBuilder sql = new StringBuilder("select * from user where 1=1 ");
List<Object> params = new ArrayList<>();
if (username != null && !username.isEmpty()) {
sql.append(" and username like ? ");
params.add("%" + username + "%");
}
if (email != null && !email.isEmpty()) {
sql.append(" and email = ? ");
params.add(email);
}
if (age != null) {
sql.append(" and age = ? ");
params.add(age);
}
// 还要处理排序、分页...
sql.append(" order by id desc limit ?, ?");
params.add(0); // offset
params.add(10); // limit
// 然后又要写一大段PreparedStatement设置参数的代码...
// 如果条件有10个,参数设置就要写10个setXXX
return null; // 省略具体实现
}
}
1.1.2 深入分析JDBC的七宗罪
第一宗罪:代码冗余度极高
从上面的例子可以看出,每个数据库操作都要重复以下步骤:
-
加载驱动
-
获取连接
-
创建Statement/PreparedStatement
-
设置参数
-
执行SQL
-
处理结果集
-
释放资源
这些样板代码占据了开发人员60%以上的精力,真正的业务逻辑反而被淹没在大量的模板代码中。
第二宗罪:SQL耦合硬编码
SQL语句以字符串形式散落在Java代码的各处:
String sql = "select * from user where id = ?";
String sql2 = "insert into user(username, password) values(?, ?)";
String sql3 = "update user set username = ? where id = ?";
这种方式带来的问题:
-
修改困难:修改SQL意味着要修改Java类并重新编译
-
可读性差:长SQL字符串在Java中难以格式化和阅读
-
无法复用:相似的SQL需要在不同地方重复编写
第三宗罪:结果集映射繁琐
从ResultSet中取出数据并封装到Java对象的过程,是JDBC开发中最令人厌烦的部分:
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setEmail(rs.getString("email"));
user.setPhone(rs.getString("phone"));
user.setAddress(rs.getString("address"));
user.setBirthday(rs.getDate("birthday"));
// ... 如果表有30个字段,这里就要写30行
这不仅繁琐,而且容易出错。如果数据库字段名变更,所有相关的getXXX调用都要修改。
第四宗罪:连接管理风险大
在finally块中释放资源是JDBC开发中最容易出错的环节:
finally {
try {
if (rs != null) rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (ps != null) ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
常见问题:
-
忘记释放连接:导致数据库连接资源耗尽,系统崩溃
-
异常处理不当:一个资源释放异常可能影响其他资源的释放
-
代码臃肿:释放资源的代码往往比业务代码还长
第五宗罪:动态SQL构建困难
当需要根据不同的查询条件动态拼接SQL时,JDBC的方式简直是一场噩梦:
StringBuilder sql = new StringBuilder("select * from user where 1=1 ");
List<Object> params = new ArrayList<>();
if (condition1 != null) {
sql.append(" and field1 = ?");
params.add(condition1);
}
if (condition2 != null) {
sql.append(" and field2 like ?");
params.add("%" + condition2 + "%");
}
if (condition3 != null) {
sql.append(" and field3 in (");
for (int i = 0; i < condition3.size(); i++) {
sql.append(i == 0 ? "?" : ",?");
}
sql.append(")");
params.addAll(condition3);
}
// 继续添加其他条件...
// 然后再用PreparedStatement设置这些参数
PreparedStatement ps = conn.prepareStatement(sql.toString());
for (int i = 0; i < params.size(); i++) {
ps.setObject(i + 1, params.get(i));
}
这种方式不仅容易出错,而且难以维护。每增加一个查询条件,都要修改代码。
第六宗罪:事务控制复杂
手动控制事务的提交和回滚,稍有不慎就会导致数据不一致:
public void transferMoney(int fromId, int toId, BigDecimal amount) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // 开启事务
// 扣减转出账户余额
updateBalance(fromId, amount.negate(), conn);
// 增加转入账户余额
updateBalance(toId, amount, conn);
conn.commit(); // 提交事务
} catch (Exception e) {
if (conn != null) {
try {
conn.rollback(); // 回滚事务
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
private void updateBalance(int userId, BigDecimal amount, Connection conn) throws SQLException {
// 注意:这里要使用传入的Connection,确保在同一事务中
try (PreparedStatement ps = conn.prepareStatement(
"update user set balance = balance + ? where id = ?")) {
ps.setBigDecimal(1, amount);
ps.setInt(2, userId);
ps.executeUpdate();
}
}
这种编程式事务的缺点:
-
代码侵入性强:事务控制代码混杂在业务逻辑中
-
易出错:忘记commit或rollback会导致严重问题
-
难以复用:每个需要事务的方法都要写类似的代码
第七宗罪:缓存机制缺失
每次查询都直接访问数据库,在高并发场景下,数据库压力巨大:
// 每次调用都查询数据库
public Product getProduct(int id) {
// 每次都执行SQL:select * from product where id = ?
return queryProductFromDB(id);
}
// 即使1秒内查询同一个商品1000次,也要执行1000次数据库查询
for (int i = 0; i < 1000; i++) {
Product p = getProduct(1); // 每次都查数据库
}
没有缓存带来的问题:
-
数据库负载高:重复查询浪费数据库资源
-
响应速度慢:每次都要经过网络通信和数据库处理
-
并发能力差:数据库连接数有限,无法支撑高并发
1.2 MyBatis的定位:解决痛点的"优雅方案"
一句话说清核心价值 :
MyBatis是一款基于Java的持久层框架,支持通过XML或注解方式实现对象与数据库表的映射。相较于传统JDBC需要手动编写SQL语句和处理结果集,MyBatis通过配置方式简化了90%的常规数据库操作代码。
核心特性对比:
| 特性 | JDBC | MyBatis |
|---|---|---|
| SQL管理 | SQL硬编码在Java中 | SQL解耦到XML或注解 |
| 自动映射 | 手动从ResultSet取值 | 自动将结果集映射为Java对象 |
| 动态SQL | 手动拼接字符串 | 提供动态SQL标签 |
| 事务管理 | 编程式事务 | 声明式事务支持 |
| 缓存机制 | 无 | 内置一级和二级缓存 |
| 代码量 | 大量样板代码 | 只需关注SQL和结果映射 |
技术边界说明:
能做什么:
-
简化数据库操作:通过SQL映射,让开发者只关注SQL本身,MyBatis处理JDBC的繁琐细节
-
灵活的动态SQL :通过
<if>、<where>、<foreach>等标签,动态构建复杂查询 -
管理数据库连接和事务:常与Spring整合实现声明式事务管理
-
提供缓存机制:一级缓存(SqlSession级别)和二级缓存(Mapper级别)提升查询效率
-
支持存储过程:可以调用数据库存储过程
不能做什么(或者说需要结合其他技术做的):
-
不是分布式解决方案:MyBatis本身不处理分布式事务,需要结合分布式事务中间件
-
不是业务容器:需要依赖Spring这样的IOC容器来管理其生命周期
-
不是全自动化ORM:相比Hibernate/JPA,MyBatis需要开发者编写SQL,灵活性更高但工作量略大
-
不处理连接池:通常需要集成第三方连接池如HikariCP、Druid
1.3 MyBatis-Plus的诞生:为简化而生
尽管MyBatis已经简化了很多,但开发者依然要为每个实体类编写重复的CRUD方法(比如insert、selectById、updateById)和对应的XML映射。例如,一个典型的用户表,你可能需要写:
<!-- UserMapper.xml -->
<insert id="insert">insert into user(...) values(...)</insert>
<delete id="deleteById">delete from user where id = #{id}</delete>
<update id="updateById">update user set ... where id = #{id}</update>
<select id="selectById">select * from user where id = #{id}</select>
<select id="selectAll">select * from user</select>
<select id="selectPage">select * from user limit #{offset}, #{size}</select>
这些代码每个实体类都要写一遍,极其枯燥。为了解决这一"最后的痛点",MyBatis-Plus应运而生。
一句话说清核心价值 :
MyBatis-Plus(简称MP)是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。
核心理念 :
MP的设计哲学可以总结为**"约定优于配置"** 。它假设了一些通用开发场景(例如,主键名为id,逻辑删除字段为deleted),并提供了默认实现。您只需遵循这些约定,就能以极少的代码量完成大量工作。
技术边界说明:
能做什么:
-
无侵入性:只做增强不做改变,对现有MyBatis项目无影响,可随时引入并与原生功能混合使用
-
代码极简 :对于单表的增删改查,无需编写任何XML SQL语句,通过内置的
BaseMapper提供一切 -
功能强大:内置分页、逻辑删除、乐观锁、自动填充、性能分析等企业级功能
-
类型安全 :独创的
LambdaQueryWrapper让您告别手写字段名,通过方法引用构建查询条件 -
内置代码生成器:一键生成Controller、Service、Mapper、Entity、XML全系列代码
-
支持多种数据库:MySQL、Oracle、DB2、H2、HSQL、SQLite、PostgreSQL、SQLServer等
不能做什么:
-
复杂的多表关联查询:最终还是需要手写SQL(但可以和MyBatis原生功能无缝协作)
-
不改变MyBatis核心执行逻辑:只是在此基础上提供便捷工具
-
不替代SQL优化:生成的SQL可能不是最优的,复杂场景仍需人工优化
第二阶段:基础认知(核心原理/语法)
2.1 MyBatis核心概念拆解
为了理解MyBatis是如何工作的,我们需要先认识它的几个核心"角色"。这些概念看似抽象,但通过生动的类比,可以轻松掌握。
2.1.1 核心组件角色定义
| 组件 | 角色 | 作用域 | 类比 |
|---|---|---|---|
| SqlSessionFactoryBuilder | 建造者 | 方法级 | 施工队队长 |
| SqlSessionFactory | 工厂 | 应用级(单例) | 厨房本身 |
| SqlSession | 会话 | 请求/方法级 | 一次烹饪过程 |
| Mapper接口 | 契约 | 应用级 | 菜单 |
| Mapper XML | 实现 | 应用级 | 食谱 |
| Mapper代理 | 执行者 | 会话级 | 厨师 |
2.1.2 大白话类比:餐馆的故事
想象你开了一家餐馆(应用程序):
SqlSessionFactoryBuilder(施工队队长):
-
角色:负责设计厨房图纸并监督装修的工程师
-
工作:读取配置文件,指挥建造过程,最终造出一个厨房(SqlSessionFactory)
-
特点:一旦厨房建好,他就退休了(方法级作用域,用完即弃)
SqlSessionFactory(厨房本身):
-
角色:餐馆的核心资产,灶台、水槽、冰箱都固定在那里
-
工作:随时准备开工,生产厨师(SqlSession)
-
特点:整个餐馆生命周期内只有一个厨房(应用级作用域/单例)
SqlSession(一次烹饪过程):
-
角色:厨师的一次烹饪过程
-
工作:每次有客人点餐,厨师就开始工作,做菜(执行SQL)
-
特点:做完菜,这次烹饪过程就结束,下次点餐再重新开始(不是线程安全的,用完即关)
Mapper接口(菜单):
-
角色:挂在墙上的菜单,列出所有可点的菜品(方法)
-
工作:告诉客人有哪些选择
Mapper XML(食谱):
- 角色:后厨的食谱,详细记录每个菜品怎么做(SQL语句)
Mapper代理(厨师):
-
角色:真正的执行者,根据菜单(接口)找到食谱(XML),然后做菜(执行SQL)
-
工作:当你点"鱼香肉丝"(调用接口方法),厨师就会按照鱼香肉丝的食谱来烹饪
2.1.3 MyBatis工作流程源码级解析
让我们深入源码,看看MyBatis是如何工作的。以下是一个简化的源码执行流程:
// 用户代码
public class MyBatisDemo {
public static void main(String[] args) throws IOException {
// 1. 读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 打开SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
// 4. 获取Mapper代理对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 5. 执行方法
User user = mapper.selectById(1);
System.out.println(user);
}
}
}
步骤1:解析配置文件(SqlSessionFactoryBuilder.build)
当调用build(inputStream)时,MyBatis内部发生:
// SqlSessionFactoryBuilder类(简化版)
public SqlSessionFactory build(InputStream inputStream) {
// 创建XML配置解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null, null);
// 解析配置文件,生成Configuration对象
Configuration config = parser.parse();
// 使用Configuration构建SqlSessionFactory
return build(config);
}
// XMLConfigBuilder.parse()内部
public Configuration parse() {
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
// 解析各个配置节点
private void parseConfiguration(XNode root) {
try {
// 解析properties
propertiesElement(root.evalNode("properties"));
// 解析settings
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 解析typeAliases
typeAliasesElement(root.evalNode("typeAliases"));
// 解析plugins
pluginElement(root.evalNode("plugins"));
// 解析environments(数据库环境)
environmentsElement(root.evalNode("environments"));
// 解析mappers(注册Mapper)
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
步骤2:构建SqlSessionFactory
Configuration对象是MyBatis的核心,它包含了所有配置信息、Mapper注册信息、SQL语句等。SqlSessionFactory的默认实现是DefaultSqlSessionFactory,它持有这个Configuration对象。
步骤3:获取SqlSession
// DefaultSqlSessionFactory.openSession()
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 从数据源获取环境
final Environment environment = configuration.getEnvironment();
// 获取事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 创建事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 创建执行器(Executor)
final Executor executor = configuration.newExecutor(tx, execType);
// 创建DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
}
}
Executor类型有三种:
-
SIMPLE:每次执行都创建新的Statement(默认)
-
REUSE:复用Statement
-
BATCH:批量执行,适合批量更新操作
步骤4:获取Mapper代理对象
// DefaultSqlSession.getMapper()
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
// Configuration.getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
// MapperRegistry.getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 获取Mapper代理工厂
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 创建代理实例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
// MapperProxyFactory.newInstance()
public T newInstance(SqlSession sqlSession) {
// 创建MapperProxy(InvocationHandler实现)
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 使用JDK动态代理创建代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy);
}
步骤5:执行Mapper方法
// MapperProxy.invoke() - 代理方法调用
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
// 如果是Object的方法(如toString、hashCode),直接调用原方法
return method.invoke(this, args);
}
// 获取缓存的方法或创建新的MapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行SQL
return mapperMethod.execute(sqlSession, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
// MapperMethod.execute()
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 根据SQL命令类型(INSERT、UPDATE、DELETE、SELECT)执行不同逻辑
switch (command.getType()) {
case INSERT: {
// 转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 执行插入
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// 根据方法返回类型执行不同查询
if (method.returnsVoid() && method.hasResultHandler()) {
// 有结果处理器
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 返回集合
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 返回Map
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 返回游标
result = executeForCursor(sqlSession, args);
} else {
// 返回单个对象
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
最终:SqlSession执行SQL
// DefaultSqlSession.selectOne()
public <T> T selectOne(String statement, Object parameter) {
// selectOne其实就是selectList获取第一个
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 从Configuration中获取MappedStatement(包含SQL信息)
MappedStatement ms = configuration.getMappedStatement(statement);
// 执行器执行查询
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
}
}
执行器Executor执行查询:
// CachingExecutor(二级缓存执行器)query()
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 生成缓存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 查询缓存
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
// 尝试从二级缓存获取
List<E> list = (List<E>) cache.getObject(key);
if (list == null) {
// 缓存未命中,委托给BaseExecutor执行
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
cache.putObject(key, list);
}
return list;
}
// 没有二级缓存,直接执行
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
// BaseExecutor.query()
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
try {
// 检查是否从一级缓存获取
List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 一级缓存命中
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 一级缓存未命中,从数据库查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
return list;
} finally {
// 清空本地缓存(如果需要)
// ...
}
}
2.2 MyBatis全局配置详解(mybatis-config.xml)
MyBatis的配置文件是框架的基石,包含了影响MyBatis行为的所有设置。下面我们详细解析每个配置项的作用和使用场景。
2.2.1 配置文件完整结构
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 1. properties:引入外部属性文件 -->
<properties resource="db.properties">
<property name="username" value="dev_user"/>
</properties>
<!-- 2. settings:全局配置参数 -->
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="false"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
<!-- 3. typeAliases:类型别名 -->
<typeAliases>
<typeAlias alias="User" type="com.example.entity.User"/>
<package name="com.example.entity"/>
</typeAliases>
<!-- 4. typeHandlers:类型处理器 -->
<typeHandlers>
<typeHandler handler="com.example.type.MyTypeHandler"/>
<package name="com.example.type"/>
</typeHandlers>
<!-- 5. plugins:插件(拦截器) -->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>
<!-- 6. environments:环境配置(可以配置多个环境) -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<environment id="production">
<transactionManager type="MANAGED"/>
<dataSource type="JNDI">
<property name="data_source" value="java:comp/env/jdbc/mybatis"/>
</dataSource>
</environment>
</environments>
<!-- 7. databaseIdProvider:数据库厂商标识 -->
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="SQL Server" value="sqlserver"/>
</databaseIdProvider>
<!-- 8. mappers:映射器配置 -->
<mappers>
<mapper resource="com/example/mapper/UserMapper.xml"/>
<mapper class="com.example.mapper.UserMapper"/>
<package name="com.example.mapper"/>
</mappers>
</configuration>
2.2.2 每个配置项详细解析
1. properties(属性)
用于引入外部属性文件,可以在配置文件中使用占位符${propertyName}。
<!-- db.properties文件内容 -->
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis_db
username=root
password=123456
<!-- 引入并使用 -->
<properties resource="db.properties">
<!-- 可以覆盖或添加属性 -->
<property name="username" value="dev_user"/>
</properties>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
属性加载顺序(后加载的会覆盖先加载的):
-
在
properties元素体内指定的属性 -
从资源路径加载的属性(
resource属性) -
从URL加载的属性(
url属性) -
方法参数传递的属性(如
build(inputStream, properties))
2. settings(设置)
这是MyBatis中最关键的配置,会改变框架的运行时行为。
| 设置名 | 描述 | 有效值 | 默认值 |
|---|---|---|---|
| cacheEnabled | 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存 | true/false | true |
| lazyLoadingEnabled | 延迟加载的全局开关 | true/false | false |
| aggressiveLazyLoading | 开启时,任何方法的调用都会加载该对象的所有延迟加载属性 | true/false | false |
| multipleResultSetsEnabled | 是否允许单个语句返回多结果集 | true/false | true |
| useColumnLabel | 使用列标签代替列名 | true/false | true |
| useGeneratedKeys | 允许JDBC支持自动生成主键 | true/false | false |
| autoMappingBehavior | 指定MyBatis应如何自动映射列到字段或属性 | NONE, PARTIAL, FULL | PARTIAL |
| autoMappingUnknownColumnBehavior | 指定发现自动映射目标未知列的行为 | NONE, WARNING, FAILING | NONE |
| defaultExecutorType | 配置默认的执行器 | SIMPLE, REUSE, BATCH | SIMPLE |
| defaultStatementTimeout | 设置数据库查询超时时间 | 任意正整数 | 未设置 |
| defaultFetchSize | 设置驱动结果集的获取大小 | 任意正整数 | 未设置 |
| safeRowBoundsEnabled | 允许在嵌套语句中使用分页 | true/false | false |
| mapUnderscoreToCamelCase | 是否开启自动驼峰命名规则映射 | true/false | false |
| localCacheScope | MyBatis本地缓存作用域 | SESSION, STATEMENT | SESSION |
| jdbcTypeForNull | 当没有为参数提供特定的JDBC类型时,指定JDBC类型 | NULL, VARCHAR, OTHER | OTHER |
| lazyLoadTriggerMethods | 指定触发延迟加载的方法 | 逗号分隔的方法列表 | equals,clone,hashCode,toString |
| defaultScriptingLanguage | 指定动态SQL生成使用的默认脚本语言 | 类型别名或全限定类名 | org.apache.ibatis.scripting.xmltags.XMLLanguageDriver |
| callSettersOnNulls | 当结果集中值为Null时,是否调用映射对象的setter方法 | true/false | false |
| logPrefix | 指定日志前缀 | 任何字符串 | 未设置 |
| logImpl | 指定MyBatis所用日志的具体实现 | SLF4J, LOG4J, LOG4J2, JDK_LOGGING, COMMONS_LOGGING, STDOUT_LOGGING, NO_LOGGING | 未设置 |
| proxyFactory | 指定Mybatis创建延迟加载对象所用代理工具 | CGLIB, JAVASSIST | JAVASSIST |
重要配置示例:
xml
<settings>
<!-- 开启驼峰命名映射:数据库user_name -> Java userName -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 使用标准日志输出SQL -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- 全局开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 设置超时时间为30秒 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 批量执行器,提高批量操作性能 -->
<setting name="defaultExecutorType" value="BATCH"/>
</settings>
3. typeAliases(类型别名)
为Java类型设置短的名字,减少XML中全限定名的冗余。
xml
<!-- 方式一:为单个类设置别名 -->
<typeAliases>
<typeAlias alias="User" type="com.example.entity.User"/>
<typeAlias alias="Product" type="com.example.entity.Product"/>
</typeAliases>
<!-- 方式二:扫描包,自动使用Bean的首字母小写的非限定类名作为别名 -->
<typeAliases>
<package name="com.example.entity"/>
</typeAliases>
<!-- com.example.entity.User 的别名为 user -->
内置类型别名(无需配置即可使用):
| 别名 | Java类型 |
|---|---|
_byte |
byte |
_long |
long |
_short |
short |
_int |
int |
_integer |
int |
_double |
double |
_float |
float |
_boolean |
boolean |
string |
String |
byte |
Byte |
long |
Long |
short |
Short |
int |
Integer |
integer |
Integer |
double |
Double |
float |
Float |
boolean |
Boolean |
date |
Date |
decimal |
BigDecimal |
bigdecimal |
BigDecimal |
object |
Object |
map |
Map |
hashmap |
HashMap |
list |
List |
arraylist |
ArrayList |
collection |
Collection |
iterator |
Iterator |
4. typeHandlers(类型处理器)
类型处理器用于Java类型和JDBC类型之间的转换。MyBatis内置了丰富的类型处理器,基本满足日常需求。
内置类型处理器示例:
// BooleanTypeHandler - boolean与BOOLEAN/INT转换
// StringTypeHandler - String与VARCHAR/CHAR转换
// DateTypeHandler - Date与TIMESTAMP转换
// BigDecimalTypeHandler - BigDecimal与DECIMAL/NUMERIC转换
自定义类型处理器:
当需要特殊类型转换时(如将JSON字符串自动转换为Java对象),可以自定义类型处理器。
// 1. 创建自定义类型处理器
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonListTypeHandler extends BaseTypeHandler<List<String>> {
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
try {
// 将List转换为JSON字符串
String json = mapper.writeValueAsString(parameter);
ps.setString(i, json);
} catch (JsonProcessingException e) {
throw new SQLException("Error converting List to JSON", e);
}
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json);
}
@Override
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json);
}
@Override
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json);
}
private List<String> parseJson(String json) {
if (json == null || json.isEmpty()) {
return new ArrayList<>();
}
try {
return mapper.readValue(json, new TypeReference<List<String>>() {});
} catch (IOException e) {
throw new RuntimeException("Error parsing JSON to List", e);
}
}
}
// 2. 配置文件中注册
<typeHandlers>
<typeHandler handler="com.example.type.JsonListTypeHandler"/>
</typeHandlers>
// 3. 在实体类中使用
public class User {
private Integer id;
private String name;
// 数据库字段hobbies存储JSON字符串:["篮球","足球","读书"]
private List<String> hobbies;
// getter/setter
}
5. plugins(插件)
插件是MyBatis提供的最强大的功能之一,允许拦截核心对象的方法调用。可以拦截的四大对象:
-
Executor (update, query, commit, rollback等)
-
ParameterHandler (getParameterObject, setParameters)
-
ResultSetHandler (handleResultSets, handleOutputParameters)
-
StatementHandler (prepare, parameterize, batch, update, query)
// 自定义插件示例:SQL执行时间监控
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
),
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)
})
public class SqlExecuteTimeInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(SqlExecuteTimeInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 执行原方法
return invocation.proceed();
} finally {
long endTime = System.currentTimeMillis();
long executeTime = endTime - startTime;
// 获取SQL信息
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
String sqlId = ms.getId();
log.info("SQL执行监控 - ID: {}, 耗时: {}ms", sqlId, executeTime);
// 如果超过阈值,记录警告
if (executeTime > 1000) {
log.warn("慢SQL告警 - ID: {}, 耗时: {}ms", sqlId, executeTime);
}
}
}
@Override
public Object plugin(Object target) {
// 使用Plugin.wrap生成代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以接收插件参数
String threshold = properties.getProperty("threshold");
log.info("慢SQL阈值设置为: {}ms", threshold);
}
}
// 配置文件注册插件
<plugins>
<plugin interceptor="com.example.plugin.SqlExecuteTimeInterceptor">
<property name="threshold" value="500"/>
</plugin>
</plugins>
6. environments(环境配置)
可以配置多个环境(开发、测试、生产),通过default属性指定默认环境。
<environments default="development">
<!-- 开发环境:使用JDBC事务管理、连接池 -->
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<!-- 生产环境:使用容器管理事务、JNDI数据源 -->
<environment id="production">
<transactionManager type="MANAGED"/>
<dataSource type="JNDI">
<property name="data_source" value="java:comp/env/jdbc/mybatis"/>
</dataSource>
</environment>
</environments>
transactionManager类型:
-
JDBC:直接使用JDBC的提交和回滚,依赖从数据源获取的连接来管理事务范围
-
MANAGED:从不提交或回滚连接,让容器(如Spring)管理事务
dataSource类型:
-
UNPOOLED:每次请求时打开和关闭连接,适合简单应用
-
POOLED:使用连接池管理连接,避免创建新的连接实例时必需的初始化和认证时间
-
JNDI:在如EJB或应用服务器这类容器中使用,容器集中或在外部配置数据源
7. databaseIdProvider(数据库厂商标识)
支持多数据库厂商,可以在同一个Mapper中针对不同数据库编写不同的SQL。
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="SQL Server" value="sqlserver"/>
</databaseIdProvider>
<!-- Mapper中使用 -->
<select id="selectUser" resultType="User">
<if test="_databaseId == 'mysql'">
select * from user limit 1
</if>
<if test="_databaseId == 'oracle'">
select * from user where rownum <= 1
</if>
</select>
8. mappers(映射器配置)
告诉MyBatis去哪里找SQL映射语句。
<!-- 方式一:使用类路径资源 -->
<mappers>
<mapper resource="com/example/mapper/UserMapper.xml"/>
<mapper resource="com/example/mapper/ProductMapper.xml"/>
</mappers>
<!-- 方式二:使用完全限定资源URL -->
<mappers>
<mapper url="file:///var/mappers/UserMapper.xml"/>
</mappers>
<!-- 方式三:使用Mapper接口类 -->
<mappers>
<mapper class="com.example.mapper.UserMapper"/>
<mapper class="com.example.mapper.ProductMapper"/>
</mappers>
<!-- 方式四:扫描包(推荐) -->
<mappers>
<package name="com.example.mapper"/>
</mappers>
2.3 最小可运行示例:MyBatis的Hello World
让我们通过一个完整的示例,体验MyBatis的工作流程。
2.3.1 准备工作:创建表和Java项目
创建数据库表:
CREATE DATABASE IF NOT EXISTS mybatis_demo DEFAULT CHARACTER SET utf8mb4;
USE mybatis_demo;
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
user_name VARCHAR(50) NOT NULL COMMENT '用户名',
password VARCHAR(100) NOT NULL COMMENT '密码',
email VARCHAR(100) COMMENT '邮箱',
phone VARCHAR(20) COMMENT '手机号',
age INT COMMENT '年龄',
gender TINYINT COMMENT '性别(1-男,2-女)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
INSERT INTO user(user_name, password, email, phone, age, gender) VALUES
('张三', '123456', 'zhangsan@example.com', '13800138001', 25, 1),
('李四', '123456', 'lisi@example.com', '13800138002', 30, 1),
('王五', '123456', 'wangwu@example.com', '13800138003', 28, 2);
2.3.2 创建Maven项目
<?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>mybatis-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- MyBatis核心依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 日志依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!-- Lombok简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
<scope>provided</scope>
</dependency>
<!-- JUnit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
</project>
2.3.3 创建实体类(User.java)
package com.example.entity;
import lombok.Data;
import java.util.Date;
/**
* 用户实体类
*/
@Data
public class User {
private Integer id;
private String userName; // 对应数据库字段 user_name
private String password;
private String email;
private String phone;
private Integer age;
private Integer gender; // 1-男, 2-女
private Date createTime;
private Date updateTime;
}
2.3.4 创建Mapper接口(UserMapper.java)
package com.example.mapper;
import com.example.entity.User;
import java.util.List;
/**
* 用户Mapper接口
*/
public interface UserMapper {
/**
* 根据ID查询用户
*/
User selectById(Integer id);
/**
* 查询所有用户
*/
List<User> selectAll();
/**
* 插入用户
*/
int insert(User user);
/**
* 更新用户
*/
int update(User user);
/**
* 根据ID删除用户
*/
int deleteById(Integer id);
/**
* 根据条件查询用户
*/
List<User> selectByCondition(String userName, Integer age);
}
2.3.5 创建Mapper XML文件(UserMapper.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">
<!-- namespace必须指向Mapper接口的全限定名 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 结果映射,用于字段名和属性名不一致的情况 -->
<resultMap id="BaseResultMap" type="User">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="user_name" property="userName" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
<result column="phone" property="phone" jdbcType="VARCHAR"/>
<result column="age" property="age" jdbcType="INTEGER"/>
<result column="gender" property="gender" jdbcType="TINYINT"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 基础列,避免写重复的列名 -->
<sql id="Base_Column_List">
id, user_name, password, email, phone, age, gender, create_time, update_time
</sql>
<!-- 根据ID查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM user
WHERE id = #{id}
</select>
<!-- 查询所有 -->
<select id="selectAll" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM user
ORDER BY id DESC
</select>
<!-- 插入用户,并返回自增主键 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (
user_name, password, email, phone, age, gender, create_time
) VALUES (
#{userName}, #{password}, #{email}, #{phone}, #{age}, #{gender}, NOW()
)
</insert>
<!-- 更新用户 -->
<update id="update">
UPDATE user
SET
user_name = #{userName},
password = #{password},
email = #{email},
phone = #{phone},
age = #{age},
gender = #{gender}
WHERE id = #{id}
</update>
<!-- 删除用户 -->
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
<!-- 条件查询(动态SQL示例) -->
<select id="selectByCondition" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM user
<where>
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
ORDER BY id DESC
</select>
</mapper>
2.3.6 创建MyBatis核心配置文件(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>
<!-- 引入外部属性文件 -->
<properties resource="db.properties"/>
<!-- 设置 -->
<settings>
<!-- 开启驼峰命名映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启日志 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- 设置超时时间 -->
<setting name="defaultStatementTimeout" value="25"/>
</settings>
<!-- 类型别名:扫描包,自动使用首字母小写的类名作为别名 -->
<typeAliases>
<package name="com.example.entity"/>
</typeAliases>
<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<!-- 使用JDBC事务管理 -->
<transactionManager type="JDBC"/>
<!-- 使用连接池 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- 映射器配置 -->
<mappers>
<mapper resource="com/example/mapper/UserMapper.xml"/>
</mappers>
</configuration>
2.3.7 创建数据库属性文件(db.properties)
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
jdbc.username=root
jdbc.password=123456
2.3.8 创建日志配置文件(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} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.example.mapper" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
2.3.9 创建测试类(MyBatisTest.java)
package com.example;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
public class MyBatisTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void setUp() throws Exception {
// 1. 读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 2. 构建SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void testSelectById() {
// 3. 打开SqlSession(try-with-resources自动关闭)
try (SqlSession session = sqlSessionFactory.openSession()) {
// 4. 获取Mapper代理对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 5. 执行查询
User user = mapper.selectById(1);
// 6. 输出结果
System.out.println("查询结果:");
System.out.println(user);
}
}
@Test
public void testSelectAll() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> userList = mapper.selectAll();
System.out.println("所有用户:");
userList.forEach(System.out::println);
}
}
@Test
public void testInsert() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 创建新用户
User user = new User();
user.setUserName("赵六");
user.setPassword("123456");
user.setEmail("zhaoliu@example.com");
user.setPhone("13800138004");
user.setAge(26);
user.setGender(2);
// 执行插入
int result = mapper.insert(user);
// 提交事务(默认是非自动提交)
session.commit();
System.out.println("插入结果:" + result);
System.out.println("生成的主键ID:" + user.getId());
}
}
@Test
public void testUpdate() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 查询要更新的用户
User user = mapper.selectById(1);
System.out.println("更新前:" + user);
// 修改信息
user.setEmail("new_email@example.com");
user.setPhone("19999999999");
// 执行更新
int result = mapper.update(user);
session.commit();
// 查询更新后的结果
User updatedUser = mapper.selectById(1);
System.out.println("更新后:" + updatedUser);
System.out.println("更新结果:" + result);
}
}
@Test
public void testDelete() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 执行删除
int result = mapper.deleteById(1);
session.commit();
System.out.println("删除结果:" + result);
// 验证删除
User user = mapper.selectById(1);
System.out.println("验证查询:" + user); // 应该为null
}
}
@Test
public void testSelectByCondition() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 测试不同条件组合
System.out.println("=== 按用户名模糊查询 ===");
List<User> list1 = mapper.selectByCondition("张", null);
list1.forEach(System.out::println);
System.out.println("=== 按年龄查询 ===");
List<User> list2 = mapper.selectByCondition(null, 30);
list2.forEach(System.out::println);
System.out.println("=== 组合条件查询 ===");
List<User> list3 = mapper.selectByCondition("李", 30);
list3.forEach(System.out::println);
}
}
}
2.4 MyBatis-Plus基础认知
了解了MyBatis的基础后,我们来看看MyBatis-Plus如何进一步简化开发。
2.4.1 MyBatis-Plus核心特性
MyBatis-Plus提供了丰富的特性,让开发效率倍增:
| 特性 | 描述 | 解决的问题 |
|---|---|---|
| 无侵入 | 只做增强不做改变,引入它不会对现有工程产生影响 | 可以平滑升级,与原生MyBatis共存 |
| 损耗小 | 启动即会自动注入基本CURD,性能基本无损耗 | 无需为每个实体编写基础CRUD方法 |
| 强大的CRUD操作 | 内置通用Mapper、通用Service,仅需少量配置即可实现单表大部分CRUD操作 | 减少90%以上的重复代码 |
| Lambda条件构造器 | 通过Lambda表达式,方便的编写各类查询条件,无需担心字段写错 | 类型安全,重构友好 |
| 主键自动生成 | 支持多达4种主键策略(内含分布式唯一ID生成器) | 解决分布式系统主键问题 |
| 支持ActiveRecord模式 | 实体类只需继承Model类即可进行强大的CRUD操作 | 更符合面向对象思维 |
| 代码生成器 | 采用代码或者Maven插件可快速生成Mapper、Model、Service、Controller层代码 | 极大提高开发效率 |
| 内置分页插件 | 基于MyBatis物理分页,开发者无需关心具体操作 | 简化分页实现 |
| 性能分析插件 | 可输出SQL语句以及其执行时间,快速揪出慢查询 | 性能优化利器 |
| 全局拦截插件 | 提供全表delete、update操作智能分析阻断 | 预防误操作 |
2.4.2 MyBatis-Plus与MyBatis的关系
MyBatis-Plus是站在MyBatis的肩膀上,提供了更高层次的抽象:
text
┌─────────────────────────────────────┐
│ 业务代码(Controller/Service) │
├─────────────────────────────────────┤
│ MyBatis-Plus(增强工具) │
│ ┌───────────────────────────────┐ │
│ │ 通用CRUD │ 条件构造器 │ │
│ │ 代码生成器│ 分页插件 │ │
│ │ 乐观锁 │ 逻辑删除 │ │
│ └───────────────────────────────┘ │
├─────────────────────────────────────┤
│ MyBatis(基础框架) │
│ ┌───────────────────────────────┐ │
│ │ SQL映射 │ 动态SQL │ │
│ │ 缓存管理 │ 事务管理 │ │
│ └───────────────────────────────┘ │
├─────────────────────────────────────┤
│ JDBC(底层驱动) │
├─────────────────────────────────────┤
│ 数据库(MySQL/Oracle等) │
└─────────────────────────────────────┘
核心区别:
-
MyBatis:提供SQL映射能力,让你优雅地写SQL
-
MyBatis-Plus :在MyBatis基础上,让你在大多数场景下不用写SQL
2.4.3 MyBatis-Plus快速入门
1. 添加依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
2. 配置文件(application.yml)
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
3. 实体类(使用注解)
MyBatis-Plus提供了一系列注解来精确描述实体类与数据库表的映射关系。
package com.example.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.util.Date;
@Data
@TableName("user") // 指定数据库表名
public class User {
@TableId(type = IdType.AUTO) // 主键自增
private Long id;
@TableField("user_name") // 指定数据库字段名(驼峰映射时可省略)
private String userName;
private String password;
private String email;
private String phone;
private Integer age;
private Integer gender;
@TableField(fill = FieldFill.INSERT) // 插入时自动填充
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充
private Date updateTime;
@TableLogic // 逻辑删除注解
private Integer deleted;
@Version // 乐观锁注解
private Integer version;
@TableField(exist = false) // 数据库不存在的字段
private String extraInfo;
}
注解详解:
| 注解 | 用途 | 属性说明 |
|---|---|---|
| @TableName | 标识实体类对应的表 | value:表名;schema:数据库schema;keepGlobalPrefix:是否保持全局表前缀 |
| @TableId | 标识主键字段 | value:主键字段名;type:主键类型(AUTO自增,ASSIGN_ID雪花算法,INPUT手动输入等) |
| @TableField | 标识非主键字段 | value:字段名;exist:是否为数据库字段;fill:自动填充策略;update:更新时set表达式 |
| @TableLogic | 逻辑删除 | value:逻辑未删除值;delval:逻辑删除值 |
| @Version | 乐观锁 | 用于实现乐观锁,更新时自动检查版本 |
| @EnumValue | 枚举字段 | 标记枚举字段在数据库中存储的值 |
4. Mapper接口
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承BaseMapper后,拥有了基础的CRUD方法
// 无需编写任何SQL和XML
// 如果有复杂查询,可以在这里定义方法,并在XML中实现
List<User> selectComplexList(@Param("name") String name);
}
BaseMapper提供的通用方法:
// 插入
int insert(T entity);
// 删除
int deleteById(Serializable id);
int deleteByMap(Map<String, Object> columnMap);
int delete(Wrapper<T> wrapper);
int deleteBatchIds(Collection<? extends Serializable> idList);
// 更新
int updateById(T entity);
int update(T entity, Wrapper<T> updateWrapper);
// 查询
T selectById(Serializable id);
List<T> selectBatchIds(Collection<? extends Serializable> idList);
List<T> selectByMap(Map<String, Object> columnMap);
T selectOne(Wrapper<T> queryWrapper);
Integer selectCount(Wrapper<T> queryWrapper);
List<T> selectList(Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(Wrapper<T> queryWrapper);
List<Object> selectObjs(Wrapper<T> queryWrapper);
Page<T> selectPage(Page<T> page, Wrapper<T> queryWrapper);
Page<Map<String, Object>> selectMapsPage(Page<T> page, Wrapper<T> queryWrapper);
5. Service层(最佳实践)
MP提供了更强大的Service层支持:
// Service接口
package com.example.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.User;
public interface UserService extends IService<User> {
// 可以添加自定义业务方法
User findByUserName(String userName);
boolean updatePassword(Long id, String newPassword);
}
// Service实现类
package com.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(rollbackFor = Exception.class)
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public User findByUserName(String userName) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, userName);
return this.getOne(wrapper);
}
@Override
public boolean updatePassword(Long id, String newPassword) {
User user = new User();
user.setId(id);
user.setPassword(newPassword);
return this.updateById(user);
}
}
IService提供的通用方法:
// 保存
boolean save(T entity);
boolean saveBatch(Collection<T> entityList);
boolean saveBatch(Collection<T> entityList, int batchSize);
// 保存或更新
boolean saveOrUpdate(T entity);
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
boolean saveOrUpdateBatch(Collection<T> entityList);
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);
// 删除
boolean removeById(Serializable id);
boolean removeByMap(Map<String, Object> columnMap);
boolean remove(Wrapper<T> queryWrapper);
boolean removeByIds(Collection<? extends Serializable> idList);
// 更新
boolean updateById(T entity);
boolean update(T entity, Wrapper<T> updateWrapper);
boolean updateBatchById(Collection<T> entityList);
boolean updateBatchById(Collection<T> entityList, int batchSize);
// 查询
T getById(Serializable id);
List<T> listByIds(Collection<? extends Serializable> idList);
List<T> listByMap(Map<String, Object> columnMap);
T getOne(Wrapper<T> queryWrapper);
Map<String, Object> getMap(Wrapper<T> queryWrapper);
int count(Wrapper<T> queryWrapper);
List<T> list(Wrapper<T> queryWrapper);
Page<T> page(Page<T> page, Wrapper<T> queryWrapper);
第三阶段:核心用法拆解
掌握了基础概念后,我们来深入探讨MyBatis和MyBatis-Plus在企业开发中的核心用法。
3.1 MyBatis核心用法详解
3.1.1 参数传递的多种方式
MyBatis支持多种参数传递方式,理解这些方式对于正确编写Mapper至关重要。
方式一:单参数传递
// Mapper接口
User selectById(Integer id);
User selectByName(String name);
// XML
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<select id="selectByName" resultType="User">
SELECT * FROM user WHERE user_name = #{name}
</select>
方式二:@Param注解(推荐)
当有多个参数时,使用@Param注解明确指定参数名。
// Mapper接口
User selectByCondition(@Param("userName") String userName,
@Param("age") Integer age);
// XML
<select id="selectByCondition" resultType="User">
SELECT * FROM user
WHERE user_name = #{userName}
AND age = #{age}
</select>
方式三:使用Map传递参数
// Mapper接口
List<User> selectByMap(Map<String, Object> params);
// 调用
Map<String, Object> params = new HashMap<>();
params.put("userName", "张三");
params.put("age", 25);
List<User> list = mapper.selectByMap(params);
// XML
<select id="selectByMap" resultType="User">
SELECT * FROM user
WHERE user_name = #{userName}
AND age = #{age}
</select>
方式四:使用JavaBean传递参数
// 参数对象
@Data
public class UserQueryParam {
private String userName;
private Integer ageMin;
private Integer ageMax;
private String email;
}
// Mapper接口
List<User> selectByQueryParam(UserQueryParam param);
// XML
<select id="selectByQueryParam" resultType="User">
SELECT * FROM user
<where>
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="ageMin != null">
AND age >= #{ageMin}
</if>
<if test="ageMax != null">
AND age <= #{ageMax}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</where>
</select>
方式五:集合/数组参数
// Mapper接口
List<User> selectByIds(@Param("ids") List<Integer> ids);
// XML(使用foreach遍历)
<select id="selectByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
3.1.2 结果映射的多种方式
方式一:自动映射(最简单)
当数据库字段名与Java属性名完全一致,或者开启了驼峰映射时,可以使用自动映射。
<!-- 开启驼峰映射后,user_name自动映射到userName -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
方式二:resultMap(最灵活)
resultMap是MyBatis中最强大的结果映射方式,可以处理复杂的映射关系。
<!-- 基础resultMap -->
<resultMap id="UserResultMap" type="User">
<!-- 主键映射 -->
<id column="id" property="id" jdbcType="INTEGER"/>
<!-- 普通字段映射 -->
<result column="user_name" property="userName" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
<result column="age" property="age" jdbcType="INTEGER"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 使用resultMap -->
<select id="selectById" resultMap="UserResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
方式三:关联查询(一对一)
// 订单实体类
@Data
public class Order {
private Integer id;
private String orderNo;
private Integer userId;
private BigDecimal amount;
private Date createTime;
// 关联的用户信息
private User user;
}
// Mapper接口
Order selectOrderWithUser(@Param("orderId") Integer orderId);
<!-- 方式1:嵌套结果(推荐) -->
<resultMap id="OrderWithUserResultMap" type="Order">
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
<result column="create_time" property="createTime"/>
<!-- 关联的用户信息 -->
<association property="user" javaType="User">
<id column="user_id" property="id"/>
<result column="user_name" property="userName"/>
<result column="email" property="email"/>
</association>
</resultMap>
<select id="selectOrderWithUser" resultMap="OrderWithUserResultMap">
SELECT
o.*,
u.id as user_id,
u.user_name,
u.email
FROM order o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.id = #{orderId}
</select>
<!-- 方式2:嵌套查询(可能导致N+1问题) -->
<resultMap id="OrderWithUserResultMap2" type="Order">
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
<result column="create_time" property="createTime"/>
<!-- 关联查询,select指定另一个查询语句 -->
<association property="user"
column="user_id"
select="com.example.mapper.UserMapper.selectById"/>
</resultMap>
<select id="selectOrderWithUser2" resultMap="OrderWithUserResultMap2">
SELECT * FROM order WHERE id = #{orderId}
</select>
方式四:集合查询(一对多)
// 用户实体类
@Data
public class User {
private Integer id;
private String userName;
private List<Order> orders; // 用户的订单列表
}
// Mapper接口
User selectUserWithOrders(@Param("userId") Integer userId);
<resultMap id="UserWithOrdersResultMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<!-- 一对多映射,ofType指定集合元素类型 -->
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
<result column="order_time" property="createTime"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="UserWithOrdersResultMap">
SELECT
u.*,
o.id as order_id,
o.order_no,
o.amount,
o.create_time as order_time
FROM user u
LEFT JOIN order o ON u.id = o.user_id
WHERE u.id = #{userId}
</select>
3.1.3 动态SQL详解(MyBatis的灵魂)
动态SQL是MyBatis最强大的特性之一,它可以帮助你根据不同的条件动态构建SQL语句。
1. if标签
最基本的条件判断标签。
xml
<select id="findActiveBlogWithTitleLike" resultType="Blog">
SELECT * FROM blog
WHERE state = 'ACTIVE'
<if test="title != null and title != ''">
AND title LIKE #{title}
</if>
</select>
<select id="findByCriteria" resultType="User">
SELECT * FROM user
<if test="name != null">
WHERE name = #{name}
</if>
<!-- 注意:如果第一个条件为null,这里会生成错误的SQL -->
<if test="age != null">
WHERE age = #{age}
</if>
</select>
<!-- 上面这种写法是错误的,因为两个WHERE会导致SQL错误 -->
2. where标签
智能处理WHERE关键字和多余的AND/OR。
xml
<select id="findByCondition" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</where>
</select>
<where>标签的作用:
-
如果内部有条件成立,自动添加
WHERE关键字 -
自动去除第一个条件前面的
AND或OR
3. set标签
用于更新语句,智能处理SET关键字和多余的逗号。
xml
<update id="updateUser">
UPDATE user
<set>
<if test="userName != null">user_name = #{userName},</if>
<if test="password != null">password = #{password},</if>
<if test="email != null">email = #{email},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="age != null">age = #{age},</if>
</set>
WHERE id = #{id}
</update>
<set>标签的作用:
-
自动添加
SET关键字 -
自动去除最后一个条件后面的逗号
4. choose/when/otherwise标签
类似Java的switch语句,多选一。
xml
<select id="findByPriority" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="name != null and name != ''">
AND name = #{name}
</when>
<when test="email != null and email != ''">
AND email = #{email}
</when>
<when test="phone != null and phone != ''">
AND phone = #{phone}
</when>
<otherwise>
AND status = 'NORMAL'
</otherwise>
</choose>
</where>
</select>
5. trim标签
更通用的标签,可以自定义前缀、后缀以及要覆盖的字符。
xml
<!-- 自定义WHERE效果 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null"> AND name = #{name}</if>
<if test="age != null"> AND age = #{age}</if>
</trim>
<!-- 自定义SET效果 -->
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</trim>
<!-- 自定义INSERT效果 -->
<insert id="insertSelective">
INSERT INTO user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userName != null">user_name,</if>
<if test="password != null">password,</if>
<if test="email != null">email,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="userName != null">#{userName},</if>
<if test="password != null">#{password},</if>
<if test="email != null">#{email},</if>
</trim>
</insert>
6. foreach标签
用于遍历集合,常用于IN查询或批量插入。
java
// Mapper接口
List<User> selectByIds(@Param("ids") List<Integer> ids);
int batchInsert(@Param("users") List<User> users);
xml
<!-- IN查询 -->
<select id="selectByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO user(user_name, password, email, age)
VALUES
<foreach collection="users" item="user" separator=",">
(#{user.userName}, #{user.password}, #{user.email}, #{user.age})
</foreach>
</insert>
<!-- 动态构建OR条件 -->
<select id="findByConditions" resultType="User">
SELECT * FROM user
<where>
<foreach collection="conditions" item="cond" separator="OR">
(${cond.field} = #{cond.value})
</foreach>
</where>
</select>
foreach属性说明:
-
collection:要遍历的集合名称(与@Param一致)
-
item:当前元素的别名
-
index:当前索引(在Map中为key)
-
open:开始字符
-
close:结束字符
-
separator:分隔符
7. bind标签
创建一个变量并绑定到上下文中,常用于模糊查询。
xml
<select id="selectByName" resultType="User">
<bind name="pattern" value="'%' + name + '%'"/>
SELECT * FROM user
WHERE user_name LIKE #{pattern}
</select>
<!-- 更复杂的用法:根据不同数据库处理 -->
<select id="selectByName" resultType="User">
<bind name="pattern" value="'%' + _parameter.getName() + '%'"/>
SELECT * FROM user
<where>
<if test="_databaseId == 'mysql'">
user_name LIKE #{pattern}
</if>
<if test="_databaseId == 'oracle'">
user_name LIKE #{pattern} ESCAPE '\'
</if>
</where>
</select>
8. sql和include标签
定义可重用的SQL片段。
xml
<!-- 定义列名片段 -->
<sql id="Base_Column_List">
id, user_name, password, email, phone, age, gender, create_time, update_time
</sql>
<!-- 定义条件片段 -->
<sql id="User_Where_Clause">
<where>
<if test="userName != null">AND user_name LIKE CONCAT('%', #{userName}, '%')</if>
<if test="age != null">AND age = #{age}</if>
<if test="email != null">AND email = #{email}</if>
</where>
</sql>
<!-- 使用片段 -->
<select id="selectById" resultType="User">
SELECT <include refid="Base_Column_List"/>
FROM user
WHERE id = #{id}
</select>
<select id="selectByCondition" resultType="User">
SELECT <include refid="Base_Column_List"/>
FROM user
<include refid="User_Where_Clause"/>
</select>
3.1.4 高级查询技巧
1. 分页查询
MyBatis原生支持内存分页(RowBounds),但不推荐。实际开发中通常使用分页插件。
java
// 使用RowBounds(不推荐,是内存分页)
public List<User> findByPage(int offset, int limit) {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
RowBounds rowBounds = new RowBounds(offset, limit);
return mapper.selectAll(rowBounds); // Mapper方法需要加RowBounds参数
}
}
使用PageHelper分页插件:
java
// 1. 引入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.2</version>
</dependency>
// 2. 配置插件(MyBatis配置文件)
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
<property name="reasonable" value="true"/>
<property name="supportMethodsArguments" value="true"/>
<property name="params" value="pageNum=pageNum;pageSize=pageSize"/>
</plugin>
</plugins>
// 3. 使用(只需在查询前调用PageHelper.startPage)
public PageInfo<User> getUsersByPage(int pageNum, int pageSize) {
// 设置分页参数
PageHelper.startPage(pageNum, pageSize);
// 紧跟的查询会自动被分页
List<User> userList = userMapper.selectAll();
// 用PageInfo包装结果,获取总条数等信息
PageInfo<User> pageInfo = new PageInfo<>(userList);
return pageInfo;
}
2. 批量操作
java
// Mapper接口
int batchInsert(List<User> users);
int batchUpdate(List<User> users);
// XML
<insert id="batchInsert" parameterType="list">
INSERT INTO user(user_name, password, email, age)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.userName}, #{user.password}, #{user.email}, #{user.age})
</foreach>
</insert>
<update id="batchUpdate" parameterType="list">
<foreach collection="list" item="user" separator=";">
UPDATE user
SET user_name = #{user.userName},
email = #{user.email},
age = #{user.age}
WHERE id = #{user.id}
</foreach>
</update>
// Java代码中使用批量执行器提升性能
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : userList) {
mapper.insert(user); // 不会立即执行,而是批量提交
}
session.commit(); // 一次性批量执行
}
3. 存储过程调用
xml
<!-- 调用存储过程 -->
<select id="callProcedure" parameterType="map" statementType="CALLABLE">
{call getUserCount(
#{gender, mode=IN, jdbcType=INTEGER},
#{count, mode=OUT, jdbcType=INTEGER}
)}
</select>
java
// Mapper接口
void callProcedure(@Param("gender") Integer gender,
@Param("count") ResultHandler<Integer> handler);
// 或者使用Map接收输出参数
Map<String, Object> callProcedure(@Param("gender") Integer gender);
// 调用
Map<String, Object> params = new HashMap<>();
params.put("gender", 1);
mapper.callProcedure(params);
Integer count = (Integer) params.get("count");
3.1.5 常见坑点与解决方案
坑点1:#{}和${}混淆导致SQL注入
java
// 错误示例:使用${}拼接参数
@Select("SELECT * FROM user WHERE name = '${name}'")
User findByName(@Param("name") String name);
// 如果传入 "' OR '1'='1",SQL变成:SELECT * FROM user WHERE name = '' OR '1'='1'
// 正确示例:使用#{}
@Select("SELECT * FROM user WHERE name = #{name}")
User findByName(@Param("name") String name);
// 使用预编译,参数作为字符串值传入,不会改变SQL结构
何时必须使用${}:
xml
<!-- 动态表名 -->
<select id="selectFromTable" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
<!-- 动态排序字段 -->
<select id="selectOrderBy" resultType="User">
SELECT * FROM user
ORDER BY ${orderColumn} ${orderType}
</select>
<!-- 注意:这些场景必须对传入值做严格校验 -->
坑点2:Mapper接口方法重载问题
java
// 错误示例:MyBatis不支持方法重载
public interface UserMapper {
User findById(Integer id); // 方法1
User findById(Integer id, String name); // 方法2(重载)
}
// MyBatis通过"全限定名+方法名"作为唯一标识,重载会导致冲突
// 正确做法:不同方法不同名
public interface UserMapper {
User findById(Integer id);
User findByIdAndName(@Param("id") Integer id, @Param("name") String name);
}
坑点3:事务提交问题
java
// 错误示例:忘记提交事务
public void insertUser(User user) {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.insert(user);
// 没有调用session.commit(),数据不会持久化
}
}
// 正确示例:手动提交
public void insertUser(User user) {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.insert(user);
session.commit(); // 提交事务
}
}
// 或者使用自动提交
public void insertUser(User user) {
try (SqlSession session = sqlSessionFactory.openSession(true)) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.insert(user); // 自动提交
}
}
坑点4:关联查询的N+1问题
xml
<!-- 问题示例:使用嵌套查询会导致N+1问题 -->
<resultMap id="OrderResultMap" type="Order">
<association property="user"
column="user_id"
select="com.example.mapper.UserMapper.selectById"/>
</resultMap>
<select id="selectOrders" resultMap="OrderResultMap">
SELECT * FROM order
</select>
<!--
执行流程:
1. 执行 selectOrders 查询所有订单(1次查询)
2. 对每个订单,执行 selectById 查询用户(N次查询)
总共执行 1+N 次查询
-->
<!-- 解决方案:使用嵌套结果进行JOIN查询 -->
<resultMap id="OrderResultMap2" type="Order">
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<association property="user" javaType="User">
<id column="user_id" property="id"/>
<result column="user_name" property="userName"/>
</association>
</resultMap>
<select id="selectOrdersWithJoin" resultMap="OrderResultMap2">
SELECT o.*, u.id as user_id, u.user_name
FROM order o
LEFT JOIN user u ON o.user_id = u.id
</select>
坑点5:模糊查询的写法
xml
<!-- 错误写法:这样写不会报错,但不会生效 -->
<select id="findByName" resultType="User">
SELECT * FROM user
WHERE user_name LIKE '%#{name}%'
</select>
<!-- 实际执行的SQL:WHERE user_name LIKE '%'?'%',参数不会替换进去 -->
<!-- 正确写法1:使用concat函数(MySQL) -->
<select id="findByName" resultType="User">
SELECT * FROM user
WHERE user_name LIKE CONCAT('%', #{name}, '%')
</select>
<!-- 正确写法2:使用bind标签 -->
<select id="findByName" resultType="User">
<bind name="pattern" value="'%' + name + '%'"/>
SELECT * FROM user
WHERE user_name LIKE #{pattern}
</select>
<!-- 正确写法3:在Java代码中拼接 -->
// Java代码
String pattern = "%" + name + "%";
mapper.findByName(pattern);
3.2 MyBatis-Plus核心用法详解
3.2.1 条件构造器Wrapper(灵魂所在)
条件构造器是MyBatis-Plus最强大的功能之一,它允许以编程方式构建复杂的查询条件,且保证类型安全。
Wrapper类继承体系:
text
Wrapper (抽象类)
├── AbstractWrapper (抽象类)
│ ├── QueryWrapper (查询条件封装)
│ ├── UpdateWrapper (更新条件封装)
│ └── AbstractLambdaWrapper (抽象Lambda类)
│ ├── LambdaQueryWrapper (Lambda查询)
│ └── LambdaUpdateWrapper (Lambda更新)
核心建议 :始终使用LambdaQueryWrapper和LambdaUpdateWrapper,它们提供编译期类型安全检查,避免字段名拼写错误。
1. QueryWrapper(基础查询)
java
// 创建QueryWrapper
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 基本条件
wrapper.eq("user_name", "张三") // 等于
.ne("status", 0) // 不等于
.gt("age", 18) // 大于
.ge("age", 18) // 大于等于
.lt("age", 60) // 小于
.le("age", 60) // 小于等于
.between("age", 20, 30) // BETWEEN
.notBetween("age", 20, 30) // NOT BETWEEN
.like("user_name", "张") // LIKE '%张%'
.notLike("user_name", "张") // NOT LIKE '%张%'
.likeLeft("email", "@qq.com") // LIKE '%@qq.com'
.likeRight("user_name", "张") // LIKE '张%'
.isNull("email") // IS NULL
.isNotNull("email") // IS NOT NULL
.in("id", 1, 2, 3) // IN (1,2,3)
.notIn("id", 1, 2, 3) // NOT IN (1,2,3)
.inSql("id", "select user_id from order") // IN (子查询)
.groupBy("age", "gender") // GROUP BY
.having("age > 10") // HAVING
.orderByAsc("age") // ORDER BY age ASC
.orderByDesc("create_time") // ORDER BY create_time DESC
.orderBy(true, true, "age"); // 条件排序
2. LambdaQueryWrapper(推荐)
java
// 创建LambdaQueryWrapper
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
// 使用方法引用指定字段,类型安全
lambdaWrapper.eq(User::getUserName, "张三")
.gt(User::getAge, 18)
.like(User::getEmail, "@qq.com")
.orderByDesc(User::getCreateTime);
// 更简洁的创建方式
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
// 带条件的链式调用
lambdaWrapper.eq(StringUtils.hasText(name), User::getUserName, name)
.ge(ageMin != null, User::getAge, ageMin)
.le(ageMax != null, User::getAge, ageMax)
.like(email != null, User::getEmail, email);
3. 复杂条件组合
java
// AND 和 OR 的组合
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 方式1:直接使用or()方法
wrapper.eq(User::getGender, 1)
.and(w -> w.like(User::getUserName, "张")
.or()
.like(User::getUserName, "李"))
.gt(User::getAge, 18);
// SQL: WHERE gender = 1 AND (user_name LIKE '%张%' OR user_name LIKE '%李%') AND age > 18
// 方式2:嵌套and/or
wrapper.eq(User::getGender, 1)
.and(w -> w.eq(User::getStatus, 1)
.or()
.eq(User::getStatus, 2))
.and(w -> w.gt(User::getAge, 18)
.lt(User::getAge, 60));
// 方式3:多个条件分组
wrapper.nested(w -> w.eq(User::getUserName, "张三")
.or()
.eq(User::getUserName, "李四"))
.and(w -> w.ge(User::getAge, 18)
.le(User::getAge, 30));
4. UpdateWrapper(更新专用)
java
// 创建UpdateWrapper
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
// 设置更新条件和更新字段
updateWrapper.eq("user_name", "张三")
.set("email", "new_email@example.com")
.set("age", 30)
.setSql("version = version + 1"); // 直接写SQL片段
// 使用LambdaUpdateWrapper(推荐)
LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
lambdaUpdateWrapper.eq(User::getUserName, "张三")
.set(User::getEmail, "new_email@example.com")
.set(User::getAge, 30)
.setSql("version = version + 1");
// 执行更新
userService.update(lambdaUpdateWrapper);
// 或者
userMapper.update(null, lambdaUpdateWrapper);
5. 实战示例集合
java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
/**
* 复杂条件查询
*/
public List<User> queryComplex(String name, Integer ageMin, Integer ageMax,
String email, List<Integer> genderList,
String orderField, Boolean isAsc) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 姓名模糊查询(非空判断)
wrapper.like(StringUtils.hasText(name), User::getUserName, name);
// 年龄区间查询
wrapper.ge(ageMin != null, User::getAge, ageMin)
.le(ageMax != null, User::getAge, ageMax);
// 邮箱精确查询
wrapper.eq(StringUtils.hasText(email), User::getEmail, email);
// IN查询
wrapper.in(genderList != null && !genderList.isEmpty(),
User::getGender, genderList);
// 动态排序
if (StringUtils.hasText(orderField)) {
boolean condition = isAsc != null && isAsc;
wrapper.orderBy(true, condition, orderField);
} else {
// 默认排序
wrapper.orderByDesc(User::getCreateTime);
}
return this.list(wrapper);
}
/**
* 统计各年龄段用户数量
*/
public List<Map<String, Object>> groupByAgeRange() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("CASE " +
"WHEN age < 18 THEN '未成年' " +
"WHEN age BETWEEN 18 AND 30 THEN '青年' " +
"WHEN age BETWEEN 31 AND 50 THEN '中年' " +
"ELSE '老年' END as age_range, " +
"COUNT(*) as count")
.groupBy("age_range");
return this.listMaps(wrapper);
}
/**
* 子查询示例
*/
public List<User> findUsersWithRecentOrders() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.inSql(User::getId,
"SELECT user_id FROM order WHERE create_time > DATE_SUB(NOW(), INTERVAL 7 DAY)");
return this.list(wrapper);
}
/**
* 存在性检查
*/
public boolean checkExists(String userName, String email) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, userName)
.or()
.eq(User::getEmail, email);
return this.count(wrapper) > 0;
}
}
3.2.2 分页插件
MyBatis-Plus内置了强大的分页插件,支持多种数据库。
1. 配置分页插件
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
paginationInterceptor.setDbType(DbType.MYSQL); // 设置数据库类型
paginationInterceptor.setOverflow(true); // 溢出总页数后是否跳到第一页
paginationInterceptor.setMaxLimit(500L); // 单页最大限制
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
2. 使用分页
java
// Service层
public Page<User> getUserPage(int current, int size, String name) {
// 创建分页对象
Page<User> page = new Page<>(current, size);
// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(name), User::getUserName, name)
.orderByDesc(User::getCreateTime);
// 执行分页查询
return this.page(page, wrapper);
}
// Controller层
@GetMapping("/users")
public Result<Page<User>> listUsers(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String name) {
Page<User> page = userService.getUserPage(current, size, name);
return Result.success(page);
}
3. 自定义分页查询
java
// Mapper接口中自定义分页方法
public interface UserMapper extends BaseMapper<User> {
// 使用MyBatis-Plus的分页对象作为参数
Page<User> selectUserPage(Page<User> page,
@Param("name") String name,
@Param("age") Integer age);
// 返回自定义VO的分页
Page<UserVO> selectUserVOPage(Page<UserVO> page,
@Param("deptId") Long deptId);
}
// XML实现
<select id="selectUserPage" resultType="com.example.entity.User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND user_name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
ORDER BY create_time DESC
</select>
3.2.3 自动填充功能
在插入或更新数据时,自动填充某些字段(如创建时间、更新时间)。
1. 实现元对象处理器
java
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
private static final Logger log = LoggerFactory.getLogger(MyMetaObjectHandler.class);
@Override
public void insertFill(MetaObject metaObject) {
log.info("开始插入填充...");
// 严格模式:如果字段有值,则不覆盖
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
this.strictInsertFill(metaObject, "version", Integer.class, 1);
// 也可以使用宽松模式(如果有值也会被覆盖)
// this.setFieldValByName("createTime", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("开始更新填充...");
// 更新时,自动填充更新时间
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
2. 实体类配置
java
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String userName;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@TableField(fill = FieldFill.INSERT)
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private Integer version;
}
// FieldFill枚举说明:
// DEFAULT: 默认不处理
// INSERT: 插入时填充
// UPDATE: 更新时填充
// INSERT_UPDATE: 插入和更新时填充
3.2.4 逻辑删除
逻辑删除是指不真正删除数据,而是通过标记字段表示已删除。
1. 全局配置
yaml
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
2. 实体类配置
java
@Data
@TableName("user")
public class User {
@TableId
private Long id;
private String userName;
@TableLogic // 逻辑删除注解
private Integer deleted;
// 或者自定义值
@TableLogic(value = "0", delval = "1")
private Integer deleteFlag;
}
3. 使用效果
java
// 删除操作(实际执行的是UPDATE)
userService.removeById(1L);
// 执行的SQL: UPDATE user SET deleted=1 WHERE id=1 AND deleted=0
// 查询操作(自动过滤已删除数据)
userService.list();
// 执行的SQL: SELECT * FROM user WHERE deleted=0
// 如果想查询包含已删除的数据
userMapper.selectList(Wrappers.<User>lambdaQuery()
.eq(User::getId, 1L)
.last("AND deleted=1")); // 手动添加条件
3.2.5 乐观锁插件
乐观锁用于解决并发更新时的数据冲突问题。
1. 配置插件
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2. 实体类配置
java
@Data
@TableName("product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
@Version // 乐观锁注解
private Integer version;
}
3. 使用示例
java
// 先查询,获取version
Product product = productService.getById(1L);
System.out.println("当前版本:" + product.getVersion());
// 修改数据
product.setPrice(new BigDecimal("99.00"));
product.setStock(product.getStock() - 1);
// 执行更新
boolean success = productService.updateById(product);
// 执行的SQL: UPDATE product SET price=99.00, stock=stock-1, version=version+1
// WHERE id=1 AND version=原版本号
if (success) {
System.out.println("更新成功,新版本:" + product.getVersion());
} else {
System.out.println("更新失败,数据已被修改");
// 可以重新查询并重试
}
3.2.6 枚举处理器
MyBatis-Plus提供了优雅的枚举处理方式。
1. 定义枚举
java
public enum GenderEnum {
MALE(1, "男"),
FEMALE(2, "女");
@EnumValue // 标记数据库存储的值
private final int code;
private final String desc;
GenderEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
2. 实体类中使用
java
@Data
@TableName("user")
public class User {
@TableId
private Long id;
private String userName;
private GenderEnum gender; // 直接使用枚举类型
}
3. 配置枚举包扫描
yaml
mybatis-plus:
type-enums-package: com.example.enums # 枚举类所在包
3.2.7 代码生成器
MyBatis-Plus的代码生成器可以一键生成Controller、Service、Mapper、Entity、XML全系列代码。
java
public class CodeGenerator {
public static void main(String[] args) {
// 数据源配置
DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder(
"jdbc:mysql://localhost:3306/mybatis_demo?useUnicode=true&characterEncoding=utf8",
"root",
"123456"
).build();
// 代码生成器
AutoGenerator generator = new AutoGenerator(dataSourceConfig);
// 全局配置
GlobalConfig globalConfig = new GlobalConfig.Builder()
.outputDir(System.getProperty("user.dir") + "/src/main/java") // 输出目录
.author("YourName") // 作者
.disableOpenDir() // 生成后不打开输出目录
.commentDate("yyyy-MM-dd") // 日期格式
.build();
// 包配置
PackageConfig packageConfig = new PackageConfig.Builder()
.parent("com.example") // 父包名
.moduleName("system") // 模块名
.entity("entity") // 实体类包名
.mapper("mapper") // mapper包名
.service("service") // service包名
.serviceImpl("service.impl") // serviceImpl包名
.controller("controller") // controller包名
.xml("mapper.xml") // xml包名
.build();
// 策略配置
StrategyConfig strategyConfig = new StrategyConfig.Builder()
.addInclude("user", "product", "order") // 需要生成的表名
.addTablePrefix("t_", "sys_") // 表前缀过滤
.entityBuilder()
.enableLombok() // 使用Lombok
.enableChainModel() // 链式模型
.enableTableFieldAnnotation() // 启用字段注解
.versionColumnName("version") // 乐观锁字段名
.logicDeleteColumnName("deleted") // 逻辑删除字段名
.addSuperEntityColumns("id", "create_time", "update_time") // 父类公共字段
.build()
.mapperBuilder()
.enableBaseResultMap() // 生成通用的resultMap
.enableBaseColumnList() // 生成通用的columnList
.build()
.serviceBuilder()
.formatServiceFileName("%sService") // Service文件名格式
.formatServiceImplFileName("%sServiceImpl") // ServiceImpl文件名格式
.build()
.controllerBuilder()
.enableRestStyle() // Rest风格
.enableHyphenStyle() // 路径使用连字符
.build();
// 执行生成
generator.execute(globalConfig, packageConfig, strategyConfig);
System.out.println("代码生成完成!");
}
}
3.2.8 多数据源支持
MyBatis-Plus支持多数据源配置,满足复杂业务需求。
1. 引入依赖
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
2. 配置文件
yaml
spring:
datasource:
dynamic:
primary: master # 默认数据源
strict: false # 严格模式
datasource:
master:
url: jdbc:mysql://localhost:3306/db_master?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave_1:
url: jdbc:mysql://localhost:3306/db_slave1?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave_2:
url: jdbc:mysql://localhost:3306/db_slave2?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
3. 使用注解切换数据源
java
@Service
@DS("master") // 类级别,默认使用master
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
@DS("slave_1") // 方法级别,覆盖类注解
public List<User> listFromSlave() {
return this.list();
}
@Override
@Transactional
@DS("master")
public boolean saveToMaster(User user) {
return this.save(user);
}
}
// 也可以在Mapper上使用
@Mapper
@DS("slave_2")
public interface UserMapper extends BaseMapper<User> {
@DS("master") // 方法级别
int insert(User user);
}
第四阶段:场景融合(与其他技术协作)
4.1 MyBatis与Spring的整合
在企业开发中,MyBatis通常与Spring/Spring Boot框架整合使用,由Spring管理数据源、事务和MyBatis的生命周期。
4.1.1 传统Spring XML整合方式
xml
<!-- 1. 数据源配置 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 2. SqlSessionFactory配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath*:com/example/mapper/*.xml"/>
<property name="typeAliasesPackage" value="com.example.entity"/>
</bean>
<!-- 3. Mapper扫描配置 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- 4. 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 5. 开启注解事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
4.1.2 Spring Boot整合方式(推荐)
1. 引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
2. 配置文件
yaml
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3. 启动类配置
java
@SpringBootApplication
@MapperScan("com.example.mapper") // 扫描Mapper接口
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4.1.3 Spring事务管理
1. 声明式事务(@Transactional)
java
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private UserMapper userMapper;
/**
* 下单操作(涉及多个数据库操作,需要事务)
*/
@Override
@Transactional(rollbackFor = Exception.class) // 遇到任何异常都回滚
public Order createOrder(OrderCreateDTO dto) {
// 1. 查询商品
Product product = productMapper.selectById(dto.getProductId());
if (product == null) {
throw new BusinessException("商品不存在");
}
// 2. 扣减库存(乐观锁)
int stockResult = productMapper.decreaseStock(product.getId(), dto.getQuantity());
if (stockResult == 0) {
throw new BusinessException("库存不足");
}
// 3. 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(dto.getUserId());
order.setProductId(dto.getProductId());
order.setQuantity(dto.getQuantity());
order.setAmount(product.getPrice().multiply(new BigDecimal(dto.getQuantity())));
order.setStatus(OrderStatus.PENDING_PAYMENT);
orderMapper.insert(order);
// 4. 记录日志(即使失败也不影响主流程)
try {
logOrderOperation(order);
} catch (Exception e) {
log.error("记录日志失败", e);
// 不抛异常,不影响主流程
}
return order;
}
/**
* 事务传播行为示例
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW) // 开启新事务
public void logOrderOperation(Order order) {
OrderLog log = new OrderLog();
log.setOrderId(order.getId());
log.setOperation("CREATE");
log.setOperator(order.getUserId().toString());
orderLogMapper.insert(log);
}
}
2. 事务传播行为
| 传播行为 | 描述 |
|---|---|
| REQUIRED(默认) | 支持当前事务,如果不存在则创建新事务 |
| SUPPORTS | 支持当前事务,如果不存在则以非事务方式执行 |
| MANDATORY | 支持当前事务,如果不存在则抛出异常 |
| REQUIRES_NEW | 创建新事务,挂起当前事务 |
| NOT_SUPPORTED | 以非事务方式执行,挂起当前事务 |
| NEVER | 以非事务方式执行,如果存在事务则抛出异常 |
| NESTED | 如果当前存在事务,则在嵌套事务内执行 |
3. 事务失效场景及解决方案
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 场景1:非public方法
@Transactional
private void privateMethod() { // @Transactional在private方法上无效
// ...
}
// 场景2:同类内部调用
public void methodA() {
methodB(); // 直接调用,事务不生效(没有经过代理)
}
@Transactional
public void methodB() {
// ...
}
// 解决方案:注入自身代理
@Autowired
private UserService self;
public void methodA() {
self.methodB(); // 通过代理调用,事务生效
}
// 场景3:异常被捕获
@Transactional
public void methodC() {
try {
// 业务操作
userMapper.insert(user);
// 模拟异常
int i = 1 / 0;
} catch (Exception e) {
// 异常被捕获,没有抛出,事务不会回滚
log.error("发生异常", e);
}
}
// 解决方案:抛出异常
@Transactional
public void methodD() {
try {
userMapper.insert(user);
int i = 1 / 0;
} catch (Exception e) {
log.error("发生异常", e);
throw e; // 重新抛出,让事务回滚
}
}
}
4.2 与Redis整合实现缓存
在高并发场景下,数据库往往成为性能瓶颈,引入Redis缓存可以大幅提升系统性能。
4.2.1 Spring Boot整合Redis
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yaml
spring:
redis:
host: localhost
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
java
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer序列化value
Jackson2JsonRedisSerializer<Object> jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 使用StringRedisSerializer序列化key
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jacksonSeial);
template.setHashValueSerializer(jacksonSeial);
template.afterPropertiesSet();
return template;
}
}
4.2.2 业务场景:商品详情缓存
java
@Service
@Slf4j
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String PRODUCT_CACHE_KEY = "product:detail:";
private static final long CACHE_TTL = 30; // 30分钟
/**
* 查询商品详情(先查缓存,再查数据库)
*/
@Override
public ProductVO getProductDetail(Long productId) {
String cacheKey = PRODUCT_CACHE_KEY + productId;
// 1. 先查缓存
ProductVO productVO = (ProductVO) redisTemplate.opsForValue().get(cacheKey);
if (productVO != null) {
log.info("缓存命中:productId={}", productId);
return productVO;
}
log.info("缓存未命中:productId={},查询数据库", productId);
// 2. 缓存未命中,查数据库
Product product = this.getById(productId);
if (product == null) {
return null;
}
// 转换为VO
productVO = convertToVO(product);
// 3. 存入缓存
redisTemplate.opsForValue().set(cacheKey, productVO, CACHE_TTL, TimeUnit.MINUTES);
return productVO;
}
/**
* 更新商品信息(删除缓存)
*/
@Override
@Transactional
public boolean updateProduct(Product product) {
boolean result = this.updateById(product);
if (result) {
// 删除缓存(也可以选择更新缓存,但删除更简单)
String cacheKey = PRODUCT_CACHE_KEY + product.getId();
redisTemplate.delete(cacheKey);
log.info("商品更新,删除缓存:productId={}", product.getId());
}
return result;
}
/**
* 批量查询商品(使用管道优化)
*/
@Override
public List<ProductVO> batchGetProducts(List<Long> productIds) {
List<String> cacheKeys = productIds.stream()
.map(id -> PRODUCT_CACHE_KEY + id)
.collect(Collectors.toList());
// 使用管道批量获取
List<Object> cacheResults = redisTemplate.executePipelined(
(RedisCallback<Object>) connection -> {
for (String key : cacheKeys) {
connection.get(key.getBytes());
}
return null;
}
);
List<ProductVO> result = new ArrayList<>();
List<Long> missedIds = new ArrayList<>();
// 处理缓存结果
for (int i = 0; i < productIds.size(); i++) {
Object cacheObj = cacheResults.get(i);
if (cacheObj != null) {
result.add((ProductVO) cacheObj);
} else {
missedIds.add(productIds.get(i));
}
}
// 批量查询未命中的商品
if (!missedIds.isEmpty()) {
List<Product> products = this.listByIds(missedIds);
for (Product product : products) {
ProductVO vo = convertToVO(product);
result.add(vo);
// 写入缓存
String cacheKey = PRODUCT_CACHE_KEY + product.getId();
redisTemplate.opsForValue().set(cacheKey, vo, CACHE_TTL, TimeUnit.MINUTES);
}
}
return result;
}
private ProductVO convertToVO(Product product) {
ProductVO vo = new ProductVO();
BeanUtils.copyProperties(product, vo);
return vo;
}
}
4.2.3 缓存穿透/击穿/雪崩解决方案
1. 缓存穿透:查询不存在的数据,导致请求直接打到数据库
java
public ProductVO getProductDetail(Long productId) {
String cacheKey = PRODUCT_CACHE_KEY + productId;
// 1. 查缓存
ProductVO productVO = (ProductVO) redisTemplate.opsForValue().get(cacheKey);
if (productVO != null) {
return productVO;
}
// 2. 检查是否是空值缓存
String nullKey = PRODUCT_NULL_KEY + productId;
Boolean hasNull = redisTemplate.hasKey(nullKey);
if (Boolean.TRUE.equals(hasNull)) {
log.info("空值缓存命中,直接返回null");
return null;
}
// 3. 查数据库
Product product = this.getById(productId);
if (product == null) {
// 4. 缓存空值(解决缓存穿透)
redisTemplate.opsForValue().set(nullKey, "", 5, TimeUnit.MINUTES);
return null;
}
// 5. 缓存结果
productVO = convertToVO(product);
redisTemplate.opsForValue().set(cacheKey, productVO, CACHE_TTL, TimeUnit.MINUTES);
return productVO;
}
2. 缓存击穿:热点key过期,大量并发请求同时打到数据库
java
public ProductVO getProductDetail(Long productId) {
String cacheKey = PRODUCT_CACHE_KEY + productId;
// 1. 查缓存
ProductVO productVO = (ProductVO) redisTemplate.opsForValue().get(cacheKey);
if (productVO != null) {
return productVO;
}
// 2. 使用分布式锁,只允许一个线程查数据库
String lockKey = PRODUCT_LOCK_KEY + productId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁,超时时间3秒
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 3, TimeUnit.SECONDS);
if (locked) {
// 获取到锁,查数据库
log.info("获取到锁,查询数据库:productId={}", productId);
Product product = this.getById(productId);
if (product != null) {
productVO = convertToVO(product);
redisTemplate.opsForValue().set(cacheKey, productVO, CACHE_TTL, TimeUnit.MINUTES);
} else {
// 缓存空值
redisTemplate.opsForValue().set(PRODUCT_NULL_KEY + productId, "", 5, TimeUnit.MINUTES);
}
return productVO;
} else {
// 没获取到锁,等待后重试
log.info("等待其他线程加载数据:productId={}", productId);
Thread.sleep(100);
return getProductDetail(productId); // 递归重试
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
// 释放锁(使用Lua脚本确保原子性)
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
Arrays.asList(lockKey), requestId);
}
}
3. 缓存雪崩:大量缓存同时过期,导致数据库压力骤增
java
// 解决方案:设置随机过期时间
public void cacheProduct(ProductVO productVO) {
String cacheKey = PRODUCT_CACHE_KEY + productVO.getId();
// 基础过期时间30分钟,加上随机0-5分钟的偏移
long ttl = CACHE_TTL + new Random().nextInt(5);
redisTemplate.opsForValue().set(cacheKey, productVO, ttl, TimeUnit.MINUTES);
}
4.3 与Elasticsearch整合实现全文检索
对于复杂的搜索场景,可以使用Elasticsearch实现高性能全文检索。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
java
@Document(indexName = "products")
public class ProductDocument {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String name;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Double)
private BigDecimal price;
@Field(type = FieldType.Integer)
private Integer stock;
@Field(type = FieldType.Date)
private Date createTime;
}
@Repository
public interface ProductSearchRepository extends ElasticsearchRepository<ProductDocument, Long> {
// 自定义搜索方法
Page<ProductDocument> findByNameOrDescription(String name, String description, Pageable pageable);
@Query("{\"bool\": {\"must\": [{\"match\": {\"name\": \"?0\"}}], \"filter\": [{\"range\": {\"price\": {\"gte\": \"?1\", \"lte\": \"?2\"}}}]}}")
Page<ProductDocument> searchByNameAndPriceRange(String name, Double minPrice, Double maxPrice, Pageable pageable);
}
@Service
public class ProductSearchService {
@Autowired
private ProductSearchRepository searchRepository;
@Autowired
private ProductService productService;
/**
* 同步MySQL数据到ES
*/
public void syncAllProducts() {
List<Product> products = productService.list();
List<ProductDocument> documents = products.stream()
.map(this::convertToDocument)
.collect(Collectors.toList());
searchRepository.saveAll(documents);
}
/**
* 搜索商品
*/
public Page<ProductDocument> search(String keyword, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return searchRepository.findByNameOrDescription(keyword, keyword, pageable);
}
private ProductDocument convertToDocument(Product product) {
ProductDocument doc = new ProductDocument();
BeanUtils.copyProperties(product, doc);
return doc;
}
}
第五阶段:企业级实战(续)
5.1 实战项目:电商订单系统(完整实现)
让我们通过一个完整的电商订单系统,综合运用MyBatis-Plus的各项特性,模拟企业级开发流程。
5.1.1 项目结构(续)
text
src/main/java/com/example/order/
├── OrderApplication.java // 启动类
├── config/
│ ├── MybatisPlusConfig.java // MyBatis-Plus配置
│ ├── RedisConfig.java // Redis配置
│ └── SwaggerConfig.java // API文档配置
├── controller/
│ ├── OrderController.java // 订单控制器
│ ├── ProductController.java // 商品控制器
│ └── UserController.java // 用户控制器
├── service/
│ ├── OrderService.java // 订单服务接口
│ ├── OrderServiceImpl.java // 订单服务实现
│ ├── ProductService.java // 商品服务接口
│ ├── ProductServiceImpl.java // 商品服务实现
│ ├── UserService.java // 用户服务接口
│ └── UserServiceImpl.java // 用户服务实现
├── mapper/
│ ├── OrderMapper.java // 订单Mapper
│ ├── ProductMapper.java // 商品Mapper
│ └── UserMapper.java // 用户Mapper
├── entity/
│ ├── Order.java // 订单实体
│ ├── Product.java // 商品实体
│ ├── User.java // 用户实体
│ └── enums/
│ ├── OrderStatus.java // 订单状态枚举
│ └── GenderEnum.java // 性别枚举
├── dto/
│ ├── OrderCreateDTO.java // 创建订单DTO
│ ├── OrderQueryDTO.java // 订单查询DTO
│ ├── OrderVO.java // 订单视图对象
│ └── PageResult.java // 分页结果封装
├── handler/
│ ├── MyMetaObjectHandler.java // 自动填充处理器
│ └── GlobalExceptionHandler.java // 全局异常处理
└── utils/
├── OrderNoGenerator.java // 订单号生成器
└── RedisLockUtil.java // 分布式锁工具
5.1.2 数据库表设计
sql
-- 用户表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`gender` tinyint(4) DEFAULT NULL COMMENT '性别(1-男,2-女)',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`balance` decimal(10,2) DEFAULT '0.00' COMMENT '账户余额',
`status` tinyint(4) DEFAULT '1' COMMENT '状态(0-禁用,1-正常)',
`version` int(11) DEFAULT '1' COMMENT '乐观锁版本号',
`deleted` tinyint(4) DEFAULT '0' COMMENT '逻辑删除(0-未删除,1-已删除)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 商品表
CREATE TABLE `product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`product_code` varchar(50) NOT NULL COMMENT '商品编码',
`product_name` varchar(200) NOT NULL COMMENT '商品名称',
`category_id` bigint(20) DEFAULT NULL COMMENT '分类ID',
`price` decimal(10,2) NOT NULL COMMENT '价格',
`stock` int(11) NOT NULL COMMENT '库存',
`description` text COMMENT '商品描述',
`main_image` varchar(500) DEFAULT NULL COMMENT '主图',
`status` tinyint(4) DEFAULT '1' COMMENT '状态(0-下架,1-上架)',
`version` int(11) DEFAULT '1' COMMENT '乐观锁版本号',
`deleted` tinyint(4) DEFAULT '0' COMMENT '逻辑删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_code` (`product_code`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
-- 订单表
CREATE TABLE `order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(32) NOT NULL COMMENT '订单号',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`pay_amount` decimal(10,2) DEFAULT NULL COMMENT '实付金额',
`status` tinyint(4) DEFAULT '0' COMMENT '订单状态(0-待付款,1-已付款,2-已发货,3-已完成,4-已取消,5-退款中)',
`payment_method` tinyint(4) DEFAULT NULL COMMENT '支付方式(1-微信,2-支付宝,3-银行卡)',
`payment_time` datetime DEFAULT NULL COMMENT '支付时间',
`delivery_time` datetime DEFAULT NULL COMMENT '发货时间',
`receive_time` datetime DEFAULT NULL COMMENT '收货时间',
`consignee` varchar(50) NOT NULL COMMENT '收货人',
`phone` varchar(20) NOT NULL COMMENT '联系电话',
`address` varchar(500) NOT NULL COMMENT '收货地址',
`remark` varchar(500) DEFAULT NULL COMMENT '订单备注',
`version` int(11) DEFAULT '1' COMMENT '乐观锁',
`deleted` tinyint(4) DEFAULT '0' COMMENT '逻辑删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
-- 订单明细表
CREATE TABLE `order_item` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) NOT NULL COMMENT '订单ID',
`order_no` varchar(32) NOT NULL COMMENT '订单号',
`product_id` bigint(20) NOT NULL COMMENT '商品ID',
`product_name` varchar(200) NOT NULL COMMENT '商品名称(快照)',
`product_image` varchar(500) DEFAULT NULL COMMENT '商品图片',
`price` decimal(10,2) NOT NULL COMMENT '商品单价',
`quantity` int(11) NOT NULL COMMENT '购买数量',
`total_amount` decimal(10,2) NOT NULL COMMENT '小计金额',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
5.1.3 实体类与枚举
用户实体 User.java
java
package com.example.order.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.example.order.entity.enums.GenderEnum;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String realName;
private String phone;
private String email;
private GenderEnum gender; // 枚举类型
private Integer age;
private BigDecimal balance;
private Integer status;
@Version
private Integer version;
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
性别枚举 GenderEnum.java
java
package com.example.order.entity.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
public enum GenderEnum {
MALE(1, "男"),
FEMALE(2, "女");
@EnumValue // 标记数据库存的值
private final int code;
@JsonValue // 标记返回JSON时展示的值
private final String desc;
GenderEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
商品实体 Product.java
java
package com.example.order.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String productCode;
private String productName;
private Long categoryId;
private BigDecimal price;
private Integer stock;
private String description;
private String mainImage;
private Integer status;
@Version
private Integer version;
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
订单状态枚举 OrderStatus.java
java
package com.example.order.entity.enums;
public enum OrderStatus {
PENDING_PAYMENT(0, "待付款"),
PAID(1, "已付款"),
SHIPPED(2, "已发货"),
COMPLETED(3, "已完成"),
CANCELLED(4, "已取消"),
REFUNDING(5, "退款中");
private final int code;
private final String desc;
OrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
public static OrderStatus fromCode(int code) {
for (OrderStatus status : values()) {
if (status.code == code) {
return status;
}
}
throw new IllegalArgumentException("未知订单状态: " + code);
}
}
订单实体 Order.java
java
package com.example.order.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.example.order.entity.enums.OrderStatus;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Data
@TableName("`order`")
public class Order {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private Long userId;
private BigDecimal totalAmount;
private BigDecimal payAmount;
private OrderStatus status; // 枚举类型
private Integer paymentMethod;
private Date paymentTime;
private Date deliveryTime;
private Date receiveTime;
private String consignee;
private String phone;
private String address;
private String remark;
@Version
private Integer version;
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@TableField(exist = false)
private List<OrderItem> orderItems; // 订单明细,非数据库字段
}
订单明细实体 OrderItem.java
java
package com.example.order.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("order_item")
public class OrderItem {
@TableId(type = IdType.AUTO)
private Long id;
private Long orderId;
private String orderNo;
private Long productId;
private String productName;
private String productImage;
private BigDecimal price;
private Integer quantity;
private BigDecimal totalAmount;
private Date createTime;
}
5.1.4 DTO 数据传输对象
创建订单DTO OrderCreateDTO.java
java
package com.example.order.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
@Data
public class OrderCreateDTO {
@NotNull(message = "用户ID不能为空")
private Long userId;
@NotBlank(message = "收货人不能为空")
private String consignee;
@NotBlank(message = "联系电话不能为空")
private String phone;
@NotBlank(message = "收货地址不能为空")
private String address;
private String remark;
@Size(min = 1, message = "至少选择一个商品")
private List<OrderItemDTO> items;
@Data
public static class OrderItemDTO {
@NotNull(message = "商品ID不能为空")
private Long productId;
@NotNull(message = "购买数量不能为空")
private Integer quantity;
}
}
订单查询DTO OrderQueryDTO.java
java
package com.example.order.dto;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import java.util.List;
@Data
public class OrderQueryDTO {
private String orderNo;
private Long userId;
private List<Integer> statusList;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
private String keyword; // 商品名称关键字
private Integer pageNum = 1;
private Integer pageSize = 10;
}
订单视图对象 OrderVO.java
java
package com.example.order.dto;
import com.example.order.entity.OrderItem;
import com.example.order.entity.enums.OrderStatus;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Data
public class OrderVO {
private Long id;
private String orderNo;
private Long userId;
private String userName;
private BigDecimal totalAmount;
private BigDecimal payAmount;
private OrderStatus status;
private String statusDesc; // 状态描述
private Integer paymentMethod;
private Date paymentTime;
private Date deliveryTime;
private Date receiveTime;
private String consignee;
private String phone;
private String address;
private String remark;
private Date createTime;
private Date updateTime;
private List<OrderItemVO> items;
@Data
public static class OrderItemVO {
private Long productId;
private String productName;
private String productImage;
private BigDecimal price;
private Integer quantity;
private BigDecimal totalAmount;
}
}
分页结果封装 PageResult.java
java
package com.example.order.dto;
import lombok.Data;
import java.util.List;
@Data
public class PageResult<T> {
private Long total; // 总记录数
private Long pageSize; // 每页大小
private Long pageNum; // 当前页码
private Long pages; // 总页数
private List<T> list; // 数据列表
public static <T> PageResult<T> of(com.baomidou.mybatisplus.extension.plugins.pagination.Page<T> page) {
PageResult<T> result = new PageResult<>();
result.setTotal(page.getTotal());
result.setPageSize(page.getSize());
result.setPageNum(page.getCurrent());
result.setPages(page.getPages());
result.setList(page.getRecords());
return result;
}
}
5.1.5 Mapper层
UserMapper.java
java
package com.example.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.order.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 扣减余额(乐观锁)
*/
@Update("UPDATE user SET balance = balance - #{amount}, version = version + 1 " +
"WHERE id = #{userId} AND balance >= #{amount} AND version = #{version}")
int decreaseBalance(@Param("userId") Long userId,
@Param("amount") java.math.BigDecimal amount,
@Param("version") Integer version);
}
ProductMapper.java
java
package com.example.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.order.entity.Product;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
/**
* 扣减库存(乐观锁)
*/
@Update("UPDATE product SET stock = stock - #{quantity}, version = version + 1 " +
"WHERE id = #{productId} AND stock >= #{quantity} AND version = #{version}")
int decreaseStock(@Param("productId") Long productId,
@Param("quantity") Integer quantity,
@Param("version") Integer version);
/**
* 增加库存(用于退款)
*/
@Update("UPDATE product SET stock = stock + #{quantity}, version = version + 1 " +
"WHERE id = #{productId} AND version = #{version}")
int increaseStock(@Param("productId") Long productId,
@Param("quantity") Integer quantity,
@Param("version") Integer version);
}
OrderMapper.java
java
package com.example.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.order.dto.OrderQueryDTO;
import com.example.order.dto.OrderVO;
import com.example.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
/**
* 自定义分页查询订单(带关联查询)
*/
IPage<OrderVO> selectOrderPage(Page<?> page, @Param("query") OrderQueryDTO query);
/**
* 根据订单ID查询订单详情(包含明细)
*/
OrderVO selectOrderDetail(@Param("orderId") Long orderId);
}
OrderMapper.xml
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.order.mapper.OrderMapper">
<resultMap id="OrderVOResultMap" type="com.example.order.dto.OrderVO">
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="user_id" property="userId"/>
<result column="user_name" property="userName"/>
<result column="total_amount" property="totalAmount"/>
<result column="pay_amount" property="payAmount"/>
<result column="status" property="status" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
<result column="payment_method" property="paymentMethod"/>
<result column="payment_time" property="paymentTime"/>
<result column="delivery_time" property="deliveryTime"/>
<result column="receive_time" property="receiveTime"/>
<result column="consignee" property="consignee"/>
<result column="phone" property="phone"/>
<result column="address" property="address"/>
<result column="remark" property="remark"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<!-- 订单明细集合 -->
<collection property="items" ofType="com.example.order.dto.OrderVO$OrderItemVO">
<result column="product_id" property="productId"/>
<result column="product_name" property="productName"/>
<result column="product_image" property="productImage"/>
<result column="price" property="price"/>
<result column="quantity" property="quantity"/>
<result column="item_total" property="totalAmount"/>
</collection>
</resultMap>
<select id="selectOrderPage" resultMap="OrderVOResultMap">
SELECT
o.*,
u.username as user_name,
oi.product_id,
oi.product_name,
oi.product_image,
oi.price,
oi.quantity,
oi.total_amount as item_total
FROM `order` o
LEFT JOIN user u ON o.user_id = u.id
LEFT JOIN order_item oi ON o.id = oi.order_id
<where>
o.deleted = 0
<if test="query.orderNo != null and query.orderNo != ''">
AND o.order_no LIKE CONCAT('%', #{query.orderNo}, '%')
</if>
<if test="query.userId != null">
AND o.user_id = #{query.userId}
</if>
<if test="query.statusList != null and query.statusList.size > 0">
AND o.status IN
<foreach collection="query.statusList" item="status" open="(" separator="," close=")">
#{status}
</foreach>
</if>
<if test="query.startTime != null">
AND o.create_time >= #{query.startTime}
</if>
<if test="query.endTime != null">
AND o.create_time <= #{query.endTime}
</if>
<if test="query.keyword != null and query.keyword != ''">
AND oi.product_name LIKE CONCAT('%', #{query.keyword}, '%')
</if>
</where>
ORDER BY o.create_time DESC
</select>
<select id="selectOrderDetail" resultMap="OrderVOResultMap">
SELECT
o.*,
u.username as user_name,
oi.product_id,
oi.product_name,
oi.product_image,
oi.price,
oi.quantity,
oi.total_amount as item_total
FROM `order` o
LEFT JOIN user u ON o.user_id = u.id
LEFT JOIN order_item oi ON o.id = oi.order_id
WHERE o.id = #{orderId} AND o.deleted = 0
</select>
</mapper>
OrderItemMapper.java
java
package com.example.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.order.entity.OrderItem;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItem> {
}
5.1.6 Service层
UserService.java
java
package com.example.order.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.order.entity.User;
public interface UserService extends IService<User> {
/**
* 扣减余额(带重试机制)
*/
boolean decreaseBalance(Long userId, java.math.BigDecimal amount, int retryTimes);
/**
* 根据用户名查询
*/
User findByUsername(String username);
}
UserServiceImpl.java
java
package com.example.order.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.order.entity.User;
import com.example.order.mapper.UserMapper;
import com.example.order.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean decreaseBalance(Long userId, java.math.BigDecimal amount, int retryTimes) {
int retry = 0;
while (retry < retryTimes) {
// 查询当前用户信息(获取版本号)
User user = this.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (user.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
// 执行扣减(乐观锁)
int result = baseMapper.decreaseBalance(userId, amount, user.getVersion());
if (result > 0) {
return true;
}
retry++;
log.warn("扣减余额乐观锁冲突,重试第{}次", retry);
}
throw new RuntimeException("扣减余额失败,请稍后重试");
}
@Override
public User findByUsername(String username) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
return this.getOne(wrapper);
}
}
ProductService.java
java
package com.example.order.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.order.entity.Product;
public interface ProductService extends IService<Product> {
/**
* 扣减库存(带重试)
*/
boolean decreaseStock(Long productId, Integer quantity, int retryTimes);
/**
* 增加库存(退款)
*/
boolean increaseStock(Long productId, Integer quantity);
/**
* 根据商品编码查询
*/
Product findByProductCode(String productCode);
}
ProductServiceImpl.java
java
package com.example.order.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.order.entity.Product;
import com.example.order.mapper.ProductMapper;
import com.example.order.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean decreaseStock(Long productId, Integer quantity, int retryTimes) {
int retry = 0;
while (retry < retryTimes) {
Product product = this.getById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
if (product.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
int result = baseMapper.decreaseStock(productId, quantity, product.getVersion());
if (result > 0) {
return true;
}
retry++;
log.warn("扣减库存乐观锁冲突,重试第{}次", retry);
}
throw new RuntimeException("扣减库存失败,请稍后重试");
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean increaseStock(Long productId, Integer quantity) {
// 退款增加库存,也要考虑乐观锁
Product product = this.getById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
int result = baseMapper.increaseStock(productId, quantity, product.getVersion());
return result > 0;
}
@Override
public Product findByProductCode(String productCode) {
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Product::getProductCode, productCode);
return this.getOne(wrapper);
}
}
OrderService.java
java
package com.example.order.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.order.dto.OrderCreateDTO;
import com.example.order.dto.OrderQueryDTO;
import com.example.order.dto.OrderVO;
import com.example.order.dto.PageResult;
import com.example.order.entity.Order;
public interface OrderService extends IService<Order> {
/**
* 创建订单(核心业务)
*/
Order createOrder(OrderCreateDTO createDTO);
/**
* 支付订单
*/
boolean payOrder(Long orderId, Integer paymentMethod);
/**
* 取消订单
*/
boolean cancelOrder(Long orderId, Long userId);
/**
* 发货
*/
boolean deliverOrder(Long orderId, String deliveryCompany, String deliveryNo);
/**
* 确认收货
*/
boolean confirmReceive(Long orderId, Long userId);
/**
* 分页查询订单
*/
PageResult<OrderVO> queryOrderPage(OrderQueryDTO queryDTO);
/**
* 查询订单详情
*/
OrderVO getOrderDetail(Long orderId, Long userId);
}
OrderServiceImpl.java
java
package com.example.order.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.order.dto.OrderCreateDTO;
import com.example.order.dto.OrderQueryDTO;
import com.example.order.dto.OrderVO;
import com.example.order.dto.PageResult;
import com.example.order.entity.*;
import com.example.order.entity.enums.OrderStatus;
import com.example.order.mapper.OrderItemMapper;
import com.example.order.mapper.OrderMapper;
import com.example.order.service.OrderService;
import com.example.order.service.ProductService;
import com.example.order.service.UserService;
import com.example.order.utils.OrderNoGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private final UserService userService;
private final ProductService productService;
private final OrderItemMapper orderItemMapper;
private final OrderMapper orderMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrder(OrderCreateDTO createDTO) {
// 1. 校验用户
User user = userService.getById(createDTO.getUserId());
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (user.getStatus() != 1) {
throw new RuntimeException("用户已被禁用");
}
// 2. 校验商品库存并计算总金额
List<OrderItem> orderItems = new ArrayList<>();
BigDecimal totalAmount = BigDecimal.ZERO;
for (OrderCreateDTO.OrderItemDTO itemDTO : createDTO.getItems()) {
Product product = productService.getById(itemDTO.getProductId());
if (product == null) {
throw new RuntimeException("商品不存在:" + itemDTO.getProductId());
}
if (product.getStatus() != 1) {
throw new RuntimeException("商品已下架:" + product.getProductName());
}
if (product.getStock() < itemDTO.getQuantity()) {
throw new RuntimeException("商品库存不足:" + product.getProductName());
}
// 创建订单明细
OrderItem item = new OrderItem();
item.setProductId(product.getId());
item.setProductName(product.getProductName());
item.setProductImage(product.getMainImage());
item.setPrice(product.getPrice());
item.setQuantity(itemDTO.getQuantity());
BigDecimal itemTotal = product.getPrice().multiply(BigDecimal.valueOf(itemDTO.getQuantity()));
item.setTotalAmount(itemTotal);
orderItems.add(item);
totalAmount = totalAmount.add(itemTotal);
}
// 3. 扣减库存(使用乐观锁,重试3次)
for (OrderCreateDTO.OrderItemDTO itemDTO : createDTO.getItems()) {
boolean success = productService.decreaseStock(itemDTO.getProductId(), itemDTO.getQuantity(), 3);
if (!success) {
throw new RuntimeException("扣减库存失败,请稍后重试");
}
}
// 4. 创建订单
Order order = new Order();
order.setOrderNo(OrderNoGenerator.generateOrderNo());
order.setUserId(createDTO.getUserId());
order.setTotalAmount(totalAmount);
order.setPayAmount(totalAmount); // 暂不考虑优惠
order.setStatus(OrderStatus.PENDING_PAYMENT);
order.setConsignee(createDTO.getConsignee());
order.setPhone(createDTO.getPhone());
order.setAddress(createDTO.getAddress());
order.setRemark(createDTO.getRemark());
this.save(order);
// 5. 保存订单明细
for (OrderItem item : orderItems) {
item.setOrderId(order.getId());
item.setOrderNo(order.getOrderNo());
orderItemMapper.insert(item);
}
log.info("订单创建成功,订单号:{},金额:{}", order.getOrderNo(), totalAmount);
return order;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean payOrder(Long orderId, Integer paymentMethod) {
// 1. 查询订单
Order order = this.getById(orderId);
if (order == null) {
throw new RuntimeException("订单不存在");
}
if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
throw new RuntimeException("订单状态不正确,无法支付");
}
// 2. 扣减用户余额
boolean balanceResult = userService.decreaseBalance(order.getUserId(), order.getPayAmount(), 3);
if (!balanceResult) {
throw new RuntimeException("支付失败,余额不足或扣款失败");
}
// 3. 更新订单状态
order.setStatus(OrderStatus.PAID);
order.setPaymentMethod(paymentMethod);
order.setPaymentTime(new Date());
boolean updated = this.updateById(order);
if (!updated) {
// 如果更新失败,理论上应该回滚余额,但这里简化处理,实际应引入补偿机制
throw new RuntimeException("订单状态更新失败");
}
log.info("订单支付成功,订单号:{}", order.getOrderNo());
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelOrder(Long orderId, Long userId) {
Order order = this.getById(orderId);
if (order == null) {
throw new RuntimeException("订单不存在");
}
// 校验权限(只能取消自己的订单)
if (!order.getUserId().equals(userId)) {
throw new RuntimeException("无权操作此订单");
}
// 只有待付款和已付款的订单可以取消
if (order.getStatus() != OrderStatus.PENDING_PAYMENT && order.getStatus() != OrderStatus.PAID) {
throw new RuntimeException("当前状态不可取消");
}
// 如果是已付款订单,需要退款(简化:直接增加库存,不处理退款)
if (order.getStatus() == OrderStatus.PAID) {
// 恢复库存
List<OrderItem> items = orderItemMapper.selectList(
new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderId, orderId));
for (OrderItem item : items) {
productService.increaseStock(item.getProductId(), item.getQuantity());
}
// 退还余额(简化:直接增加)
User user = userService.getById(userId);
user.setBalance(user.getBalance().add(order.getPayAmount()));
userService.updateById(user);
}
// 更新订单状态
order.setStatus(OrderStatus.CANCELLED);
this.updateById(order);
log.info("订单已取消,订单号:{}", order.getOrderNo());
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deliverOrder(Long orderId, String deliveryCompany, String deliveryNo) {
Order order = this.getById(orderId);
if (order == null) {
throw new RuntimeException("订单不存在");
}
if (order.getStatus() != OrderStatus.PAID) {
throw new RuntimeException("订单未付款或已发货");
}
order.setStatus(OrderStatus.SHIPPED);
order.setDeliveryTime(new Date());
// 可以添加物流信息字段,这里简化
return this.updateById(order);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirmReceive(Long orderId, Long userId) {
Order order = this.getById(orderId);
if (order == null) {
throw new RuntimeException("订单不存在");
}
if (!order.getUserId().equals(userId)) {
throw new RuntimeException("无权操作此订单");
}
if (order.getStatus() != OrderStatus.SHIPPED) {
throw new RuntimeException("订单未发货或状态错误");
}
order.setStatus(OrderStatus.COMPLETED);
order.setReceiveTime(new Date());
return this.updateById(order);
}
@Override
public PageResult<OrderVO> queryOrderPage(OrderQueryDTO queryDTO) {
Page<OrderVO> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
// 使用自定义Mapper方法进行复杂查询
com.baomidou.mybatisplus.core.metadata.IPage<OrderVO> voPage =
orderMapper.selectOrderPage(page, queryDTO);
return PageResult.of(voPage);
}
@Override
public OrderVO getOrderDetail(Long orderId, Long userId) {
OrderVO vo = orderMapper.selectOrderDetail(orderId);
if (vo == null) {
throw new RuntimeException("订单不存在");
}
// 校验权限(普通用户只能看自己的订单,管理员可以看所有)
if (userId != null && !vo.getUserId().equals(userId)) {
throw new RuntimeException("无权查看此订单");
}
// 设置状态描述
if (vo.getStatus() != null) {
vo.setStatusDesc(vo.getStatus().getDesc());
}
return vo;
}
}
5.1.7 工具类
OrderNoGenerator.java(订单号生成器)
java
package com.example.order.utils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicLong;
/**
* 订单号生成器:时间戳 + 机器ID + 序列号
*/
public class OrderNoGenerator {
private static final DateTimeFormatter DATE_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
private static final AtomicLong SEQUENCE = new AtomicLong(0);
private static final int MAX_SEQUENCE = 9999;
/**
* 生成订单号:18位时间戳 + 2位机器ID + 4位序列号 = 24位
*/
public static String generateOrderNo() {
// 时间戳部分
String timestamp = LocalDateTime.now().format(DATE_FORMAT); // 17位
// 机器ID(可以配置,这里写死01)
String machineId = "01";
// 序列号(0-9999循环)
long seq = SEQUENCE.incrementAndGet();
if (seq > MAX_SEQUENCE) {
SEQUENCE.set(0);
seq = 0;
}
String seqStr = String.format("%04d", seq);
return timestamp + machineId + seqStr;
}
public static void main(String[] args) {
// 测试生成
for (int i = 0; i < 10; i++) {
System.out.println(generateOrderNo());
}
}
}
RedisLockUtil.java(分布式锁工具)
java
package com.example.order.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLockUtil {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
/**
* 尝试获取锁
* @param key 锁的key
* @param timeout 超时时间(秒)
* @return 锁标识(用于释放锁)
*/
public String tryLock(String key, long timeout) {
String requestId = UUID.randomUUID().toString();
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, requestId, timeout, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
return requestId;
}
return null;
}
/**
* 释放锁(使用Lua脚本保证原子性)
*/
public boolean unlock(String key, String requestId) {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList(key), requestId);
return Long.valueOf(1).equals(result);
}
}
5.1.8 配置类
MybatisPlusConfig.java
java
package com.example.order.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.example.order.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(500L); // 单页最大限制
paginationInterceptor.setOverflow(true); // 溢出总页数后是否跳到第一页
interceptor.addInnerInterceptor(paginationInterceptor);
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
RedisConfig.java
java
package com.example.order.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer序列化value
Jackson2JsonRedisSerializer<Object> jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 使用StringRedisSerializer序列化key
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jacksonSeial);
template.setHashValueSerializer(jacksonSeial);
template.afterPropertiesSet();
return template;
}
}
SwaggerConfig.java(API文档配置)
java
package com.example.order.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.order.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("电商订单系统API文档")
.description("基于MyBatis-Plus的实战项目")
.version("1.0")
.build();
}
}
MyMetaObjectHandler.java(自动填充处理器)
java
package com.example.order.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.debug("开始插入填充...");
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
this.strictInsertFill(metaObject, "version", Integer.class, 1);
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
}
@Override
public void updateFill(MetaObject metaObject) {
log.debug("开始更新填充...");
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
GlobalExceptionHandler.java(全局异常处理)
java
package com.example.order.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Map<String, Object> handleException(Exception e) {
log.error("系统异常:", e);
Map<String, Object> result = new HashMap<>();
result.put("code", 500);
result.put("message", e.getMessage());
return result;
}
@ExceptionHandler(RuntimeException.class)
public Map<String, Object> handleRuntimeException(RuntimeException e) {
log.error("业务异常:", e);
Map<String, Object> result = new HashMap<>();
result.put("code", 400);
result.put("message", e.getMessage());
return result;
}
}
5.1.9 Controller层
OrderController.java
java
package com.example.order.controller;
import com.example.order.dto.OrderCreateDTO;
import com.example.order.dto.OrderQueryDTO;
import com.example.order.dto.OrderVO;
import com.example.order.dto.PageResult;
import com.example.order.entity.Order;
import com.example.order.service.OrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@Api(tags = "订单管理")
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@ApiOperation("创建订单")
@PostMapping
public Result<Order> createOrder(@Valid @RequestBody OrderCreateDTO createDTO) {
Order order = orderService.createOrder(createDTO);
return Result.success(order);
}
@ApiOperation("支付订单")
@PutMapping("/{orderId}/pay")
public Result<Boolean> payOrder(@PathVariable Long orderId,
@RequestParam Integer paymentMethod) {
boolean success = orderService.payOrder(orderId, paymentMethod);
return Result.success(success);
}
@ApiOperation("取消订单")
@PutMapping("/{orderId}/cancel")
public Result<Boolean> cancelOrder(@PathVariable Long orderId,
@RequestParam Long userId) {
boolean success = orderService.cancelOrder(orderId, userId);
return Result.success(success);
}
@ApiOperation("订单发货")
@PutMapping("/{orderId}/deliver")
public Result<Boolean> deliverOrder(@PathVariable Long orderId,
@RequestParam String deliveryCompany,
@RequestParam String deliveryNo) {
boolean success = orderService.deliverOrder(orderId, deliveryCompany, deliveryNo);
return Result.success(success);
}
@ApiOperation("确认收货")
@PutMapping("/{orderId}/receive")
public Result<Boolean> confirmReceive(@PathVariable Long orderId,
@RequestParam Long userId) {
boolean success = orderService.confirmReceive(orderId, userId);
return Result.success(success);
}
@ApiOperation("分页查询订单")
@GetMapping("/page")
public Result<PageResult<OrderVO>> queryOrderPage(OrderQueryDTO queryDTO) {
PageResult<OrderVO> pageResult = orderService.queryOrderPage(queryDTO);
return Result.success(pageResult);
}
@ApiOperation("查询订单详情")
@GetMapping("/{orderId}")
public Result<OrderVO> getOrderDetail(@PathVariable Long orderId,
@RequestParam(required = false) Long userId) {
OrderVO vo = orderService.getOrderDetail(orderId, userId);
return Result.success(vo);
}
}
统一返回结果封装 Result.java
java
package com.example.order.controller;
import lombok.Data;
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
5.1.10 启动类
java
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
5.2 企业开发规范总结
在以上实战项目中,我们遵循了以下企业级开发规范:
-
代码分层清晰:
-
Controller:接收请求、参数校验、返回结果
-
Service:业务逻辑、事务管理
-
Mapper:数据库操作
-
Entity:实体类,与数据库表对应
-
DTO:数据传输对象,避免实体暴露
-
-
命名规范:
-
类名:大驼峰,如
OrderServiceImpl -
方法名:小驼峰,如
createOrder -
常量:全大写,下划线分隔,如
MAX_RETRY_TIMES -
数据库字段:下划线命名,如
user_name,通过配置自动映射到驼峰属性
-
-
异常处理:
-
统一使用全局异常处理器,返回标准格式
-
业务异常抛出
RuntimeException或其子类
-
-
日志规范:
-
使用
@Slf4j注解,关键步骤打印日志 -
日志级别合理:INFO 记录正常流程,WARN 记录异常情况,ERROR 记录严重错误
-
-
配置分离:
-
数据库连接、Redis等配置放在
application.yml中 -
不同环境(dev、test、prod)使用不同的配置文件
-
-
接口文档:
- 使用 Swagger 生成 API 文档,方便前后端联调
-
事务管理:
-
在 Service 层方法上添加
@Transactional,确保数据一致性 -
合理设置事务传播行为和回滚规则
-
-
参数校验:
- 使用 Bean Validation(如
@NotNull)在 DTO 上进行参数校验
- 使用 Bean Validation(如
-
乐观锁控制并发:
- 使用
@Version注解和乐观锁插件,处理高并发下的数据竞争
- 使用
-
代码生成:
- 可以使用 MyBatis-Plus 代码生成器快速生成基础代码,减少重复劳动
第六阶段:复盘升华
6.1 MyBatis与MyBatis-Plus的最佳实践
经过前面的学习,我们总结出在企业中使用MyBatis/MyBatis-Plus的最佳实践:
6.1.1 如何选择:MyBatis 还是 MyBatis-Plus?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 新项目,单表操作居多 | MyBatis-Plus | 通用CRUD、条件构造器极大提高效率 |
| 遗留系统,已有大量XML | MyBatis-Plus(平滑引入) | 只增强不改变,可逐步替换 |
| 复杂SQL、多表关联查询 | MyBatis + MyBatis-Plus | 复杂SQL手写,简单操作用MP |
| 对SQL完全掌控 | MyBatis | 无需MP的自动生成,但可选择性使用 |
核心原则:MyBatis-Plus 是增强工具,不是替代品。两者可以完美共存。
6.1.2 参数传递与SQL编写
-
始终使用
#{},避免 SQL 注入。只有在动态表名、排序字段等极少数场景使用${},且必须对传入值做白名单校验。 -
善用
<sql>和<include>提取重复的SQL片段,提高可维护性。 -
复杂查询优先考虑在 XML 中手写 SQL,利用 MyBatis 的动态SQL能力,而不是在 Java 代码中拼接。
6.1.3 条件构造器的正确用法
-
优先使用 Lambda 式 Wrapper ,如
LambdaQueryWrapper,避免字段名硬编码,重构友好。 -
充分利用条件构造器的条件判断 ,如
wrapper.like(StringUtils.hasText(name), User::getName, name),减少 if 语句。 -
注意 Wrapper 的复用:每次查询创建新的 Wrapper 实例,不要复用。
6.1.4 分页最佳实践
-
使用 MyBatis-Plus 内置分页插件,配置简单,支持多种数据库。
-
分页参数统一封装 ,如
PageResult,避免重复代码。 -
大页码深度分页问题 :当页码过大时,使用
last方法优化,如last("limit 100000,10")改为基于游标的分页。
6.1.5 缓存使用策略
-
一级缓存(SqlSession级别)默认开启,但在 Spring 整合环境中,每次 Service 方法调用可能对应不同的 SqlSession,因此效果有限。不要依赖一级缓存。
-
二级缓存(Mapper级别)对查询频率极高、修改极少的字典表等场景有用,但需谨慎:
-
开启二级缓存后,所有查询结果会被缓存,更新操作会清空缓存。
-
对于多表关联查询,如果其中一个表更新,相关缓存不会自动失效,容易产生脏数据。建议关联查询不使用二级缓存。
-
推荐使用 Redis 等外部缓存,通过手动编码实现更精细的控制。
-
6.1.6 性能优化技巧
-
批量操作使用批量执行器:
java
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); // 执行多次 insert/update,最后 commit() -
避免 N+1 查询 :使用 JOIN 或
@Collection的嵌套结果代替嵌套查询。 -
合理使用索引:分析慢SQL,添加合适索引。
-
分页查询优化 :对于大表,使用
last方法限制最大偏移量,或使用游标分页。 -
使用
select指定需要的列 ,避免select *。 -
乐观锁重试机制 :在并发高的场景,乐观锁冲突时增加重试,如上面的
decreaseStock方法。
6.1.7 代码规范
-
实体类使用 Lombok 简化代码。
-
Mapper 接口继承
BaseMapper,获得通用CRUD方法。 -
Service 层继承
IService和ServiceImpl,获得通用 Service 方法。 -
枚举使用
@EnumValue注解,优雅处理数据库存储。 -
自动填充处理器统一处理创建时间、更新时间。
6.2 技术演进
持久层框架的演进反映了开发者对效率、灵活性和可维护性不断追求的过程。
6.2.1 从 JDBC 到 MyBatis
-
JDBC:最底层,需要手动处理所有细节,代码冗余,易出错。
-
Spring JDBC Template:简化了资源管理和异常处理,但结果集映射仍需手动。
-
MyBatis:通过 SQL 映射,将 SQL 与 Java 代码解耦,提供自动映射和动态SQL,成为主流。
6.2.2 从 MyBatis 到 MyBatis-Plus
-
MyBatis 解决了 SQL 映射的问题,但仍有大量重复的 CRUD SQL 需要编写。
-
MyBatis-Plus 在 MyBatis 基础上,提供了通用 CRUD、条件构造器、代码生成器等,极大提升开发效率。
6.2.3 未来趋势
-
JPA / Hibernate:全自动 ORM 框架,适合简单场景,但复杂 SQL 难以优化。
-
MyBatis-Plus 持续进化:支持更多数据库、提供更多插件(多租户、动态表名等)、与微服务架构更好集成。
-
响应式编程:随着 WebFlux 等响应式框架的流行,可能会出现响应式的持久层框架(如 R2DBC + MyBatis 的响应式版本)。
-
多数据源、分布式事务:与 Seata 等分布式事务框架的整合会更加紧密。
6.3 面试高频问题与解答
问题1:MyBatis 中 #{} 和 ${} 的区别是什么?
答案:
-
#{}是预编译处理,会生成预编译 SQL,对应 JDBC 的?占位符,可以防止 SQL 注入。 -
${}是字符串替换,直接将参数值拼接到 SQL 中,无法防止 SQL 注入。 -
使用场景:绝大部分情况用
#{};只有需要动态传入表名、列名、排序字段等时才用${},且必须对传入值做严格校验。
问题2:MyBatis 的一级缓存和二级缓存了解吗?
答案:
-
一级缓存:SqlSession 级别的缓存,默认开启。在同一个 SqlSession 中执行两次相同的查询,第二次会直接从缓存中获取,不再查询数据库。当执行增删改操作时,一级缓存会清空。
-
二级缓存:Mapper 级别的缓存,跨 SqlSession,需要手动开启。多个 SqlSession 可以共享二级缓存,缓存粒度更粗。二级缓存适合读多写少的表,但需要注意缓存一致性问题,尤其是关联查询。
-
在 Spring 整合环境下,由于 SqlSession 由 Spring 管理,每次 Service 方法调用可能对应不同的 SqlSession,一级缓存效果有限。二级缓存需要谨慎配置。
问题3:MyBatis 中 Mapper 接口的工作原理是什么?
答案 :
Mapper 接口是使用 JDK 动态代理实现的。MyBatis 启动时会解析 Mapper 接口和对应的 XML 文件(或注解),为每个 Mapper 接口生成一个 MapperProxy 代理对象。当调用接口方法时,MapperProxy 会根据方法名找到对应的 MappedStatement(包含 SQL 信息),然后委托给 SqlSession 执行。最终通过 Executor 完成数据库操作。
问题4:MyBatis-Plus 的条件构造器有什么优势?
答案:
-
类型安全:Lambda 条件构造器通过方法引用指定字段名,避免硬编码字符串,重构时不易出错。
-
链式编程:可以连续添加多个条件,代码简洁易读。
-
动态条件 :支持条件判断,如
like(condition, column, value),减少 if 语句。 -
支持复杂组合:可以嵌套 and/or,构建复杂查询条件。
问题5:MyBatis-Plus 的通用 CRUD 是如何实现的?
答案 :
MyBatis-Plus 通过继承 BaseMapper 接口,内置了一系列 CRUD 方法,如 insert、deleteById、selectById 等。这些方法在 MyBatis-Plus 启动时,通过 SqlInjector 将 SQL 语句动态注入到 MyBatis 的配置中。本质上,它们也是通过 MappedStatement 注册到 MyBatis 的,和手写的 XML 没有区别,只是由框架自动生成了。
问题6:如何处理 MyBatis-Plus 的多表关联查询?
答案 :
MyBatis-Plus 主要擅长单表操作,对于多表关联,有以下几种方案:
-
在 Mapper 中自定义方法 ,并在 XML 中手写 SQL,使用
resultMap进行关联映射。 -
使用
@Select注解,直接写 SQL。 -
使用
@TableName(autoResultMap = true)结合@TableField(typeHandler = ...),但复杂关联不推荐。 -
在 Service 层进行多次查询,手动组装数据(适用于简单场景)。
问题7:MyBatis 中如何实现分页?有什么注意事项?
答案 :
MyBatis 本身支持内存分页(RowBounds),但不推荐。实际开发中通常使用分页插件,如 PageHelper 或 MyBatis-Plus 自带的分页插件。
-
使用分页插件时,只需在查询前调用
PageHelper.startPage(pageNum, pageSize),或使用 MyBatis-Plus 的Page对象。 -
注意事项:
-
分页插件只对紧随其后的第一个查询生效。
-
避免在分页查询中使用复杂的关联查询导致性能问题。
-
大页码深度分页应使用游标或子查询优化。
-
问题8:MyBatis-Plus 的乐观锁插件如何使用?
答案:
-
配置乐观锁插件
OptimisticLockerInnerInterceptor。 -
在实体类版本字段上添加
@Version注解。 -
更新数据时,先查询出版本号,然后执行更新,插件会自动在更新 SQL 中加入版本条件
WHERE version = oldVersion,并更新版本号。 -
如果更新失败(影响行数为0),说明数据已被其他事务修改,可以重试。
问题9:MyBatis 中如何解决 N+1 查询问题?
答案 :
N+1 问题通常发生在使用嵌套查询的关联查询中。例如,查询订单列表,然后对每个订单查询用户信息,就会产生 1 + N 次查询。
解决方案:
-
使用 JOIN 查询一次性查出所有数据,通过
resultMap的association或collection映射。 -
使用懒加载,并在业务层批量获取数据。
问题10:MyBatis-Plus 的逻辑删除功能如何实现?
答案:
-
全局配置逻辑删除字段名和值(或在实体类字段上添加
@TableLogic注解)。 -
执行
delete方法时,实际执行的是UPDATE语句,将逻辑删除字段更新为删除值。 -
执行查询时,MyBatis-Plus 会自动拼接条件
deleted=0,过滤已删除数据。 -
如果想查询包含已删除的数据,需要手动编写 SQL 或使用
wrapper.last("AND deleted=1")。
问题11:MyBatis 的插件原理是什么?如何自定义插件?
答案 :
MyBatis 允许拦截四大对象的方法调用:Executor、ParameterHandler、ResultSetHandler、StatementHandler。通过实现 Interceptor 接口,并在配置文件中注册,可以对这些方法进行增强。
自定义插件步骤:
-
实现
Interceptor接口。 -
使用
@Intercepts和@Signature注解指定要拦截的方法。 -
在
intercept方法中编写增强逻辑。 -
在配置文件中注册插件。
问题12:在 Spring Boot 中如何配置 MyBatis-Plus 的多数据源?
答案 :
可以使用 MyBatis-Plus 的 dynamic-datasource starter:
-
引入依赖
dynamic-datasource-spring-boot-starter。 -
在配置文件中配置多个数据源。
-
在 Service 或 Mapper 上使用
@DS注解指定数据源。 -
默认支持读写分离、负载均衡等。
6.4 结语
从 JDBC 的繁琐到 MyBatis 的优雅,再到 MyBatis-Plus 的高效,持久层框架的发展始终围绕「提高开发效率、保证代码质量」这一核心目标。掌握 MyBatis 和 MyBatis-Plus,不仅是学会使用工具,更是理解如何通过框架解决实际问题,如何在灵活性和便捷性之间找到平衡。
希望通过这篇六万字的详尽教程,你能从「问题驱动」到「基础认知」,再到「核心用法」「场景融合」「企业实战」,最后「复盘升华」,建立起对持久层框架的完整知识体系。在今后的开发中,能够根据业务场景选择合适的技术,写出高质量、可维护的代码。