学习mybatis的使用与原理解析

mybatis官方文档:mybatis – MyBatis 3 | 简介

提示:长文警告
本文是本人在学习mybatis的过程中不断积累,记录的一篇笔记。没有整理得特别清晰易懂,如有错误还请指正。


mybatis的学习使用 

概念

MyBatis 是一款优秀的持久层框架

MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程。

MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 实体类 pojo【Plain Old Java Objects,普通的 Java对象】映射成数据库中的记录。

MyBatis 本是apache的一个开源项目ibatis, 2010年这个项目由apache 迁移到了google code,并 且改名为MyBatis 。

MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) -->对象关系映射

MyBatis优点

简单易学:本身就很小且简单。

没有任何第三方依赖,最简单安装只要两个jar文件+配置几个 sql映射文件就可以了,易于学习,易于使用,通过文档和源代码,可以比较完全的掌握它的 设计思路和实现。

灵活:mybatis不会对应用程序或者数据库的现有设计强加任何影响。 sql写在xml里,便于统 一管理和优化。通过sql语句可以满足操作数据库的所有需求。

解除sql与程序代码的耦合:通过提供DAO层,将业务逻辑和数据访问逻辑分离,使系统的设 计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性。

提供xml标签,支持编写动态sql。

使用步骤

1. 搭建实验数据库
2. 导入MyBatis相关jar包 or 导入maven依赖
    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

<!--Maven静态资源过滤问题-->
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

3. 编写MyBatis核心配置文件:mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
	PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC"/>
			<dataSource type="POOLED">
				<property name="driver" value="com.mysql.jdbc.Driver"/>
				<property name="url" value="jdbc:mysql:///mybatis?
                    useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=UTC"/>
				<property name="username" value="root"/>
				<property name="password" value="pass"/>
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<mapper resource="com/ming/dao/xxxMapper.xml"/>
	</mappers>
</configuration>

注意:根据当前数据库更改数据库名称

        导入mysql 8.x 的driver是com.mysql.cj.jdbc.Driver;

        若发生ssl错误,useSSL为false;

        mapper标签可以通过三种方式映射:resource、class、package(url不推荐)

        接口与Mapper文件尽量同包同名,方便管理

4. 编写MyBatis工具类

public class MybatisUtils {
	private static SqlSessionFactory sqlSessionFactory;
	
    static {
		try {
			String resource = "mybatis-config.xml";
			InputStream inputStream = Resources.getResourceAsStream(resource);
			sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
    
	//获取SqlSession连接
	public static SqlSession getSession(){
		return sqlSessionFactory.openSession();
	}
}

5. 创建实体类

6. 编写Mapper接口

7. 编写Mapper.xml配置文件,绑定dao接口

<?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.ming.dao.XxxMapper">
	<!--编写sql语句-->
    
</mapper>

8. 编写Text类

public class MyTest {
    @Test
    public void test1(){
        SqlSession session = MyBatisUtils.getSession();
        UserMapper mapper = session.getMapper(UserMapper.class);
        List<User> users = mapper.selectUser();
        for (User user : users) {
            System.out.println(user);
        }
        session.close();	//记得关闭session
    }
}

9. 编写insert、delete、update语句

        编写接口,Mapper.xml,最后需要提交事务才能提交到数据库!

session.commit();

10. 通过map传参的方法注入参数(不正规,但支持大量传参)

int addUser(Map<String,Object> map);

<insert id="addUser" parameterType="map">
        insert into user values(#{id},#{name},#{password})
    </insert>
    
Map<String,Object> map = new HashMap<String, Object>();
        map.put("id",4);
        map.put("name","javaweb");
        map.put("password","mybatis");
        int i = mapper.addUser(map);

Properties优化

1. 在resources目录下新建一个jdbc.properties文件

driver=com.mysql.jdbc.Driver
url=jdbc:mysql:///mybatis?useSSL=false&useUnicode=true&characterEncoding=utf88&serverTimezone=UTC
username=root
password=pass

2. 将文件导入properties配置文件

<configuration>
	<!--导入properties文件-->
	<properties resource="jdbc.properties"/>
    
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC"/>
			<dataSource type="POOLED">
				<property name="driver" value="${driver}"/>
				<property name="url" value="${url}"/>
				<property name="username" value="${username}"/>
				<property name="password" value="${password}"/>
			</dataSource>
		</environment>
	</environments>
</configuration>

typeAliases优化

1. 代替名称

<!--配置别名,注意在configuration的顺序-->
<typeAliases>
	<typeAlias type="com.ming.pojo.Xxx" alias="Xxx"/>
</typeAliases>

2. 指定包,在包下搜索

<typeAliases>
	<package name="com.ming.pojo"/>
</typeAliases>

生命周期和作用域

作用域理解:

SqlSessionFactoryBuilder 的作用在于创建 SqlSessionFactory,创建成功后, SqlSessionFactoryBuilder 就失去了作用,所以它只能存在于创建 SqlSessionFactory 的方法中, 而不要让其长期存在。 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是 局部方法变量)。

SqlSessionFactory 可以被认为是一个数据库连接池,它的作用是创建 SqlSession 接口对象。因为 MyBatis 的本质就是 Java 对数据库的操作,所以 SqlSessionFactory 的生命周期存在于整个 MyBatis 的应用之中,所以一旦创建了 SqlSessionFactory,就要长期保存它,直至不再使用 MyBatis 应用,所以可以认为 SqlSessionFactory 的生命周期就等同于 MyBatis 的应用周期。

由于 SqlSessionFactory 是一个对数据库的连接池,所以它占据着数据库的连接资源。如果创建多 个 SqlSessionFactory,那么就存在多个数据库连接池,这样不利于对数据库资源的控制,也会导 致数据库连接资源被消耗光,出现系统宕机等情况,所以尽量避免发生这样的情况。

因此在一般的应用中我们往往希望 SqlSessionFactory 作为一个单例,让它在应用中被共享。所以 说 SqlSessionFactory 的最佳作用域是应用作用域。

如果说 SqlSessionFactory 相当于数据库连接池,那么 SqlSession 就相当于一个数据库连接 (Connection 对象),你可以在一个事务里面执行多条 SQL,然后通过它的 commit、rollback 等方法,提交或者回滚事务。所以它应该存活在一个业务请求中,处理完整个请求后,应该关闭这 条连接,让它归还给 SqlSessionFactory,否则数据库资源就很快被耗费精光,系统就会瘫痪,所 以用 try...catch...finally... 语句来保证其正确关闭。

所以 SqlSession 的最佳的作用域是请求或方法作用域。

结果集映射

当java中实体类设计与数据库的字段不一致时,查询得到字段的值为null;

解决方法一:sql语句 as 关键字,命名与实体类一致

解决方法二:使用结果集映射-> ResultMap

<resultMap id="userMap" type="user">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="password" property="pwd"/>
    </resultMap>

    <!--编写sql语句-->
    <select id="selectUser" resultMap="userMap">
        select * from user
    </select>

日志

标准日志实现:

<settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

LOG4J

Log4j是Apache的一个开源项目

通过使用Log4j,我们可以控制日志信息输送的目的地:控制台,文本,GUI组件....

我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。

最令人感兴趣的就 是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

使用步骤:

1. 导包

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

2. 创建配置文件 log4j.properties

log4j.rootLogger=DEBUG,console,file

log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n

log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/ming.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n

log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

3. setting设置日志实现

<settings>
	<setting name="logImpl" value="LOG4J"/>
</settings>

可以看到程序生成了log/ming.log文件

limit实现分页

#语法
SELECT * FROM table LIMIT stratIndex,pageSize
SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15
#为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1:
SELECT * FROM table LIMIT 95,-1; // 检索记录行 96-last.
#如果只给定一个参数,它表示返回最大的记录行数目:
SELECT * FROM table LIMIT 5; //检索前 5 个记录行
#换句话说,LIMIT n 等价于 LIMIT 0,n。

使用注解开发

//查询全部用户
@Select("select id,name,pwd password from user")
public List<User> getAllUser();

注解增删改,同步事务,在MyBatisUtils工具类中的getSession()方法:

//获取SqlSession连接
public static SqlSession getSession(){
	return getSession(true); //事务自动提交
}
public static SqlSession getSession(boolean flag){
	return sqlSessionFactory.openSession(flag);
}

多对一处理

创建接口、创建Mapper.xml,按照结果进行嵌套处理,像SQL中的连表查询

<select id="getStudents" resultMap="StudentTeacher">
        select s.id sid,s.name sname,t.id tid,t.name tname
        from student s,teacher t
        where s.tid = t.id
    </select>

    <resultMap id="StudentTeacher" type="Student">
        <id property="id" column="sid"/>
        <result property="name" column="sname"/>
        <association property="teacher" javaType="Teacher">
            <id property="id" colunm="id"/>
            <result property="name" column="tname"/>
        </association>
    </resultMap>

一对多处理

按照结果嵌套处理(优先推荐)

<select id="getTeacher" resultMap="TeacherStudent">
	select s.id sid, s.name sname , t.name tname, t.id tid
	from student s,teacher t
	where s.tid = t.id and t.id=#{id}
</select>
<resultMap id="TeacherStudent" type="Teacher">
	<result property="name" column="tname"/>
	<collection property="students" ofType="Student">
		<result property="id" column="sid" />
		<result property="name" column="sname" />
		<result property="tid" column="tid" />
	</collection>
</resultMap>

按照查询嵌套处理

<select id="getTeacher2" resultMap="TeacherStudent2">
	select * from teacher where id = #{id}
</select>
<resultMap id="TeacherStudent2" type="Teacher">
	<!--column是一对多的外键 , 写的是一的主键的列名-->
	<collection property="students" javaType="ArrayList"
	ofType="Student" column="id" select="getStudentByTeacherId"/>
</resultMap>
<select id="getStudentByTeacherId" resultType="Student">
	select * from student where tid = #{id}
</select>

association是用于一对一和多对一,而collection是用于一对多的关系

JavaType和ofType都是用来指定对象类型的

JavaType是用来指定pojo中属性的类型

ofType指定的是映射到list集合属性中pojo的类型。

动态SQL

关键词:if、where、set、choose、foreach、sql片段

参考官方文档,挺详细的

缓存

一级缓存:

一级缓存默认是开启的,只在一次SqlSession中有效,也就是拿到连接到关闭连接。

缓存失效:

  1. 查询不同sql;

  2. 增删改会修改数据,因此必定会刷新缓存;

  3. 查询不同的mapper.xml;

  4. 手动清理缓存:sqlSession.clearCache();

二级缓存:

<setting name="cacheEnabled" value="true"/>
  • 二级缓存也叫全局缓存,(一级缓存作用域太小

  • 基于namespace级别的缓存,一个命名空间对应一个二级缓存;

在mapper.xml中添加

<cache/>
也可以自定义参数
<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

工作原理:

  • 一个会话SqlSession查询一条数据,这个数据放在当前会话的一级缓存中;

  • 当前会话关闭,这个会话对应一级缓存没了;一级缓存的数据被保存到二级缓存中;

  • 新的会话查询信息可以从二级缓存中获取内容;

  • 不同的mapper查出的数据会放在自己对应的缓存map中


mybatis底层原理探究

mybatis如何找到数据源

//使用Mybatis第一步:获取sqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//在这里打断点
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

在最后一步打断点,进行debug追踪,进入SqlSessionFactoryBuilder的build方法

public SqlSessionFactory build(InputStream inputStream) {
    return this.build((InputStream)inputStream, (String)null, (Properties)null);
}
//继续进入build方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    SqlSessionFactory var5;
    try {
        //创建了XML配置的这个类,可能跟我们的配置文件相关;
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        var5 = this.build(parser.parse());
    } catch (Exception var14) {
        ...
    } finally {
    	...
    }
    return var5;
}

进入parse方法

public Configuration parse() {
    //parsed默认值为false
    if (this.parsed) {
        //仅允许这个XML配置类使用一次
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    } else {
        this.parsed = true;
        //对标签进行解析成为一个XNode对象,再执行parseConfiguration方法
        this.parseConfiguration(this.parser.evalNode("/configuration"));
        return this.configuration;
    }
}

//把标签的内容解析成为一个mybatis独有的XNode对象
public XNode evalNode(String expression) {
    return this.evalNode(this.document, expression);
}
//XNode对象的基本变量
private final Node node;
private final String name;
private final String body;
private final Properties attributes;
private final Properties variables;
private final XPathParser xpathParser;

上面parse提到的方法中进入

//将这个root参数右键Evaluate Expression
private void parseConfiguration(XNode root) {
    ...
}

将结果复制粘贴,可以发现,这个XNode对象的值正是如下:

<configuration>
    <properties resource="db.properties">
        <property name="username" value="root"/>
        <property name="password" value="pass"/>
    </properties>
    <typeAliases>
        <package name="cn.pojo"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="cn/dao/UserMapper.xml"/>
    </mappers>
</configuration>

这里大致可以理解,mybaits从硬盘加载了这个配置文件到内存当中,使用Configuration实例来维护,使用者通过这个实例来获取,而这部分内容就是mybatis的全局配置。

> org.apache.ibatis.session.SqlSessionFactoryBuilder.build(java.io.InputStream)
	> org.apache.ibatis.builder.xml.XMLConfigBuilder.parse
		> org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration
			> org.apache.ibatis.builder.xml.XMLConfigBuilder.environmentsElement
				> org.apache.ibatis.builder.xml.XMLConfigBuilder.dataSourceElement
					> org.apache.ibatis.session.Configuration.setEnvironment#####

mybatis解析sql语句

在解析"/configuration"时最后会解析到 this.mapperElement(root.evalNode("mappers"));

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        Iterator var2 = parent.getChildren().iterator();
        while(true) {
            while(var2.hasNext()) {
                XNode child = (XNode)var2.next();
                String resource;
                //优先级最高的package
                if ("package".equals(child.getName())) {
                    resource = child.getStringAttribute("name");
                    this.configuration.addMappers(resource);
                } else {
                    resource = child.getStringAttribute("resource");
                    String url = child.getStringAttribute("url");
                    String mapperClass = child.getStringAttribute("class");
                    XMLMapperBuilder mapperParser;
                    InputStream inputStream;
                    //优先级第二的resource
                    if (resource != null && url == null && mapperClass == null) {
                        ErrorContext.instance().resource(resource);
                        inputStream = Resources.getResourceAsStream(resource);
                        //创建XMLMapperBuilder对象并执行parse
                        mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
                        mapperParser.parse();
                        //接着是url
                    } else if (resource == null && url != null && mapperClass == null) {
                        ErrorContext.instance().resource(url);
                        inputStream = Resources.getUrlAsStream(url);
                        mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
                        mapperParser.parse();
                    } else {
                        //一个mapper标签存在多个属性的非法情况
                        if (resource != null || url != null || mapperClass == null) {
                            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                        }
                        Class<?> mapperInterface = Resources.classForName(mapperClass);
                        this.configuration.addMapper(mapperInterface);
                    }
                }
            }
            return;
        }
    }
}

从这里可以看出,mapper标签的执行顺序:package > resource > url > mapperClass; 一个mappers可以有多个mapper,但一个mapper只能有一个url、resource或class;

需要注意的是resource和url的解析方式基本相同,而class的解析方式步骤与它们相反。

<mapper resource="com/ming/dao/xxxMapper.xml"/>

resource和url是直接引入xml,那我们就先解析xml,然后通过xml的名称空间反射生成mapper的class对象,再通过动态代理生产class对象的代理对象。

<mapper class="com.ming.dao.xxxMapper"/>

是mapper接口的全限定名,就是上面的那个名称空间,所以先生成class对象和代理对象,然后通过拼接字符串就是全限定名+“.xml”获取xml的名称,然后再解析xml。

多文件映射调用addMappers方法,跟进可以看到

public void addMappers(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
        //通过解析工具类找到包下所有的mapper的名称
        resolverUtil.find(new IsA(superType), packageName);
        //通过反射获得class对象放进集合里
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        Iterator var5 = mapperSet.iterator();
        //循环调用,此时是class的解析方式
        while(var5.hasNext()) {
            Class<?> mapperClass = (Class)var5.next();
            this.addMapper(mapperClass);
        }
    }

判断出标签后会创建对应的XMLMapperBuilder对象,执行parse方法;

同样的可以看到parse方法内是解析"/mapper"的configurationElement方法,进入这个方法

private void configurationElement(XNode context) {
    ...
    this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}

同样获取这个参数context,可以发现

<mapper namespace="cn.dao.UserMapper">
    <select resultType="cn.pojo.User" id="selectUser">
        select * from user
    </select>
    <select resultType="cn.pojo.User" parameterType="int" id="getUserById">
        select * from user where id = #{id}
    </select>
    <insert parameterType="cn.pojo.User" id="addUser">
        insert into user (id,name,password) values (#{id},#{name},#{password})
    </insert>
    <update parameterType="cn.pojo.User" id="updateUser">
        update user set password = #{password} where id = #{id}
    </update>
    <delete parameterType="cn.pojo.User" id="deleteUser">
        delete from user where id = #{id}
    </delete>
</mapper>

获取到的XNode对象正是我们mapper.xml文件下的的mapper标签。

在刚刚的方法中打断点,进入buildStatementFromContext就可以找到Statement,

private void buildStatementFromContext(List<XNode> list) {
    ...
}

这个list就存放了所有的sql.

> org.apache.ibatis.session.SqlSessionFactoryBuilder.build(java.io.InputStream)
	> org.apache.ibatis.builder.xml.XMLConfigBuilder.parse
		> org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration
			> org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement
				> org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement
					> org.apache.ibatis.builder.xml.XMLMapperBuilder.parseStatementNode
						> org.apache.ibatis.session.Configuration.addMappedStatement#####

这里可以理解为:把每个xml中的各个sql解析成一个个MapperStatement对象装在Configuration维护的一个Map集合中,key值是id,value是mapperstatement对象-----然后把解析过的xml的名字和名称空间装在set集合中,通过名称空间反射生成的mapper的class对象以及class对象的代理对象装在Configuration对象维护的mapperRegistry中的Map中。

简而言之:主要就是把每个sql标签解析成mapperstatement对象装进集合,然后把mapper接口的class对象以及代理对象装进集合,方便后来使用。

mybatis执行解析

执行SqlSessionFactory的openSession方法,在defaultSqlSessionFactory类中的openSession方法内执行并返回openSessionFromDataSource方法。

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    DefaultSqlSession var8;
    try {
        //获取Environment
        Environment environment = this.configuration.getEnvironment();
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        //选择并创建执行器
        Executor executor = this.configuration.newExecutor(tx, execType);
        var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
        this.closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
        ErrorContext.instance().reset();
    }

    return var8;
}
//进入newExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? this.defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Object executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }
        if (this.cacheEnabled) {
            executor = new CachingExecutor((Executor)executor);
        }
        Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
        return executor;
    }

mybatis执行器顶层接口Executor 》 BaseExecutor 》(SimpleExecutor,ReuseExecutor,BatchExecutor)

另外还有一个直接实现顶层接口Executor的CachingExecutor。它不自己处理,而是委托其他处理Executor来处理,所以说是缓存执行器。

openSession可以认为是做准备工作,即选择好了执行器。

SqlSession根据Statement ID, 在mybatis配置对象Configuration中获取到对应的MappedStatement对象,然后调用mybatis执行器来执行具体的操作。

在我们的方法如User userById = mapper.getUserById(1);设置断点,通过反射获得执行的方法是selectOne方法,继续跟进发现selectOne是调用SelectList如下:

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        List var5;
        try {
            //从Configuration中找出对应id的MapperStatement对象
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            //执行器去调用查询,进入这个query
            var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception var9) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
        } finally {
            ErrorContext.instance().reset();
        }
        return var5;
    }

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //	query方法去获取BoundSql对象,这个对象可以说是真正的sql语句,它把占位符替换成参数。
        BoundSql boundSql = ms.getBoundSql(parameterObject);
    //	创建缓存key
        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

继续调用query,query最后是通过委托调用的query

 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ...
        //这里面有一个queryFromDatabase方法
        if (list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
  	...   
 }

 如方法名所示,从数据库查询,进入这个方法,可以看到有一个doQuery方法:

   private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

        List list;
        try {
            list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            this.localCache.removeObject(key);
        }

        this.localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            this.localOutputParameterCache.putObject(key, parameter);
        }

        return list;
    }
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;

        List var9;
        try {
            //获取Configuration对象
            Configuration configuration = ms.getConfiguration();
            //从Configuration对象获取会话处理器
            StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            //执行prepareStatement方法,目的就是为了返回一个会话对象
            stmt = this.prepareStatement(handler, ms.getStatementLog());
            var9 = handler.query(stmt, resultHandler);
        } finally {
            this.closeStatement(stmt);
        }

        return var9;
    }

进入prepareStatement方法

    Connection connection = getConnection(statementLog);
	//这里大致可以理解为就是在创建会话对象
    stmt = handler.prepare(connection, transaction.getTimeout());
	//这里是参数处理器负责将用户传递的java类型参数转为jdbc的数据类型
    handler.parameterize(stmt);

 进入prepare方法

//有这样一句
statement = this.instantiateStatement(connection);
 

protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = this.boundSql.getSql();
    if (this.mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
        String[] keyColumnNames = this.mappedStatement.getKeyColumns();
        return keyColumnNames == null ? connection.prepareStatement(sql, 1) : connection.prepareStatement(sql, keyColumnNames);
    } else {
        return this.mappedStatement.getResultSetType() == ResultSetType.DEFAULT ? connection.prepareStatement(sql) : connection.prepareStatement(sql, this.mappedStatement.getResultSetType().getValue(), 1007);
    }
}

 

这里返回的都是prepareStatement,说明mybatis默认防止sql注入攻击

重新回到doQuery方法,接着执行query

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    //这里再把会话转为PreparedStatement,可以确认刚刚防注入的说法
    PreparedStatement ps = (PreparedStatement)statement;
    //PreparedStatement对象执行execute方法,跟jdbc一样
    ps.execute();
    //最后返回一个结果集处理器
    return this.resultSetHandler.handleResultSets(ps);
}

结果集处理器将执行获得的结果集ResultSet转为List类型。

到这里就明白了,mybatis执行的过程是封装了jdbc操作数据库的步骤,最终还是和jdbc操作数据库的步骤一样,它的封装就是为了我们更方便地传参和处理结果集。

查询是走query,doQuery;增删改是走update,doUpdate,大同小异。

 

> org.apache.ibatis.session.defaults.DefaultSq1SessionFactory.openSession() 
	> org.apache.ibatis.session.Configuration.newExecutor (org.apache.ibatis.transaction.Transaction, org. 
		> org.apache.ibatis.executor.SimpleExecutor 
> org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(java.lang.String, java.lang.Object) 		 
	> org.apache.ibatis.session.defaults.DefaultSq1Session.selectList(java.lang.String, java.lang.Object)
	//下面部分还是难看
		> org.apache.ibatis.executor.CachingExecutor.query(org.apache.ibatis.mapping.MappedStatement, Object, RowBounds, ResultHandler) 
		> org.apache.ibatis.executor.CachingExecutor.query(org.apache.ibatis.mapping.MappedStatement, ...)
> org.apache.ibatis.executor.BaseExecutor.queryFromDatabase
> org.apache.ibatis.executor.SimpleExecutor.doQuery 
> org.apache.ibatis.executor.statement.PreparedStatementHandler.query 
> org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets 

mybatis处理流程

SqlSessionFactoryBuilder
	> parse解析
	Configuration
		> build
		SqlSessionFactory
			> openSession
			SqlSession: 作为mybatis工作的顶层API接口,表示与数据库交互的会话
				> Query
				Executor: mybatis的执行器,是mybatis调度的核心, 
					> newStatementHandler
					StatementHandler:封装了JDBC Statement操作,如设置参数、将Statement结果转换为List
						> handleResultSets
						ResultSetHandler:将JDBC返回的ResultSet结果转换成List

总结

- 根据配置文件(全局、sql映射)初始化出Configuration对象
- 创建一个DefaultSqlSession对象
	它里面包含Configuration以及Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor)

- DefaultSqlSession.getMapper(),拿到Mapper接口对应的MapperProxy(里面有DefaultSqlSession)
- 执行CRUD方法:
- 调用DefaultSqlSession的CRUD(Executor)
	会创建一个StatementHandler对象,同时也会创建出ParameterHandler和ResultSetHandler
- 调用StatementHandler预编译以及设置参数值,使用ParameterHandler来给sql设置参数
- ResultSetHandler封装结果

补充

关于参数

parameterType参数的特殊数据类型。很少使用,具体看文档。

大多时候,只须简单指定属性名,顶多要为可能为空的列指定 jdbcType,其他的事情交给 MyBatis 自己去推断就行了。

#{property,javaType=int,jdbcType=NUMERIC}

java数据类型与数据库数据类型的对应

javaType:double				jdbcType:numeric/double
javaType:java.sql.Date 		jdbcType:Date
javaType:float				jdbcType:Float
javaType:string				jdbcType:varchar char 
javaType:long				jdbcType:int

${}与#{}

使用#{}语法时,mybatis会创建PreparedStatement参数占位符,通过占位符安全地设置参数。

${}是直接插入不转义的字符串。

也就是${}会直接替换,#{}会插入?预处理

数据库列名与属性名不匹配解决

  1. 在select语句设置列别名

  2. 使用结果集映射resultMap,Java属性名id1,username,pwd;mysql字段id,name,password

//通过resultMap解决列名与属性名不匹配问题
    <resultMap id="userMap" type="cn.pojo.User">
        <id property="id1" column="id"/>
        <result property="username" column="name"/>
        <result property="pwd" column="password"/>
    </resultMap>
    <select id="getUserById" parameterType="long" resultMap="userMap">
        select id,name,password from user where id = #{id1}
    </select>

最后

本人很少写博客,基本学习笔记都是在自己的Typora上,当然学的不精,笔记可能思路不是特别清晰,见谅。以后有空会考虑写写博客的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值