*Java 沉淀重走长征路*之——《MyBatis与MyBatis-Plus一文打尽!》

前言:为什么我们需要持久层框架?

在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方法(比如insertselectByIdupdateById)和对应的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>

属性加载顺序(后加载的会覆盖先加载的):

  1. properties元素体内指定的属性

  2. 从资源路径加载的属性(resource属性)

  3. 从URL加载的属性(url属性)

  4. 方法参数传递的属性(如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关键字

  • 自动去除第一个条件前面的ANDOR

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更新)

核心建议 :始终使用LambdaQueryWrapperLambdaUpdateWrapper,它们提供编译期类型安全检查,避免字段名拼写错误。

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 &lt;= #{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 企业开发规范总结

在以上实战项目中,我们遵循了以下企业级开发规范:

  1. 代码分层清晰

    • Controller:接收请求、参数校验、返回结果

    • Service:业务逻辑、事务管理

    • Mapper:数据库操作

    • Entity:实体类,与数据库表对应

    • DTO:数据传输对象,避免实体暴露

  2. 命名规范

    • 类名:大驼峰,如 OrderServiceImpl

    • 方法名:小驼峰,如 createOrder

    • 常量:全大写,下划线分隔,如 MAX_RETRY_TIMES

    • 数据库字段:下划线命名,如 user_name,通过配置自动映射到驼峰属性

  3. 异常处理

    • 统一使用全局异常处理器,返回标准格式

    • 业务异常抛出 RuntimeException 或其子类

  4. 日志规范

    • 使用 @Slf4j 注解,关键步骤打印日志

    • 日志级别合理:INFO 记录正常流程,WARN 记录异常情况,ERROR 记录严重错误

  5. 配置分离

    • 数据库连接、Redis等配置放在 application.yml

    • 不同环境(dev、test、prod)使用不同的配置文件

  6. 接口文档

    • 使用 Swagger 生成 API 文档,方便前后端联调
  7. 事务管理

    • 在 Service 层方法上添加 @Transactional,确保数据一致性

    • 合理设置事务传播行为和回滚规则

  8. 参数校验

    • 使用 Bean Validation(如 @NotNull)在 DTO 上进行参数校验
  9. 乐观锁控制并发

    • 使用 @Version 注解和乐观锁插件,处理高并发下的数据竞争
  10. 代码生成

    • 可以使用 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 性能优化技巧
  1. 批量操作使用批量执行器

    java

    复制代码
    SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
    // 执行多次 insert/update,最后 commit()
  2. 避免 N+1 查询 :使用 JOIN 或 @Collection 的嵌套结果代替嵌套查询。

  3. 合理使用索引:分析慢SQL,添加合适索引。

  4. 分页查询优化 :对于大表,使用 last 方法限制最大偏移量,或使用游标分页。

  5. 使用 select 指定需要的列 ,避免 select *

  6. 乐观锁重试机制 :在并发高的场景,乐观锁冲突时增加重试,如上面的 decreaseStock 方法。

6.1.7 代码规范
  • 实体类使用 Lombok 简化代码。

  • Mapper 接口继承 BaseMapper,获得通用CRUD方法。

  • Service 层继承 IServiceServiceImpl,获得通用 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 方法,如 insertdeleteByIdselectById 等。这些方法在 MyBatis-Plus 启动时,通过 SqlInjector 将 SQL 语句动态注入到 MyBatis 的配置中。本质上,它们也是通过 MappedStatement 注册到 MyBatis 的,和手写的 XML 没有区别,只是由框架自动生成了。

问题6:如何处理 MyBatis-Plus 的多表关联查询?

答案

MyBatis-Plus 主要擅长单表操作,对于多表关联,有以下几种方案:

  1. 在 Mapper 中自定义方法 ,并在 XML 中手写 SQL,使用 resultMap 进行关联映射。

  2. 使用 @Select 注解,直接写 SQL。

  3. 使用 @TableName(autoResultMap = true) 结合 @TableField(typeHandler = ...),但复杂关联不推荐。

  4. 在 Service 层进行多次查询,手动组装数据(适用于简单场景)。

问题7:MyBatis 中如何实现分页?有什么注意事项?

答案

MyBatis 本身支持内存分页(RowBounds),但不推荐。实际开发中通常使用分页插件,如 PageHelper 或 MyBatis-Plus 自带的分页插件。

  • 使用分页插件时,只需在查询前调用 PageHelper.startPage(pageNum, pageSize),或使用 MyBatis-Plus 的 Page 对象。

  • 注意事项:

    • 分页插件只对紧随其后的第一个查询生效。

    • 避免在分页查询中使用复杂的关联查询导致性能问题。

    • 大页码深度分页应使用游标或子查询优化。

问题8:MyBatis-Plus 的乐观锁插件如何使用?

答案

  1. 配置乐观锁插件 OptimisticLockerInnerInterceptor

  2. 在实体类版本字段上添加 @Version 注解。

  3. 更新数据时,先查询出版本号,然后执行更新,插件会自动在更新 SQL 中加入版本条件 WHERE version = oldVersion,并更新版本号。

  4. 如果更新失败(影响行数为0),说明数据已被其他事务修改,可以重试。

问题9:MyBatis 中如何解决 N+1 查询问题?

答案

N+1 问题通常发生在使用嵌套查询的关联查询中。例如,查询订单列表,然后对每个订单查询用户信息,就会产生 1 + N 次查询。

解决方案:

  • 使用 JOIN 查询一次性查出所有数据,通过 resultMapassociationcollection 映射。

  • 使用懒加载,并在业务层批量获取数据。

问题10:MyBatis-Plus 的逻辑删除功能如何实现?

答案

  1. 全局配置逻辑删除字段名和值(或在实体类字段上添加 @TableLogic 注解)。

  2. 执行 delete 方法时,实际执行的是 UPDATE 语句,将逻辑删除字段更新为删除值。

  3. 执行查询时,MyBatis-Plus 会自动拼接条件 deleted=0,过滤已删除数据。

  4. 如果想查询包含已删除的数据,需要手动编写 SQL 或使用 wrapper.last("AND deleted=1")

问题11:MyBatis 的插件原理是什么?如何自定义插件?

答案

MyBatis 允许拦截四大对象的方法调用:Executor、ParameterHandler、ResultSetHandler、StatementHandler。通过实现 Interceptor 接口,并在配置文件中注册,可以对这些方法进行增强。

自定义插件步骤:

  1. 实现 Interceptor 接口。

  2. 使用 @Intercepts@Signature 注解指定要拦截的方法。

  3. intercept 方法中编写增强逻辑。

  4. 在配置文件中注册插件。

问题12:在 Spring Boot 中如何配置 MyBatis-Plus 的多数据源?

答案

可以使用 MyBatis-Plus 的 dynamic-datasource starter:

  1. 引入依赖 dynamic-datasource-spring-boot-starter

  2. 在配置文件中配置多个数据源。

  3. 在 Service 或 Mapper 上使用 @DS 注解指定数据源。

  4. 默认支持读写分离、负载均衡等。

6.4 结语

从 JDBC 的繁琐到 MyBatis 的优雅,再到 MyBatis-Plus 的高效,持久层框架的发展始终围绕「提高开发效率、保证代码质量」这一核心目标。掌握 MyBatis 和 MyBatis-Plus,不仅是学会使用工具,更是理解如何通过框架解决实际问题,如何在灵活性和便捷性之间找到平衡。

希望通过这篇六万字的详尽教程,你能从「问题驱动」到「基础认知」,再到「核心用法」「场景融合」「企业实战」,最后「复盘升华」,建立起对持久层框架的完整知识体系。在今后的开发中,能够根据业务场景选择合适的技术,写出高质量、可维护的代码。

相关推荐
liuccn2 小时前
GeoTools跟GDAL 库的关系与区别以及应用场景
java·arcgis
brave_zhao2 小时前
javafx中能有异步调用业务方法吗
java
王夏奇2 小时前
python中的深浅拷贝和上下文管理器
java·服务器·前端
皙然2 小时前
深入理解 Java HashMap:从底层原理、源码设计到面试考点全解析
java·开发语言·面试
元Y亨H2 小时前
RuoYi-Cloud-Vue 架构全解析:微服务+前后端分离
java·微服务
子超兄2 小时前
ThreadLocal相关问题
java
啊唯不困3 小时前
AI智能应用开发(Java)起点-终点 -1、java的前世今生andJava环境配置、jdk下载,以及Idea下载和基本应用
java·开发语言·intellij-idea
_muffinman3 小时前
Java学习笔记-第2章 运算和语句
java·笔记·学习
荒夜长歌3 小时前
传统java行业跳槽面试汇总(后续会更新)
java·面试·跳槽