likes
comments
collection
share

水煮MyBatis(二一)- 动态SQL:@Provider

作者站长头像
站长
· 阅读数 19

前言

Mybatis里的动态SQL,估计用到的同学不是很多,毕竟在xml文件中定义sql语句的方式,已经可以满足绝大部分的开发需求,方便又简单。没有痛点,也就少了动力。这一章就来聊聊这块,对于有代码洁癖的人来说,还是很赏心悦目的。

四个注解

@Provider系列的注解有四个:

  • @SelectProvider,被定义用来提供查询方法的SQL;
  • @UpdateProvider,被定义用来提供更新方法的SQL;
  • @DeleteProvider,被定义用来提供删除方法的SQL;
  • @InsertProvider,被定义用来提供保存方法的SQL;

官方例子


public interface UserMapper {

  // 保存用户数据
  @InsertProvider(type = SqlProvider.class, method = "insert")
  void insert(User user);

  public static class SqlProvider {
    // 对应@InsertProvider注解里的method,返回对应sql
    public static String insert() {
      return "INSERT INTO users (id, name) VALUES(#{id}, #{name})";
    }
  }
}

序列图

此章节主要源码都在ProviderSqlSource里。 水煮MyBatis(二一)- 动态SQL:@Provider

源码

这里介绍的源码不多,主要是两个部分

  • sql拼接;
  • 反射执行provider方法;

sql拼接

sql拼接,主要依赖AbstractSQL里的静态方法,下面讲一下update语句。


  List<String> sets = new ArrayList<>();
  List<String> tables = new ArrayList<>();
  List<String> where = new ArrayList<>();
  
  public T UPDATE(String table) {
    // 指定类型为update
    sql().statementType = SQLStatement.StatementType.UPDATE;
    // 设定表名,tables集合添加元素
    sql().tables.add(table);
    // 返回当前sqlBuilder对象
    return getSelf();
  }
  
    private String updateSQL(SafeAppendable builder) {
       // 拼接 UPDATE [table_name]
      sqlClause(builder, "UPDATE", tables, "", "", "");
      joins(builder);
      // 拼接sets集合
      sqlClause(builder, "SET", sets, "", "", ", ");
      // 拼接where条件集合
      sqlClause(builder, "WHERE", where, "(", ")", " AND ");
      // 拼接限定条件
      limitingRowsStrategy.appendClause(builder, null, limit);
      return builder.toString();
    }

注意: sql拼接是按照一定顺序的,tables -> sets -> where,就算是我们在代码里,刻意打乱顺序,也没有影响,比如:

 WHERE("md5 = #{md5}");
 SET("update_time = NOW()");
 UPDATE(tableName);

可以不拼接吗?

其实是可以的,毕竟Provider本质上,就是提供了待执行的sql预处理语句。看官方的例子,其实就没有使用拼接,在后面的例子里,如果不进行参数判空,可以写成这样:

public String updateStatus() {
    return "UPDATE tb_image SET update_time = NOW(), status = ? WHERE (md5 = ?) AND (status = ?)";
}

反射执行provider方法

  private String invokeProviderMethod(Object... args) throws Exception {
    Object targetObject = null;
    if (!Modifier.isStatic(providerMethod.getModifiers())) {
      // 如果是非静态方法,则需要一个类实例
      targetObject = providerType.getDeclaredConstructor().newInstance();
    }
    // 反射执行@Provider里指定的方法
    CharSequence sql = (CharSequence) providerMethod.invoke(targetObject, args);
    // 返回sql语句
    return sql != null ? sql.toString() : null;
  }

这里返回的sql语句,是基于JDBC预处理语法的字符串,例如 UPDATE tb_image SET update_time = NOW(), status = ? WHERE (md5 = ?) AND (status = ?)

参数是怎么处理的

CharSequence sql = (CharSequence) providerMethod.invoke(targetObject, args); 这一行代码里,指定了方法参数【args】,首先说明,provider方法里的参数,都来自于Mapper里的方法参数值,从params里获取对应参数名称的值,写入到args;

  private Object[] extractProviderMethodArguments(Map<String, Object> params, String[] argumentNames) {
    Object[] args = new Object[argumentNames.length];
    for (int i = 0; i < args.length; i++) {
      if (providerContextIndex != null && providerContextIndex == i) {
        args[i] = providerContext;
      } else {
        // 关键就是这一句,从params里获取对应参数名称的值,写入到args;
        args[i] = params.get(argumentNames[i]);
      }
    }
    return args;
  }

方法参数介绍

  • params:Mapper里对应方法的所有参数;
  • argumentNames:Provider里方法的参数名称;

这是下面例子里的参数处理结果 水煮MyBatis(二一)- 动态SQL:@Provider

我的例子

provider

注意provider里方法的参数,可以和mapper参数一致,也可以缺失几个。引用mapper的参数,主要是为了进行逻辑分支判定

public class ImageDynamicProvider {
    /**
     * 图片更新
     *
     * @return sql
     */
    public String updateStatus(Integer newStatus, Integer oldStatus) {
        Table table = ImageInfo.class.getAnnotation(Table.class);
        String tableName = table.name();
        return new SQL() {
            {
                UPDATE(tableName);
                SET("update_time = NOW()");
                // 判定是否需要更新状态
                if (newStatus != null) {
                    SET("status = #{newStatus}");
                }
                WHERE("md5 = #{md5}");
                // 判定是否需要此条件
                if (oldStatus != null) {
                    AND().WHERE("status = #{oldStatus}");
                }
            }
        }.toString();
    }
}

引用Provider

在Mapper对应的方法上面,根据具体类型,选择注解。此处是更新语句,所以使用@UpdateProvider,参数提供了具体的类和方法,供后续执行反射方法。

    /**
     * 更新状态
     *
     * @param md5 图片摘要信息
     */
    @UpdateProvider(value = ImageDynamicProvider.class, method = "updateStatus")
    int updateStatusByProvider(@Param(value = "md5") String md5,
                               @Param(value = "oldStatus") int oldStatus,
                               @Param(value = "newStatus") int newStatus);

测试用例

    @Test
    public void provider() {
        imageInfoMapper.updateStatusByProvider( "6e705a7733ac5gbwopmp02", 50, 199);
    }

输出

==>  Preparing: UPDATE tb_image SET update_time = NOW(), status = ? WHERE (md5 = ?) AND (status = ?)
==> Parameters: 199(Integer), 6e705a7733ac5gbwopmp02(String), 50(Integer)
<==    Updates: 1

小问题

如果既有Provider,也有xml方法映射。就是说我们定义了@Provider注解,又在xml中写了mapper方法的映射sql语句,这种场景,Mybatis在启动时就会报错。

nested exception is java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.essay.dao.ImageInfoMapper.updateStatusProvider