MyBatis

MyBatis 简介

为什么使用 MyBatis

MyBatis 是一个优秀的持久层框架,框架相对于与数据库交互的工具,如 DbUtils 而言,最大的区别在于,框架代表的是整体的解决方案,包括事务处理等,而 DbUtils 则只是简单的 CRUD ,功能简单,且 SQL 语句是编写在 Java 代码里面的,造成了代码耦合度过高的情况。二者最形象的比喻为: DbUtils 等工具类就像搓衣板,而 MyBatis 等框架,则好比洗衣机。

MyBatis 与 Hibernate 的区别

Hibernate 是一个全自动全映射ORM (Object Relation Mapping:对象关系映射) 框架,旨在消除 SQL ,将与数据库交互的:编写 SQL ,预编译,设置参数,执行 SQL ,封装结果 等步骤,全部不透明的进行自动执行,若开发者要自定义 SQL 语句,还要额外学习 HQL 的知识,优点在于,SQL 与 Java 代码分离,降低耦合度,开发效率高,缺点在于,对 SQL 的定制程度低,额外的学习成本高,总体缺乏灵活性。

MyBatis 则是一个半自动的轻量级的框架,虽然也将 编写 SQL ,预编译,设置参数,执行 SQL ,封装结果 等步骤进行了统一封装,但是,对于关键的 编写 SQL 步骤,开发人员则可以通过配置文件的方式,自定义 SQL 语句,提高了 SQL 编写的灵活度,将 SQL 与 Java 代码分离,同时 SQL 由开发人员控制,学习成本也较低。

MyBatis 的使用

下载 MyBatis

JAR 和官方文档包(mybatis-3.4.1.zip):https://github.com/mybatis/mybatis-3/releases/download/mybatis-3.4.1/mybatis-3.4.1.zip

源码包(mybatis-3-mybatis-3.4.1.zip):https://github.com/mybatis/mybatis-3/archive/mybatis-3.4.1.zip

MyBatis中文官方文档:https://mybatis.org/mybatis-3/zh/index.html

MyBatis 初体验

准备工作

提供数据库,创建 t_employee 员工表

提供 t_employee 表对应的 JavaBean:Employee

package Bean;/*
MyBatis 测试 Bean*/
public class Employee {private Integer id;private String last_name;private String email;private String gender;public Employee(Integer id, String last_name, String email, String gender) {this.id = id;this.last_name = last_name;this.email = email;this.gender = gender;}public Employee() {}@Overridepublic String toString() {return "Employee{" +"id=" + id +", last_name='" + last_name + '\'' +", email='" + email + '\'' +", gender='" + gender + '\'' +'}';}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getLast_name() {return last_name;}public void setLast_name(String last_name) {this.last_name = last_name;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}public String getGender() {return gender;}public void setGender(String gender) {this.gender = gender;}
}
导入 MyBatis 相关 jar 包

MyBatis包: mybatis-3.4.1.jar,导入数据库连接的 jar 包: mysql-connector-java-5.1.7-bin.jar,同时为了测试方便,查看控制台,需要再导入 log4j 日志包: log4j.jar,为了保证 log4j 的正常运行,需要在类路径下加入 log4j.xml 配置文件,为了统一,将配置文件统一放在 src 目录下的 config目录中。

从 XML 中构建 SqlSessionFactory

关于 SqlSessionFactory

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。

创建 MyBatis 的配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//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="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/></dataSource></environment></environments><!--配置 SQL 语句映射配置文件--><mappers><mapper resource="config/employeeMapper.xml"/></mappers>
</configuration>
根据配置文件创建 SqlSessionFactory 对象
/*** 1.根据 mybatis-config.xml(全局配置文件) 配置文件,创建一个 SqlSessionFactory 对象* @throws IOException*/
public void test() throws IOException {String resource = "config/mybatis-config.xml";InputStream inputStream;inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}

从 SqlSessionFactory 中获取 SqlSession

关于 SqlSession

既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。例如:

获取 SqlSession
//从 SqlSessionFactory 获取 SqlSession 实例,该实例能直接执行以及映射的 sql 语句。
SqlSession sqlSession = sqlSessionFactory.openSession();
创建 SQL 语句映射配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace:名称空间
命名空间的作用有两个,一个是利用更长的全限定名来将不同的语句隔离开来,同时也实现了上面见到的接口绑定。就算你觉得暂时用不到接口绑定,你也应该遵循这里的规定,以防哪天你改变了主意。 长远来看,只要将命名空间置于合适的 Java 包命名空间之中你的代码会变得更加整洁,也有利于你更方便地使用 MyBatis。
-->
<mapper namespace="mybatis.employeeMapper"><!--select标签:用于查询id属性:该标签的唯一标识resultType:返回值类型,值为返回值类型的全类名--><select id="selectEmployee" resultType="Bean.Employee">##{id}:从传递过来的参数值取出 id 值select *from t_employeewhere id = #{id}</select>
</mapper>

注意:新创建的 SQL 语句映射配置文件一定要在 MyBatis 的全局配置文件中指定文件路径

<mappers><mapper resource="config/employeeMapper.xml"/>
</mappers>

如上,通过 MyBatis 全局配置文件中的 mappers 标签指定 SQL 语句映射配置文件的位置

编写测试方法使用 MyBatis 操作数据库

/*** 1.根据 mybatis-config.xml(全局配置文件) 配置文件,创建一个 SqlSessionFactory 对象** @throws IOException*/
@Test
public void test() throws IOException {String resource = "config/mybatis-config.xml";InputStream inputStream;inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//从 SqlSessionFactory 获取 SqlSession 实例,该实例能直接执行已经映射的 sql 语句。SqlSession sqlSession = sqlSessionFactory.openSession();/*参数分别为:sql 的唯一标识:配置文件中,对应的 sql 标签的 id 值,该值具有唯一性,但是有可能其他配置文件中也具有该 id 值,所以,为了保险起见,还要加上该标签所属配置文件的 namespace 名称空间值,称为:全限定名。执行 sql 所用到的参数*///执行查询操作Employee employee = sqlSession.selectOne("mybatis.employeeMapper.selectEmployee", 1);System.out.println(employee);//关闭资源sqlSession.close();
}

注意:查询的 bean 对象的属性名,必须要和表中的字段相对应,否则无法查询到相应的字段值。或者在 SQL 语句中,对字段起别名。

接口式编程

使用接口式编程的好处

对于参数类型以及返回值类型,拥有更强的类型检查。

因为是接口式的,定义的是一种规范且通过代理类实现接口,所以,不局限于 MyBatis 框架,也可以使用 Hibernate ,或者是直接实现接口来使用 JDBC 操作等,更具灵活性。

代码演示如下

/*** 接口式编程* 接口式编程,实质上就是通过将接口与 MyBatis 的 SQL 映射配置文件 employeeMapper.xml 进行绑定* MyBatis 就会自动为接口创建代理类对象,通过代理类对象去执行增删改查操作。** 接口与 MyBatis 的 SQL映射配置文件的绑定要点为:* 1. SQL 映射配置文件的名称空间与接口的全类名进行绑定* 2. SQL 映射配置文件的增删改查标签与接口中对应的增删改查方法名进行绑定* 通过上述绑定,实质上就是在通过接口类的代理对象执行增删改查操作时,去执行* SQL 映射配置文件中对应的 SQL 语句*/
@Test
public void test2() {//获取 SqlSession 实例SqlSession sqlSession = getSqlSession();//获取接口的实现类对象(实质上是获取代理类对象)EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);//调用接口中的方法Employee employeeById = employeeMapper.getEmployeeById(1);System.out.println(employeeMapper.getClass());System.out.println(employeeById);//关闭资源sqlSession.close();
}

阶段总结

1.接口式编程

原生: Dao ===> DaoImpl

MyBatis 接口式: Mapper ===> xxMapper.xml

2.SqlSession 代表和数据库的一次会话,用完需要关闭

3.SqlSession 和 Connection 一样,都是非线程安全的,因此,SqlSession 的实例,不能声明为全局的或者是共享的,因为在多线程环境下,很同期造成资源的竞争,因此,每次使用,都应该重新去获取一个 SqlSession 的实例对象。

4.MyBatis 的 Mapper 接口没有实现类,实际上是 MyBatis 为该接口生成代理类对象。

5.两个重要的配置文件

1.MyBatis 的全局配置文件: 包含数据库连接池信息以及事务管理信息…等系统运行环境信息。

2.SQL映射配置文件: 保存了每一个 SQL 语句的映射信息,MyBatis 就是通过该配置文件,将 SQL 抽取了出来,交给开发人员。

MyBatis全局配置文件

引入 D T D 约束

此操作只针对于 eclips ,在 IDEA 中,dtd 文件已被自动引入。

关于 D T D

文档类型定义(DTD)可定义合法的 XML 文档构建模块。它使用一系列合法的元素来定义文档的结构,DTD 可被成行地声明于 XML 文档中,也可作为一个外部引用。( 实际效果为:在编写相关的 xml 配置文件结构时,引入 D T D 后,会有提示效果 )

引入外部配置文件

MyBatis中使用 properties 标签来引入外部的配置文件,例如数据源的配置文件

<!--
MyBatis 可以使用 properties 标签来引入 外部 properties 配置文件来使用
resource属性:引入类路径下的配置文件资源
url属性:引入网络或磁盘路径下的配置文件资源
-->
<properties resource="config/jdbc.properties">
</properties>

Settings 运行时行为设置

Setting 简介

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。

Setting 的简单使用(详细的使用在后续涉及到时再介绍)
注意事项

此处有一个小问题,但配置文件中的标签配置无需时,MyBatis 全局配置文件便会报:The content of element type “configuration” must match 的错误,原因在于 MyBatis 全局配置文件对各个标签的先后顺序有要求,必须按照 D T D 约束中的顺序来放置标签。properties 标签在最前面。详细的 MyBatis 全局配置文件的 D T D 约束文件如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!--Copyright 2009-2016 the original author or authors.Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.-->
<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)><!ELEMENT databaseIdProvider (property*)>
<!ATTLIST databaseIdProvider
type CDATA #REQUIRED
><!ELEMENT properties (property*)>
<!ATTLIST properties
resource CDATA #IMPLIED
url CDATA #IMPLIED
><!ELEMENT property EMPTY>
<!ATTLIST property
name CDATA #REQUIRED
value CDATA #REQUIRED
><!ELEMENT settings (setting+)><!ELEMENT setting EMPTY>
<!ATTLIST setting
name CDATA #REQUIRED
value CDATA #REQUIRED
><!ELEMENT typeAliases (typeAlias*,package*)><!ELEMENT typeAlias EMPTY>
<!ATTLIST typeAlias
type CDATA #REQUIRED
alias CDATA #IMPLIED
><!ELEMENT typeHandlers (typeHandler*,package*)><!ELEMENT typeHandler EMPTY>
<!ATTLIST typeHandler
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
handler CDATA #REQUIRED
><!ELEMENT objectFactory (property*)>
<!ATTLIST objectFactory
type CDATA #REQUIRED
><!ELEMENT objectWrapperFactory (property*)>
<!ATTLIST objectWrapperFactory
type CDATA #REQUIRED
><!ELEMENT reflectorFactory EMPTY>
<!ATTLIST reflectorFactory
type CDATA #REQUIRED
><!ELEMENT plugins (plugin+)><!ELEMENT plugin (property*)>
<!ATTLIST plugin
interceptor CDATA #REQUIRED
><!ELEMENT environments (environment+)>
<!ATTLIST environments
default CDATA #REQUIRED
><!ELEMENT environment (transactionManager,dataSource)>
<!ATTLIST environment
id CDATA #REQUIRED
><!ELEMENT transactionManager (property*)>
<!ATTLIST transactionManager
type CDATA #REQUIRED
><!ELEMENT dataSource (property*)>
<!ATTLIST dataSource
type CDATA #REQUIRED
><!ELEMENT mappers (mapper*,package*)><!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
><!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>
代码演示
<!--
settings:包含很多重要的设置项
setting:每一个 setting 代表一个设置项
name:设置项的名
value:设置项的取值
-->
<settings><!--通过开启 mapUnderscoreToCamelCase 设置项当数据库的字段名与 JavaBean 中的属性名存在驼峰关系时会自动从从经典数据库列名映射到经典 JavaBean 属性名,而无需在 SQL 语句中修改别名例如:last_name ==> lastName;--><setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

typeAliases(类型别名)

typeAliases 简介

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全类名书写格式,那么,在使用到该 Java 类的全类名处,便可以使用 typeAliases 标签中设置过的对应别名来代替全类名。

代码演示
<!--typeAliases
可以为 Java 类的全类名起别名
同时,起的别名在使用时,不区分大小写
-->
<typeAliases><!--为某一个 Java 类的全类名起别名type 属性:需要起别名的某一个 Java 类的全类名路径例如:Bean.Employee,在不指定别名的情况下,会有一个默认值: employee (Java 类的首字母小写)或者使用:alias 属性:指定一个别名--><typeAlias type="Bean.Employee" alias="employee"></typeAlias><!--批量起别名package:为某个包下的所有 Java 类,批量起默认别名(默认别名:Java 类名首字母小写)name 属性:指定包名(为指定的包以及下面的所有后代包中的所有 Java 类起默认别名)--><package name="Bean"/><!--@Alias 注解为该类起别名若使用了该注解在类上面,给该类起别名,那么在 MyBatis 全局配置文件中通过 package 标签来指定的默认类名便不会生效而是使用 Alias 的值作为该 Java 类的别名,例如:@Alias("employee")-->
</typeAliases>

注:

1.更建议使用全类名,因为比较方便在配置文件中直接通过全类名找到对应的 Java 类。

2.MyBatis 为 Java 的基本数据类型提供了默认的别名,为: _类型名,例如 int ==> _int ,对应其他数据类型以及包装类的别名,则为:类名首字母小写,例如: String ==> string,详细可参考 MyBatis 文档:https://mybatis.org/mybatis-3/zh/configuration.html#typeAliases

typeHandler(类型处理器)

typeHandler 简介

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。实质上就相当于在操作数据库时,对数据库与 Java 中的数据类型进行对应的转换,具体的转换规则以及使用方法,参考 Mybatis 的中文文档:https://mybatis.org/mybatis-3/zh/configuration.html#typeHandlers

注意:针对于时间类型的处理器,在 MyBatis 3.4 以后的版本,都已经默认支持 JSR-310 API ,开发者无需自己配置。

plugins(插件)

此处暂时略过,后续补充完 MyBatis 原理知识后再来详细解读。

environments(环境配置)

environments 简介

MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。

注意:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境,所以,如果想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推。

代码演示
<!--environment 用于配置某一种环境
必须有 transactionManager(事务管理器) 与 dataSource(数据源) 标签
id 属性:代表当前环境的唯一标识
-->
<environment id="test"><!--事务管理器1.type 属性:事务管理器的类型,取值有: JDBC(JdbcTransactionFactory)/MANAGED(ManagedTransactionFactory)实际上二者的取值,使用的是别名,在 org.apache.ibatis.session.Configuration 类中,注册了许多别名。2.若使用了 Spring 框架,则没有必要使用 MyBatis 的事务管理3.自定义事务管理器:只需要实现 TransactionFactory 接口,并指定 type 的值为自定义事务管理器的全类名即可。--><transactionManager type="JDBC"></transactionManager><!--数据源1.type 属性:配置一种类型的数据源(取值使用的依然是 org.apache.ibatis.session.Configuration 类中的别名)2.取值有:UNPOOLED(UnpooledDataSourceFactory)/POOLED(PooledDataSourceFactory)/JNDI(JndiDataSourceFactory)3.自定义数据源:实现 DataSourceFactory 接口,并指定 type 的值为自定义数据源的全类名即可。--><dataSource type="POOLED"><property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${name}"/><property name="password" value="${password}"/></dataSource>
</environment>

注:

以上内容了解即可,在使用 Spring 时,一般使用 Spring 的事务与数据源配置。

databaseIdProvider(数据库ID提供程序)

databaseIdProvider 简介

MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。 MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。 如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。

代码演示
<!--databaseIdProvider(数据库ID提供程序)添加此标签后,可以根据不同的数据库厂商执行不同的语句
type 属性:取值有:DB_VENDOR(VendorDatabaseIdProvider),也是 Configuration 类下的别名
作用在于,得到数据库厂商的标识,MyBatis 就能根据数据库厂商标识(例如:MySQL,Oracle,SQL Server 等),执行不同的 SQL
-->
<databaseIdProvider type="DB_VENDOR"><!--property 为数据库厂商标识起别名name:数据库厂商标识value:指定的别名--><property name="MySQL" value="mysql"/>
</databaseIdProvider>
SQL 映射中指定数据库厂商
<!--databaseId:通过数据库厂商 id 指定一个数据库厂商来执行该 select 标签中的 SQL 语句-->
<select id="getEmployeeById" resultType="employee" databaseId="mysql">select *from t_employewhere id = #{id}
</select>

Mappers (SQL映射注册)

mappers简介

用于定义 SQL 映射配置文件,告诉 MyBatis 去哪里查找 SQL 语句。

代码演示
    <!--mappers:配置 SQL 语句映射配置文件,将其注册到全局配置文件中--><mappers><!--注册一个 SQL 映射注册配置文件resource:引用类路径url:引用网络或磁盘路径下--><mapper resource="Config/employeeMapper.xml"/><!--注册接口class:1.引用接口,值为接口全类名(接口必须与 SQL 映射配置文件在同一包下且 SQL 映射配置文件与接口同名)2.没有 SQL 映射配置文件,值为接口全类名(在接口上通过注解映射 SQL)--><!--推荐比较重要的或复杂的 Dao 接口,为了便于维护,通过 SQL 映射文件来实现不重要的或简单的 Dao 接口,为了开发快速,使用注解方式实现--><mapper class="MyBatisSqlMapper.EmplpyeeMapperAnnotation"></mapper><!--paceage:批量注册,将指定包下的注册配置文件或注册接口方式对应的接口以及配置文件进行批量注册注:使用注册配置文件的方式时,SQL 映射文件也必须与接口位于同一包下且名称相同--><package name="MyBatisSqlMapper"/></mappers>
补充(基于注解的 SQL 映射接口)
import Bean.Employee;
import org.apache.ibatis.annotations.Select;/*
基于注解的 SQL 映射配置文件。*/
public interface EmplpyeeMapperAnnotation {/*@Select 通过该注解映射一个 SQL ,注解的值就为对应的 SQL 语句*/@Select({" select * from t_employee where id = #{id}"})public Employee getEmployeeById(Integer id);
}

MyBatisSQL 映射文件

MyBatisSQL映射文件简介

MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。

CRUD 增删改查

提供 SQL 映射接口
package MyBatisSqlMapper.MyBatisCRUDMapper_XML;import Bean.Employee;public interface MyBatisCRUDMapper {//查询操作Employee getEmployeeById(Integer id);/*MyBatis 允许直接定义以下返回值类型,并且无法在 SQL 映射配置文件中设置Long Integer Boolean(包括基本类型以及包装类型)*///添加操作Long addEmployee(Employee employee);//修改操作Long updateEmployee(Employee employee);//删除操作Long deleteEmployee(Integer id);
}
添加 SQL 映射配置文件绑定接口(使用注册配置文件方式)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="MyBatisSqlMapper.MyBatisCRUDMapper_XML.MyBatisCRUDMapper"><!--查询resultType:指定返回值类型--><select id="getEmployeeById" resultType="Bean.Employee">select *from t_employeewhere id = #{id}</select><!--添加parameterType:指定参数类型(可以省略)--><insert id="addEmployee">insert into t_employee (last_name, gender, email)values (#{lastName}, #{gender}, #{email})</insert><!--删除--><delete id="deleteEmployee">deletefrom t_employeewhere id = #{id}</delete><!--修改--><update id="updateEmployee">update t_employeeset last_name = #{lastName},gender    = #{gender},email     = #{email}where id = #{id}</update>
</mapper>
代码测试
package MyBatisTest;import Bean.Employee;
import MyBatisSqlMapper.MyBatisCRUDMapper_XML.MyBatisCRUDMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;import java.io.IOException;
import java.io.InputStream;public class MyBatisCRUDTest {//MyBatis 全局配置文件路径private static String RESOURCE = "Config/mybatis-config.xml";//获取 SqlSession 实例private SqlSession getSqlSession() {InputStream inputStream = null;try {inputStream = Resources.getResourceAsStream(RESOURCE);} catch (IOException e) {e.printStackTrace();}SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//使用布尔参数指定是否自动提交数据sqlSessionFactory.openSession(true);//获取到的 SqlSession 不会自动提交数据return sqlSessionFactory.openSession();
}@Testpublic void myBatisCRUDTest() {SqlSession sqlSession = null;try {sqlSession = getSqlSession();MyBatisCRUDMapper mapper = sqlSession.getMapper(MyBatisCRUDMapper.class);Employee employeeById = mapper.getEmployeeById(1);System.out.println("查询到:" + employeeById);mapper.addEmployee(new Employee(null, "Jack", "Jack@163.com", "1"));Employee employeeById1 = mapper.getEmployeeById(9);System.out.println("添加了:" + employeeById1);//收到提交数据,否则,数据库中的数据不会发生变化sqlSession.commit();mapper.updateEmployee(new Employee(1, "Buck", "Buck@163.com", "1"));Employee employeeById2 = mapper.getEmployeeById(1);System.out.println("修改了:" + employeeById2);sqlSession.commit();mapper.deleteEmployee(8);sqlSession.commit();Employee employeeById3 = mapper.getEmployeeById(8);if (employeeById3 == null) {System.out.println("以删除!");} else {System.out.println("删除失败!");}} finally {if (sqlSession != null) {sqlSession.close();}}}
}

insert 获取自增长主键的值

使用简介

mysql 支持获获取自增主键值,MyBatis 也是利用 statement.getGenreatedKeys()来获取。

代码演示
<!--
useGeneratedKeys:使用自增主键获取主键值策略,该属性还可以用于 update 标签中
keyProperty:指定对应的主键属性,也就是 MyBatis 获取到主键值以后,将这个值封装给 JavaBean 的那个属性(值为该属性的属性名)
执行 SQL 后,通过对应的属性便可以获取到自增主键值。
-->
<insert id="addEmployee" useGeneratedKeys="true" keyProperty="id">insert into t_employee (last_name, gender, email)values (#{lastName}, #{gender}, #{email})
</insert>

使用序列生成自增长主键

简介

主要针对于不支持自增主键的数据库,例如 Oracle 。

代码演示

当前没有使用 Oracle ,演示略。

单个参数 & 多个参数 & 命名参数

单个参数

对于传入的单个参数,MyBatis 不会做特殊处理

直接通过 #{参数名} : 取出参数值,此处取出时的参数名也不必和传入时的参数名相同

例如:

select *
from t_employee
where id = #{id}

或者

select *
from t_employee
where id = #{idabc}
多个参数

直接根据参数名获取参数时,出现如下错误:

Error querying database. Cause: org.apache.ibatis.binding.BindingException: Parameter ‘id’ not found. Available parameters are [0, 1, param1, param2]

原因在于 MyBatis 对于多个参数,会做特殊处理:多个参数会被封装成一个 Map ,#{ } 就是从 Map 中取值。对于 Map 中的参数,格式为 key : param1 ==> value:参数值,param2 ==> value:参数值 … param6 ==> value:参数值,以此类推。因此实际使用过程中,取值方式为:

#多个参数时,取值则根据参数传入的顺序,通过固定格式的 key: paramX 取出参数值。
select * from t_employee where id = #{param1} and last_name = #{param2}

或者

#多个参数时,还可以根据参数传入的顺序,通过索引作 key 获取参数值
select * from t_employee where id = #{0} and last_name = #{1}
命名参数

在参数的前面,使用 @Param 注解,明确的指出,MyBatis 封装参数时,参数在 Map 中对应的 key。

例如:

Employee getEmployeeByIdAndLaseName(@Param("id") Integer id, @Param("lastName") String lastName);

使用时,直接传入指定的参数名获取参数值:

# 使用 @Param 注解,明确指定参数名时,可以直接使用指定的参数名获取值
select *
from t_employee
where id = #{id}
and last_name = #{lastName}

总结:多个参数时,推荐使用 @Param 注解,通过命名的方式来传入参数,使得参数的获取更加规范。

参数处理 Pojo & Map & To

Pojo

如果参数个数较多,又正好是一个业务模型,那么,推荐直接传入 Pojo ,使用 #{属性名} 就能取出值

Map

若多个参数不是业务模型中的属性,没有对应的 Pojo ,为了方便,可以传入 Map ,使用 #{key} 就能取出 Map 中对应的值

To

如果多个参数,不是业务模型中的数据,但是经常使用,推荐将多个参数单独编写一个 To (Transfer Object) 数据传输对象,通过对象来传入参数

参数封装的思考

针对传入的参数,还有以下几种模式

MyBatis 封装 Map 的过程(源码分析)

概述

参数多时会使用 ParamNameResolver 封装 map,为了不混乱,我们可以使用 @Param 来指定封装时使用的key
通过 #{key} 就可以取出 map 中的值;

源码分析
/*
多个参数时:
参数为 id = 1, lastName = Buck//1、names:{0=id, 1=lastName};调用构造器的时候就确定好了确定流程:1.获取每个标了 @Param 注解的参数的注解值:id,lastName;赋值给 name;2.每次解析一个参数给 map 中保存信息:(key:参数索引,value:name的值)name的值:标注了 @Param 注解:注解的值没有标注:1.全局配置:useActualParamName(jdk1.8):name=参数名2.name=map.size();相当于当前元素的索引例如:{0=id, 1=lastName,2=2}总结:name 是一个保存多个参数名的 Map 集合,是 ParamNameResolver 中的一个属性,在传入参数时,若标注了 @Param 注解,则该参数名在 Map 对应的 key 则为 @Param 的值,若没有标注 @Param ,则参数名在 Map 中对应的 key 为该元素在 Map 中的索引,如上述举例中的 2=2 。
*/
//获取 names 的源码如下(直接在 ParamNameResolver 的构造方法中获取):
public ParamNameResolver(Configuration config, Method method) {//获取参数的 Class 实例的数组集合Class<?>[] paramTypes = method.getParameterTypes();//获取每一个参数的注解的数组集合Annotation[][] paramAnnotations = method.getParameterAnnotations();//初始化一个 MapSortedMap<Integer, String> map = new TreeMap();/*获取参数注解数组的长度(实际上对应的是参数的个数,一个参数可能有多个注解,因此上面使用的是二位数组)*/int paramCount = paramAnnotations.length;//遍历每一个参数中的注解for(int paramIndex = 0; paramIndex < paramCount; ++paramIndex) {//如果当前参数不是特殊参数if (!isSpecialParameter(paramTypes[paramIndex])) {String name = null;//获取当前参数的注解(可能有多个注解)Annotation[] arr$ = paramAnnotations[paramIndex];//获取当前参数注解的个数int len$ = arr$.length;//遍历当前参数的注解for(int i$ = 0; i$ < len$; ++i$) {//获取当前参数的一个注解Annotation annotation = arr$[i$];//如果该注解是 @Param 类型的if (annotation instanceof Param) {this.hasParamAnnotation = true;//那么字符串 name 的值为该参数 @Param 的值name = ((Param)annotation).value();//退出循环,继续遍历下一个参数break;}}//如果当前参数的注解遍历完后,字符串 name 为 nullif (name == null) {//查看该参数是否有 全局配置 if (config.isUseActualParamName()) {//如果有,那么 name 的值为实际传入参数时的参数名name = this.getActualParamName(method, paramIndex);}//如果参数没有全局配置且 name 为 null(没有 @Param 注解)if (name == null) {// name 值就为 Map 的大小(实际就是对应了参数此时在 Map 中存储的索引值,因为此时 Map 中没有存入数据,索引值正好从 0 开始)name = String.valueOf(map.size());}}//遍历完一个参数,就放入 Map 中,map.put(paramIndex, name);}}//最后利用 Map 对 ParamNameResolver 的 names 属性进行赋值this.names = Collections.unmodifiableSortedMap(map);}public Object getNamedParams(Object[] args) {//获取 names 中参数名的个数final int paramCount = names.sizes//1、参数为null直接返回if (args == null || paramCount == 0) {return null;//2、如果只有一个元素,并且没有 Param 注解;args[0]:单个参数直接返回} else if (!hasParamAnnotation && paramCount == 1) {return args[names.firstKey()];//3、多个元素或者有 @Param 标注} else {final Map<String, Object> param = new ParamMap<Object>();int i = 0;//4、遍历names集合:{0=id, 1=lastName,2=2}for (Map.Entry<Integer, String> entry : names.entrySet()) {//names集合的value作为key;  names集合的key又作为参数数组 arges 中取值的参考(因为传入的参数与参数值,必定是一 一对应的) args[0]:1 , args[1]:"Tom" ......//保存到 Map 集合中param.put(entry.getValue(), args[entry.getKey()]);//效果如下::{id=args[0]:1,lastName=args[1]:Tom,2=args[2]}//额外的将每一个参数也保存到 Map 集合中,使用新的key:param1...paramN//效果:有 @Param 注解可以使用 #{指定的key}获取参数值,若没有,则 #{param1}final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);if (!names.containsValue(genericParamName)) {param.put(genericParamName, args[entry.getKey()]);}i++;}//最后返回经过处理的参数 Map 集合。return param;}}
}

# 与 $ 取值的区别

区别

######{} : 是以预编译的形式,将参数设置到 SQL 语句中,防止 SQL 注入

${} : 取出的值,直接封装到 SQL 中,类似于字符串的拼接,会有 SQL 注入的安全问题

大多数情况下,都应该使用 #{} ,在某些情况下:
比如分表(按照年份拆分): select * from 2017_salary ,对应表名等,不支持占位符的地方,就应使用 ${} 进行传入

例如:select * from ${年份}_salary,或者排序:select * from t_employee order by ${字段名} ${升序或降序}

总结:针对于 SQL 语句中,不能使用占位符预编译的位置,就能使用 ${} ,对应需要传入参数使用到了占位符的地方,由于存在 SQL 注入等安全问题,就应该使用 #{}

{ } 取值的相关规则
{ } 在取值时,可以规定参数的一些规则

javaType(Java类型),jdbcType(jdbc类型),mode(用于存储过程),numericScale(保留小数的位数),resultMap(规定结果集为 Map ),typeHandler(类型处理器),jdbcTypeName(jdbc类型,与上面一样),expresssion(表达式,MyBatis 未来准备支持的功能,当前版本 3.4.1)

其中,jdbcType 通常需要在某种特定条件下被设置:在数据为 null 时,有些数据库可能不能识别 MyBatis 对 null 的默认处理,比如 Oracle(报错): JdbcType OTHER :无效的类型,因为 MyBatis 对所有的 null 都映射的是原生 Jdbc 的 OTHER 类型,因此,Oracle 不能正确处理。

解决办法为:

在为空的字段中,添加 jdbcType=NULL,设置其类型为 NULL 而不是 OTHER,例如 #{email,jdbcType=NULL} 或者在全局配置文件中进行设置(此操作更为一劳永逸)

<!--设置空字段时,MyBatis 处理后的类型(NULL、VARCHAR、OTHER)-->
<setting name="jdbcTypeForNull" value="NULL"/>

注:此操作针对 Oracel 数据库,当前并未使用,不做详细演示。

Select 返回 LIst

1.定义接口中的方法返回值类型为集合

//模糊查询多条数据,返回 List
List<Employee> getEmployeeByLikeLastName(String lastName);
  1. 编写对应的 SQL 映射配置文件
<!--查询返回集合
如果返回的是一个集合,返回值类型 resultType 仍然为集合里面元素的类型-->
<select id="getEmployeeByLikeLastName" resultType="Bean.Employee">select *from t_employeewhere last_name like #{lastName}
</select>

3.调用方法,返回的结果就为 List

Select 记录封装为 Map

Map 封装单条记录

1.编写接口方法

//返回一个 Map,key 为数据库中的字段名,value 就是字段名对应的值 (单条记录)
Map<String, Object> getEmployeeMapById(Integer id);

2.编写 SQL 映射配置文件

<!--查询返回 Map (单条记录)
返回值类型就为 Map ,因为 MyBatis 为 Map 起了别名,因此写为 map
返回的结果:key 为数据库中对应的字段名,value 就是字段名对应的值
返回单条记录时,resultType 的返回类型是什么,MyBatis 就会将单条记录封装成对应的类型。
-->
<select id="getEmployeeMapById" resultType="map">
select *
from t_employee
where id = #{id}
</select>

3.测试

/*
查询返回 Map (单条记录)
*/
@Test
public void testResultMap() {SqlSession sqlSession = getSqlSession();
MyBatisCRUDMapper mapper = sqlSession.getMapper(MyBatisCRUDMapper.class);
Map<String, Object> employeeMapById = mapper.getEmployeeMapById(1);
System.out.println("empl");
/*
封装单条记录时 MyBatis 将单条记录的每个字段及其值,封装成 key(字段):value(值) 的形式,
因此需要通过字段名来获取对应的值。
*/
System.out.println(employeeMapById.get("id"));
System.out.println(employeeMapById.get("last_name"));
}
Map 封装多条记录

1.编写接口方法

//需求:返回一个 Map (多条记录): 规定 key 为该数据库中,某一条记录的主键,value 为该记录封装成的对象
/*
@MapKey 注解
使用该注解,能够告诉 MyBatis 在封装多条记录到 Map 中时,需要指定数据模型(JavaBean)中的哪一个属性为 Map 的 key,
*/
@MapKey("id")
Map<Integer,Employee> getEmployeeMapsByLastName(String lastName);

2.编写 SQL 映射配置文件

<!--查询返回 Map (多条记录)
返回多条记录时,最终的返回值类型(也就是将多条记录封装成什么类型)是在接口的方法返回值类型中定义的,
而在 SQL 映射文件中,需要定义的是每条数据封装成什么类型。
-->
<select id="getEmployeeMapsByLastName" resultType="Bean.Employee">select *from t_employeewhere last_name like #{lastName}
</select>

3.测试

/*
查询返回 Map (多条记录)
*/
@Test
public void testResultMaps() {SqlSession sqlSession = getSqlSession();MyBatisCRUDMapper mapper = sqlSession.getMapper(MyBatisCRUDMapper.class);Map<Integer, Employee> employeeMaps = mapper.getEmployeeMapsByLastName("%");Set<Map.Entry<Integer, Employee>> entries = employeeMaps.entrySet();for (Map.Entry<Integer, Employee> entry : entries) {// key 值就为通过 @MapKey 指定的属性名System.out.println(entry.getKey() + " : " + entry.getValue());}
}

自定义结果映射规则

编写 SQL 映射配置文件
<!--resultMap(与 resultType 只能二选一)
自定义结果集的映射规则
-->
<!--自定义某个 JavaBean 的封装规则
type:自定义的 JavaBean 类型
id:该 resultMap 规则的唯一 id ,方便引用
-->
<resultMap id="MyEmp" type="Bean.Employee"><!--指定主键列的封装规则使用 id 标签定义主键,MyBatis 会有底层优化--><id column="id" property="id"/><!--使用 result 定义其他普通列column:指定数据库中的哪一列(也就是那个字段)与 property 中指定的 JavaBean 属性对应property:指定对应的 JavaBean 属性--><result column="last_name" property="lastName"/><!--其他未指定的列,若命名符合规则,会自动封装但是,通常情况下,只要指定了 resultMap 就应该将每一列进行指定--><result column="email" property="email"/><result column="gender" property="gender"/>
</resultMap>
使用
<select id="getEmployeeById" resultMap="MyEmp">select *from t_employeewhere id = #{id}
</select>

关联查询

级联属性封装结果
使用场景

场景一 : 查询 Employee 的同时,查询员工对应的部门

1.新建 t_dept 部门表并为 t_employee 员工表建立外键关联

1.1 建立部门表

创建部门表

CREATE TABLE t_dept(
id INT(11) PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(255)
)

1.2 建立外键关联

建立外键关联

alter table t_employee add constraint fk_emp_dept
foreign key(d_id) references t_dept(id)

2.创建部门表对应的 JavaBean : Department

package Bean;public class Department {private Integer id;private String departmentName;public Department() {}public Department(Integer id, String departmentName) {this.id = id;this.departmentName = departmentName;}public void setDepartmentName(String departmentName) {this.departmentName = departmentName;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getDepartmentName() {return departmentName;}@Overridepublic String toString() {return "Department{" +"id=" + id +", departmentName='" + departmentName + '\'' +'}';}
}

3.在员工 JavaBean 模型中,添加 Department 属性 : department

4.编写 SQL 映射文(此处是多表查询,因为新建了一张 t_dept 部门表)

<!--级联属性封装结果
-->
<!--定义 resultMap 封装规则-->
<resultMap id="EmpAndDeptByEmpId" type="Bean.Employee"><id column="emp_id" property="id"/><result column="lastName" property="lastName"/><result column="gender" property="gender"/><!--当封装的 JavaBean 对象中,关联了其他 JavaBean 对象,需要通过如下的级联形式进行赋值--><result column="dept_id" property="department.id"/><result column="deptName" property="department.departmentName"/>
</resultMap>
<select id="getEmployeeAndDept" resultMap="EmpAndDeptByEmpId">select emp.id         emp_id,emp.last_name  lastName,emp.gender     gender,emp.d_id       e_dept_id,emp.email      email,dept.dept_name deptName,dept.id        dept_idfrom `t_employee` emp,`t_dept` deptwhere emp.d_id = dept.idand emp.id = #{id}
</select>

5.代码测试(略)

associatioon 定义关联对象封装规则
代码演示
<!--级联属性封装结果(2)
-->
<!--定义 resultMap 封装规则-->
<resultMap id="EmpAndDeptByEmpId2" type="Bean.Employee"><id column="emp_id" property="id"/><result column="lastName" property="lastName"/><result column="gender" property="gender"/><result column="email" property="email"/><!--association(联合,协会)可以指定关联的 JavaBean 对象(一个 association 只针对单个关联的对象)property 属性:指定 JavaBean 中的那个属性为联合的对象javaType 属性:指定联合的 JavaBean 对象的类型id:指定主键字段result:指定其他普通字段--><association property="department" javaType="Bean.Department"><id column="dept_id" property="id"/><result column="deptName" property="departmentName"/></association>
</resultMap>
associatioon 分步查询
代码演示
<!--使用 associatioon  进行分步查询
1.先按照员工 id 查询员工信息
2.根据查询员工信息中的 d_id 字段,查出部门表中对应的数据
3.部门设置到员工中去
-->
<resultMap id="employeeAndDeptByIdStep" type="Bean.Employee"><id column="id" property="id"/><result column="lastName" property="lastName"/><result column="gender" property="gender"/><result column="email" property="email"/><!--定义关联对象的封装规则property:指定一个联合的对象select:调用那个方法进行查询,值为相关配置文件的 namespace 加上查询方法的 id 属性值column:指定那一列的值传递给 select 属性调用的方法流程:使用指定的方法,传入(column 指定的参数)查出对象,封装给 property 指定的 JavaBean 属性--><association property="department"select="MyBatisSqlMapper.DepartmentMapper_XML.DepartmentMapper.getDeptpartmentById" column="d_id"></association>
</resultMap>
<select id="getEmployeeAndDeptByIdStep" resultMap="employeeAndDeptByIdStep">select *from t_employeewhere id = #{id}
</select>
associatioon 分步查询 & 延迟加载
分步查询代码演示
<!--使用 associatioon  进行分步查询
1.先按照员工 id 查询员工信息
2.根据查询员工信息中的 d_id 字段,查出部门表中对应的数据
3.部门设置到员工中去
-->
<resultMap id="employeeAndDeptByIdStep" type="Bean.Employee"><id column="id" property="id"/><result column="lastName" property="lastName"/><result column="gender" property="gender"/><result column="email" property="email"/><!--定义关联对象的封装规则property:指定一个联合的对象select:调用那个方法进行查询,值为相关配置文件的 namespace 加上查询方法的 id 属性值column:指定那一列的值传递给 select 属性调用的方法流程:使用指定的方法,传入(column 指定的参数)查出对象,封装给 property 指定的 JavaBean 属性--><association property="department"select="MyBatisSqlMapper.DepartmentMapper_XML.DepartmentMapper.getDeptpartmentById" column="d_id"></association>
延迟加载及其代码演示

1.简介

associatioon 延迟加载(按需加载,需要到时候才去查询加载对应的值) 不使用延迟加载的情况下,每次查询 Employee 对象的时候,都将 Department 一起查询出来,现在可以使部门信息在需要的时候再去查询:在上面面分步查询的基础上,在 MyBatis 的全局配置文件上,加上两个配置如下:

2.代码演示

<!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。
特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。
默认值为 true ,尽管默认为 true 可以不写,但还是推荐显示的指定
其值,即使它是默认的,可以防止版本更替带来的问题
-->
<setting name="lazyLoadingEnabled" value="true"/><!--开启时,任一方法的调用都会加载该对象的所有延迟加载属性。
否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。-->
<setting name="aggressiveLazyLoading" value="false"/>
collection 定义关联集合封装规则
代码演示
<!--collection 定义关联集合封装规则
使用场景:查询部门时,将部门对应的所有员工信息也查询出来
-->
<resultMap id="deptpartmentByIdPlus" type="Bean.Department"><id column="did" property="id"/><result column="deptName" property="departmentName"/><!--collection 嵌套结果集的方式,定义集合对象的封装规则ofType:指定集合中元素的类型--><collection property="employees" ofType="Bean.Employee"><!--定义集合中元素的封装规则--><id column="eid" property="id"/><result column="email" property="email"/><result column="gender" property="gender"/><result column="lastName" property="lastName"/></collection>
</resultMap>
<select id="getDeptpartmentByIdPlus" resultMap="deptpartmentByIdPlus">select d.id        did,d.dept_name deptName,e.id        eid,e.last_name lastName,e.email     email,e.gender    gender,e.d_id      e_didfrom t_employee e,t_dept dwhere d.id = e.d_idand d.id = #{id}
</select>
collection 分步查询&延迟加载
分步查询代码演示
<!--按照 id 查询部门信息以及部门所属员工信息(分步查询)-->
<resultMap id="deptpartmentByIdStep" type="Bean.Department"><id column="id" property="id"/><result column="dept_name" property="departmentName"/><collection property="employees" ofType="Bean.Employee"select="MyBatisSqlMapper.EmployeeMapperPlus_XML.EmployeeMapperPlus.getEmployeesByDeptId"column="id"></collection>
</resultMap>
<select id="getDeptpartmentByIdStep" resultMap="deptpartmentByIdStep">select *from t_deptwhere id = #{id}
</select>
<!--按照部门 id 查出所有员工-->
<select id="getEmployeesByDeptId" resultType="Bean.Employee">select *from t_employeewhere d_id = #{deptId}
</select>
延时加载测试
/*collection 分步查询与延迟加载演示*/
@Test
public void test5() {SqlSession sqlSession = getSqlSession();DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);Department deptpartmentByIdStep = mapper.getDeptpartmentByIdStep(2);//在延迟加载的前提下,只获取了Department 的 id,因此按需加载了 Department 的 idSystem.out.println(deptpartmentByIdStep.getId());//调用 toString() 时,因为调用了 departmentName 属性,此时才被按需加载System.out.println(deptpartmentByIdStep);List<Employee> employees = deptpartmentByIdStep.getEmployees();for (Employee employee : employees) {System.out.println(employee);}
}
分步查询与延时加载步骤梳理

首先,分步中,首先被查询出的结果中,肯定包含下一步查询所需要的参数,因为是按照部门 id 查询部门信息以属与该部门的员工信息,所以,首先查出了部门信息,通过 collection 标签中的 select 属性指定下一步查询需要的方法,以及通过 column 属性在之前查出的结果中指定参数,进行下一步查询,最后执行下一步查询。延迟加载的体现在于, 所以的 SQL 语句是否同时发出,使用延迟加载时, 只有在需要某一数据时,才会执行相应的 SQL 语句,不使用延迟加载时,则是一次执行完所有 SQL 语句。

fetchType & 分步查询传递多列值
传递多列值

1.可以将多列的值封装为 Map 传递

将下一步查询所需要的参数的参数名作为 key ,将当前查询结果中的字段名作为 value ,通过多组 key=value 传递多列值,以此类推。

2.演示

2.1.如下,分步查询所需参数为 : deptId

<!--按照部门 id 查出所有员工-->
<select id="getEmployeesByDeptId" resultType="Bean.Employee">select *from t_employeewhere d_id = #{deptId}
</select>

2.2.如下,查询结果中存在一个字段(数据库中查询出的字段)名为 : id

<id column="id" property="id"/>

3.根据以上数据,封装为 Map ,为分步查询传入多列值: column=“deptId=id”

<collection property="employees" ofType="Bean.Employee"select="MyBatisSqlMapper.EmployeeMapperPlus_XML.EmployeeMapperPlus.getEmployeesByDeptId"column="deptId=id" fetchType="eager">
</collection>
fetchType
<!--fetchType="lazy" : 表示默认使用延迟加载lazy(懒惰的):表示延迟加载eager(渴望的,立即):表示立即加载
使用 fetchType 属性时,尽管 MyBatis 全局配置文件开启了延迟加载
也可以通过 lazy/eager 属性来单独控制延迟加载的开启/关闭
使用如下:
-->
fetchType="eager"

resultMap_discriminator 鉴别器

代码演示
<!--
应用场景:查询 t_employee 员工表如果查出的是女生:就把部门信息查询出来,否则不查询如果是男生,把 last_name 字段的值,赋值给 email
-->
<resultMap id="MyDiscriminator" type="Bean.Employee"><id column="id" property="id"/><result column="lastName" property="lastName"/><result column="gender" property="gender"/><result column="email" property="email"/><!--discriminator鉴别器:MyBatis 可以使用 discriminator 判断某列的值,然后根据某列的值,改变封装行为column:要进行判断的字段javaType:字段值对应的 Java 类型--><discriminator javaType="string" column="gender"><!--女生(0) 执行分步查询,查出部门信息resultType:指定封装的结果类型--><case value="0" resultType="Bean.Employee"><association property="department"select="MyBatisSqlMapper.DepartmentMapper_XML.DepartmentMapper.getDeptpartmentById"column="d_id"></association></case><!--男生(1) 将 email 属性的值赋值为 last_name 字段的值--><case value="1" resultType="Bean.Employee"><id column="id" property="id"/><result column="last_name" property="lastName"/><result column="gender" property="gender"/><result column="last_name" property="email"/></case></discriminator>
</resultMap><select id="getEmployeesByDiscriminator" resultMap="MyDiscriminator">select *from t_employee
</select>

动态 SQL

简介

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

if 判断 OGNL

代码演示
<!--if
查询员工,要求:携带了哪个字段,查询条件就带上该字段的值
test:判断表达式(OGNL),从参数中取值,进行判断,而不是数据库中的字段,判断结果为真,则拼接 if 标签内的 SQL 语句遇见特殊符号,应该使用转义
ognl 会进行字符串与数值的转换判断
-->
<select id="getEmployeesConditionIf" resultType="Bean.Employee">select *from t_employeewhere<if test="id!=null">id = #{id}</if><if test="lastName!=null and lastName!=''">and last_name like #{lastName}</if><if test="email!=null and email.trim()!=''">and email = #{email}</if><if test="gender==0 or gender==1">and gender = #{gender}</if>
</select>
注意事项
//查询的时候,如果某些条件没带上,SQL 拼装可能会出问题
/*
解决办法
1.在 where 子句后面加上 1=1,2=2 这种布尔值为 true 的表达式,后面的条件语句都为 and 例如:
where 1=1 and id = ? and lastName = ? .....
2.MyBatis 使用 where 标签来将所有的查询条件包括在内,一但缺少条件,MyBatis 就会将多出来的 and 或者 or 去掉
where 只会去掉每个 if 标签中的 SQL 语句最前面开头的 and/or 当 and/or 不在 SQL 语句开头时,MyBatis 便不会去掉。
因此,使用 where 需要将 and/or 写在 SQL 语句开头。
*/

trim 自定义字符串截取

<!--trim (where, set)
prefix:前缀,trim 体中是整个字符串拼串后的结果,prefix 给拼串后的整个字符串加一个前缀
prefixOverrides:前缀覆盖,去掉整个字符串前面多余的字符
suffix:后缀,给拼串后的整个字符串加一个后缀
suffixOverrides:后缀覆盖,去掉整个字符串后面多余的字符
-->
<select id="getEmployeesConditionTrim" resultType="Bean.Employee">select *from t_employee<trim prefix="where" suffixOverrides="and"><if test="id!=null">id = #{id} and</if><if test="lastName!=null and lastName!=''">last_name like #{lastName} and</if><if test="email!=null and email.trim()!=''">email = #{email} and</if><if test="gender==0 or gender==1">gender = #{gender}</if></trim>
</select>

set 与 if 结合的动态更新

set 标签版
<!--需求:更新员工信息,传进来的参数,哪一列有值,就更新哪一列
set 标签版,会替换吊 set 语句中多余的逗号
--><update id="updateEmployee">-->update t_employee--><set>--><if test="lastName!=null">-->last_name = #{lastName},--></if>--><if test="email!=null">-->email = #{email},--></if>--><if test="gender!=null">-->gender = #{gender}--></if>--></set>-->where id = #{id}--></update>-->
trim 标签版本
<!--trim 标签版
可能会存在多余逗号。因此也可以使用 trim 标签,将拼串出的 SQL 语句中,后缀的逗号覆盖掉
-->
<update id="updateEmployee">update t_employeeset<trim suffixOverrides=","><if test="lastName!=null">last_name = #{lastName},</if><if test="email!=null">email = #{email},</if><if test="gender!=null">gender = #{gender}</if></trim>where id = #{id}
</update>

foreach 遍历集合

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"open="(" separator="," close=")">#{item}
</foreach>
</select>

注:

你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

<!--当传入的参数是一个集合(List,数组,Map...)时,使用 foreach 将集合中遍历出的元素,赋值给指定变量
collection 指定迭代的对象(List,数组,Map 等)
index 使用 List 时,是当前迭代的索引当使用 Map 对象时 index 是键,iteam 是值
item 本次迭代获取到的元素
open 开头的字符
close 结尾的字符
separator 中间的分隔符
-->
<select id="getEmployeesConditionForeach" resultType="Bean.Employee">select * from t_employee where id in<foreach collection="list" index="i" item="id" open="(" separator="," close=")">#{id}</foreach>
</select>

foreach 批量插入的两种方式

方式 1
<!--foreach 批量插入数据(方法1)
-->
<insert id="addEmployee">insert into t_employee (last_name, gender, email, d_id) values<foreach collection="list" item="employee" separator=",">( #{employee.lastName} , #{employee.gender} , #{employee.email} , #{employee.department.id} )</foreach>
</insert>
方式2
<!--foreach 批量插入数据(方法2,不推荐使用)每次都执行完整的插入语句,语句之间用分号分隔注:使用分号分隔时,需要在 jdbc.properties 配置文件的 url 属性的值后面,加上参数 allowMultiQueries=true ,代表可以执行多重 SQL 语句--><insert id="addEmployee"><foreach collection="list" item="employee" separator=";">insert into t_employee (last_name, gender, email, d_id) values(#{employee.lastName},#{employee.gender},#{employee.email},#{employee.department.id})</foreach></insert>

内置参数 _parameter & _databaseId

<!--内置参数,在 MyBatis 中,不只被方法传递过来的参数可以用来判断或取值
MyBatis 还有两个默认内置参数:
_parameter:代表整个参数,如果是单个参数,_paramter 就是这个参数如果是多个参数,会被封装为一个 Map, _paramter 就代表这个 Map注: _paramter 经常被用于判断,例如:--><if test="_paramter!=null"><if/><!--_databaseId:如果配置了 databaseIdProvider 标签,_databaseId 代表当前数据库的别名可以通过 if 标签,判断 _databaseId 的值来动态调用不同的 SQL 语句,例如:--><if test="_databaseId=='mysql'">select * from t_employee</if>
<if test="_databaseId=='oracle'">select * from tab_employee</if>

bind 绑定

<!--bind:可以将 OGNL 表达式的值,绑定到一个变量中,方便后来直接通过变量名引用这个变量的值
value:表达式的值
name:绑定的变量名
-->
<select id="getEmployeeByLastName" resultType="Bean.Employee"><bind name="_lastName" value="'%'+lastName+'%'"/>select *from t_employeewhere last_name like #{_lastName}
</select>

sql 抽取可重用的 sql 片段(方便引用)

<!--sql 抽取可重用的 sql 片段
<include refid="insertColumn"></include>:引用外部定义的 sql
refid:外部定义的 sql 的唯一标识
-->
<sql id="insertColumn">last_name, gender, email, d_id</sql><!--同时,if 标签等也能够引用-->
<sql id="insertIf">
<trim suffixOverrides=","><if test="lastName!=null">last_name = #{lastName},</if><if test="email!=null">email = #{email},</if><if test="gender!=null">gender = #{gender}</if>
</trim>
</sql>

Mybatis 缓存

简介

一级缓存

一级缓存 : 本地缓存,与数据库同一次会话期间,查询到的数据,会放在本地缓存中,以后若要获取相同的数据,直接从缓存中拿,无需查询数据库

一级缓存是 SqlSession 级别的缓存,是一直开启的。不同的 SqlSession 之间是不同的一级缓存,两个不同的一级缓存之间是不能共用的。实质上就是 SqlSession 级别的一个 Map

代码演示
//一级缓存体验
@Test
public void test() {SqlSession sqlSession = getSqlSession();try {EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);Employee employeeById = mapper.getEmployeeById(1);System.out.println(employeeById);Employee employeeById1 = mapper.getEmployeeById(1);/*通过多次调用查询方法,获取 employee 对象,然后进行比较,发现两次获取的 employee 的地址值都一样,说明两次获取的             employee 对象是同一个,证明了 MyBatis 将相同的数据存储在了一级缓存之中,多次查询时,直接获取的是缓存中的值。*/System.out.println(employeeById1 == employeeById);} finally {sqlSession.close();}
}
一级缓存失效的四种情况

1.SqlSession 不同,但获取了多次 SqlSession 时,不同的 SqlSession 获取到的数据,不能共用,因此只能去查询数据库,没有使用到一级缓存。

2.SqlSession 相同,但查询条件不同

3.SqlSession 相同,但两次查询之间,执行了增删改操作

4.SqlSession 相同,但两次查询之间执行了 SqlSession 的清除缓存的操作:clearCache()

缓存失效代码演示
/*
一级缓存失效情况1( SqlSession 不同)
*/
@Test
public void test1() {SqlSession sqlSession = null;SqlSession sqlSession1 = null;try {sqlSession = getSqlSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);Employee employeeById = mapper.getEmployeeById(1);System.out.println(employeeById);//重新获取一个 SqlSessionsqlSession1 = getSqlSession();EmployeeMapper mapper1 = sqlSession1.getMapper(EmployeeMapper.class);Employee employeeById1 = mapper1.getEmployeeById(1);System.out.println(employeeById1 == employeeById);} finally {if (sqlSession != null) {sqlSession.close();}if (sqlSession1 != null) {sqlSession1.close();}}
}/*
一级缓存失效情况2( SqlSession 相同,查询条件不同,查询的结果也不同,因此,无法直接从一级缓存中获取,需要重新查询查询数据库)
*/
@Test
public void test2() {SqlSession sqlSession = null;try {sqlSession = getSqlSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);Employee employeeById = mapper.getEmployeeById(1);System.out.println(employeeById);Employee employeeById1 = mapper.getEmployeeById(3);System.out.println(employeeById1 == employeeById);} finally {if (sqlSession != null) {sqlSession.close();}}
}/*
一级缓存失效情况3( SqlSession 相同,两次查询之间,执行了增删改操作,一级缓存会失效,
因为 任何一次增删改都可能会对数据库中的数据造成影响,再次查询不应该从一级缓存直接获取,而是重新查询数据库)
*/
@Test
public void test3() {SqlSession sqlSession = null;try {sqlSession = getSqlSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);Employee employeeById = mapper.getEmployeeById(1);//执行增删改之类的操作Employee employee = new Employee(null, "周星星", "zxx@163.com", "1", new Department(1, null));mapper.addEmployee(employee);Employee employeeById1 = mapper.getEmployeeById(1);System.out.println(employeeById1 == employeeById);} finally {if (sqlSession != null) {sqlSession.close();}}
}/*
一级缓存失效情况4( SqlSession 相同,手动清除了一级缓存,因为一级缓存中已经没有了之前查询的数据,因此需要从从数据库中重新查询)
*/
@Test
public void test4() {SqlSession sqlSession = null;try {sqlSession = getSqlSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);Employee employeeById = mapper.getEmployeeById(1);System.out.println(employeeById);//清除缓存sqlSession.clearCache();Employee employeeById1 = mapper.getEmployeeById(1);System.out.println(employeeById1 == employeeById);} finally {if (sqlSession != null) {sqlSession.close();}}
}

二级缓存

二级缓存 : 全局缓存是基于 namespace 级别的缓存,一个 namespace 对应一个二级缓存

二级缓存工作机制

1.一个会话,查询一条数据,这个数据就会被放在当前会话的一级缓存中

2.如果当前会话关闭了,一级缓存的数据会被保存到二级缓存中,新的会话查询信息就可以参照二级缓存中的数据

3.SqlSession === EmployeeMapper == > Employee
DepartmentMapper == > Department

不同的 namespace 查出的数据会放在自己对应的缓存中。

二级缓存使用 & 细节

使用步骤

1.在 MyBatis 全局配置文件中开启全局二级缓存配置

<!--开启二级缓存,默认为开启状态-->
<setting name="cacheEnabled" value="true"/>

2.去 SQL 映射配置文件中配置二级缓存:

<!--cache 开启二级缓存eviction:缓存的回收策略LRU:最近最少使用的| 移除最长时间不使用的对象FIFO:先进先出| 按照对象进入缓存的顺序来移除他们SDFT:软引用| 移除基于垃圾回收器状态和软引用规则的对象WEAK:弱引用| 更积极的移除基于垃圾回收器状态和弱引用规则的对象默认为:LRUflushInterval:缓存刷新间隔多长时间清空一次,默认不清空,可以设置一个毫秒值readOnly:缓存是否只读默认为:falsetrue:只读,MyBatis 认为,所有从缓存中获取数据的操作都是只读操作,不会修改数据,为了加快获取速度,直接就会将数据在缓存中的引用交给用户,不安全,速度快false:非只读,MyBatis 认为获取到的数据可能会被修改,MyBatis 会利用序列化 & 反序列化的技术克隆一份用户需要的数据,安全,速度稍慢size:缓存存放多少个元素type:指定自定义缓存的全类名,自定义缓存需要实现 Cache 接口-->
<cache eviction="LRU" flushInterval="60000" readOnly="false" size="1024"></cache>

3.由于缓存默认非只读,所有,放入缓存的 JavaBean 需要实现序列化接口:Serializable

代码演示

/*
二级缓存测试*/
@Test
public void test5() throws Exception {SqlSession sqlSession = null;SqlSession sqlSession1 = null;InputStream in = null;try {in = Resources.getResourceAsStream(RESOURCE);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);sqlSession = sqlSessionFactory.openSession();sqlSession1 = sqlSessionFactory.openSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);Employee emp01 = mapper.getEmployeeById(1);System.out.println(emp01);//开启二级缓存的情况下,某一个会话一旦关闭,就会将数据放入二级缓存/*注:1.两个 SqlSession 会话必须使用同一个 sqlSessionFactory 生成,否则当一个 SqlSession 会话关闭时另一个 SqlSession 会话无法访问到之前被关闭的 SqlSession 提交到二级缓存中的数据。2.只有当一个 SqlSession 会话关闭时,其影响了的数据才会被放入二级缓存之中,在 SqlSession 会话被关闭之前会话影响的数据都是放在一级缓存之中的。3.尽管全局配置文件中配置了开启二级缓存,但是,当 SQL 映射配置文件中没有开启二级缓存时,二级缓存将不会生效*/sqlSession.close();//当其他会话查询数据时,就能访问到二级缓存中的数据而不用再次发送 SQL 语句EmployeeMapper mapper1 = sqlSession1.getMapper(EmployeeMapper.class);Employee emp02 = mapper1.getEmployeeById(1);System.out.println(emp02);sqlSession1.close();} finally {if (in != null) {in.close();}if (sqlSession != null) {sqlSession.close();}if (sqlSession1 != null) {sqlSession1.close();}}
}

缓存相关设置以及属性

1.全局配置文件中,有 cacheEnabled=“true” ,false : 关闭缓存(二级缓存关闭)(一级缓存一直开启)

2.每个 select 标签都有 useCache=“true” (是否使用缓存) ,false : 不使用缓存(一级缓存依然可以使用,二级缓存不会被使用)

3.每个增删改标签都有 flushCache=“true” (是否清空缓存) , 因此,增删改操作之后,一级二级缓存都会被清空,再次查询数据时,需要重新查询数据库,实际上,flushCache 在查询标签中也存在,默认为 false,为 true 的话,每次查询都将清空一二级缓存。

4.sqlSession 的方法, sqlSession.clearCache();只是清除当前 sqlSession 的一级缓存

5.全局配置文件中,还有 loaclCacheScope(本地缓存作用域,影响的是一级缓存),取值有 SESSION (代表当前 sqlSession 会话的所有数据,都会保存在该 SqlSession 的一级缓存中) 和 STATEMENT (本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存,因此可以禁用掉该 SqlSession 的一级缓存)

MyBatis 缓存原理图示

第三方缓存整合

整合 ehcache 准备工作

1.导入相关 jar 包:ehcache-core-2.6.8.jar(ehcache核心包),slf4j-api-1.6.1.jar(日志包1),slf4j-log4j12-1.6.2.jar(日志包2),mybatis-ehcache-1.0.3.jar(MyBatis 整合包)

2.导入 chcache.xml 到类路径下

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"><!-- 缓存的磁盘保存路径 --><diskStore path="D:\Documents\Java代码\MyBatis\ehcache"/><defaultCachemaxElementsInMemory="10000"maxElementsOnDisk="10000000"eternal="false"overflowToDisk="true"timeToIdleSeconds="120"timeToLiveSeconds="120"diskExpiryThreadIntervalSeconds="120"memoryStoreEvictionPolicy="LRU"></defaultCache>
</ehcache><!--属性说明:diskStore:指定数据在磁盘中的存储位置。defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略以下属性是必须的:maxElementsInMemory - 在内存中缓存的element的最大数目maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上以下属性是可选的:timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)-->

3.直接在 SQL 映射文件中引用

<!--使用 ehcache
type:指定自定义 cache 的位置
也可以使用 <cache-ref namespace=""/> 指定和哪个名称空间下的缓存一样,而无需再次配置缓存
-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>

MyBatis & Spring 整合

导入相关 jar 包

相关 jar 包如下

注:MyBatis 整合 Spring 关键在于需要导入 mybattis-spring-x.x.x.jar 的整合 jar 包

编写 MyBatis 相关配置文件

1.导入 MyBatis 全局配置文件到 config / mybatis_config目录下

2.在 dao 包下编写相关 dao 接口

3.在 config / mybatis_mapper / 目录下编写 SQL 映射配置文件,与 dao 中对应的接口绑定

目录结构如下:

整合 Spring & SpringMVC 配置文件编写

1.编写 spring 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><!--开启注解扫描--><context:component-scan base-package="main" use-default-filters="false"><context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan><!--配置数据源,整合 其他框架,事务等--></beans>

2.编写 SpringMVC 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:mvc="http://www.springframework.org/schema/mvc"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="main" use-default-filters="false"><!--只扫描控制器--><context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan><!--MVC驱动,其中一个功能就是和 DefaultServlet 搭配使用来处理静态资源的释放--><mvc:annotation-driven></mvc:annotation-driven><!--配置 Tomcat 服务器默认的Servlet,以便在请求没有对应的目标方法映射时,调用此Servlet处理请求--><mvc:default-servlet-handler></mvc:default-servlet-handler><!--视图解析器--><bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="prefix" value="/WEB-INF/views/"></property><property name="suffix" value=".jsp"></property></bean>
</beans>

4.编写 web.xml 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><!--Spring 配置--><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:main/config/spring_config/applicationContex.xml</param-value></context-param><!--SpringMVC 配置--><!--前端控制器,用于处理所有请求--><servlet><servlet-name>DispatcherServlet</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><!--自定义SpringMVC的配置文件路径与名称--><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:main/config/springMVC_config/mvc.xml</param-value></init-param></servlet><!--拦截所有请求--><servlet-mapping><servlet-name>DispatcherServlet</servlet-name><url-pattern>/</url-pattern></servlet-mapping>
</web-app>

整合 Spring & SpringMVC 关键配置编写

MyBatis 整合 Spring 的目的

1.Spring 管理所有组件,包括 mapper 的实现类,当 Service 调用 Dao 层面时,只需要使用 Spring @AutoWired 自动注入即可

2.Spring 用来管理事务等

关键配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx.xsd http://mybatis.org/schema/mybatis-springhttp://mybatis.org/schema/mybatis-spring.xsd"><!--开启注解扫描--><context:component-scan base-package="main" use-default-filters="false"><context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan><!--配置数据源,整合 其他框架,事务等--><!--Spring 事务管理DataSourceTransactionManager--><!--引入数据库配置文件--><context:property-placeholder location="main/config/myBatis_config/jdbc.properties"></context:property-placeholder><!--配置数据源--><bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"><property name="driverClassName" value="${driver}"></property><property name="url" value="${url}"></property><property name="username" value="${name}"></property><property name="password" value="${password}"></property></bean><!--配置事务管理器--><bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"></property></bean><!--开启基于注解的事务--><tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven><!--创建 SqlSessionFactory 对象--><bean class="org.mybatis.spring.SqlSessionFactoryBean"><!--指定一个数据源,该数据源也是 SqlSesson 对象被创建时所使用的--><property name="dataSource" ref="dataSource"></property><!--指定 MyBatis 全局配置文件的位置--><property name="configLocation" value="main/config/myBatis_config/mybatis-config.xml"></property><!--指定 SQL 映射配置文件的位置--><property name="mapperLocations" value="main/config/myBatis_mapper/*.xml"></property></bean><!--扫描所有的 mappeer 接口,使其能够自动注入--><mybatis-spring:scan base-package="main/dao"></mybatis-spring:scan>
</beans>

整合测试

整合测试存在问题,后续再次解决。

Mybatis 逆向工程

简介

准备工作

1.下载 jar 包:mybatis-generator-core-1.3.5.jar 导入

2.编写 mbg.xml 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfigurationPUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN""http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration><!--使用该配置文件,规定如何生成表对应的 SQL 映射文件和 mapper 接口--><!--targetRuntime="MyBatis3Simple":生成简单版的CRUDMyBatis3:豪华版--><context id="DB2Tables" targetRuntime="MyBatis3Simple"><!-- jdbcConnection:指定如何连接到目标数据库 --><jdbcConnection driverClass="com.mysql.jdbc.Driver"connectionURL="jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true"userId="root"password="dhj461212"></jdbcConnection><!--Java类型解析器--><javaTypeResolver><property name="forceBigDecimals" value="false"/></javaTypeResolver><!-- javaModelGenerator:指定javaBean的生成策略targetPackage="test.model":目标包名targetProject="\MBGTestProject\src":目标工程--><javaModelGenerator targetPackage="bean"targetProject=".\src"><property name="enableSubPackages" value="true"/><property name="trimStrings" value="true"/></javaModelGenerator><!-- sqlMapGenerator:sql映射配置文件生成策略: --><sqlMapGenerator targetPackage="mapper"targetProject=".\config"><property name="enableSubPackages" value="true"/></sqlMapGenerator><!-- javaClientGenerator:指定mapper接口所在的位置 --><javaClientGenerator type="XMLMAPPER" targetPackage="dao"targetProject=".\src"><property name="enableSubPackages" value="true"/></javaClientGenerator><!-- 指定要逆向分析哪些表:根据表要创建javaBean --><table tableName="t_dept" domainObjectName="Department"></table><table tableName="t_employee" domainObjectName="Employee"></table></context>
</generatorConfiguration>

3.调用测试方法,生成简单版 SQL 映射以及 mapper 接口等

@Test
public void test() throws Exception {List<String> warnings = new ArrayList<>();boolean overwrite = true;File configFile = new File("mbg.xml");ConfigurationParser cp = new ConfigurationParser(warnings);Configuration config = cp.parseConfiguration(configFile);DefaultShellCallback callback = new DefaultShellCallback(overwrite);MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);myBatisGenerator.generate(null);
}

测试(简单版)

@Test
public void testMyBatisSimple() {SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();SqlSession sqlSession = sqlSessionFactory.openSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);List<Employee> employees = mapper.selectAll();for (Employee employee : employees) {System.out.println(employee);}
}

测试(复杂版)

//当前使用的是 复杂版 mbg.xml 配置文件,因此上面的该测试方法中涉及到的接口等失效
@Test
public void testMyBatis3() {SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();SqlSession sqlSession = sqlSessionFactory.openSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);// 参数为 xxxExample,代表封装的是查询条件,若传入 null ,则查询全部List<Employee> employees = mapper.selectByExample(null);for (Employee employee : employees) {System.out.println(employee);}//封装查询条件的 example(实例)EmployeeExample employeeExample = new EmployeeExample();// 创建一个 createCriteria(创建标准),用于拼装查询条件EmployeeExample.Criteria criteria = employeeExample.createCriteria();System.out.println("条件拼装查询-----");/*拼装查询条件*///姓名 like %e%criteria.andLastNameLike("%");//性别值为 1criteria.andGenderEqualTo("0");//id 为 1 - 20 之间,范围为两端闭区间:[1,20]criteria.andIdBetween(1, 20);/*拼装一个 or 条件*///创建一个新的 criteriaEmployeeExample.Criteria criteria1 = employeeExample.createCriteria();criteria1.andEmailLike("%yilisb%");/*将之前的 criteria 调用 or 方法,传入新的 criteria 作为参数,代表两个 criteria 查询条件为 or 的关系,以此类推*/employeeExample.or(criteria1);List<Employee> employees1 = mapper.selectByExample(employeeExample);for (Employee employee : employees1) {System.out.println(employee);}
}

MyBatis 底层原理

基本运行流程

1.获取 SqlSessionFactory 对象
2.获取 SqlSession 对象
3.获取接口的代理对象(MapperProxy)
4.执行代理对象的增删改查方法

SqlSessionFactory 初始化

流程图

文字说明

1.首先会调用 SqlSessionFactoryBuild 中的 build 方法,该方法读取了一个指向 MyBatis 全局配置文件的 InputStream 输入流

2.然后创建一个解析器 parser ,解析全局配置文件以及 SQL 映射配置文件中的所有内容,最终返回一个 Configuration 对象

3.将该对象传入 SqlSessionFactoryBuilder 中的重载 build() 方法中,build() 方法结合 Configuration 对象,创建了一个 DefaultSqlSessionFactory 对象,由于 DefaultSqlSessionFactory 实现了 SqlSessionFactory 接口,因此直接返回 DefaultSqlSessionFactory 对象即可。

注:创建 DefaultSqlSessionFactory 时,使用的是 DefaultSqlSessionFactory 的有参构造器:

public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;
}

通过该构造器,能够对 DefaultSqlSessionFactory 中的 Configuration 属性进行赋值,使得最后返回的 SqlSessionFactory 包含了 MyBatis 全局配置文件以及 SQL 映射配置文件中的全部信息。相关源码如下:

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {SqlSessionFactory var5;try {//创建 XMLConfigBuilder 对象XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);//对 inputStream 指向的配置文件进行解析,同时调用重载的 build 方法创建 DefaultSqlSessionFactory 并返回。var5 = this.build(parser.parse());} catch (Exception var14) {throw ExceptionFactory.wrapException("Error building SqlSession.", var14);} finally {ErrorContext.instance().reset();try {inputStream.close();} catch (IOException var13) {}}return var5;
}//重载的 build 方法
public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);
}

流程总结

获取 SqlSessionFactory 时,先通过 SqlSessionFactoryBuilder 中的 build() 传入了指向配置文件的 InputStream 输入流。此时并没有进行配置文件的解析。

然后继续调用重载的 build() 方法,将 InputStream 作为参数传入,在该重载方法中使用 XMLConfigBuilder 的有参构造器,传入 InputStream 创建了一个 XMLConfigBuilder 的实例对象:parser

继续调用另一个重载的 build() 方法且通过 parser.parse() 对 InputStream 指向的全局配置文件中的所有内容进行解析,包括 SQL 映射配置文件中的信息,解析完成后返回的是一个包含 全局配置文件以及 SQL 映射配置文件所有信息的 Configuration 对象。

最后,调用 DefaultSqlSessionFactory 的有参构造器,传入 Configuration 对象,创建一个 DefaultSqlSessionFactory 并最终返回。

openSession() 获取 SqlSession 对象

流程图

注:返回的 SqlSession 的实现类 DefaultSqlSession ,其中包含了 Configuretion ,Executor 对象,其中,四大对象的 Executor 对象也是在获取 DefaultSqlSession 的过程中,被首次创建了的。关键源码如下:

//Executor 对象被首次创建
Executor executor = this.configuration.newExecutor(tx, execType);
//最终返回 SqlSession 接口的实现类: DefaultSqlSession
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);

文字说明

1.在获取 SqlSession 时,首先调用的是 DefaultSqlSessionFactory 中的 openSession() 方法,在该方法内,继续调用 openSessionFromDataSource() 方法,该方法意为根据 DataSource 数据源创建一个 SqlSession 会话,该方法的参数包括:执行器类型(例如:简单执行器 SimpleExecutor ,可复用的执行器 ReuseExecutor ,批处理执行器等 BatchExecutor),事务隔离级别,是否自动提交这三个参数。 相关源码如下:

openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);

2.在 openSessionFromDataSource() 方法内,通过 Configuration 保存的配置文件信息,获取 Environment 对象,该对象保存的是MyBatis 的运行环境信息,包括配置的数据源,事务管理器等。

3.然后根据 Environment 对象中的 DataSource 数据源,openSessionFromDataSource 方法的参数: level 隔离级别,autoCommit 是否自动提交,等三个参数,创建一个 Transaction 对象 tx。

4.根据这个 tx 对象和方法参数中的 execType 对象,获取一个执行器对象 Executor ,这其中,在获取 Executor 对象调用的 configuration 对象的 newExecutor 方法中,有如下细节:首先会判断执行器的类型,然后会检查是否开启了二级缓存,使用 CachingExecutor 来包装 Executor ,最后会使用每一个拦截器再次包装 Executor 对象,最后返回包装后的 Executor 对象。相关源码如下:

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 = (Executor)this.interceptorChain.pluginAll(executor);return executor;//返回 Executor 对象
}

5.最后根据 Configuretion ,executor ,autoCommit 三个参数,创建一个 DefaultSqlSession 类的对象,该类实现了 SqlSession 接口,最终返回 DefaultSqlSession 对象。相关源码如下:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;DefaultSqlSession var8;try {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;
}

流程总结

通过 SqlSessionFactory 获取 SqlSession 时,首先调用的实际上是 DefaultSqiSessionFactory 中的 openSesion() 方法,该方法又继续调用了一个 openSessionFromDataSource() 方法,在方法参数中,调用 Configuration 中的 getDefaultExecutorType 传入一个默认的执行器类型,参数 TransactionIsolationLevel(事务隔离级别) 为 null,参数 autoCommit(是否自动提交) 默认为 false。

在 openSessionFromDataSource 方法中,通过 configuration 的 getEnvironment()方法 获取了一个 Environment 对象,根据这个 Environment 对象,创建了一个 TransactionFactory 事务工厂对象,然后通过调用这个 TransactionFactory 的 newTransaction() 方法,传入 DataSource(数据源),level (事务隔离级别,在调用无参的 openSession() 时,该参数为 null ),autoCommit(是否自动提交,在调用无参的 openSession() 时,该参数默认为 false ) 等参数,创建一个 Transaction 对象 tx

然后调用 Configuration 中的 newExecutor() 方法,将 tx ,和 execType 作为参数传入,创建一个 Executor 对象

最后,调用 DefaultSqlSession 的有参构造器,将 Executor,Configuration,autoCommit 对象作为参数传入,创建一个 DefaultSqlSession 对象,最终返回。

getMapper 获取到接口的代理对象

流程图

文字解析

1.通过上面流程中获取到的 SqlSession 对象,首先调用的其实是 DefaultSqlSession 中的 getMapper() 方法,在该 getMapper() 方法中调用的是 Configuration 中的 getMapper() 方法,然后调用 Configuretion 的属性 MapperRegistry 中的 getMapper() 方法,该方法中,首先根据映射器类型 type(实际上就是接口的Class实例) 在保存已知映射器的 knownMappers 中(一个 HashMap ) 查找是否有该类型的映射器,若没有查找到该映射器,则会抛出绑定异常:“Type " + type + " is not known to the MapperRegistry.”,说明该映射器几口接口没有在 MyBatis 全局配置文件中注册。

2.若是在 knownMappers 找到了经过注册的映射接口的 Class 实例,则调用 MapperProxyFactory 中的 newInstance() 方法,该方法传入一个 SqlSession 对象,该 SqlSession 实际上就是最初调用 getMapper 获取映射器接口代理类的 DefaultSqlSession 对象,其中包含了全局配置文件,数据源,是否自动提交,SQL 查询语句等等等所有信息(该参数在最开始的 DefultSqlSession 调用 getMapper 时,就已经通过 this 传入了),通过这个 SqlSession 对象,映射器接口的 Class 实例,以及 MapperProxyFactory 中的属性 methodCache,创建了一个接口映射器的 InvocationHandler 实例对象:mapperProxy (用作于后续创建接口代理类对象的参数) ,将其再次传入MapperProxyFactory 中的重载 newInstance() 方法,该方法中,通过代理类 Proxy 调用 newProxyInstance() 方法,最终获取了一个映射器接口的代理类并返回。

相关源码如下:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {//根据获取到的映射器接口 Class 实例,创建 MappeProxyFactory 对象MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);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);}}
}
protected T newInstance(MapperProxy<T> mapperProxy) {//通过下方映射器接口的 InvocationHandler 实例对象,创建一个映射器接口的代理对象并返回,也就是最终返回的映射器接口代理     类,如:EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class); 中的 mapper return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}//上面源码中, mapperProxyFactory.newInstance(sqlSession) 调用的 newInstance() 方法
public T newInstance(SqlSession sqlSession) {//首先获取映射器接口的 InvocationHandler 实例对象MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);return this.newInstance(mapperProxy);
}

查询的原理

流程图

注:

限于个人水平,此处的源码分析尚不完整,大致的源码运行原理,优先参考流程图。

源码分析

/*当调用映射器接口中的 增删改查 方法时,实际上调用的是映射器接口的代理对象中的 invoke() 方法,通过反射获取到了当前调用的 增删改查的方法对象 Method 以及方法参数 args 还有当前被增强的对象,也就是被代理的对象,也是当前调用的增删改查方法实际所属的对象 proxy 通过这三个参数,就可以准确调用到被代理类中的方法。*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//如果调用到的方法是 Object 中的方法,直接通过反射调用即可返回即可。if (Object.class.equals(method.getDeclaringClass())) {try {return method.invoke(this, args);} catch (Throwable var5) {throw ExceptionUtil.unwrapThrowable(var5);}} else {/*否则该方法就是增删改查方法,将当前的 Method 包装成一个 MapperMethod 此 MapperMethod 是由 MyBatis 定义的,用于增删改查方法的执行。 */ MapperMethod mapperMethod = this.cachedMapperMethod(method);/*调用 MapperMethod 中的 execute 来真正的执行 增删改查方法,最终返回结果 */return mapperMethod.execute(this.sqlSession, args);}
}

execute() 执行细节

/*
execute() 方法就是 MapperMethod 中用于执行增删改查方法的,方法参数中传入了一个 SqlSession 和 agrs 二者包含了配置文件中的信息,数据源,SQL 语句,执行器类型,事务以及增删改查方法的参数等等所有与数据库交互的信息。
*/
public Object execute(SqlSession sqlSession, Object[] args) {Object param;Object result;//判断 SQL 语句的类型是什么:增 删 改 查switch(this.command.getType()) {/*对于增删改 SQL 语句,都是先调用 convertArgsToSqlCommandParam() 方法,将参数转化到 SQL 语句中去然后通过 rowCountResult() 方法返回影响的行数,保存到 result 中*/case INSERT:param = this.method.convertArgsToSqlCommandParam(args);result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));break;case UPDATE:param = this.method.convertArgsToSqlCommandParam(args);result = this.rowCountResult(sqlSession.update(this.command.getName(), param));break;case DELETE:param = this.method.convertArgsToSqlCommandParam(args);result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));break;/*对于查询的 SQL 语句*/case SELECT://如果当前查询方法返回值是 void 且(hasResultHandler)有结果处理程序,则执行结果处理程序,返回 null 即可。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 if (this.method.returnsCursor()) {result = this.executeForCursor(sqlSession, args);} else {/*如果都不是,说明不是返回多个记录的,那么依然先将参数转换到 SQL 语句中,调用 selectOne 查询出单条记录,保存到结             果 result 中。注意:尽管是调用的 selectOne() 方法,但是实际上依然调用的是 DefultSqlSession 中的 selectList() 方法,最后返回             的是 list.get(0) 这一个元素。*/param = this.method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(this.command.getName(), param);}break;//如果是一个刷新操作,直接执行刷新语句case FLUSH:result = sqlSession.flushStatements();break;default:/*在都不是的情况下,抛出异常:Unknown execution method for: " + this.command.getName(),意为:未知的执行方法:“ + this.command.getName()*/throw new BindingException("Unknown execution method for: " + this.command.getName());}/*在执行完增删改查方法后,如果返回的结果为 null 并且返回的类型是最原始的 且 调用的增删改查返回返回值不为 void 则抛出异常Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return       type (" + this.method.getReturnType() + "),意为:映射器方法'“ + this.command.getName()+”尝试从具有原始返回类型(“ + this.method.getReturnType()+”)的方法)中返回     null*/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 {//否则的话,返回最终查询到的结果 result return result;}
}

selectList 执行细节

//不论是查询单个还是多个记录,实际上都是调用的 DefaultSqlSession 中的 selectList() 方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {List var5;try {//获取一个 MappedStatement 对象,该对象封装了增删改查标签的详细信息MappedStatement ms = this.configuration.getMappedStatement(statement);/*调用 executor 中的 query() 方法进行查询,其中,通过 wrapCollection() 方法进行参数的包装细节如下:private Object wrapCollection(Object object) {DefaultSqlSession.StrictMap map;//如果是一个 Colection 就将 collection 作为一个 key 将 Collection 类型的 object 对象作为 value 封装到 Map 中 下         //面的 List 和 Array 的封装逻辑也是如此if (object instanceof Collection) {map = new DefaultSqlSession.StrictMap();map.put("collection", object);if (object instanceof List) {map.put("list", object);}return map;} else if (object != null && object.getClass().isArray()) {map = new DefaultSqlSession.StrictMap();map.put("array", object);return map;} else {return object;}                   |}                       |   以上 wrapCollection() 方法,实际上就是下面 query() 方法参数中的 wrapCollection() 调用|V    */  var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);||   |V/*通过上面 executor.query(...) 的调用 继续进行查询executor.query() 细节如下:public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {//首先会通过 获取 MappedStatement 的 getBoundSql() 方法获取绑定的 SQL 语句 BoundSql 对象中就包含 SQL 语句和参数         //的值等信息BoundSql boundSql = ms.getBoundSql(parameterObject);//创建出一个缓存CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);再次调用 query() 方法进行查询:细节如下|...public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {//获取缓存Cache cache = ms.getCache();if (cache != null) {this.flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {this.ensureNoOutParams(ms, parameterObject, boundSql);//注:此处查询的是二级缓存List<E> list = (List)this.tcm.getObject(cache, key);if (list == null) {//如果没有缓存,继续调用执行 delegate(Executor类型的对象,这里实际上是 BaseExecutor) 中的查询方法 query()细节如下|...public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if (this.closed) {throw new ExecutorException("Executor was closed.");} else {if (this.queryStack == 0 && ms.isFlushCacheRequired()) {this.clearLocalCache();}List list;try {++this.queryStack;//先在本地一级缓存中通过传递过来的 CacheKey 获取对应的数据list = resultHandler == null ? (List)this.localCache.getObject(key) : null;if (list != null) {this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {//如果没有缓存,则调用 queryFromDatabase() 从数据库中查询细节如下:|...private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {//先在本地缓存中放置一个 key ,value 对应一个占位符this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLAC    Statement       List list;try {>>>>>最终调用过程:// 再次调用本类中的重载查询方法 doQuery(),该方法是一个抽象方法,根据 Executor 的类型,有不同的实现,此处使用的是// SimpleExecutor 简单执行器,其中实现的 query() 方法,在该 query() 方法中 根据 Executor 的类型,调用的是// SimpleStatementHandler 中实现的 query 方法,该方法中,利用原生 JDBC 中的 Statement 调用其中的 execute 执行           // 了 SQL 语句,最终返回处理的结果集。//注:由此可以看出,MyBatis 底层的增删改查,使用的也依然是原生的 JDBC,同时,四大对象中的 SimpleStatementHandler//在此过程中被创建>>>>>   || 上面的调用逻辑由下面的 doQuery() 调用引出。|V list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {this.localCache.removeObject(key);}//将查询到的结果保存到本地缓存中,key 就为参数传递进来的 keythis.localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {this.localOutputParameterCache.putObject(key, parameter);}return list;}||...           | 该方法实际上就是下面 queryFromDatabase() 的调用|Vlist = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {--this.queryStack;}if (this.queryStack == 0) {Iterator i$ = this.deferredLoads.iterator();while(i$.hasNext()) {BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next();deferredLoad.load();}this.deferredLoads.clear();if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {this.clearLocalCache();}}return list;}}                              |||...                          | 上面的 query() ,实际上就是如下的 query() 方法的调用||Vlist = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);this.tcm.putObject(cache, key, list);}return list;}}return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}                 |||上面的 query() 就是下面 query() 方法的调用|...          ||Vreturn this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}*/ } catch (Exception var9) {throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);} finally {ErrorContext.instance().reset();}return var5;
}

查询原理总结

1.通过接口类的代理对象来调用接口中定义的增删改查方法

2.代理对象中通过反射来调用映射接口中的对映方法,将调用的方法封装为一个 MapperMethod 对象

3.在 MapperMethod 对象,又通过 DefaultSqlSession 对象来执行增删改查方法。

4.在 DefaultSqlSession 中又通过 Executor(四大对象) 对象来调用增删改查方法,同时会创建一个 StatementHandler(四大对象) 对象用于预编译 SQL 语句和设置参数等工作。

5.在创建 StetementHandler 的时候,还创建了两个对象,分别是: ParmeterHandler(四大对象),ResultHandler(四大对象) ,分别用于StatementHandler 在设置预编译参数以及处理结果集时使用。

6.而以上的设置预编译参数和处理结果集对象时,使用的都是 Typehandler ,在整个过程中,都是使用 TypeHandler 来进行数据库结果集与 JavaBean 类型的映射。

7.以上的过程,使用的都是原生 JDBC 的 Statement 和 PreparedStatement

总结: Mapper 映射接口的代理对象调用 DefaultSqlSession 执行增删改查,DefaultSqlSession 调用 Executor 执行增删改查,Executor 又使用 StatementHandler 执行增删改查,而增删改查中涉及到的设置预编译参数和处理结果集又分别使用的是 ParamterHandler 和 ResultSetHandler 来进行的,而 ParmterHandler 和 ResultSetHandler 在设置参数和处理结果是,都是调用的 TypeHandler ,而整个流程的底层,又都是基于原生 JDBC 中的 Statement 和 PreparedStatement 来实现。

整个运行流程总结

1.根据配置文件(全局,SQL 映射) 初始化 Configuration 对象

2.创建一个 DefaultSqlSession 对象,其中包含 Configuration 和 Executor 对象(根据全局配置文件中的 defaultExecutorType 参数,创建对应的 Executor 对象,默认为 Simple )

3.DefaultSqlSession.getMapper() ,拿到 Mapper 接口对应的 MapperProxy

4 MapperProxy 里面包含了 DefaultSqlSession

5.执行增删改查 ,调用的是 DefaultSqlSession 中的增删改查,实际上是调用 Executor 增删改查,过程中会创建 StatementHandler 对象,同时也会创建出 ParameHandler 和 ResultSetHandler ,然后调用的是 StatementHandler 进行预编译以及设置参数值(实际上使用的是 ParamterHandler),然后调用自身的增删改查方法。

6.在增删改查完成后,进行结果的封装吗(实际上使用的是之前创建的 ResulterHandler 来封装结果)

注:四大对象每个创建时,都会有 interceptorChain.pluginAll() 的调用。

MyBatis 插件

MyBatis 插件原理

插件原理:
四大对象的创建
1.每个创建出来的对象,不是直接返回的,而是先调用 interceptorChain.pluginAll() 后,然后再返回创建的四大对象
2.获取到所有的 Interceptor(拦截器接口,编写的插件就需要实现此接口),然后调用 pluginAll() 方法,返回包装后的 target 对象,也就是目标对象的代理对象
3.根据插件机制,就可以使用插件为目标对象创建一个代理对象,实际上就是 AOP(面向切面)的编程方式,编写的插件可以为四大对象创建出代理对象,代理对象就能拦截到四大对象中的每一个执行
pluginAll()详细实现如下:
public Object pluginAll(Object target) {Interceptor interceptor;for(Iterator i$ = this.interceptors.iterator(); i$.hasNext(); target = interceptor.plugin(target)) {interceptor = (Interceptor)i$.next();}

MyBatis 插件的编写

1.编写 Interceptor 接口的实现类

import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;import java.util.Properties;/*
使用 @Intercepts 完成插件的前面,告诉 MyBatis 当前插件拦截的详细信息
type 属性:指定拦截四大对象中的哪一个对象
method 属性:指定拦截的那个方法
args 属性:指定方法的参数以区分重载的方法*/
@Intercepts({@Signature(type = StatementHandler.class, method = "parameterize", args = Statement.class)
})
public class MyFirstPlugin implements Interceptor {/*拦截目标对象中的目标方法的执行*/@Overridepublic Object intercept(Invocation invocation) throws Throwable {/*使用动态代理,在调用目标方法的操作 invocation.proceed() 前后,可以执行一些操作这也是插件的作用,同时对于插件的开发,也要注意,因为在目标方法被拦截时,执行的操作,涉及到了 MyBatis 底层的运行*///执行目标方法Object proceed = invocation.proceed();//返回执行后的返回值return proceed;}/*用于包装目标对象,为目标对象创建一个代理对象*/@Overridepublic Object plugin(Object o) {/*借助 MyBatis 提供的 Plugin 类中的 wrap() 方法使用当前的 Intercept 来进行目标对象的代理对象的创建在 wrap() 方法中,实际上也是通过 Proxy.newProxyInstance() 来创建代理对象的*/Object wrap = Plugin.wrap(o, this);//返回当前创建的动态代理对象return wrap;}/*将插件注册时的properties属性值设置进来*/@Overridepublic void setProperties(Properties properties) {//使用 properties 可以获取插件配置的属性信息System.out.println("插件配置的信息:" + properties);}
}

2.在全局配置文件中进行配置

<!--plugin 注册插件
interceptor 属性:插件的全类名
也可以通过 plugin 标签中的 property 标签进行参数的传入
-->
<plugins><plugin interceptor="MyPlug.MyFirstPlugin"><property name="name" value="root"/><property name="password" value="123456"/></plugin>
</plugins>

3.测试

@Test
public void test() {SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();SqlSession sqlSession = sqlSessionFactory.openSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);System.out.println(mapper);Employee employeeById = mapper.getEmployeeById(1);System.out.println(employeeById);
}

WARN 03-07 22:20:38,953 Could not examine class ‘Bean/Department.class’ due to a java.lang.NoClassDefFoundError with message: Bean/Department (wrong name: bean/Department) (ResolverUtil.java:264)
WARN 03-07 22:20:38,960 Could not examine class ‘Bean/Employee.class’ due to a java.lang.NoClassDefFoundError with message: Bean/Employee (wrong name: bean/Employee) (ResolverUtil.java:264)
setProperties() 插件配置的信息:{password=123456, name=root}
Logging initialized using ‘class org.apache.ibatis.logging.stdout.StdOutImpl’ adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Created connection 503195940.
Returned connection 503195940 to pool.
plugin():创建代理对象
org.apache.ibatis.binding.MapperProxy@7403c468
plugin():创建代理对象
plugin():创建代理对象
plugin():创建代理对象
Opening JDBC Connection
Checked out connection 503195940 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1dfe2924]
> Preparing: select * from t_employee where id = ?
intercept():拦截到目标方法
> Parameters: 1(Integer)
< Columns: id, last_name, gender, email, d_id
< Row: 1, 伊丽莎白, 0, yilisb@163.com, 1
<== Total: 1
Employee{id=1, lastName=‘伊丽莎白’, email=‘yilisb@163.com’, gender=‘0’}

Process finished with exit code 0

注:通过对日志文件的打印看出,插件确实拦截到了目标对象中的目标方法,且创建了指定的四大对象的代理对象,并在一开始输出了插件中配置的文件信息。

多个插件运行流程

1.一个插件会产生一个目标对象的代理对象,多个插件时,就会层层代理,产生多个代理对象,代理的顺序会根据配置文件中配置的插件的顺序来代理。

2.因为是按顺序代理的,所以,最后代理的插件的代理对象,会成为最大的代理对象(形象的说,也就是最外层的代理对象),因此,执行拦截方法时,也是从最外层代理对象中开始执行的,所以,全局配置文件中最后配置的代理对象反而会先执行。

3.总结,创建代理对象时,按照插件在配置文件中的先后顺序进行代理,而且是层层代理,而执行目标方法的时候,是按照全局配置文件中的逆序执行的:全局配置文件中最后配置的插件,先执行。

插件的开发

public class MySecondPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {/*动态的改变一下SQL运行的参数:查询的是一号员工,通过插件,修改为三号员工原理: 根据 MyBatis 的底层运行原理,StatementHandler 中会通过 ParamterHandler 来设置参数而在 ParamterHandler 中,通过 paramterObject 来封装参数,因此只需要拦截该对象改变其值即可。*///拿到 StatementHandler ==> ParamterHandler ==> paramterObjectObject target = invocation.getTarget();System.out.println("当前拦截到的对象:" + target);//获取当前目标对象的元数据(StatementHandler)MetaObject metaObject = SystemMetaObject.forObject(target);//拿到 paramterObject 对象Object value = metaObject.getValue("parameterHandler.parameterObject");System.out.println("SQL语句使用的参数是:" + value);//设置 paramterObject 对象的值(也就是SQL语句参数的值)metaObject.setValue("parameterHandler.parameterObject", 7);System.out.println("MySecondPlugin -> intercept():拦截到目标方法 " + invocation.getMethod());//拦截完成后,再执行目标方法Object proceed = invocation.proceed();return proceed;}

分页插件的使用

1.导入相关 jar 包:jsqlparser-0.9.5.jar , pagehelper-5.0.0-rc.jar

2.在全局配置文件中配置分页插件

<!--分页插件:PageHelper-->
<plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>

3.分页插件的使用

/*
分页插件测试*/
@Test
public void testPlug() {SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();SqlSession sqlSession = null;try {sqlSession = sqlSessionFactory.openSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);/*使用 PageHelper 中的分页方法来进行分页参数分别代表pageNum:第几页pageSize:每页几条数据*/Page<Object> objects = PageHelper.startPage(7, 3);List<Employee> employees = mapper.getEmployees();for (Employee employee : employees) {System.out.println(employee);}//还可以获取以下信息System.out.println("当前页码:" + objects.getPageNum());System.out.println("总记录数:" + objects.getTotal());System.out.println("每页的记录数:" + objects.getPageSize());System.out.println("总页码:" + objects.getPages());/*使用 PageInfo 对象navigatePages 参数:可以规定每个页面显示多少个页码*/PageInfo<Employee> pageInfo = new PageInfo<>(employees, 5);System.out.println("是否是第一页:" + pageInfo.isIsFirstPage());//使用 pageInfo 的 getNavigatepageNums() 可以直接获取每个页面连续显示的页码,而不用进行页码的计算,//例如每页显示 5 个页码,每个页码显示 3 条记录数,则第 7 页的连续页码为: 4 5 6 7 8System.out.println("连续显示的页码");int[] navigatepageNums = pageInfo.getNavigatepageNums();for (int navigatepageNum : navigatepageNums) {System.out.println(navigatepageNum + " ");}} finally {if (sqlSession != null) {sqlSession.close();}}
}

临时补充:

/*
临时记起的一种 for 循环语法,与 MyBatis 无关*/
@Test
public void test4() {k:for (int i = 0; i < 10; i++) {for (int j = 0; j < 10; j++) {if (j == 3) {System.out.println(j);break k;}}}
}

MyBatis 扩展知识

BatchExecutor & Spring 中配置批量 SqlSession

/*使用批量操作
使用指定的 Executor 来执行批量操作
*/
@Test
public void testBatch() {SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();//获取指定 Executor 类型的 SqlSessionSqlSession sqlSession = null;try {sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);long start = System.currentTimeMillis();for (int i = 0; i < 9999; i++) {String s = UUID.randomUUID().toString().substring(0, 5) + i;mapper.addEmployee(new Employee(null, "" + s, s + "@163.com", "1",new Department(1, "开发部")));}long end = System.currentTimeMillis();/*经过实测使用批量执行器 BatchExecutor 执行用时约:四千毫秒而使用简单执行器 SimpleExecutor 执行用时约:一万两千毫秒区别在于:使用 SimpleExecutor 时,每次都会进行预编译 SQL 语句,数据库执行一次而使用 BatchExecutor 时,数据库每次编译 SQL 语句后会等待下次的 SQL 语句编译,最后一次性执行完成。*/System.out.println("插入数据耗时:" + (end - start));sqlSession.commit();} finally {if (sqlSession != null) {sqlSession.close();}}
}

在 Spring 配置文件中配置 SqlSession 的指定 Executor

<!--配置一个可以进行批量执行的 sqlSession
使用该配置文件后,获取到的 SqlSession 就被配置文件中的 executorType 属性指定了 Executor
实际上,SqlSessionTemplate 实现的就是 SqlSession 接口
-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"><constructor-arg name="sqlSessionFactory" ref="sqlSessionFactoryBean"></constructor-arg><constructor-arg name="executorType" value="BATCH"></constructor-arg>
</bean>

自定义类型处理器

使用指定的类型处理器处理枚举类型的数据

1.在配置文件中配置

<!--配置指定的类型处理器
javaType:指定处理什么类型时,使用该类型处理器,若不设置,则全部类型都使用该类型处理器
-->
<typeHandlers><typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="bean.EmployeeStastus"/>
</typeHandlers>

2.代码测试

/*
MyBatis 处理枚举
MyBatis 在保存枚举类型时,默认保存的是枚举的名字
使用指定的 EnumOrdinalTypeHandler 枚举类型处理器,则保存的是枚举的索引值*/
@Test
public void testEnum() {SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();SqlSession sqlSession = null;try {sqlSession = sqlSessionFactory.openSession();EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);Employee testEnum = new Employee(null, "testEnum", "enum@183.com", "1");mapper.addEmployee(testEnum);sqlSession.commit();Employee employeeById = mapper.getEmployeeById(testEnum.getId());System.out.println(employeeById);EmployeeStastus login = EmployeeStastus.LOGIN;System.out.println("枚举的索引:" + login.ordinal());System.out.println("枚举的名字:" + login.name());} finally {if (sqlSession != null) {sqlSession.close();}}
}

自定义枚举类型的处理器

1.创建自定义类型处理器

package test;import bean.EmployeeStastus;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;/*
自定义的枚举类型处理器
1.实现 TypeHandler 接口,或者继承 TypeHandler 下的 BaseHandler
实现 TypeHandler 时的泛型表示 该类型处理器用于处理什么样的类型*/
public class MyTypeHandler implements TypeHandler<EmployeeStastus> {/*定义当前数据如何保存到数据库中*/@Overridepublic void setParameter(PreparedStatement preparedStatement, int i, EmployeeStastus employeeStastus, JdbcType jdbcType) throws SQLException {/*通过原生 JDBC 的方式来自定义设置参数参数1 代表参数传入 SQL 语句时的索引位置参数2 代表自定义需要传入的参数的数据*/preparedStatement.setString(i, employeeStastus.getCode().toString());}/*按照返回的结果集中的参数名来获取指定数据自定义返回的结果集中指定数据的返回结果*/@Overridepublic EmployeeStastus getResult(ResultSet resultSet, String s) throws SQLException {int code = resultSet.getInt(s);//根据从数据库中拿到的枚举的状态码,返回一个枚举对象return EmployeeStastus.getEmpStatusByCode(code);}/*按照返回的结果集中的索引位置来获取指定数据*/@Overridepublic EmployeeStastus getResult(ResultSet resultSet, int i) throws SQLException {int code = resultSet.getInt(i);//根据从数据库中拿到的枚举的状态码,返回一个枚举对象return EmployeeStastus.getEmpStatusByCode(code);}/*使用存储过程通过索引值来获取*/@Overridepublic EmployeeStastus getResult(CallableStatement callableStatement, int i) throws SQLException {int code = callableStatement.getInt(i);//根据从数据库中拿到的枚举的状态码,返回一个枚举对象return EmployeeStastus.getEmpStatusByCode(code);}
}

2.在全局配置文件中配置

<typeHandlers><!--配置自定义的类型处理器来处理枚举类型的数据--><typeHandler handler="test.MyTypeHandler" javaType="bean.EmployeeStastus"/>
</typeHandlers>

3.保存,查询数据时,指定 TypeHandler

保存

在编写保存 SQL 语句时,对某一字段指定类型处理器,例如:#{employeeStatus,typeHandler=xxxx }

typeHandler 值为自定义类型处理器的全类名

查询

在查询数据时,通过 resultMap 指定某一字段的类型处理器,typeHandler 值为自定义类型处理器全类名

<resultMap id="emp" type="bean.Employee"><result column="empStatus" property="employeeStatus" typeHandler="test.MyTypeHandler"></result>
</resultMap>

注:如果在参数位置使用自定义的类型处理器,应该保证查询和保存数据使用的类型处理器是一样的。

至此,MyBatis 基本操作的学习,告一段落

MyBatis入门基本操作相关推荐

  1. 【Mybatis从入门到实战教程】第一章 Mybatis 入门

    一.Mybatis 入门 1.1 什么是MyBatis MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了goo ...

  2. Mybatis入门从新手村到打低级野怪

    课程目标 搭建MyBatis开发环境 完成基于注解,单表增删改查操作 完成基于注解,多表增删改查操作 完成基于通用Mapper,单表增删改查操作 完成分页操作 完成基于XML,单表增删改查操作 完成基 ...

  3. Mybatis入门之动态sql

    Mybatis入门之动态sql 通过mybatis提供的各种标签方法实现动态拼接sql. 1.if.where.sql.include标签(条件.sql片段) <sql id="sel ...

  4. MyBatis1:MyBatis入门

    MyBatis是什么 MyBatis是什么,MyBatis的jar包中有它的官方文档,文档是这么描述MyBatis的: MyBatis is a first class persistence fra ...

  5. MyBatis(1):MyBatis入门

    MyBatis是什么 MyBatis是什么,MyBatis的jar包中有它的官方文档,文档是这么描述MyBatis的: MyBatis is a first class persistence fra ...

  6. MyBatis-学习笔记02【02.Mybatis入门案例】

    Java后端 学习路线 笔记汇总表[黑马程序员] MyBatis-学习笔记01[01.Mybatis课程介绍及环境搭建][day01] MyBatis-学习笔记02[02.Mybatis入门案例] M ...

  7. mybatis入门(七)之日志

    转载自    mybatis入门(七)之日志 Mybatis 的内置日志工厂提供日志功能,内置日志工厂将日志交给以下其中一种工具作代理: SLF4J Apache Commons Logging Lo ...

  8. mybatis入门(一)之基础安装

    转载自  mybatis入门 安装 要使用 MyBatis, 只需将 mybatis-x.x.x.jar 文件置于 classpath 中即可. 如果使用 Maven 来构建项目,则需将下面的 dep ...

  9. Mybatis入门程序增删改查操作

    学习目标 了解Mybatis的基本知识 熟悉Mybatis的工作原理 掌握Mybatis入门程序的编写 文章目录 1.初始Mybatis 2.Mybatis入门程序 3.Mybatis操作总结 1.初 ...

最新文章

  1. RT-thread内核之进程间通信
  2. python3.5怎么安装pip-在python3.5中使用pip
  3. 使用xib封装一个view的步骤
  4. 不需要网络的调频收音机_测试工程师真的不需要懂网络知识么?
  5. GDI文字旋转90度
  6. 【cocos2d-x从c++到js】20:脚本语言风格的JS代码
  7. CODEVS.5037.线段树练习4加强版(分块 区间k的倍数)
  8. 程序员数学基础【四、取模应用-判断奇偶数、判断素数、求两个数的最大公约数、水仙花数】(Python版本)
  9. HDU - 6635 Nonsense Time (暴力LIS)
  10. 中国移动MM7 API用户手册(七)
  11. Flutter StreamBuilder 实现的一个倒计时功能
  12. python画折线图-手把手教你Python yLab的绘制折线图的画法
  13. MSP430 MSP430单片机软件开发集成环境CCS
  14. 三维空间点到直线距离计算
  15. geronimo.jar_触手可及的创新,Apache Geronimo 3.0的新增功能
  16. 没有CUE的情况下APE刻录CD
  17. 基于ArcGIS的城市住房选址分析(以郑州市为例)
  18. win10 获取超级管理员权限,administrator不是最高权限解决办法
  19. distill_basic_teacher
  20. Linux常用命令——newusers命令

热门文章

  1. 诺基亚n1系统更新显示无网络_国行版诺基亚8 Sirocco推送安卓9.0系统更新
  2. 建国钞带领纪念钞强势崛起
  3. php 点卡销售,某在线数字点卡销售程序源代码(专业版)
  4. REST Client工具使用——对接接口调试
  5. 面试为什么需要了解JVM
  6. python打印数组部分元素_Python打印输出数组中全部元素
  7. 挑战我的年华,赢我光辉岁月
  8. 查询操作系统的版本,CPU序列号以及操作系统所在的SN
  9. 坚果pro2MIUI10修改按键功能
  10. M1卡破解(自从学校升级系统之后,还准备在研究下)【转】