MyBatis 学习笔记(二)MyBatis常用特性运用

概要

接上一篇MyBatis 学习笔记(一)MyBatis的简介与使用以及与其他ORM框架的比较,今天我们接着来学习MyBatis的一些常用特性,包括别名,类型处理器,动态SQL

如何使用MyBatis

在本小节,我将通过一个例子介绍MyBatis 中一些常用特性的运用,包括类型处理器,动态SQL等等。

别名

MyBatis 中有个比较好用的特性就是别名,这是为了减少在配置文件中配置实体类的全限定名的冗余。运用如下:
首先在MyBatis的配置文件中配置别名:

    <!--别名处理-->
    <typeAliases>
        <typeAlias type="com.jay.chapter2.entity.Student" alias="Student"/>
    </typeAliases> 

然后,在需要使用该实体类的映射文件中进行添加即可。

    <resultMap id="studentResult" type="Student">
    //省略其他无关配置
    </resultMap>

类型处理器的运用

在实际开发中,我们经常要对枚举类进行处理,例如,人的性别分为男,女,我们数据库中可能存的是0,1; 但是页面显示的话需要显示男,女,所以,我们在使用MyBatis时查询结果时就要通过转换器进行转换。
MyBatis 内置了很多类型处理器(typeHandlers),详细可以参考MyBatis官方文档,对枚举类的处理的是通过EnumTypeHandler和EnumOrdinalTypeHandler两个处理器来处理了,
在这里插入图片描述
但是其只能处理简单的枚举,例如:

public enum SexEnum {
    MAN,
    FEMALE,
    UNKNOWN;
} 

对于复杂的枚举类型,则不能处理。例如:

 MAN("0", "男")

我们来查看源码分析下原因,我们以EnumTypeHandler为例来说明下。

public class EnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

  private final Class<E> type;

  public EnumTypeHandler(Class<E> type) {
    if (type == null) {
      throw new IllegalArgumentException("Type argument cannot be null");
    }
    this.type = type;
  }

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
    if (jdbcType == null) {
      ps.setString(i, parameter.name());
    } else {
      ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); 
    }
  }

  @Override
  public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
    String s = rs.getString(columnName);
    return s == null ? null : Enum.valueOf(type, s);
  }

  @Override
  public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    String s = rs.getString(columnIndex);
    return s == null ? null : Enum.valueOf(type, s);
  }

  @Override
  public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    String s = cs.getString(columnIndex);
    return s == null ? null : Enum.valueOf(type, s);
  }
}

分析上述源码,setNonNullParameter方法包装PreparedStatement进行SQL插值操作,设置的值是enum.name() ,即enum的toString() ,存储的枚举值的名称,而getNullableResult 方法返回的是Enum.valueOf(type, s)。而EnumOrdinalTypeHandler转换器也只能处理Int,String 类型。故我们需要自定义转换器来处理。分析MyBatis 源码我们可以得知,各个转换器都是继承BaseTypeHandler 基类的。为了实现代码的通用性,首先我们实现了一个枚举基类,然后定义一个通用的转换器。
枚举基类:

public interface BaseEnum<E extends Enum<?>, T> {
    /**
     * 真正与数据库进行映射的值
     * @return
     */
    T getKey();

    /**
     * 显示的信息
     * @return
     */
    String getValue();
}

在枚举记录中我们定义了两个通用的获取key和value的方法,接着我们定义 一个枚举类SexEnum来实现枚举基类

public enum SexEnum implements BaseEnum<SexEnum, String> {
    MAN("0", "男"),
    WEMAN("1", "女"),;
    private String key;
    private String value;

    final static Map<String, SexEnum> SEX_MAP = new HashMap<>();

    static {
        for (SexEnum sexEnums : SexEnum.values()) {
            SEX_MAP.put(sexEnums.key, sexEnums);
        }
    }

    SexEnum(String key, String value) {
        this.key = key;
        this.value = value;
    }
    @Override
    public String getKey() {
        return key;
    }

    @Override
    public String getValue() {
        return value;
    }

    public static SexEnum getEnums(String key) {
        return SEX_MAP.get(key);
    }
}

接下来我们再来看看通用的转换器类。

public class GeneralEnumHandler<E extends BaseEnum> extends BaseTypeHandler<E> {
    private Class<E> type;
    private E[] enums;
    
    public GeneralEnumHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
        //获取实现枚举基类所有的枚举类
        this.enums = type.getEnumConstants();
        if (enums == null) {
            throw new IllegalArgumentException(type.getSimpleName()
                    + "does not represent an enum type.");
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException {
        if (jdbcType == null) {
            preparedStatement.setObject(i, e.getKey());
        } else {
        	//将枚举类的key,存入数据库中
            preparedStatement.setObject(i, e.getKey(), jdbcType.TYPE_CODE);
        }
    }

    @Override
    public E getNullableResult(ResultSet resultSet, String s) throws SQLException {
        if (resultSet.wasNull()) {
            return null;
        }
        Object key = resultSet.getObject(s);
        return locateEnumsStatus(key);
    }

    @Override
    public E getNullableResult(ResultSet resultSet, int i) throws SQLException {
        if (resultSet.wasNull()) {
            return null;
        }
        Object key = resultSet.getObject(i);
        return locateEnumsStatus(key);
    }

    @Override
    public E getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        if (callableStatement.wasNull()) {
            return null;
        }
        Object key = callableStatement.getObject(i);
        return locateEnumsStatus(key);
    }

    /*
     * 根据枚举类的key,获取其对应的枚举
     * @param key
     * @return
     */
    private E locateEnumsStatus(Object key) {
        if (key instanceof Integer) {
            for (E anEnum : enums) {
                if (anEnum.getKey() == key) {
                    return anEnum;
                }
            }
            throw new IllegalArgumentException("未知的枚举类型:" + key + ",请核对" + type.getSimpleName());
        }
        if (key instanceof String) {
            for (E anEnum : enums) {
                if (anEnum.getKey().equals(key)) {
                    return anEnum;
                }
            }
            throw new IllegalArgumentException("未知的枚举类型:" + key + ",请核对" + type.getSimpleName());
        }
        throw new IllegalArgumentException("未知的枚举类型:" + key + ",请核对" + type.getSimpleName());
    }

}

代码编写好之后,我们需要在所使用的Mapper映射文件中进行必要的配置。

 <result property="sexEnum" column="sex"
                typeHandler="com.jay.chapter2.Handler.GeneralEnumHandler"/>

最后我们来编写一个测试类来处理一下

public class SimpleMyBatis3Test {

    private SqlSessionFactory sqlSessionFactory;
    /**
     *
     */
    @Before
    public void setUp() {
        String config = "chapter2/mybatis-cfg.xml";
        try {
            InputStream inputStream = Resources.getResourceAsStream(config);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testSelectStudent() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        Student2Mapper mapper = sqlSession.getMapper(Student2Mapper.class);
        Student student = mapper.selectStudentById(1);
        System.out.println("------>返回结果"+student.toString());
    }
}

测试结果如下:
在这里插入图片描述

动态SQL的使用

MyBatis的强大特性之一便是它的动态SQL,主要是处理 根据不同条件拼接SQL语句,例如拼接时添加必要的空格,去掉列表中的最后一列的逗号,MyBatis的动态SQL 元素是基于OGNL的表达式。
在这里插入图片描述详细的运用可以参考MyBatis官方文档
下面以一个例子作为一个示范。自此处我们有一张Student表,测试数据如下:
在这里插入图片描述
首先,我们在mybatis-cfg.xml配置好映射文件

    <!-- 加载映射文件-->
    <mappers>
        <mapper resource="chapter2/xml/Student3Mapper.xml"/>
    </mappers>

然后,我们编写映射文件,将运用相关的动态SQL表达式。

mapper namespace="com.jay.chapter2.mapper.Student3Mapper">
    <resultMap id="studentResult" type="com.jay.chapter2.entity.Student">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="sexEnum" column="sex"
                typeHandler="com.jay.chapter2.Handler.GeneralEnumHandler"/>
    </resultMap>

    <!--运用where,if元素进行条件拼接-->
    <select id="selectStudent" resultMap="studentResult">
        SELECT * FROM student
        <where>
            <if test="id!=null">
                id=#{id}
            </if>
            <if test="name!=null">
                AND name like CONCAT('%',#{name},'%')
            </if>
        </where>
    </select>

    <!--运用foreach元素进行实现in查询-->
    <select id="selectByIds" resultMap="studentResult">
        SELECT * FROM student
        <where>
            <if test="ids!=null">
                id IN
                <foreach collection="ids" open="(" close=")" separator="," item="id">
                    #{id}
                </foreach>
            </if>
        </where>
    </select>

    <!--运用choose,when,otherwise元素实现多条件分支-->
    <select id="selectByNameOrAge" resultMap="studentResult">
        SELECT * FROM student
        <where>
            <choose>
                <when test="name!=null">
                    AND name like CONCAT('%',#{name},'%')
                </when>
                <when test="age!=null">
                    AND age=#{age}
                </when>
                <otherwise>
                    1=1
                </otherwise>
            </choose>
        </where>
    </select>

    <!--运用set元素sql拼接问题-->
    <update id="updateStudent" parameterType="Student">
        UPDATE student
        <set>
            <if test="name!=null">
                name=#{name},
            </if>
            <if test="age!=null">
                age=#{age},
            </if>
        </set>
        <where>
            id=#{id}
        </where>
    </update>
</mapper>

该动态SQL对应的DAO 接口如下:

public interface Student3Mapper {

    /**
     * 查询学生
     * @param id
     * @param name
     * @return
     */
    List<Student> selectStudent(@Param("id") Integer id,
                                @Param("name") String name);

    /**
     * 批量查询学生
     * @param ids
     * @return
     */
    List<Student> selectByIds(@Param("ids") List<Integer> ids);

    /**
     * @param name
     * @param age
     * @return
     */
    List<Student> selectByNameOrAge(@Param("name") String name,
                                    @Param("age") Integer age);

    /**
     * 更新学生记录
     * @param student
     * @return
     */
    boolean updateStudent(Student student);
}

接口类和其对应的映射文件编写完成之后,接着我们来编写测试类进行测试下。

public class SimpleMyBatis3Test extends BaseMyBatisTest {

    @Test
    public void testSelectStudent() {
        SqlSession session = sqlSessionFactory.openSession();
        Student3Mapper mapper = session.getMapper(Student3Mapper.class);
        List<Student> students = mapper.selectStudent(1, null);
        System.out.println("----》只传id返回结果"+students.toString());
        List<Student> students1 = mapper.selectStudent(null, "平平");
        System.out.println("----》只传name返回结果"+students1.toString());
    }

    @Test
    public void testSelectByIds() {
        SqlSession session = sqlSessionFactory.openSession();
        Student3Mapper mapper = session.getMapper(Student3Mapper.class);
        List<Integer> ids = new ArrayList<>();
        ids.add(1);
        ids.add(2);
        List<Student> students = mapper.selectByIds(ids);
        System.out.println("---->通过ids查询返回结果" + students.toString());
    }
    @Test
    public void  selectByNameOrAge() {
        SqlSession session = sqlSessionFactory.openSession();
        Student3Mapper mapper = session.getMapper(Student3Mapper.class);
        List<Student> students = mapper.selectByNameOrAge("美美", null);
        System.out.println("---->selectByNameOrAge通过name查询返回结果" + students.toString());
        List<Student> students1 = mapper.selectByNameOrAge(null, 1);
        System.out.println("----->selectByNameOrAge通过age查询返回的结果" + students1.toString());

    }
    @Test
    public void testUpdateStudent() {
        SqlSession session = sqlSessionFactory.openSession();
        Student3Mapper mapper = session.getMapper(Student3Mapper.class);
        Student student = new Student();
        student.setId(1);
        student.setName("小伟");
        student.setAge(29);
        boolean result = mapper.updateStudent(student);
        System.out.println("--->updateStudent更新结果" + result);
    }
}

测试结果如下:
在这里插入图片描述

参考文献

MyBatis 3官方文档
mybatis枚举自动转换(通用转换处理器实现)

源代码

https://github.com/XWxiaowei/MyBatisLearn/tree/master/mybatisDemo




作者:码农飞哥
微信公众号:码农飞哥