朋友亲历!Java 面试问 MyBatis 扩展题:商品表查询左连接订单表的实战解法

这篇博客详细说明了 MyBatis 扩展实现左连接的方法

最近,身边一位同为 Java 开发者的朋友去面试,遇到一道让他印象深刻的题目 —— 如何在不修改原有 SQL 的情况下,用 MyBatis 实现商品表查询时左连接订单表?这看似简单的需求,实则暗藏玄机,考验着开发者对 MyBatis 扩展机制的理解与应用能力。朋友面试结束后和我聊起这道题,我也来了兴致。回想自己八年开发历程,类似的复杂需求在实际项目中层出不穷,MyBatis 的灵活扩展往往是解决问题的关键。

实际业务场景分析

在电商系统里,商品展示是非常核心的功能。商品表(product)存储着商品的基本信息,包括商品 ID(product_id)、商品名称(product_name)、价格(product_price)等;订单表(order)记录了每一笔订单的详情,如订单 ID(order_id)、用户 ID(user_id)、关联的商品 ID(product_id)以及订单时间(order_time)。

运营部门提出这样一个需求:在商品列表页展示商品信息时,需要同时显示每个商品对应的订单数量,哪怕某个商品暂时还没有产生订单,也要在列表中展示出来,并且订单数量显示为 0。这就意味着,我们需要在查询商品表数据的 SQL 语句基础上,左连接订单表,通过聚合统计得到每个商品的订单数量。

如果按照传统方式,我们需要修改每个查询商品的 SQL 语句,添加左连接和聚合统计的逻辑,这不仅工作量大,而且后期维护成本高。这时,MyBatis 的扩展能力就派上用场了,我们可以通过拦截器机制,在不修改原有 SQL 语句的前提下,动态地为查询商品的 SQL 添加左连接订单表的逻辑。

MyBatis 扩展实现左连接的具体步骤及代码详解

1. 定义基础 Mapper 接口和 XML 映射文件

首先,创建商品表的 Mapper 接口ProductMapper,定义查询所有商品的方法selectAllProducts,该方法将返回商品列表。

import java.util.List;
// 商品表的Mapper接口,定义与商品表相关的数据库操作方法
public interface ProductMapper {
    // 查询所有商品的方法,返回商品列表
    List<Product> selectAllProducts();
}

接着,创建对应的ProductMapper.xml映射文件,编写基础的查询商品 SQL 语句。这里只是简单地从商品表中查询商品的 ID、名称和价格。

<?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.mapper.ProductMapper">
    <!-- 定义查询所有商品的SQL语句,resultType指定返回的结果类型为Product实体类 -->
    <select id="selectAllProducts" resultType="com.example.domain.Product">
        SELECT
            p.product_id,
            p.product_name,
            p.product_price
        FROM
            product p
    </select>
</mapper>

其中,Product是商品的实体类,包含了与数据库表字段对应的属性,例如:

public class Product {
    private Long product_id;
    private String product_name;
    private BigDecimal product_price;
    // 省略getter和setter方法
}

2. 实现 MyBatis 拦截器

MyBatis 提供的拦截器机制,允许我们在 SQL 执行的过程中对其进行修改和扩展。我们创建一个SqlLeftJoinInterceptor拦截器类,实现Interceptor接口。

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
// 定义拦截器,通过@Intercepts注解指定拦截Executor的query方法
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SqlLeftJoinInterceptor implements Interceptor {
    // 拦截方法,在SQL执行前进行处理
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取方法调用的参数
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        // 获取执行SQL的Executor对象
        Executor executor = (Executor) invocation.getTarget();
        // 获取原始的BoundSql对象,包含SQL语句、参数映射等信息
        BoundSql boundSql = ms.getBoundSql(parameter);
        String sql = boundSql.getSql();
        // 判断当前SQL是否是查询商品表的SQL,如果是则进行左连接订单表的扩展
        if (sql.contains("FROM product")) {
            // 添加左连接订单表并进行聚合统计的SQL语句
            sql = sql + " LEFT JOIN `order` o ON p.product_id = o.product_id GROUP BY p.product_id, p.product_name, p.product_price";
            // 创建新的BoundSql对象,替换原始的SQL语句
            BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
            // 创建新的MappedStatement对象,替换原始的MappedStatement
            MappedStatement newMs = copyMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
            args[0] = newMs;
        }
        // 继续执行原始的查询操作
        return invocation.proceed();
    }
    // 用于包装目标对象,生成代理对象
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    // 设置拦截器的属性,这里暂时未使用
    @Override
    public void setProperties(Properties properties) {
    }
    // 复制MappedStatement对象,用于替换修改后的SQL信息
    private MappedStatement copyMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }
    // 自定义的SqlSource实现类,用于包装BoundSql对象
    public static class BoundSqlSqlSource implements SqlSource {
        private final BoundSql boundSql;
        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }
        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }
}

3. 配置拦截器

在 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>
    <plugins>
        <!-- 配置自定义的拦截器 -->
        <plugin interceptor="com.example.interceptor.SqlLeftJoinInterceptor">
        </plugin>
    </plugins>
</configuration>

测试与验证

编写测试代码,调用ProductMapper的selectAllProducts方法,查看查询结果是否符合预期。

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        // 加载MyBatis配置文件
        String resource = "mybatis-config.xml";
        InputStream inputStream = Main.class.getClassLoader().getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            // 获取ProductMapper接口实例
            ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
            // 调用查询所有商品的方法
            List<Product> products = productMapper.selectAllProducts();
            for (Product product : products) {
                System.out.println(product);
            }
        }
    }
}

运行测试代码后,我们会发现查询结果中,每个商品对象都包含了经过左连接和聚合统计后的订单数量信息,成功满足了业务需求。

总结

通过这次 MyBatis 扩展实现左连接的实践,我们可以看到 MyBatis 拦截器机制的强大之处。它允许我们在不修改原有 SQL 语句的基础上,动态地对 SQL 进行扩展和修改,大大提高了代码的可维护性和扩展性。在实际项目中,我们可以根据不同的业务需求,灵活运用 MyBatis 的扩展能力。不过,在使用拦截器时要注意性能问题,避免因过度使用拦截器影响系统的整体性能。希望这篇博客能帮助大家更好地掌握 MyBatis 的扩展应用。

以上从实际场景出发,结合注释代码讲解了 MyBatis 扩展实现左连接。若你觉得某些部分还需更深入阐述,或想了解其他扩展案例,随时和我说。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天摸鱼的java工程师

谢谢老板,老板大气。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值