• 如何把MyBatis原来基于XML配置SQL语句改成在application.yml文件中配置?
  • 发布于 2个月前
  • 188 热度
    0 评论
介绍
之前接到一个奇怪的需求,要把原来的一些基于MyBatis XML的SQL,改成写在配置文件application.yml中做成配置项,本着能少改就少改的原则,底层依然保持使用MyBatis,只是需要替换掉这些XML,下面记录一下实现过程及过程中遇到的问题。

MyBatis 是一个流行的 Java 持久层框架,它允许你使用 XML 或注解来配置 SQL 语句,并通过动态 SQL 技术来构建灵活的查询。动态 SQL 允许你在 SQL 语句中使用条件判断和循环,以便根据不同的输入参数生成不同的 SQL 语句。

这里再简单介绍一下常用的标签,帮大家巩固一下:
标签 说明
#{} 预处理语句参数,用于绑定变量值,防止SQL注入
${} 直接拼接SQL语句,不进行参数绑定,存在SQL注入风险
select 用于指定查询语句,通常包含一个返回结果的select语句
insert 用于指定插入数据的SQL语句
update 用于指定更新数据的SQL语句
delete 用于指定删除数据的SQL语句
resultType 指定查询结果的类型,通常与实体类对应
resultMap 用于映射查询结果到对象模型,支持嵌套结果映射
if 根据条件判断是否包含某段SQL语句,使用test属性指定判断条件
choose/when/otherwise 根据条件选择不同的SQL语句片段,类似于switch语句
foreach 遍历集合或数组,生成SQL语句片段,常用于批量操作或生成动态SQL
trim 用于修剪SQL语句,指定需要保留的部分,可结合prefix和suffix使用
where 生成WHERE子句,可结合prefix和suffix使用,用于拼接条件判断语句
set 生成SET子句,常用于UPDATE操作中指定更新的列
foreach 遍历集合或数组,生成SQL语句片段,常用于批量操作或生成动态SQL
简单例子
1.<if> 标签:
<select id="getUser" parameterType="map" resultType="User">
  SELECT * FROM user
  <if test="name != null">
    AND name = #{name}
  </if>
  <if test="age != null">
    AND age = #{age}
  </if>
</select>
上述示例中,根据输入参数的name和age是否为空,生成相应的SQL语句。

2.<choose>、<when> 和 <otherwise> 标签:
<select id="getUser" parameterType="map" resultType="User">
  SELECT * FROM user
  <choose>
    <when test="name != null">
      AND name = #{name}
    </when>
    <when test="age != null">
      AND age = #{age}
    </when>
    <otherwise>
      AND is_active = 1
    </otherwise>
  </choose>
</select>
上述示例中,根据输入参数的name和age是否为空,选择不同的SQL语句片段。如果都为空,则默认使用is_active = 1作为条件。

3.<foreach> 标签:
<select id="getUsersByRole" parameterType="map" resultType="User">
  SELECT * FROM user WHERE role IN 
  <foreach item="role" index="index" collection="roles" open="(" separator="," close=")">
    #{role}
  </foreach>
</select>
上述示例中,根据输入参数roles集合中的元素,生成相应的SQL语句,用于查询符合指定角色的用户。除了上述示例中的标签外,MyBatis还提供了其他标签,如<trim>、<where>、<set>等,用于更灵活地构建SQL语句。

正文
第一版
XML 解析和注册类实现关系:

SqlSessionFactoryBuilder 作为整个 Mybatis 的入口,提供建造者工厂,包装 XML 解析处理,并返回对应 SqlSessionFactory 处理类。通过解析把 XML 信息注册到 Configuration 配置类中,再通过传递 Configuration 配置类到各个逻辑处理类里,包括 DefaultSqlSession 中,这样就可以在获取映射器和执行SQL的时候,从配置类中拿到对应的内容了。


1.自定义SqlSessionFactory:
@Configuration
public class MybatisPlusConfig {

    @Value("${custom.sql1:}")
    public String sql1;

    @Bean
    public PerformanceInterceptor performanceInterceptor() {
        PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
        /* <!-- SQL 执行性能分析,开发环境使用,线上不推荐。maxTime 指的是 sql 最大执行时长 --> */
        performanceInterceptor.setMaxTime(1000);
        /* <!--SQL是否格式化 默认false--> */
        performanceInterceptor.setFormat(true);
        return performanceInterceptor;
    }

    /**
     * 配置分页插件
     * 堆代码 duidaima.com
     * @return
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }

    /**
     * 引入业务库数据源配置
     */
    @Resource(name = "businessDataSource")
    private DataSource businessDataSource;

    @Bean
    public SqlSessionFactory businessSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        sessionFactory.setDataSource(businessDataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactory.setMapperLocations(resolver.getResources("classpath*:/mapper/business/**/*.xml"));

        // 将配置添加至sessionFactory
        Properties properties = new Properties();
        properties.put("sql1", sql1.replaceAll("#'\\{([^'}]+)}'", "#\\{$1\\}"));
        sessionFactory.setConfigurationProperties(properties);

        //添加分页功能
        sessionFactory.setPlugins(new Interceptor[]{
                paginationInterceptor()
        });

        //map接收返回值值为null的问题,默认是当值为null,将key返回
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setCallSettersOnNulls(true);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        //打印sql 拦截器
        MybatisSqlLoggerInterceptor interceptor = new MybatisSqlLoggerInterceptor();
        configuration.addInterceptor(interceptor);
        sessionFactory.setConfiguration(configuration);
        //设置全局GlobaleConfig用于解决Oracle主键自增
        if (businessDataSource.getDriverClassName().contains("OracleDriver")) {
            //设置全局GlobaleConfig用于解决Oracle主键自增
            GlobalConfig.DbConfig config = new GlobalConfig.DbConfig();
            config.setKeyGenerator(keyGenerator());
            GlobalConfig globalConfig = new GlobalConfig();
            globalConfig.setDbConfig(config);
            sessionFactory.setGlobalConfig(globalConfig);
        }

        return sessionFactory.getObject();
    }

    @Bean(name = "txManager1")
    public PlatformTransactionManager txManager1() {
        return new DataSourceTransactionManager(businessDataSource);
    }

    /***
     * 设置oracle主键自增
     * @return
     */
    @Bean(name = "keyGenerator")
    public IKeyGenerator keyGenerator() {
        return new OracleKeyGenerator();
    }
}
2.编写mapper.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="org.example.dao.business.EntMapper">
    <select id="sql1"  resultType="org.example.entity.business.User" >
        ${sql1}
    </select>
</mapper>
仔细的小伙伴会发现这里使用了$,获取SqlSessionFactory里Properties属性值,只能用$,#获取不到,这也为后面漏洞扫描埋下了伏笔

3.配置application.yml:
custom:
  sql1: "select * from user where name = #'{name}' and age = #'{age}'"
4.漏洞说明:

没办法只能另找其他方案,功夫不负有心人,还真找到了。

第二版
@XXXProvider 是 MyBatis 中的一个注解,用于指定一个类或者类的某个方法提供 SQL 查询语句,该注解常用于动态 SQL 的场景。

例如:根据不同的参数生成不同的查询语句。使用 @SelectProvider 注解的方式可以让 MyBatis 在运行时根据注解指定的类或方法来生成对应的 SQL 查询语句,从而实现动态 SQL 功能。
注解 作用
@SelectProvider 用于动态生成查询SQL语句
@InsertProvider 用于动态生成新增SQL语句
@UpdateProvider 用于动态生成更新SQL语句
@DeleteProvider 用于动态生成删除SQL语句
1.获取配置信息:
@Component
public class MybatisHelp {
    public static String sql_1;

    @Value("${custom.sql1:}")
    private void setSql1(String sql1){
        sql1 = sql1.replaceAll("#'\\{([^']+)}'", "#\\{$1\\}");
        sql_1 = sql1;
    };

    public String sql_1(String name,Integer age){
        return sql_1;
    }
}
这里只所以使用@Value给静态变量注入值,是因为静态变量被所有类实例对象所共享,在内存中只有一个副本,当且仅当在类初次加载时会被初始化,@XXXProvider在启动时就会加载相应的类和方法获取SQL,直接采用@Value方法是获取不到配置项的

2.创建mapper
@Mapper
public interface UserMapper extends BaseMapper<User> {

    @SelectProvider(type = MybatisHelp.class, method = "sql_1")
    List<User> listParam(@Param("name") String name,@Param("age") Integer age);
}

用户评论