SpringBoot中使用Mybatis的TypeHandler简单案例及源码简析

1、TypeHandler是什么?

TypeHandler是Mybatis中Java对象和数据库JDBC之间进行类型转换的桥梁

是Mybatis内部的一个接口,实现它就可以完成Java对象到数据库之间的转换

内部结构如下:

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  /**
   * @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
   */
  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

第一个方法是从Java对象到数据库的转换,后面三个的是从数据库到Java对象的转换

2、如何使用TypeHandler

  • 直接实现TypeHandler接口

  • Mybatis中有一个抽象类BaseTypeHandler,实现了TypeHandler并进行了扩展

采用直接继承BaseTypeHandler

有一张数据库表,其中有一个details字段为json类型,其DDL为

CREATE TABLE `test` (
 `id` int NOT NULL AUTO_INCREMENT,
 `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
 `details` json NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

将Java对象转换成数据库中json字段, 通过实现自定义的TypeHandler,完成对test表的查询和插入

首先定义JavaBean

@Data
public class TestDO {

    private int id;
    private LocalDateTime createTime;
    private PersonDO personDO;

}
@Data
public class PersonDO {

    private String name;
    private int age;

}

TestDO对应test数据库表,PersonDO对应着数据库中的details字段

接下来继承BaseTypeHandler

@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({PersonDO.class})
public class ObjectJSONTypeHandler extends BaseTypeHandler<PersonDO> {

    private Gson gson = new Gson();

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, PersonDO parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, gson.toJson(parameter));
    }

    @Override
    public PersonDO getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return gson.fromJson(rs.getString(columnName), PersonDO.class);
    }

    @Override
    public PersonDO getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return gson.fromJson(rs.getString(columnIndex), PersonDO.class);
    }

    @Override
    public PersonDO getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return gson.fromJson(cs.getString(columnIndex), PersonDO.class);
    }
}

注意:

  • @MappedJdbcTypes代表对应的数据库中字段的类型,json类型本质上也是字符串
  • @MappedTypes代表要转换的JavaBean对象
  • Java对象与JSON格式的互换借助Google的Gson完成

剩下的,就是在mapper文件中查询和插入时,指定要使用的typeHandler

<?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.demoxxx.TestMapper" >

    <resultMap id="resultMap" type="com.demo.xxx.TestDO">
        <id column="id" property="id"/>
        <result column="create_time" property="createTime"/>
        <result column="details" property="personDO" typeHandler="com.demo.xxx.ObjectJSONTypeHandler"/>
    </resultMap>

    <select id="selectById" parameterType="int" resultMap="resultMap">
        select id,create_time,details
        from test
        where id=#{param1};
    </select>

    <insert id="insert">
        insert into test(create_time,details) values (#{createTime},#{personDO, typeHandler=com.demo.xxx.ObjectJSONTypeHandler})
    </insert>
</mapper>

或者,不在<resultMap>中指定的话,可以在application.yml文件中指定扫描的类(SpringBoot),这样<resultMap>和insert中就可以不写typeHandler

mybatis:
  type-handlers-package: com.xxx.typehandler

区别在于如果写在配置文件中,任何使用PersonDO的地方都会进行转换,写在mapper中,只有对应的SQL会进行转换

3、源码简析

当在yml中指定TypeHandler时,它的在于SqlSessionFactoryBean中进行,具体为其中的buildSqlSessionFactory方法

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    XMLConfigBuilder xmlConfigBuilder = null;
    
    ...省略

    if (!isEmpty(this.plugins)) {
      Stream.of(this.plugins).forEach(plugin -> {
        targetConfiguration.addInterceptor(plugin);
        LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
      });
    }

    if (hasLength(this.typeHandlersPackage)) {
      scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
          .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
          .filter(clazz -> ClassUtils.getConstructorIfAvailable(clazz) != null)
          .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
    }

    if (!isEmpty(this.typeHandlers)) {
      Stream.of(this.typeHandlers).forEach(typeHandler -> {
        targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
        LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
      });
    }
    ...省略
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
  }

scanClasses方法进行加载自定义的typeHandler

在Mapper中自定义时,依然也是在SqlSessionFactoryBean的buildSqlSessionFactory方法,不过是在scanClasses下面,毕竟没在配置文件中配置TypeHandler

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    XMLConfigBuilder xmlConfigBuilder = null;
    
    ...省略
	if (this.mapperLocations != null) {
      if (this.mapperLocations.length == 0) {
        LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not 		found.");
      } else {
        for (Resource mapperLocation : this.mapperLocations) {
          if (mapperLocation == null) {
            continue;
          }
          try {
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
            xmlMapperBuilder.parse();
          } catch (Exception e) {
            throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
          } finally {
            ErrorContext.instance().reset();
          }
          LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
    }
    ... 省略
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);

这里会扫描mapper文件,扫描完后就会进入xmlMapperBuilder.parse()中解析

//XmlMapperBuilder
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

具体的解析"/mapper"元素

private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

可以看到有resultMap的解析,有insert的解析,这里只看resultMap的解析

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
    ...省略
    for (XNode resultChild : resultChildren) {
      if ("constructor".equals(resultChild.getName())) {
        processConstructorElement(resultChild, typeClass, resultMappings);
      } else if ("discriminator".equals(resultChild.getName())) {
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } else {
        List<ResultFlag> flags = new ArrayList<>();
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    省略...
  }

重点在于buildResultMappingFromContext方法中

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
      property = context.getStringAttribute("name");
    } else {
      property = context.getStringAttribute("property");
    }
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    String nestedResultMap = context.getStringAttribute("resultMap",
        processNestedResultMappings(context, Collections.emptyList(), resultType));
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
  }

可以看到其中String typeHandler = context.getStringAttribute(“typeHandler”);

在这里完成了Typehandler的加载

总结一下的话

  • 在配置文件中配置,会直接通过文件的配置路径读取
  • 在mapper中配置,会解析mapper文件,从中得到具体的配置
使用SpringBootMyBatis实现动态SQL和分页的详细流程如下: 1. 添加依赖 在pom.xml文件添加以下依赖: ```xml <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency> ``` 2. 配置数据源 在application.properties文件配置数据源信息,例如: ```properties spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver ``` 3. Mapper文件 在Mapper文件编写动态SQL的语句,例如: ```xml <select id="queryByCondition" parameterType="map" resultMap="BaseResultMap"> select * from user <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="age != null"> and age = #{age} </if> </where> </select> ``` 4. 接口 在接口定义方法,例如: ```java List<User> queryByCondition(Map<String, Object> paramMap); ``` 5. Service层 在Service层调用Mapper层的方法,例如: ```java public List<User> queryByCondition(String name, Integer age, Integer pageNum, Integer pageSize) { Map<String, Object> paramMap = new HashMap<>(); paramMap.put("name", name); paramMap.put("age", age); PageHelper.startPage(pageNum, pageSize); return userDao.queryByCondition(paramMap); } ``` 6. Controller层 在Controller层调用Service层的方法,例如: ```java @GetMapping("/queryByCondition") public Result queryByCondition(String name, Integer age, Integer pageNum, Integer pageSize) { List<User> userList = userService.queryByCondition(name, age, pageNum, pageSize); PageInfo<User> pageInfo = new PageInfo<>(userList); return Result.success(pageInfo); } ``` 至此,SpringBootMyBatis实现动态SQL和分页的流程已经介绍完毕。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值