Mybatis整理以及Sql执行流程

Mybatis是支持定制化sql、存储过程及高级映射的优秀的持久层框架,其主要就完成了2件事情:

  • 封装JDBC操作
  • 利用反射打通Java类与Sql语句之间的相互转换
    MyBatis的主要设计目的就是让我们对执行SQL语句时对输入输出的数据管理更加方便,所以方便地写出SQL和方便地获取SQL的执行结果才是MyBatis的核心竞争力
与原生JDBC的对比

原生JDBC的缺点:

  1. 原生的JDBC操作数据库时,需要频繁的开关链接
  2. 查询数据库的结果集,需要人为的进行封装
  3. JDBC中没有缓存处理
  4. JDBC的sql语句写到Java文件

Mybatis框架

  1. 内部提供数据库连接池不需要频繁开关链接
  2. 半自动对象关系映射、实现结果集自动封装,但是sql需要自己写
  3. 有缓存而且是二级缓存
  4. mybatis把sql写到xml配置文件中
Mybatis的主要成员
  • Configuration:MyBatis所有的配置信息都保存在Configuration对象中,配置文件中的大部分配置都会 存储到该类中
  • SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能
    • 根据statement id,在mybatis配置对象configuration中获取到对应的mappedstatement对象,然后调用执行器来执行具体操作
  • Executor:MyBatis执行器,是MyBatis调度的核心,负责sql语句的生成和查询缓存的维护
    1. 根据传递的参数,完成sql语句的动态解析,生成BoundSql对象,供StatementHandler使用
    2. 为查询创建缓存,以提高性能
    3. 创建JDBC的Statement链接对象,传递给StatementHandler对象,返回List查询结果
  • StatementHandler:封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参数等
    1. 对于JDBC的preparedStatement类型的对象,创建过程中,sql语句字符串会包含若干个?占位符,然后再赋值。StatementHandler通过parameterize(statement)方法对statement进行设值
    2. StatementHandler通过List query(Statement statement,ResultHandler resultHandler)方法来完成执行Statement,和将Statement对象返回的resultSet封装成List
  • ParameterHandler:负责对用户传递的参数转换成JDBC Statement所对应的数据类型,对statement对象的?占位符进行赋值
  • ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
  • TypeHandler:负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换
  • MappedStatement:MappedStatement维护一条<select|update|delete|insert>节点的封装
  • SqlSource:负责根据用户传递的parameterObject,动态的生成SQL语句,将信息封装到BoundSql对象中并返回
  • BoundSql:表示动态生成的SQL语句以及相应的参数信息
    在这里插入图片描述
Mybatis接口调用原理

当程序执行时,通过接口方法调用

  1. 根据当前接口的路径匹配映射文件中的namespace
  2. 根据接口方法匹配映射文件中的id标识
    如果执行正确则能成功将数据返回给接口,否则将报错
    在这里插入图片描述
Mybatis的Sql执行顺序
简化版
  1. 创建sql会话工厂(sqlSessionFactory,这里用到了建造者模式),创建时需要使用到Mybatis的核心配置文件,在配置文件中需要制定映射配置文件
  2. 通过会话工厂得到会话对象
  3. 通过会话对象执行增删改查操作,在执行操作时需要找到对应的sql语句,而sql语句是存在于映射文件(mapper.xml)中的所以需要预先配置好映射文件(在映射文件中书写sql语句、装配参数和结果集映射相关操作)
    在这里插入图片描述
详细1…
  1. 读取xml文件将属性和链接数据库的操作封装在Configuration对象中供后面的组件使用(namespace+Statementid)
  2. 创建sql会话工厂(sqlSessionFactory,这里用到了建造者模式)
  3. 通过通过sqlsesionfactory得到sqlsession(openSession)
  4. 为Mapper接口生成实现类(MapperProxy动态代理)
  5. 当代理类执行方法时,sqlsession执行SQL语句
  6. StatementHandler预编译
  7. ParameterHandler设置参数
  8. Executor执行
  9. ResultSetHandler封装结果集为List
Configuration文件的读取

其实就是XML文件Mapper信息的读取SAXReader
数据库连接信息以及所有Mapper的方法包括sql的类型、方法名、sql语句、返回类型和参数类型

方便理解,并不是源码
public class MapperBean {
	private String interfaceName; //接口名
    private List<Function> list; //接口下所有方法
}
public class Function {
	private String sqltype;  
    private String funcName;  
    private String sql;       
    private Object resultType;  
    private String parameterType; 
}
Configuration对象就是维护了一个Map<String,MapperBean>
源码
public interface CommonMapper {
    List<String> findTimeList(CommonReport arg);
}
Mapper.xml的具体sql就不写了
Class XXX{
private static SqlSessionFactory sqlSessionFactory;

    static{
        String resource = "DbConfiguration.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream,"fydb");
    }
 private static List<String> getTimeListFromDb(CommonReport arg) {
        List<String> result = null;
        SqlSession sqlSession = sqlSessionFactory.openSession();
        CommonMapper commonMapper = sqlSession.getMapper(CommonMapper.class);
        result = commonMapper.findTimeList(arg);
        sqlSession.close();
        return result;
    }
}

我这里关注了sql的执行流程,XML的读取就不看了。
简单说明一下。MyBatis 在解析配置文件的节点的过程中,会调用 MapperRegistry 的 addMapper 方法将 Class 到 MapperProxyFactory 对象的映射关系存入到 knownMappers。
直接看getMapper操作

public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;
    private Executor executor;
    private boolean dirty;
    public <T> T getMapper(Class<T> type) {
        return this.configuration.getMapper(type, this);
    }
    ~~~
}
public class Configuration {
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return this.mapperRegistry.getMapper(type, sqlSession);
    }
}
public class MapperRegistry {
    private Configuration config;
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);//从 knownMappers 中获取与 type 对应的 MapperProxyFactory
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        } else {
            try {
                //创建代理代理对象
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception var5) {
                throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
            }
        }
    }
}

Configuration类中有很多属性。。没有注解。。。
得到MapperProxyFactory对象后,即可调用工厂方法为Mapper创建代理对象

public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);//JDK动态代理
    }
    public T newInstance(SqlSession sqlSession) {
        MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
        return this.newInstance(mapperProxy);
    }
}

而JDK动态代理最后一个参数是关键,实现了InvocationHandler接口,然后将对象作为参数传给重载方法,并在重载方法中调用 JDK 动态代理接口为 Mapper 生成代理对象。
当代理对象执行接口方法时,会被Mapper内的invoke回调函数捕获

public class MapperProxy<T> implements InvocationHandler, Serializable {
    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //如果是定义在Object类的方法,则直接执行,当时这一步没看懂。。多亏了田忠波前辈的注解。。
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
        // 从缓存中获取 MapperMethod 对象,若缓存未命中,则创建 MapperMethod 对象
            MapperMethod mapperMethod = this.cachedMapperMethod(method);
            // 调用 execute 方法执行 SQL
            return mapperMethod.execute(this.sqlSession, args);
        }
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());
            this.methodCache.put(method, mapperMethod);
        }

        return mapperMethod;
    }
}

MapperMethod的创建

public class MapperMethod {
    private final SqlCommand command;
    private final MethodSignature method;
    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    // 创建 SqlCommand 对象,该对象包含一些和 SQL 相关的信息
    this.command = new SqlCommand(config, mapperInterface, method);
    // 创建 MethodSignature 对象,由类名可知,该对象包含了被拦截方法的一些信息     this.method = new MethodSignature(config, mapperInterface, method);
    } 
}

最后通过该对象中的 execute 方法执行 SQL(方法中传入了sqlSession),就是这一步进行数据库操作,那么先来看看关键的SqlSession对象
SqlSession重要的四个对象

  1. Execute:调度执行StatementHandler、ParmmeterHandler、ResultHandler执行相应的SQL语句;
  2. StatementHandler:使用数据库中Statement(PrepareStatement)执行操作,即底层是封装好了的prepareStatement;
  3. ParammeterHandler:处理SQL参数;
  4. ResultHandler:结果集ResultSet封装处理返回。
    源码中当然有,但是找起来比较麻烦
package org.apache.ibatis.session.defaults;
public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;
    private Executor executor;
    private boolean dirty;
}
接着来看一下Executor
package org.apache.ibatis.executor;
public class SimpleExecutor extends BaseExecutor {
    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Connection connection = this.getConnection(statementLog);
        Statement stmt = handler.prepare(connection);
        handler.parameterize(stmt);
        return stmt;
    }
    ~~~//其他几个方法都有StatementHandler的出现
}
package org.apache.ibatis.executor.statement;
public abstract class BaseStatementHandler implements StatementHandler {
    protected final Configuration configuration;
    protected final ObjectFactory objectFactory;
    protected final TypeHandlerRegistry typeHandlerRegistry;
    protected final ResultSetHandler resultSetHandler;
    protected final ParameterHandler parameterHandler;
    protected final Executor executor;
    protected final MappedStatement mappedStatement;
    protected final RowBounds rowBounds;
    protected BoundSql boundSql;
}
好的找齐了

这些对象归属结构也对应了sql执行的顺序,再来看看execute方法源码

public class MapperMethod {
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        Object param;
        if (SqlCommandType.INSERT == this.command.getType()) {
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
        } else if (SqlCommandType.UPDATE == this.command.getType()) {
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
        } else if (SqlCommandType.DELETE == this.command.getType()) {
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
        } else {
            if (SqlCommandType.SELECT != this.command.getType()) {
                throw new BindingException("Unknown execution method for: " + this.command.getName());
            }

            if (this.method.returnsVoid() && this.method.hasResultHandler()) {
                this.executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (this.method.returnsMany()) {
                result = this.executeForMany(sqlSession, args);
            } else if (this.method.returnsMap()) {
                result = this.executeForMap(sqlSession, args);
            } else {
                param = this.method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(this.command.getName(), param);
            }
        }

        if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
            throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
        } else {
            return result;
        }
    }
}
public class DefaultSqlSession implements SqlSession {    
    public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        } else if (list.size() > 1) {
            throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
            return null;
        }
    }
        public <E> List<E> selectList(String statement) {
        return this.selectList(statement, (Object)null);
    }

    public <E> List<E> selectList(String statement, Object parameter) {
        return this.selectList(statement, parameter, RowBounds.DEFAULT);
    }

    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        List var6;
        try {
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            List<E> result = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
            var6 = result;
        } catch (Exception var10) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var10, var10);
        } finally {
            ErrorContext.instance().reset();
        }

        return var6;
    }
}

可以看到selectOne方法也很真实。。最后还是调用了selectList方法。。。
接下来执行前的设置参数等等问题下回再学了。。。

其他一些基础实操问题
#和$的区别

#{}含有预编译的效果,能够防止sql注入共计,为参数添加了一堆""
对 传 递 进 来 的 参 数 直 接 拼 接 在 s q l 中 以 列 名 会 参 数 时 使 用 {}对传递进来的参数直接拼接在sql中 以列名会参数时使用 sql使,以及需要使用declare的时候,因为declare需要在sql执行前先声明
例子可以看我之前的博客https://blog.csdn.net/qq_36879870/article/details/89919572

实体类与表中的字段名不一样怎么办
  1. 字段定义别名
  2. resultMap映射
如何获取自动生成的(主)键值

mysql

    <insert id="insertUser" parameterType="cn.itcast.mybatis.po.User">
        <selectKey keyProperty="id" order="AFTER" resultType="int">
            select LAST_INSERT_ID()
        </selectKey>
        INSERT INTO USER(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})
    </insert>

oracle
先查询序列得到主键,将主键设置到对象中,再将对象插入数据库


    <!-- oracle
    在执行insert之前执行select 序列.nextval() from dual取出序列最大值,将值设置到user对象 的id属性
     -->
    <insert id="insertUser" parameterType="cn.itcast.mybatis.po.User">
        <selectKey keyProperty="id" order="BEFORE" resultType="int">
            select 序列.nextval() from dual
        </selectKey>
        
        INSERT INTO USER(id,username,birthday,sex,address) VALUES( 序列.nextval(),#{username},#{birthday},#{sex},#{address})
    </insert> 
如何传递多个参数
  1. 顺序传参
//对应的xml,#{0}代表接收的是dao层中的第一个参数,#{1}代表dao层中第二参数,更多参数一致往后加即可。

<select id="selectUser"resultMap="BaseResultMap">  
    select *  fromuser_user_t   whereuser_name = #{0} anduser_area=#{1}  
</select>  
  1. 使用@param注解来命名参数

        public interface usermapper { 
         user selectuser(@param(“username”) string username, 
         @param(“hashedpassword”) string hashedpassword); 
        }
         <select id=”selectuser” resulttype=”user”> 
         select id, username, hashedpassword 
         from some_table 
         where username = #{username} 
         and hashedpassword = #{hashedpassword} 
    </select>

  1. 使用Map来装载,mybatis根据key自动找到对应Map中value
  2. list,动态sql
  3. 对象
动态sql?有哪些?执行原理?

mybatis动态sql可以让我们在xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的能力。
trim|where|set|foreach|if|choose|when|otherwise|bind

<where>
    <if test="shopCategoryId!=null">sql语句</if>
    <foreach item="numberList" collection="list" open="(" separator="," close=")">
          #{numberList.num}
      </foreach>
</where>

执行原理:使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能

Mybatis的XML映射文件中,不通的XML映射文件,id是否可以重复

如果配置了namespace的话当然可以重复,因为我们的statement实际上就是namespace+id
但是如果没有配置namespace的话,那么相同的id就会导致覆盖了

为什么说Mybatis是半自动ORM映射工具?与全自动的区别在哪里

Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。
而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql,所以,称之为半自动ORM映射工具。

通常一个xml映射文件,都会写一个Dao接口与之对应,请问这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

Dao接口,就是我们说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数
mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法拼接字符串作为key值,可唯一定位一个MappedStatement。
Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
Dao接口里的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。

Mybatis有哪些Executor执行器?他们之间的区别是什么?

Mybatis有三种基本的Executor执行器

  1. SimpleExecutor:没执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象
  2. ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置在Map<String,Statement>内,供下一次使用
  3. BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理(addBatch)中,等待统一执行(execute),它缓存了多个Statement对象,每个Statement对象都是addBatch完毕后,等待逐一执行executeBatch批处理,与JDBC批处理相同

田忠波前辈这本书对我受益匪浅,因为Mybatis源码一点注解都莫得。。。看的很是费力
有些还没看完先马着哈哈哈
MyBatis 源码分析系列文章合集

参考
MyBatis框架及原理分析
《深入理解mybatis原理》 MyBatis的架构设计以及实例分析
Mybatis常见面试题

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值