Mybatis核心知识小结

前置知识

说说什么是mybatis吧

答: 是一个半ORM(对象关系映射)框架,它封装了JDBC的,使得我们在开发数据查询的业务代码时可以更加专注的去编写SQL本身,无需手动去创建驱动、建立连接、创建statement 等操作,这就使得我们可以非常非常高效完成SQL操作功能,以及调优也非常的方便。
当然它也是优缺点的,对于复杂的SQL还是需要自己手动编写,这就对码农SQL功底有着较高的要求了,而且它的SQL语句非常依赖数据库,这就使得可能这个数据库可以用的SQL换一个数据库就不行了(PS:前段时间处理兼容国产数据库问题搞得头大)。

你刚刚说了ORM框架,能不能告诉我什么是ORM?为什么说Mybatis的半自动ORM框架呢?

答: ORM说白了就是建立数据库字段和Java对象(POJO)的一种映射关系技术,而Mybatis由于建立这种映射需要我们手动编写SQL,所以说它是半自动的。

我们已经有JDBC了,为什么需要Mybatis呢?

**答:**因为JDBC有下面几个缺点:

  1. 建立连接麻烦
  2. SQL写在代码里面不好维护。
  3. 传参也很麻烦。
  4. 处理结果也很麻烦。

那它和Hibernate 有哪些区别知道嘛?

答: 首先一点是Mybatis是半自动的ORM框架,而Hibernate 是全自动的ORM框架,而且前者Java对象是和SQL语句形成映射,所以进行多表联查的配置非常简单,而后者则是一表和Java对象的方式构成映射关系,所以多表配置的关系比较复杂。
MybatisSQL都是需要手写的,且支持动态操作、编写存储过程、动态生成表明等操作,但不支持数据无关系(即编写的SQL仅仅支持某些数据库使用)。而Hibernate 支持缓存日志级联以及HQL(Hibernate Query Language)这种方式确保功能和数据库无关性,虽然开销略大,但是兼容性就比Mybatis要好很多了,当然这种数据无关性的框架自然不好优化了。
所以Mybatis适合于迭代快的项目,而Hibernate 使用于业务比较稳定的项目。

核心知识

迁至步骤,搭建一个demo环境

在讨论下面这些面试题之前,我认为我们有必要搭建一套实验环境,首先我们的pom文件必须引入下面这些依赖

<!--Mybatis核心-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>

        <!--junit测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!--MySQL驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>



        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.15</version>
        </dependency>

然后数据库建立两三张实验表,就以笔者为例,笔者建立了user1user2phone

create table user1(id varchar(10),name varchar(10));
create table user2(id varchar(10),name varchar(10));
create table phone(id varchar(10),phone_number varchar(10));

然后分别为这些表建立对应的POJO对象。完成后我们进行Mybatis配置文件配置,首先在resources目录下创建一个mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="logImpl" value="LOG4J"/>
        <!--开启Mybatis支持延迟加载-->
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"></setting>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:xxxxxx"/>
                <property name="username" value="xxxx"/>
                <property name="password" value="xxxxx"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/User1Mapper.xml"/>
    </mappers>
</configuration>

然后再新建一个User1Mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zsy.mapper.User1Mapper">
    <select id="select" resultType="com.zsy.po.User1">
        select * from user1 where id = #{id}
    </select>


    <select id="selectBySeq" resultType="com.zsy.po.User1">
        select * from user1 where id = #{arg0} and name= #{arg1}
    </select>

    <select id="selectByAnnotation" resultType="com.zsy.po.User1">
        select * from user1 where id = #{id} and name= #{name}
    </select>

    <select id="selectByMap" parameterType="java.util.Map" resultType="com.zsy.po.User1">
        select * from user1 where id = #{id} and name= #{name}
    </select>

    <select id="selectByJavaBean" parameterType="com.zsy.po.User1" resultType="com.zsy.po.User1">
        select * from user1 where id = #{id} and name= #{name}
    </select>

    <select id="selectConvert" resultType="com.zsy.po.UserDto">
        select id as uId, name as uName from user1 where id = #{id} and name= #{name}
    </select>

    <select id="selectConvert2" resultMap="userResultMap">
        select * from user1 where id = #{id} and name= #{name}
    </select>


    <resultMap id="userResultMap" type="com.zsy.po.UserDto">
        <id property="uId" column="id"/>
        <result property="uName" column="name"/>
    </resultMap>


    <select id="selectLike" resultType="com.zsy.po.User1">
        select * from user1 where name like CONCAT('%',#{name},'%')
    </select>


    <select id="selectAssociation" resultMap="peopleResultMap">
        select u1.*,u2.id as u2_id,u2.name as u2_name from user1 u1
        left join user2 u2 on u1.id=u2.id
    </select>


    <resultMap id="peopleResultMap" type="com.zsy.po.User1">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <!--一对一结果映射-->
        <association property="user2" javaType="com.zsy.po.User2">
            <id column="u2_id" property="id"/>
            <result column="u2_name" property="name"/>
        </association>
    </resultMap>


    <select id="selectAssociation2" resultMap="peopleResultMap2">
        select u1.*,p1.id as p_id,p1.phone_number as phone_number from user1 u1
        left join phone p1 on u1.id=p1.id
    </select>


    <resultMap id="peopleResultMap2" type="com.zsy.po.User1Dto2">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <!--一对多结果映射-->
        <collection property="phoneList" ofType="com.zsy.po.Phone">
            <!--注意 id标签标记的字段会使得这个值的数据只有一个-->
            <!--<id column="p_id" property="id"/>-->
            <result column="p_id" property="id"/>
            <result column="phone_number" property="phoneNumber"/>
        </collection>
    </resultMap>


    <select id="selectLazyQuery" resultMap="peopleResultMap3">
        select * from user1 where id=#{id}
    </select>

    <select id="selectById" resultType="com.zsy.po.User2">
        select * from user2 where id=#{id}
    </select>


    <resultMap id="peopleResultMap3" type="com.zsy.po.User1">
        <id property="id" column="id"/>
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <!--一对一结果映射-->
        <association property="user2" column="id" javaType="com.zsy.po.User2"
                     select="com.zsy.mapper.User1Mapper.selectById"></association>
    </resultMap>


</mapper>

最后创建User1Mapper.java即可

public interface User1Mapper {
    User1 select(String id);


    User1 selectBySeq(String id,String name);


    User1 selectByAnnotation(@Param("id") String id, @Param("name") String name);

    User1 selectByMap(Map<String,String> params);


    User1 selectByJavaBean(User1 params);


    UserDto selectConvert(@Param("id") String id, @Param("name") String name);


    UserDto selectConvert2(@Param("id") String id, @Param("name") String name);

    User1 selectLike(@Param("name") String name);


    List<User1> selectAssociation ();


    List<User1Dto2> selectAssociation2 ();


    User1 selectLazyQuery(String id);

    User2 selectById(String id);
}

Mybatis使用过程知道吗?可以给我演示一下嘛?

**答:**整体分为这么4个步骤吧:

  1. 创建SqlSessionFactory
  2. 创建sqlSession
  3. 执行SQL(在此之间可能还有一步获取Mapper)
  4. 提交事务
  5. 关闭session

代码基础示例如下

// 可以从配置或者直接编码来创建SqlSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2)通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3)通过sqlsession执行数据库操作
        User1Mapper user1Mapper = sqlSession.getMapper(User1Mapper.class);
        User1 user = user1Mapper.select("1");

        logger.info("查询结果:[{}]", user.toString());


 		 if (sqlSession != null) {
            sqlSession.close();
        }

那它每个组件的生命周期了解嘛?

答: emmm,这个嘛,我们完全可以基于上面说的几个类进行分析:

  1. 首先是SqlSessionFactoryBuilder,这个类纯是用于创建SqlSessionFactory 的,所以就是一个方法的东西属于方法级。
  2. SqlSessionFactory 用于建立sqlSession的,相当于一个数据库连接池。经常会被用到,这是个重量级对象,一般是单例且应用级。
  3. SqlSession就是用于执行SQL的,且存在线程安全问题,一般是一次请求就销毁或者方法级。
  4. User1Mapper 不用说了,方法级。

Mybatis几种传参方式了解嘛?

**答:**我们不妨一个个举例吧

基于参数索引传参,这种方式可读性很差,基本不用

Java代码

 User1 selectBySeq(String id,String name);

xml配置

 <select id="selectBySeq" resultType="com.zsy.po.User1">
        select * from user1 where id = #{arg0} and name= #{arg1}
    </select>

使用示例

//根据参数顺序查询,可读性不强 不建议使用
        User1 user1 = user1Mapper.selectBySeq("1", "小明");
        logger.info("根据参数顺序查询:[{}]", user1);//根据参数顺序查询,可读性不强 不建议使用

基于注解传参

Java代码如下所示,通过注解告知框架多参数情况下,每一个形参对应xml文件的哪个参数。

 User1 selectByAnnotation(@Param("id") String id, @Param("name") String name);

xml配置如下,可以看到参数为idname和上面对应

<select id="selectByAnnotation" resultType="com.zsy.po.User1">
        select * from user1 where id = #{id} and name= #{name}
    </select>

测试代码如下所示,输出结果也正常

  //参数注解法查询,参数两少的情况建议使用
        User1 user11 = user1Mapper.selectByAnnotation("1", "小明");
        logger.info("参数注解法查询:[{}]", user11);

Map传参法,对于多变的情况很合适

java代码

User1 selectByMap(Map<String,String> params);

xml配置如下所示,将parameterType生命为JavaMap对象即可

 <select id="selectByMap" parameterType="java.util.Map" resultType="com.zsy.po.User1">
        select * from user1 where id = #{id} and name= #{name}
    </select>

测试代码,可以看到params 中按照xml配置的参数大小写进行传参

//Map参数法,多参数 可变情况多情况下建议使用
        Map<String, String> params = new HashMap<>();
        params.put("id", "1");
        params.put("name", "小明");
        User1 user12 = user1Mapper.selectByMap(params);
        logger.info("Map参数法:[{}]", user12);

最常用的就算Java bean传参法,比较易于排查问题

Java代码

 User1 selectByJavaBean(User1 params);

xml配置

<select id="selectByJavaBean" parameterType="com.zsy.po.User1" resultType="com.zsy.po.User1">
        select * from user1 where id = #{id} and name= #{name}
    </select>

测试代码

  // Java bean查询法
        User1 param = new User1();
        param.setId("1");
        param.setName("小明");
        User1 user13 = user1Mapper.selectByJavaBean(param);
        logger.info("Java bean 查询法:[{}]", user13);

你刚刚说的都是字段和表字段一致的情况,那如果我们的类字段和表字段不一致怎么办呢?

**答:**有两种方式,一种的SQL语句处理,如下所示我们的UserDto类,用uId存数据库中的id,那么我们SQL语句就给id起个别名就好了

 <select id="selectConvert" resultType="com.zsy.po.UserDto">
        select id as uId, name as uName from user1 where id = #{id} and name= #{name}
    </select>

还有一种就是用resultMap标签建立映射关系了,如下便是sql配置

 <select id="selectConvert2" resultMap="userResultMap">
        select * from user1 where id = #{id} and name= #{name}
    </select>

然后userResultMap映射的配置

 <resultMap id="userResultMap" type="com.zsy.po.UserDto">
        <id property="uId" column="id"/>
        <result property="uName" column="name"/>
    </resultMap>

Mybatis是否可以映射Enum枚举类?可不可以给我说说如何做到?

**答:**可以的,Mybatis会自动完成数据库字段转Java枚举的映射操作,我们不妨看看下面这个示例

首先我们可以创建一张数据表,插入一条boyMALE

create table user3(name varchar(255) primary key,gender varchar(255));

然后创建一个描述gender 的枚举

public enum GenderEnum {
    MALE,FEMALE;

}

User映射类代码

public class User3 {

    private String name;
    private GenderEnum gender;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public GenderEnum getGender() {
        return gender;
    }

    public void setGender(GenderEnum gender) {
        this.gender = gender;
    }


    @Override
    public String toString() {
        return "User3{" +
                "name='" + name + '\'' +
                ", gender=" + gender +
                '}';
    }
}

编写查询的xml

    <select id="selectUser3" resultType="com.zsy.po.User3">
        select * from user3 where name = #{name}
    </select>

测试代码,可以看到输出结果有枚举的值,说明自动映射成功了。

 User3 boy = user1Mapper.selectUser3("boy");
logger.info("枚举映射:[{}]",boy);// 输出结果 [main] INFO com.zsy.mapper.MyBatisTest - 枚举映射:[User3{name='boy', gender=MALE}]

实现原理也很简单,既然是Mybatis自己完成映射转换,那我们不妨就从结果处理的源码中查看详情,入口就从DefaultResultSetHandler这个类看起,如下所示,可以看到查询结束后处理结果的逻辑会走到handleRowValues

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
        try {
            if (parentMapping != null) {
             ......
            } else if (this.resultHandler == null) {
                DefaultResultHandler defaultResultHandler = new DefaultResultHandler(this.objectFactory);
                //处理查询出来的结果
                this.handleRowValues(rsw, resultMap, defaultResultHandler, this.rowBounds, (ResultMapping)null);
                multipleResults.add(defaultResultHandler.getResultList());
            } else {
              .......
            }
        } finally {
            this.closeResultSet(rsw.getResultSet());
        }

    }

因为我们的结果集没有嵌套所以走到handleRowValuesForSimpleResultMap

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
        if (resultMap.hasNestedResultMaps()) {
           ........
        } else {
        //结果没有嵌套结构,就走到这里
            this.handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
        }

    }

在往前走,进入getRowValue准备将数据库值转为Java

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
       ........

		// 如果还有数据要处理
        while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
          	// 获取数据库查询结果并转成Java对象
            Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
           .........
        }

    }

再步入getRowValue,开始进行类型转换applyAutomaticMappings

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
      // 创建结果对象
        Object rowValue = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
        if (rowValue != null && !this.hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
           // 如果需要自动转换,则执行applyAutomaticMappings
            if (this.shouldApplyAutomaticMappings(resultMap, false)) {
                foundValues = this.applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
            }

           ........
        }

        return rowValue;
    }

核心代码来了,applyAutomaticMappings会通过createAutomaticMappings创建每个字段对应的映射处理类,例如String就会找到String对应的handler,而枚举就会找到枚举对应的EnumTypeHandler

private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
//创建每个字段对应映射的处理Handler
        List<DefaultResultSetHandler.UnMappedColumnAutoMapping> autoMapping = this.createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
        boolean foundValues = false;
        if (!autoMapping.isEmpty()) {
            Iterator var7 = autoMapping.iterator();

            while(true) {
                DefaultResultSetHandler.UnMappedColumnAutoMapping mapping;
                Object value;
                do {
                    if (!var7.hasNext()) {
                        return foundValues;
                    }

                    mapping = (DefaultResultSetHandler.UnMappedColumnAutoMapping)var7.next();
                    // 通过对应类型Handler完成数据库字段转为Java结果,而我们的枚举类则会调用EnumTypeHandler
                    value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
                    if (value != null) {
                        foundValues = true;
                    }
                } while(value == null && (!this.configuration.isCallSettersOnNulls() || mapping.primitive));

                metaObject.setValue(mapping.property, value);
            }
        } else {
            return foundValues;
        }
    }

那如果我自定义了一个枚举,实现这种映射关系呢?

答: 由上面的源码分析我们可知,每一个SQL转换都是通过一个TypeHandler类进行转换,所以如果我们希望实现自定义转换的话,完全继承TypeHandler的共用类实现一个转换类,完成自定义枚举转换逻辑。

为了演示自定义类型和数据库字段的转换,我们不妨创建一张用户表

create table USER4 (id varchar(10),name varchar(10),sex int);

然后插入一条数据,如下所示,需要补充一下我们这张表的性别字段只有两种情况,一个1,另一个是2。

insert into user4 values ('1','jack',1);

所以我们希望性别这个字段要转为Java的自定义枚举类,以提高代码可读性。

对此我们首先定义一个枚举的接口,代码如下所示,其中int为数据库的值,value为显示的文本语义。

public interface IEnum {
    int getKey();

    void setKey(int key);

    String getValue();

    void setValue(String value);

}

然后我们定义一个性别的枚举类,代码如下

public enum SexEnum implements IEnum {

    MAN(1, "男"), WOMAN(2, "女");

    private int key;
    private String value;

    private SexEnum(int key, String value) {
        this.key = key;
        this.value = value;
    }

    public int getKey() {
        return key;
    }

    public void setKey(int key) {
        this.key = key;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

然后我们继承MybatisTypeHandler编写自定义转换逻辑:

public class EnumKeyTypeHandler extends BaseTypeHandler<IEnum> {
    private Class<IEnum> type;
    private final IEnum[] enums;

    /**
     * 设置配置文件设置的转换类以及枚举类内容,供其他方法更便捷高效的实现
     *
     * @param type 配置文件中设置的转换类
     */
    public EnumKeyTypeHandler(Class<IEnum> type) {
        if (type == null)
            throw new IllegalArgumentException("Type argument cannot be null");
        this.type = type;
        this.enums = type.getEnumConstants();
        if (this.enums == null)
            throw new IllegalArgumentException(type.getSimpleName()
                    + " does not represent an enum type.");
    }

    @Override
    public IEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 根据数据库存储类型决定获取类型,本例子中数据库中存放INT类型
        int dbValue = rs.getInt(columnName);

        if (rs.wasNull()) {
            return null;
        } else {
            // 根据数据库中的code值,定位IEnum子类
            return locateIEnum(dbValue);
        }
    }

    @Override
    public IEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        // 根据数据库存储类型决定获取类型,本例子中数据库中存放INT类型
        int dbValaue = rs.getInt(columnIndex);
        if (rs.wasNull()) {
            return null;
        } else {
            // 根据数据库中的code值,定位IEnum子类
            return locateIEnum(dbValaue);
        }
    }

    public IEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        // 根据数据库存储类型决定获取类型,本例子中数据库中存放INT类型
        int dbValue = cs.getInt(columnIndex);
        if (cs.wasNull()) {
            return null;
        } else {
            // 根据数据库中的code值,定位IEnum子类
            return locateIEnum(dbValue);
        }
    }

    public void setNonNullParameter(PreparedStatement ps, int i, IEnum parameter, JdbcType jdbcType)
            throws SQLException {
        // baseTypeHandler已经帮我们做了parameter的null判断
        ps.setInt(i, parameter.getKey());

    }

    /**
     * 枚举类型转换,由于构造函数获取了枚举的子类enums,让遍历更加高效快捷
     *
     * @param key 数据库中存储的自定义code属性
     * @return code对应的枚举类
     */
    private IEnum locateIEnum(int key) {
        for (IEnum status : enums) {
            if (status.getKey() == key) {
                return status;
            }
        }
        throw new IllegalArgumentException("未知的枚举类型:" + key + ",请核对" + type.getSimpleName());
    }

}

完成之后我们就可以编写用户类了

public class User4 {
    private String id;
    private String name;
    private SexEnum sex;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public SexEnum getSex() {
        return sex;
    }

    public void setSex(SexEnum sex) {
        this.sex = sex;
    }


    @Override
    public String toString() {
        return "User4{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", sex=" + sex.getValue() +
                '}';
    }
}

重点来了,因为我们希望将数据库的int类型转为Java的枚举类型,所以我们xml配置时就需要使用自定义类型转换器处理

<select id="selectByConvert2Enum" resultMap="enumMap">
        select * from user4 where id=#{id}
    </select>


    <resultMap id="enumMap" type="com.zsy.po.User4">
        <result  property = "sex"  column = "sex"  javaType = "com.zsy.po.SexEnum"  typeHandler = "com.zsy.po.EnumKeyTypeHandler" />
    </resultMap>

最后我们编写一下测试代码

 User4 user14 = user1Mapper.selectByConvert2Enum("1");
        logger.info("自定义枚举转换:[{}]", user14);

输出结果如下,可以看到我们数据库的数字转换为枚举中的value了。

[main] INFO com.zsy.mapper.MyBatisTest - 自定义枚举转换:[User4{id='1', name='jack', sex=}]

#{}和${}的有什么区别?

答: 总结出大概以下几点吧:

  1. 前者会在预编译阶段被处理,后者不会,只是单纯的字符串替换。
  2. 处理#{}时候,会将值变成字符串,然后将#{}替换为?,用PreparedStatementset操作进行赋值。
  3. 前者可以放置SQL注入,提高系统安全性。后者有可能被SQL注入。
  4. 前者的替换是在DBMS中,后者是在DBMS外。

模糊查询like语句该怎么写呢?

**答:**可以使用'%{变量}%',有可能导致SQL注入问题

或者下面这种CONCAT('%',#{变量},'%'),我比较常用

 <select id="selectLike" resultType="com.zsy.po.User1">
        select * from user1 where name like CONCAT('%',#{name},'%')
    </select>

好,那Mybatis一对一、一对多的查询怎么做可以给我说说嘛?

答:

先来说说一对一吧,有两种方式,举个例子,例如我们的希望User1类中有个User2对象,我们的POJO代码如下

public class User1 {
    private String id;
    private String name;

    private User2 user2;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    public User2 getUser2() {
        return user2;
    }

    public void setUser2(User2 user2) {
        this.user2 = user2;
    }

    @Override
    public String toString() {
        return "User1{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", user2=" + user2 +
                '}';
    }
}

查询语句手动起个别名,使用resultMap 配好每个数据库查询的结果值和Java对象映射即可,一对一关联的用association 标签处理即可

<select id="selectAssociation" resultMap="peopleResultMap">
        select u1.*,u2.id as u2_id,u2.name as u2_name from user1 u1
        left join user2 u2 on u1.id=u2.id
    </select>



    <resultMap id="peopleResultMap" type="com.zsy.po.User1">
        <id property="id" column="id" />
        <result property="name" column="name"/>
        <!--一对一结果映射-->
        <association property="user2" javaType="com.zsy.po.User2">
            <id column="u2_id" property="id"/>
            <result column="u2_name" property="name"/>
        </association>
    </resultMap>

一对多同理,例如我们一个用户有多个手机号,我们的Java对象如下,可以看到一个用户可能有多个手机号,我们的user内部有个成员属性phoneList

public class User1Dto2 {
    private String id;
    private String name;

    private List<Phone> phoneList;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Phone> getPhoneList() {
        return phoneList;
    }

    public void setPhoneList(List<Phone> phoneList) {
        this.phoneList = phoneList;
    }

    @Override
    public String toString() {
        return "User1Dto2{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", phoneList=" + phoneList +
                '}';
    }
}

那我们的xml配置如下,写好SQL外层做好字段关联,一对多的映射结果用collection 标签处理即可。

 <select id="selectAssociation2" resultMap="peopleResultMap2">
        select u1.*,p1.id as p_id,p1.phone_number as phone_number from user1 u1
        left join phone p1 on u1.id=p1.id
    </select>


    <resultMap id="peopleResultMap2" type="com.zsy.po.User1Dto2">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <!--一对多结果映射-->
        <collection property="phoneList" ofType="com.zsy.po.Phone">
            <!--注意 id标签标记的字段会使得这个值的数据只有一个-->
            <!--<id column="p_id" property="id"/>-->
            <result column="p_id" property="id"/>
            <result column="phone_number" property="phoneNumber"/>
        </collection>
    </resultMap>

Mybatis动态语句了解嘛?

答: 常用的无非就算if、set、where、foreach这些。我们不妨通过几个示例了解一下:

例如我们某些查询必传一个参数,非必要一个参数,就可以使用动态if查询

动态查询

 <select id="selectIf" resultMap="enumMap">
        select * from user4 where id=#{id}
        <if test="name!=null">
            and name=#{name}
        </if>
    </select>

再或者我们希望某个参数有传就取传的值,反之取默认值,就可以用choose

<select id="selectChoose" resultMap="enumMap">
        select * from user4 where
        <choose>
            <when test="id!=null">
                id=#{id}
            </when>
            <otherwise>
                id=2
            </otherwise>
        </choose>
    </select>

如果所有参数都可变,那么就需要使用 <where>标签了

<select id="selectWhere" resultMap="enumMap">
        select * from user4
        <where>
            <if test="id!=null">
                and id=#{id}
            </if>
            <if test="name!=null">
                and name=#{name}
            </if>
        </where>
    </select>

我们希望动态范围查询的话,就可以使用foreach标签

 <select id="selectForEach" resultType="com.zsy.po.User1">
        select * from user1
        <where>
            <foreach item="item" index="index" collection="list"
                     open="ID in (" separator="," close=")" >
                #{item}
            </foreach>
        </where>
    </select>

动态更新

如果每个更新的字段都看你不更新,我们就可以使用动态更新<set>

  <update id="updatebySet">
        update user1
        <set>
            <if test="id!=null">id=#{id},</if>
            <if test="name!=null">name=#{name},</if>
        </set>
        where id=#{id}
    </update>

动态插入

如果我们希望批量插入的话,建议使用下面这种,如果参数是集合类型,则用collection标签,然后遍历的每一项作为item,用separator,分割,批量生成value

 <insert id="batchInsert">
        INSERT INTO user1(id,name)
        VALUES
        <foreach collection="userList" item="user" separator=",">
            (#{user.id},#{user.name})
        </foreach>
    </insert>

通过输出结果我们也可以看到这种方式是将value组装完成一次性插入的。

 INSERT INTO user1(id,name) VALUES (?,?) , (?,?)

进阶知识

Mybatis懒加载了解嘛?能不能给我说说它是如何做到的?

**答:**介绍这个示例,我们首先要修改mybatis-config.xml的配置,开启懒加载

    <settings>
        <setting name="logImpl" value="LOG4J"/>
        <!--开启Mybatis支持延迟加载-->
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"></setting>
    </settings>

然后我们编写带有嵌套的resultMap 的结果集

<select id="selectLazyQuery" resultMap="peopleResultMap3">
        select * from user1 where id=#{id}
    </select>


<!--resultMap 内部嵌套一个查询user2的association -->
    <resultMap id="peopleResultMap3" type="com.zsy.po.User1">
        <id property="id" column="id"/>
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <!--一对一结果映射-->
        <association property="user2" column="id" javaType="com.zsy.po.User2"
                     select="com.zsy.mapper.User1Mapper.selectById"></association>
    </resultMap>

<!--嵌套的查询sql-->

 <select id="selectById" resultType="com.zsy.po.User2">
        select * from user2 where id=#{id}
    </select>

测试代码如下,从日志中可以看到,如果我们没有getUser2,就不会触发查询user2SQL

  User1 user17 = user1Mapper.selectLazyQuery("1");
        logger.info("懒加载:[{}]", user17.getName());

输出结果,可以看到并没有查询User2

2022-11-26 00:38:01,954 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - ==>  Preparing: select * from user1 where id=?
2022-11-26 00:38:01,985 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - ==> Parameters: 1(String)
2022-11-26 00:38:02,214 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - <==      Total: 1
[main] INFO com.zsy.mapper.MyBatisTest - 懒加载:[小明]

修改测试代码

   User1 user17 = user1Mapper.selectLazyQuery("1");
        logger.info("懒加载:[{}]", user17.getName());
        logger.info("懒加载:[{}]", user17.getUser2());

可以看到,查询user2SQL在日志打印出来了

2022-11-26 00:39:05,567 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - ==>  Preparing: select * from user1 where id=?
2022-11-26 00:39:05,600 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - ==> Parameters: 1(String)
2022-11-26 00:39:05,807 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - <==      Total: 1
2022-11-26 00:39:05,814 [main] DEBUG [com.zsy.mapper.User1Mapper.selectById] - ==>  Preparing: select * from user2 where id=?
2022-11-26 00:39:05,814 [main] DEBUG [com.zsy.mapper.User1Mapper.selectById] - ==> Parameters: 1(Long)
[main] INFO com.zsy.mapper.MyBatisTest - 懒加载:[小明]
[main] INFO com.zsy.mapper.MyBatisTest - 懒加载:[User2{id='1', name='小明2'}]

它的工作原理也很简单,如果我们有配置懒加载,在在查询外层SQL并组装对象时,看到xml有嵌套属性且我们配置了懒加载,就会通过代理生成一个结果对象,若没有get懒加载字段,就不会进行sql查询,反之。

为了解释这条工作原理,我们不妨修改一下IDEA的配置。我们可以通过debug来了解,注意懒加载原理是基于动态代理,为了避免IDEA调用toString触发懒加载,我们需要进行如下设置

IDEA 中点击 settings->Build,Execution,Deploynent->Debugger->Data view->Java 去掉Enable 'toString()' object view的勾,禁用该功能。

在这里插入图片描述

首先我们先来debug一下非懒加载的查询,看看对象是否生成就是原生对象而代理生成的,我们的示例代码如下下

 User1 user = user1Mapper.select("1");
logger.info("查询结果:[{}]", user.toString());

对应xml配置,可以看到结果就是User1,并没有什么嵌套的属性

 <select id="select" resultType="com.zsy.po.User1">
        select * from user1 where id = #{id}
    </select>

debug时,我们DefaultResultSetHandlerhandleResultSets作为入口,代码会走到handleResultSet就可以进入结果对象生成以及SQL结果转换

 public List<Object> handleResultSets(Statement stmt) throws SQLException {
      .....忽略
        String[] resultSets = this.mappedStatement.getResultSets();
        if (resultSets != null) {
            while(rsw != null && resultSetCount < resultSets.length) {
                ResultMapping parentMapping = (ResultMapping)this.nextResultMaps.get(resultSets[resultSetCount]);
                if (parentMapping != null) {
                    String nestedResultMapId = parentMapping.getNestedResultMapId();
                    ResultMap resultMap = this.configuration.getResultMap(nestedResultMapId);
                    //处理查询结果
                    this.handleResultSet(rsw, resultMap, (List)null, parentMapping);
                }

                rsw = this.getNextResultSet(stmt);
                this.cleanUpAfterHandlingResultSet();
                ++resultSetCount;
            }
        }

        return this.collapseSingleResultList(multipleResults);
    }

步入handleResultSet,由于resultHandler 为空,进入handleRowValues,处理每个SQL列值

 private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
        try {
            if (parentMapping != null) {
                this.handleRowValues(rsw, resultMap, (ResultHandler)null, RowBounds.DEFAULT, parentMapping);
            } else if (this.resultHandler == null) {
            
               //resultHandler 为空,进入handleRowValues
                this.handleRowValues(rsw, resultMap, defaultResultHandler, this.rowBounds, (ResultMapping)null);
                multipleResults.add(defaultResultHandler.getResultList());
            } else {
            ......
            }
        } finally {
            this.closeResultSet(rsw.getResultSet());
        }

    }

进入handleRowValues,因为我们的xmlSQL没有嵌套结构,所以查询走到handleRowValuesForSimpleResultMap进入简单结果处理

 public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
        if (resultMap.hasNestedResultMaps()) {
        .....
        } else {
            this.handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
        }

    }

然后走到handleRowValuesForSimpleResultMapgetRowValue进行结果对象生成

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
      
        while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
           
            // 获取sql结果转为Java对象rowValue 
            Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
            this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
        }

    }

进入getRowValue,调用createResultObject创建结果对象,但不赋值

 private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
        ResultLoaderMap lazyLoader = new ResultLoaderMap();
        //生成Java对象返回
        Object rowValue = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
       .........省略
        }

        return rowValue;
    }

核心步骤来了,执行createResultObject因为我们这个结果对象不是嵌套的resultMap,所以对象就是通过createResultObject反射直接生成的User对象

 private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
        this.useConstructorMappings = false;
        List<Class<?>> constructorArgTypes = new ArrayList();
        List<Object> constructorArgs = new ArrayList();
        //创建结果对象user
        Object resultObject = this.createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
        if (resultObject != null && !this.hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
            //因为我们没有配置resultMap,所以不走下面的迭代器
            Iterator var9 = propertyMappings.iterator();

            while(var9.hasNext()) {
                ResultMapping propertyMapping = (ResultMapping)var9.next();
                //如果属性包含嵌套结果以及配置为懒加载,则需要通过代理方式生成对象
                if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                    resultObject = this.configuration.getProxyFactory().createProxy(resultObject, lazyLoader, this.configuration, this.objectFactory, constructorArgTypes, constructorArgs);
                    break;
                }
            }
        }

        this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
        //返回一个没赋值的user
        return resultObject;
    }

如下图User1,是一个都没赋值的空对象,而且对象类型就是User1,并不是代理对象

在这里插入图片描述

对象创建后就开始赋值了,我们回到getRowValue,执行applyAutomaticMappings就会将SQL结果通过对应的类型handler赋值到对象上。

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
        ResultLoaderMap lazyLoader = new ResultLoaderMap();
        //生成没赋值的Java对象
        Object rowValue = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
        if (rowValue != null && !this.hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            MetaObject metaObject = this.configuration.newMetaObject(rowValue);
            boolean foundValues = this.useConstructorMappings;
            if (this.shouldApplyAutomaticMappings(resultMap, false)) {
            //通过mybatis自带的handler处理每一个属性对照rowValue 的成员属性进行赋值操作
                foundValues = this.applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
            }

          .....
        }

//返回结果
        return rowValue;
    }

以上就是非懒加载的对象查询,然后我们再来看看懒加载的对象生成,通过debug我们可以发现在createResultObject时,因为我们的xml配置了一个嵌套的User2,如下所示

  <resultMap id="peopleResultMap3" type="com.zsy.po.User1">
        <id property="id" column="id"/>
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <!--一对一结果映射-->
        <association property="user2" column="id" javaType="com.zsy.po.User2"
                     select="com.zsy.mapper.User1Mapper.selectById"></association>
    </resultMap>

所以下面这段代码,在遍历成员属性时发现有个嵌套的User2对象,且我们配置开启了懒加载,所以代码会走到createProxy

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
        this.useConstructorMappings = false;
        List<Class<?>> constructorArgTypes = new ArrayList();
        List<Object> constructorArgs = new ArrayList();
        //先创建一个非代理的结果对象
        Object resultObject = this.createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
        if (resultObject != null && !this.hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        	//获取结果对象映射
            List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
            //遍历成员变量的映射对象ResultMapping 
            Iterator var9 = propertyMappings.iterator();

            while(var9.hasNext()) {
                ResultMapping propertyMapping = (ResultMapping)var9.next();
                //如果有嵌套的成员属性以及开启懒加载配置
                if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                // 基于代理生成对象
                    resultObject = this.configuration.getProxyFactory().createProxy(resultObject, lazyLoader, this.configuration, this.objectFactory, constructorArgTypes, constructorArgs);
                    break;
                }
            }
        }

        this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
        return resultObject;
    }

如下图,因为嵌套的User2,使得对象需要通过代理创建,对结果的get方法进行增强,只有get嵌套属性时才触发懒加载。

在这里插入图片描述

可以看到这个对象就是代理生成的,而不是User对象

在这里插入图片描述

回到我们的业务代码,当我们调用 user17.getUser2()时,因为get的值是嵌套属性,触发JavassistProxyFactoryinvoke方法

 logger.info("懒加载:[{}]", user17.getUser2());

源代码如下所示,源码根据我们的get方法的属性得知,当前get的属性是懒加载的属性,于是触发this.lazyLoader.load(property);进行SQL查询组装Java结果。

public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
            String methodName = method.getName();

            try {
                ResultLoaderMap var6 = this.lazyLoader;
                synchronized(this.lazyLoader) {
                  
                  //有懒加载器以及当前方法不是finalize
                    if (this.lazyLoader.size() > 0 && !"finalize".equals(methodName)) {
                        if (!this.aggressive && !this.lazyLoadTriggerMethods.contains(methodName)) {
                          ......
                          //我们代码getUser2是get方法
                            } else if (PropertyNamer.isGetter(methodName)) {
                                property = PropertyNamer.methodToProperty(methodName);
                                //lazyLoader包含user2这个属性
                                if (this.lazyLoader.hasLoader(property)) {
                                //触发懒加载调用sql查询并处理结果返回
                                    this.lazyLoader.load(property);
                                }
                            }
                        } else {
                            this.lazyLoader.loadAll();
                        }
                    }
                }

                return methodProxy.invoke(enhanced, args);
            } catch (Throwable var10) {
                throw ExceptionUtil.unwrapThrowable(var10);
            }
        }
    }

mybatis的mapper配置文件里,如何实现同一段sql的重复引用?

使用sql标签配合include标签

 <!--  <sql>抽离重复代码    <include>进行引用拼接-->
 
<sql id="findAllStudentsql">
    select * from student_tb
</sql>
 
<select id="findAllStudent" resultType="Student">
    <include refid="findAllStudentsql"></include>  where id >40 
</select>

参考文献

Mybatis 懒加载使用及源码分析

IDEA 调试 Mybatis 源码的一个小坑

MyBatis 查询映射自定义枚举

第十三个设计模式——命令行模式

mybatis相关:重复代码的抽离与拼接(<sql>与<include>)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

shark-chili

您的鼓励将是我创作的动力

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

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

打赏作者

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

抵扣说明:

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

余额充值