MyBatis 3学习笔记
MyBatis 3
一、MyBatis简介
优秀的持久层框架,支持支持自定义 SQL、存储过程以及高级映射,专注于SQL的编写。
? 为什么不使用工具类进行数据库操作:
? 功能简单,sql语句编写在Java代码里面,是一种硬编码高耦合的方式,不推荐。
Hibernate:
? 另一种持久层框架,全自动全映射ORM(Object Relation Mapping)框架,旨在消除SQL。
? 主要特点:一个JavaBean对象与数据库中一条记录建立映射,中间过程对于程序员来讲是黑箱操作,无须关注SQL语句的编写。
? 缺点:框架自动生成SQL语句,程序员无法进行SQL优化;JavaBean中每一个字段都在数据库中有相应映射,加重了数据库的负担。
? 需求:SQL语句交给我们开发人员编写,希望SQL不失去灵活性。于是,诞生了MyBatis:
MyBatis官方文档
二、MyBatis接口式编程 Hello World
1. 搭建环境
? lombok介绍:
? Lombok 是一种 Java? 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注解实现这一目的。
? 通过@Data注解,让被修饰的类自动生成getter和setter方法、构造器、equals方法、或者hash方法等。
? 如果觉得@Data这个注解有点简单粗暴的话,Lombok提供一些更精细的注解,比如@Getter,@Setter,(这两个是field注解),@ToString,@AllArgsConstructor(这两个是类注解)。
? log4j介绍:
? Apache的开源项目log4j是一个功能强大的日志组件,提供方便的日志记录。可以查看在debug时候的sql语句。
? 一般在resources目录下创建log4j.properties文件,进行配置。
1.1 Maven
导入依赖:
junit
junit
4.11
test
org.projectlombok
lombok
RELEASE
compile
org.mybatis
mybatis
3.4.6
compile
org.junit.jupiter
junit-jupiter
RELEASE
compile
log4j
log4j
1.2.17
mysql
mysql-connector-java
8.0.26
1.2 Bean
@Data
public class Employee {
private int id;
private String name;
private String age;
}
1.3 配置文件
放在resources目录下:
<?xml version="1.0" encoding="UTF-8" ?>
数据库配置文件properties,同样也存放在resources目录下
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatisdb
username=root
password=zouwenhao
日志的配置文件properties,resources目录下:
log4j.rootLogger=DEBUG,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=[%t] [%c]-[%p] %m%n
mapper配置文件,在这里面编写sql:
1.4 通过SqlSession建立与数据库的会话
老版本的编程方法:
/* MyBatis操作数据库的整体思路:
* 1、根据xml配置文件(全局配置文件)创建一个SqlSessionFactory对象,有数据源一些运行环境信息 (config)
* 2、sql映射配置文件;配置了每一个sql,以及sql的封装规则等。 (mapper)
* 3、将sql映射文件注册在全局配置文件中 (config配置文件中的mapper标签进行注册)
* 4、写代码:
* 1)、根据全局配置文件得到SqlSessionFactory;
* 2)、使用sqlSession工厂,获取到sqlSession对象使用他来执行增删改查(一个与数据库操作的代理对象)
* 一个sqlSession就是代表和数据库的一次会话,用完关闭,使用close方法
* 3)、使用sql的唯一标志来告诉MyBatis执行哪个sql。sql都是保存在sql映射文件中的。
*/
// 返回SqlSessionFactory对象
public SqlSessionFactory getSqlSessionFactory() throws IOException {
// MyBatis全局配置文件路径
String resource = "mybatis_config.xml";
// 获取MyBatis全局配置文件的输入流
InputStream inputStream = Resources.getResourceAsStream(resource);
// 获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
return sqlSessionFactory;
}
// 查询测试,老的方法
@Test
public void testHello() throws IOException {
// 1.获取SqlSessionFactory实例
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2.获取SqlSession实例,能直接执行已经映射的sql语句
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 为了避免mapper映射出现重名的情况,一般使用 命名空间+查询id
/*
* 第一个参数:sql的唯一标识,statement Unique identifier matching the statement to use.
* 第二个参数:执行sql要用的参数,parameter A parameter object to pass to the statement.
* */
Employee employee = sqlSession.selectOne("com.atguigu.mybatis.mapper.selectEmp", 1001);
System.out.println(employee);
} finally {
// sqlSession表示与数据库的一次对话,用完之后必须手动关闭
sqlSession.close();
}
}
1.5 接口式编程(推荐)
? 使用接口与配置文件进行动态绑定,接口中的方法与select标签进行绑定
-
第一步:创建接口,getEmpById方法返回Employee对象
public interface EmployeeMapper { public Employee getEmpById(Integer id); } -
第二步:修改原EmployeeMapper.xml文件
-
第三步:编写测试类
思路是:拿到接口的实现类对象,然后调用接口的方法,进行数据库的查询操作
@Test public void testHelloByInterface() throws IOException { // 1.获取SqlSessionFactory实例 SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); // 2.获取SqlSession实例,与数据库建立会话 SqlSession sqlSession = sqlSessionFactory.openSession(); try { // 3. 获取接口的实现类对象 // 会为接口自动的创建一个代理对象,代理对象去执行增删改查方法 EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);//获取代理对象 Employee emp = mapper.getEmpById(1001); System.out.println(emp); } finally { sqlSession.close(); } }? 使用接口式编程的好处:
? 拥有更强的类型检查,以及返回值的限制,能够将DAO层与实现进行分离出来。
1.6 总结
-
SqlSession和connection一样都不是线程安全的,因此不能共享,每次使用都应该去获取新的对象。
-
SqlSession代表和数据库的一次会话,用完就必须关闭。
-
mapper接口没有实现类,但是MyBatis会为这个接口生成一个代理对象:(将接口与xml进行绑定)
? EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
-
两个重要的配置文件:
? mybatis的全局配置文件:包含数据库连接池的信息、事务管理器信息等
? sql映射文件:保存了每一个sql语句的映射信息,包括这个sql语句的标识、返回的信息类型(select标签)
三、MyBatis-全局配置文件
? 全局配置文件中的dtd约束,对应了语法规则,在编写时,会有相关提示信息
1. properties_引入外部配置文件
......
如果属性在不只一个地方进行了配置,那么 MyBatis 将按照下面的顺序来加载:
? – 在 properties 元素体内指定的属性首先被读取。
? – 然后根据 properties 元素中的 resource 属性读取类路径下属性文件或根据 url 属性指定的路径读取属性文件,并覆盖已读取的同名属性。
? – 最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。
2. settings_运行时行为设置
? 以是否开启驼峰命名规则映射为例:
3. typeAliases_别名处理器(不推荐使用)
? 将全类名与别名建立映射关系,这样在一些使用全类名的条件下,可以使用别名。
4. typeHandlers_类型处理器(了解)
? MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。部分如下所示:
5. plugins_插件(本章了解)
? MyBatis 允许你在映射SQL语句执行过程中的某一过程进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
-
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
-
ParameterHandler (getParameterObject, setParameters)
-
ResultSetHandler (handleResultSets, handleOutputParameters)
-
StatementHandler (prepare, parameterize, batch, update, query)
通常使用插件来修改MyBatis的一些核心行为,插件通过动态代理机制。
6. environments_运行环境
? MyBatis可以配置多种环境,比如开发、测试和生 产环境需要有不同的配置。
? 每种环境使用一个environment标签进行配置并指 定唯一标识符。
? 可以通过environments标签中的default属性指定一个环境的标识符来快速的切换环境。
? 配置当前运行环境,比如数据库、事务管理器的信息,可以创建多个运行环境,如下所示:有MySQL和Oracle两种数据库环境。
? type: JDBC | MANAGED | 自定义
– JDBC:使用了 JDBC 的提交和回滚设置,**依赖于从数据源得到的连接来管理事务范围**。
? JdbcTransactionFactory
? – MANAGED:不提交或回滚一个连接,让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下 文)。
? ManagedTransactionFactory
? – 自定义:实现TransactionFactory接口,type=全类名/ 别名
? type: UNPOOLED | POOLED | JNDI | 自定义
? – UNPOOLED:不使用连接池, UnpooledDataSourceFactory
? – POOLED:使用连接池, PooledDataSourceFactory
? – JNDI: 在EJB或应用服务器这类容器中查找指定的数据源
? – 自定义:实现DataSourceFactory接口,定义数据源的获取方式。
? 实际开发中我们使用Spring管理数据源,并进行事务控制的配置来覆盖上述配置。
7. databaseProvider_多数据库支持
? 下图databaseId指定了在什么数据库环境下,才会执行这条sql语句。至于当前是在什么环境下,则在environments标签下设置default属性值,用于设置当前数据库运行环境。
? 例如:当前在MySQL环境下,因此一旦需要执行这条语句,那么就会执行这条语句,如果是Oracle环境下,就不会执行这条语句,而是会执行另一条databaseId为oracle的语句。
8. mappers_sql映射注册
? 将sql映射注册到全局配置中来
? 注解方式的举例:将sql语句写在注解上,注意在全局配置文件中使用class属性进行接口注册
public interface EmployeeMapperAnnotation {
@Select({"select * from tbl_employee where id=#{id}"})
Employee getEmpById(Integer paramInteger);
}
? 测试方法的编写:
@Test
public void testHelloByAnno() throws IOException {
// 1.获取SqlSessionFactory实例
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2.获取SqlSession实例,与数据库建立会话
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 3. 获取接口的实现类对象
EmployeeMapperAnnotation mapper = sqlSession.getMapper(EmployeeMapperAnnotation.class);
Employee emp = mapper.getEmpById(1001);
System.out.println(emp);
} finally {
sqlSession.close();
}
}
四、MyBatis-映射文件(重要)
1. 增删改查
? 前提,MyBatis配置环境已经像前文一样配置完毕!
1.1 接口方法
public interface EmployeeMapper {
Employee getEmpById(Integer paramInteger);
// mybatis允许增删改直接定义以下类型返回值
// Integer、Long、Boolean、void
Long addEmp(Employee employee);
Long updateEmp(Employee employee);
Long deleteEmp(Integer id);
}
1.2 Mapper配置文件
? 在mapper配置文件中,建立与接口方法的映射,同时记得在MyBatis全局配置文件中,进行注册:
? EmployeeMapper.xml:
insert into info(name, age) values (#{name}, #{age})
update info
set name=#{name}, age=#{age}
where id=#{id}
delete from info where id=#{id}
1.3 测试
@Test
public void EmpTest() throws IOException {
// 1. 获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2. 获取sqlSession对象,但并不会自动提交
SqlSession sqlSession = sqlSessionFactory.openSession();
// 自动提交:sqlSessionFactory.openSession(true);
try {
// 3. 获取接口的代理对象
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
// 4. 更新数据
// Long result = mapper.updateEmp(new Employee(1001, "Paul", 23));
// 4. 添加数据,id是自增的,所以可以传一个null
Long result = mapper.addEmp(new Employee(null, "Dick", 21));
// 4. 删除操作,指定id值
// Long result = mapper.deleteEmp(1005);
System.out.println(result > 0 ? "操作成功!" : "操作失败!");
// 5. 手动提交事务
sqlSession.commit();
} finally {
// 5. 关闭会话
sqlSession.close();
}
}
注意:
-
openSession()获取到的SqlSession实例是不会自动提交的,需要在sql语句执行完,手动commit提交,如果想自动提交,则使用openSession(true)方法获取实例。
-
关于MySQL数据库使用自增id,当我们需要使用自增id时,类对象只需要将自增id传入一个null值即可,但一定要保证数据库中该键是开启了自动增长。如果报以下错误:
java.sql.SQLException: Field 'id' doesn't have a default value
? 说明没有开启自动增长,开启后,需要重启以下数据库!
2. Insert
- MySQL数据库获取自增主键的值:
insert into tbl_employee(last_name,email,gender)
values(#{lastName},#{email},#{gender})
-
Oracle数据库不支持自增:
使用序列来模拟自增,就需要手动执行sql语句,才查询下一个应该自增的值。
有两种解决思路:
-
先执行查询sql,获取到自增主键值后,再进行insert插入语句。
? 用BEFORE修饰selectKey标签,并通过 .nextva 获取自增后的主键值,再进行插入操作
-
先执行insert插入语句,使用 .nextval 表示新记录自增主键值,然后执行AFTER修饰的selectKey标签,使用 .currval 获取当前新纪录自增后的主键值。
-
select EMPLOYEES_SEQ.nextval from dual
insert into employees(EMPLOYEE_ID,LAST_NAME,EMAIL)
values(#{id},#{lastName},#{email})
3. 参数处理
3.1 单个参数/多个参数/命名参数
? 在mapper配置文件中,在将接口方法与CRUD标签进行映射的时候,需要使用接口方法中的参数作为sql语句查询的条件。MyBatis对于单个参数和多个参数有着不一样的处理:
? 单个参数:
? MyBatis不会做特殊处理,
? #{参数名/任意名}:取出参数值。
? 多个参数:
? Mybatis会做特殊处理。
? 多个参数会被封装成 一个map,规则如下:
? key:param1...paramN,或者参数的索引也可以
? value:传入的参数值
? #{}就是从map中获取指定的key的值;
异常:
org.apache.ibatis.binding.BindingException:
Parameter 'id' not found.
Available parameters are [1, 0, param1, param2]
错误操作:
方法:public Employee getEmpByIdAndLastName(Integer id,String lastName);
取值:#{id}, #{lastName} (正确取值操作:#{param1}, #{param2})
? 命名参数:
? 明确指定封装参数时map的key;在接口方法中使用@Param注解;@Param("id")
? 多个参数会被封装成 一个map,
? key:使用@Param注解指定的值
? value:参数值
? #{指定的key}取出对应的参数值,如 #{id}, #{lastName}
Long deleteEmp(@Param("id") Integer id, @Param("name") String name)
3.2 POJO/Map/TO
? POJO:
? 如果多个参数正好是我们业务逻辑的数据模型,我们就可以直接传入pojo;
? #{属性名}:取出传入的pojo的属性值
? 就像上文中测试,因为数据参数传入的是一个实例对象,就可以直接使用属性值来获取值。
? Map:
? 如果多个参数不是业务模型中的数据,没有对应的pojo,不经常使用,为了方便,我们也可以传入map
? #{key}:取出map中对应的值
? 示例:
? TO:
? 如果多个参数不是业务模型中的数据,但是经常要使用,推荐来编写一个TO(Transfer Object)数据传输对象:
? Page{
? int index;
? int size;
? }
3.3 MyBatis的参数封装(源码理解)
? MyBatis如何处理参数的:
? 底层调用的sqlSession对象的一些增删改查的API
? 总结:参数多的时候,MyBatis会封装map,但为了不混乱,我们可以使用@Param来指定封
装时使用的key;
? #{key}就可以取出map中的值;
? (@Param("id")Integer id,@Param("lastName")String lastName);
ParamNameResolver解析参数封装map的:
? 该类有参构造器:
/** names参数的确定流程:
1.获取每一个被@Param注解修饰的参数的值,如:id,lastName的值,并赋值给name;
2.每次解析一个参数给map中保存信息:(key:参数索引,value:name的值)
name的值要分两种情况:
第一种:标注了param注解,那么name的值就是注解指定的值。
第二种:没有标注(下面代码)
1.全局配置:useActualParamName(jdk1.8):name=参数名
2.name=map.size();相当于当前元素的索引,所以最终保存形式如下:
{0=id, 1=lastName,2=2}
*/
public ParamNameResolver(Configuration config, Method method) {
................
if (name == null) {
if (config.isUseActualParamName()) { // 全局配置过没有?
name = this.getActualParamName(method, paramIndex);
}
if (name == null) {
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
}
this.names = Collections.unmodifiableSortedMap(map);
}
? getNamedParams()方法:对args参数,用map类型进行封装
? 在遍历names集合的时候,进行了两次封装:
? 第一次封装:使得用户可以使用指定的key去获取参数,往往是用@Param进行修饰的,满足用户自定义param的需求。
? 第二次封装:是使用MyBatis提供的一种默认的方式,通过#{param1...paramN}这种方式进行参数值的获取。
// args传的参数:args[1,"Tom",'hello']
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
//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 param = new ParamMap
3.4 参数值的获取——#与$取值的区别
? #{}:可以获取map中的值或者pojo对象属性的值;
? ${}:可以获取map中的值或者pojo对象属性的值;
主要区别:$是先拼接后编译,#是先编译后拼接,所以#没有SQL注入的风险。
select * from tbl_employee where id=${id} and last_name=#{lastName}
Preparing: select * from tbl_employee where id=2 and last_name=?
? 对比:
? #{}:是以预编译的形式,将参数设置到sql语句中;PreparedStatement;防止sql注入
? ${}:取出的值直接拼装在sql语句中;会有安全问题;
? 大多情况下,我们去取参数的值都应该去使用 #{} ;
? 原生jdbc不支持占位符的地方,我们就可以使用 ${} 进行取值
? 比如分表、排序。。。。;按照年份分表拆分
select * from ${year}_salary where xxx;
select * from tbl_employee order by ${f_name} ${order}
# 取值时指定参数相关规则
{}:更丰富的用法:
? 规定参数的一些规则:
? javaType、 jdbcType、 mode(存储过程)、 numericScale、
? resultMap、 typeHandler、 jdbcTypeName、 expression(未来准备支持的功能);
? jdbcType通常需要在某种特定的条件下被设置:
? 在我们数据为null的时候,有些数据库可能不能识别mybatis对null的默认处理。比如Oracle(报错);
? JdbcType OTHER:无效的类型;因为mybatis对所有的null都映射的是原生Jdbc的OTHER类型,oracle不能
正确处理;
? 由于全局配置中:jdbcTypeForNull=OTHER;Oracle并不支持,解决的两种办法:
? 1、#{email, jdbcType=NULL}; 在sql语句中进行设置
? 2、jdbcTypeForNull=NULL:直接在全局配置文件中,通过setting标签配置
4. Select
4.1 返回记录封装在List
接口方法
public interface EmployeeMapper {
Employee getEmpById(Integer paramInteger);
Long addEmp(Employee employee);
Long updateEmp(Employee employee);
// List
List getEmpByAge(String paramName);
}
Mapper配置文件中的sql
? 使用模糊查询
测试方法
? 在测试方法中,传入模糊查询的条件:查姓名是J开头的记录
@Test
public void ListTest() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession(true);
try {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
List employeeList = mapper.getEmpByAge("J%");
System.out.println(employeeList);
} finally {
sqlSession.close();
}
}
结果
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByAge]-[DEBUG] ==> Preparing: select * from info where name like ?
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByAge]-[DEBUG] ==> Parameters: J%(String)
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByAge]-[DEBUG] <== Total: 2
[Employee(id=1005, name=Jack, age=23), Employee(id=1006, name=James, age=26)]
4.2 返回记录封装在Map
接口方法
// 封装到map对象中,单个对象
Map getEmpByMap(Integer paramId);
// 多条记录封装到map对象中,Map
@MapKey("id") // 这个注解设置了mybatis在封装为map的过程中,将查询结果的id属性作为map的key
Map getEmpByNameReturnMap(String name);
Mapper配置xml文件
测试方法
@Test
public void MapTest() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession(true);
try {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
// Map employeeMap = mapper.getEmpByMap(1003); // 单条记录
Map employeeMap = mapper.getEmpByNameReturnMap("J%"); // 返回名字为J开头的记录
System.out.println(employeeMap); // 打印map,调用的是实现子类的toString()方法
} finally {
sqlSession.close();
}
}
结果
// 单条记录封装在Map中
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByMap]-[DEBUG] ==> Preparing: select * from info where id = ?
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByMap]-[DEBUG] ==> Parameters: 1003(Integer)
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByMap]-[DEBUG] <== Total: 1
{name=Mary, id=1003, age=21}
// 多条记录封装在Map中,在接口方法中,设置了map中id值作为key键
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByNameReturnMap]-[DEBUG] ==> Preparing: select * from info where name like ?
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByNameReturnMap]-[DEBUG] ==> Parameters: J%(String)
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByNameReturnMap]-[DEBUG] <== Total: 2
{1005=Employee(id=1005, name=Jack, age=23), 1006=Employee(id=1006, name=James, age=26)}
4.3 自定义结果集映射规则(重要)
? resultType:是跟自动封装有关的,在mapper配置文件中指定封装为什么类型,MyBatis就将查询结果封装为对应类型的数据格式。
? 但存在这样的情况,就是查询出来的结果集中,列名跟JavaBean的属性字段名不一样,这个时候是封装不成功的(打印出来,属性值为null),对此上文提到过有两种解决方案:
? 第一种:使用@Param注解,起别名
? 第二种:开启驼峰命名法,如果符合要求的话
? 第三种:使用自定义结果集映射
自定义resultMap,实现高级结果集的映射。
4.4 关联查询
级联属性封装结果
Mapper配置文件:
association定义关联对象封装规则
association可以指定联合的JavaBean对象:在resultType标签中
相当于使用嵌套的方式:在mapper配置文件中完整的代入如下所示:
association分步查询
? 注意分步查询与级联封装中,association的用法区别。主要是根据sql语句来的。
JavaBean对象:
@Data
public class Department {
private Integer id;
private String departmentName;
private List emps;
public Department(Integer id, String departmentName, List emps) {
this.id = id;
this.departmentName = departmentName;
this.emps = emps;
}
}
@Data
public class Employee {
private Integer id;
private String name;
private Integer age;
private Department dept;
public Employee(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
}
Department:
接口方法:
public interface DepartmentMapper {
Department getDeptById(Integer id);
}
mapper配置文件:将查询department信息的sql语句与接口方法进行绑定,准备后面的分步查询
<?xml version="1.0" encoding="UTF-8" ?>
在EmployeeMapper的配置文件中:
结果:
? 查1号员工信息,在执行第一条查询sql语句的时候,也去执行了查部门信息的sql语句,然后获取到该员工的部门信息,最后打印输出。
延迟加载
? 当前的问题:
? 因为在Employee这个Bean对象中,有一个Department类属性Dept,而我们每次查询Employee对象的时候,都会将其Dept属性一起查询出来,如果我们并不立即使用这个属性的信息的话,这样会导致性能的下降。所以想在使用部门信息的时候,再去查询。用延迟加载解决:
? 延迟加载,就是按需加载,当需要使用某个关联属性的时候,如输出打印操作,才会进行执行。
? 在分段查询的基础上,全局配置中设置两个参数:
? 对aggressiveLazyLoading:开启时候,所有属性都会加载,关闭时,才会按需加载。
注意如果报异常:
Error creating lazy proxy. Cause: java.lang.NullPointerException
? 需要把mybatis的包换成Mybatis-3.5.1.jar及其以上的版本
collection定义关联集合封装规则
association:定义关联对象封装规则。
collection:定义关联集合封装规则。
上文都是只关联一个对象,现在是这个类的某个属性,关联多个对象的情况:
? 场景二:
? 查询部门的时候将部门对应的所有员工信息也查询出来
使用传统SQL语句:查询开发部的员工信息
mapper配置文件:
测试:
collection分步查询
? 场景二:
? 查询部门的时候将部门对应的所有员工信息也查询出来,用分步查询的方法
MySQL数据库
? info表:员工信息表
? department表:部门信息表
大致思路
- 先执行查询特定部门的sql语句
- 然后嵌套查询在这个部门里面的员工信息
Bean代码
@Data
public class Department {
private Integer id;
private String departmentName;
private List emps;
public Department() {
}
public Department(Integer id, String departmentName, List emps) {
this.id = id;
this.departmentName = departmentName;
this.emps = emps;
}
}
@Data
public class Employee {
private Integer id;
private String name;
private Integer age;
private Department dept;
public Employee() {
}
public Employee(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
Dao与其映射配置文件
DepartmentMapper接口方法:
public interface DepartmentMapper {
Department getDeptById(Integer id);
Department getDeptByIdStep(Integer id);
}
建立DepartmentMapper接口与其mapper配置文件的映射:
<?xml version="1.0" encoding="UTF-8" ?>
嵌套:EmployeeMapperPlus接口方法:用于查询某部门的员工信息
public interface EmployeeMapperPlus {
List getEmpsByDeptId(Integer paramInteger);
}
? 建立EmployeeMapperPlus接口与其mapper配置文件的映射:
<?xml version="1.0" encoding="UTF-8" ?>
测试
@Test
public void CollectionTest() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession(true);
try {
DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
Department deptByIdStep = mapper.getDeptByIdStep(1); // 查询部门ID为1的部门信息
System.out.println(deptByIdStep); // 打印部门信息
} finally {
sqlSession.close();
}
}
结果
[main] [com.atguigu.mybatis.dao.DepartmentMapper.getDeptByIdStep]-[DEBUG] ==> Preparing: select id, departmentName from department where id=?
[main] [com.atguigu.mybatis.dao.DepartmentMapper.getDeptByIdStep]-[DEBUG] ==> Parameters: 1(Integer)
[main] [com.atguigu.mybatis.dao.DepartmentMapper.getDeptByIdStep]-[DEBUG] <== Total: 1
[main] [com.atguigu.mybatis.dao.EmployeeMapperPlus.getEmpsByDeptId]-[DEBUG] ==> Preparing: select * from info where depId = ?
[main] [com.atguigu.mybatis.dao.EmployeeMapperPlus.getEmpsByDeptId]-[DEBUG] ==> Parameters: 1(Integer)
[main] [com.atguigu.mybatis.dao.EmployeeMapperPlus.getEmpsByDeptId]-[DEBUG] <== Total: 3
Department(id=1, departmentName=业务部, emps=[Employee{id=1001, name='Smith', age=23}, Employee{id=1003, name='Mary', age=21}, Employee{id=1005, name='Jack', age=23}])
discriminator鉴别器(了解)
判断某列的值,然后进行重新封装,改变原来的封装行为。
五、MyBatis-动态SQL
? 动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
? 是基于OGNL表达式的
1. if
2. where
引出问题:
? 在上一节中,如果在测试方法中,没有传入id 的值但传入了lastName的值,那么就会出现类似于:
... where and last_name like #{lastName}
? 这样的sql语句,肯定是错误的。两个解决方法:
? 第一种:第一条语句where后面加上 1 = 1,而下面的每条if标签,最前面都写上and,如下
? 第二种:MyBatis推荐!使用where标签
? 用where标签会将所有的查询条件包括在内,MyBatis就会对where标签中拼接的sql语句进行处理,将sql语句多出来的and或or去掉。(仅限于sql语句的开头,即where标签中只会去掉第一个多出来的and或者or)
3. trim_自定义字符串截取
? 在上一节的情况下,如果用户在if标签的内的sql语句的末尾加上and,即使在where标签内,mybatis并不会处理sql语句尾巴的and,此时会出现sql语句的错误:
? 若id、lastName和gender均为空,而email有值,则sql:
... where last_name like #{lastName} and
? sql出错!
trim标签的介绍:
4. choose_分支选择
? 相当于Java中的 switch-case
? 如果在测试方法中,传入了多个参数,此时就类似于switch-case一样,哪个参数在前面先判断,只会进入一个分支,会选择该参数条件成立下的sql语句,并拼接执行,结束。
5. set与if结合的动态更新
原来的更新,是写死了,必须提供sql语句set后的字段的值,下面举个例子:
update info
set name=#{name}, age=#{age}
where id=#{id}
现在要实现动态更新,即测试方法中,只需要提供需要更新的字段属性名称,实现效果:
第一种方法:使用set标签
update info
name=#{name},
age=#{age}
where id=#{id}
set标签会自动去掉sql语句末尾多的逗号,如果有逗号的话。
第二种方法:使用trim标签
? 添加前缀set,后缀覆盖,去掉所有的逗号
update info
last_name=#{lastName},
email=#{email},
gender=#{gender}
where id=#{id}
6. foreach_遍历集合
想实现的动态sql效果:
select * from tbl_employee where id in(?, ..., ?)
用户传入参数:两个或多个
select * from tbl_employee where id in(1, 2)
select * from tbl_employee where id in(2, 3, 4)
7. MySQL下foreach批量插入的两种方式
实现一条sql语句插入多个value值,根据用户输入的value值数目,进行动态插入。
第一种方式:有多少条想插入的记录,就执行多少次insert插入语句,或者执行一次,一次插入多条记录
注意:
? 第一种方法:执行多条插入语句的方法中,foreeach间隔符是分号 ; ,因为多条插入语句sql用分号进行隔开。
? 第二种方法:而执行一条插入语句但values有多条记录的方法中,foreeach的间隔符是逗号, ,因为是分隔多条value记录。
第一种方式:需要这种方式需要数据库连接属性allowMultiQueries=true
insert into tbl_employee(last_name,email,gender,d_id)
values(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
第二种方式:推荐这种方法,效率更高!
insert into tbl_employee()
values
(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
8. Oracle下foreach批量插入的两种方式(了解)
insert into employees(employee_id,last_name,email)
values(employees_seq.nextval,#{emp.lastName},#{emp.email});
insert into employees(employee_id,last_name,email)
select employees_seq.nextval,lastName,email from(
select #{emp.lastName} lastName,#{emp.email} email from dual
)
<--或者-->
insert into employees(employee_id,last_name,email)
select #{emp.lastName} lastName,#{emp.email} email from dual
9. _parameter & _databaseId & bind
10. sql_抽取可重用的sql片段
定义sql抽取:
employee_id,last_name,email
last_name,email,gender,d_id
使用示例:
insert into tbl_employee(
)
values
(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
六、MyBatis-缓存机制
? MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。缓存可以极大的提升查询效率。
MyBatis系统中默认定义了两级缓存。
一级缓存和二级缓存。
? 1、默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本地缓存)开启。
? 2、二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
3、为了提高扩展性。MyBatis定义了缓存接口Cache。我们可以通过**实现Cache接口来自定**
义二级缓存。
1. 体验封装的存在
? 都是来查1号员工,最后判断输出 true,说明emp02是直接使用之前查询的结果,使用了缓存。
2. 两级缓存
两级缓存:
? 一级缓存:(本地缓存)sqlSession级别的缓存,一级缓存是一直开启的;类似于一个Map,查询之前去看看这个要查的数据,有没有在map中。
? 与数据库同一次会话期间查询到的数据,将会放在本地缓存中
? 以后如果需要获取相同数据的话,直接就会去缓存中拿去,就没必要再去访问数据库
? 如果开启新的sqlSession,那么两个sqlSession会有属于自己的缓存
? 一级缓存失效的四种情况:(没有使用到当前一级缓存的情况,还需要再向数据库发出查询)
? 1. sqlSession不同,数据存放在各自的缓存中
? 2. sqlSession相同,查询条件不同(当前缓存中没有这个数据)
? 3. sqlSession相同,但两次查询之间执行了增删改操作(因为这次增删改操作可能会对当前数据有影响,所以不能选择还继续用缓存中的数据)
? 4. sqlSession相同,但手动清除了一级缓存。( openSession.clearCache() )
? 二级缓存:(全局缓存)基于namespace级别的缓存,一个namespace对应一个二级缓存。
? 工作机制:
? 1. 一个会话,查询一条数据,这个数据就会被放在当前会话的一级缓存中
? 2. 如果会话关闭,那么一级缓存中的数据会被保存到二级缓存中,新的会话查询信息,就可以先去参照二级缓存
? 3. sqlSession == EmployeeMapper ==》Employee数据
? DepartmentMapper==》Department数据
? 不同namespace查出来的数据,会放在自己对应的缓存中,如上面的两个mapper配置文件,由于定义了各自的命名空间,所以也就是两个二级缓存(底层实现其实是map)
3. 二级缓存使用&细节
? 默认是开启二级缓存的,但为了防止被版本更新改为默认关闭,所以还是最好在MyBatis全局配置中手动开启。
? 需要注意的,只有会话关闭之后,一级缓存的数据才会写入到二级缓存中去。
? 使用步骤:
? 第一步:在MyBatis全局配置文件中,显示开启全局二级缓存配置
? 第二步:在mapper配置xml文件中,配置使用二级缓存
? cache标签属性:
? 和缓存有关的设置/属性:
? 1)默认cacheEnabled=true;
? 如果false的话:关闭缓存(二级缓存关闭,一级缓存一直可用)
? 2)每个select标签都默认useCache="true";
? 如果false的话:不使用缓存(二级缓存关闭,一级缓存依然可以使用)
? 3)每个增删改标签默认flushCache="true";查询标签是默认为false,查询完不清空缓存
? 增删改执行完成后就会清除缓存
? 如果flushCache="false"的话, 一级缓存、二级缓存都会被清空,虽然去查了,也就是命中了key,但没有对应value值
? 4)openSession.clearCache();只会清除当前sqlSession的一级缓存
? 5)localCacheScope:全局设置setting中的属性,本地缓存作用域(相当于一级缓存)
? SESSION:当前会话的所有数据都会被保存到一级缓存中
? STATEMENT:禁用一级缓存
第三步:我们的POJO需要实现序列化接口
测试一级缓存的测试方法:
@Test
public void testFirstLevelCache() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = (EmployeeMapper)openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(Integer.valueOf(1));
System.out.println(emp01);
Employee emp02 = mapper.getEmpById(Integer.valueOf(1));
System.out.println(emp02);
System.out.println((emp01 == emp02));
} finally {
openSession.close();
}
}
? 如果一级缓存开启的话,最终结果会打印 true 。
测试二级缓存的测试方法:
@Test
public void testSecondLevelCache() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
SqlSession openSession2 = sqlSessionFactory.openSession();
EmployeeMapper mapper = (EmployeeMapper)openSession.getMapper(EmployeeMapper.class);
EmployeeMapper mapper2 = (EmployeeMapper)openSession2.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(Integer.valueOf(1));
System.out.println(emp01);
openSession.close();
Employee emp02 = mapper2.getEmpById(Integer.valueOf(1));
System.out.println(emp02);
openSession2.close();
}
? 如果二级缓存开启的话,那么在debug打印的信息中,可以发现只执行了一条sql语句并显示有命中信息,也就是第二条语句去查询的时候,命中了二级缓存中的数据。
? 需要注意的是,由于sqlSession会话只有关闭的时候,才会把一级缓存的数据写入到二级缓存中,因为如果将上面测试方法中两个session会话给放到最后查询执行完才关闭的话,那么debug显示出来的信息是执行了两条sql语句。
4. 缓存原理示意图
5. 第三方缓存框架EhCache整合
? MyBatis缓存的底层就是map,由于在MyBatis中cache是接口类型,往往需要使用第三方强大的缓存框架进行整合。
? EhCache:
? EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认
CacheProvider。Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
Spring 提供了对缓存功能的抽象:即允许绑定不同的缓存解决方案(如Ehcache),但本身
不直接提供缓存功能的实现。它支持注解方式使用缓存,非常方便。
? pom.xml引入依赖:
net.sf.ehcache
ehcache
2.10.6
七、MyBatis-Spring整合(SSM整合)
后面会更新SSM整合的学习文档,翻翻我博客看看,如果没有就说明还没有更新
八、MyBatis-逆向工程(代码生成器)
MyBatis Generator:
? 简称MBG,是一个专门为MyBatis框架使用者定制的代码生成器,可以快速的根据表生成对
应的映射文件、接口、以及bean类。支持基本的增删改查,以及QBC风格的条件查询。但是表连
接、 存储过程等这些复杂sql的定义需要我们手工编写。
maven导入依赖:
org.mybatis.generator
mybatis-generator-core
1.3.7
? 简单来说,就是能够根据数据库的表,然后生成对应的JavaBean、mapper接口方法以及sql与接口映射配置文件。
1. MBG配置文件
注意:
1. 根据项目需求生成不同级别的CRUD版本
2. 注意生成文件的输出地址
<?xml version="1.0" encoding="UTF-8"?>
2. 执行逆向生成
@Test
public void testMBG() throws XMLParserException, IOException, InvalidConfigurationException, SQLException, InterruptedException {
List warnings = new ArrayList();
boolean overwrite = true;
File configFile = new File("src/main/resources/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);
}
3. 测试
targetRuntime="MyBatis3Simple"
效果展示:
? 主要看接口类,生成了哪些支持的CRUD操作:
? sql映射配置xml文件:
<?xml version="1.0" encoding="UTF-8"?>
delete from department
where id = #{id,jdbcType=INTEGER}
insert into department (id, departmentName)
values (#{id,jdbcType=INTEGER}, #{departmentname,jdbcType=VARCHAR})
update department
set departmentName = #{departmentname,jdbcType=VARCHAR}
where id = #{id,jdbcType=INTEGER}
测试查询所有记录:
@Test
public void test() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
List list = mapper.selectAll();
System.out.println(list);
} finally {
sqlSession.close();
}
}
结果:
[main] [com.atguigu.mybatis.dao.EmployeeMapper.selectAll]-[DEBUG] ==> Preparing: select id, name, age, depId from info
[main] [com.atguigu.mybatis.dao.EmployeeMapper.selectAll]-[DEBUG] ==> Parameters:
[main] [com.atguigu.mybatis.dao.EmployeeMapper.selectAll]-[DEBUG] <== Total: 6
[Employee{id=1001, name='Smith', age=23, depid=1}, Employee{id=1002, name='Tom', age=23, depid=2}, Employee{id=1003, name='Mary', age=21, depid=1}, Employee{id=1004, name='Dick', age=21, depid=2}, Employee{id=1005, name='Jack', age=23, depid=1}, Employee{id=1006, name='James', age=26, depid=2}]
targetRuntime="MyBatis3"
支持复杂的CRUD操作
与simple的最明显差别,就是多了个两个Example的bean对象。
支持的操作:
测试:
@Test
public void testMyBatis3() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = (EmployeeMapper)openSession.getMapper(EmployeeMapper.class);
// xxxExample这个类,是用来封装查询条件的
EmployeeExample example = new EmployeeExample();
// criteria是用来拼接查询条件的
EmployeeExample.Criteria criteria = example.createCriteria();
// 拼接Name的模糊查询,%a%
criteria.andNameLike("%a%"); // 查询员工名字中有字母a的
// 拼接部门id为1
criteria.andDepidEqualTo("1"); // 查询部门id为1的
// 如果查询条件由or连接两个条件组成,则还需要创建criteria用来拼接另一个条件
EmployeeExample.Criteria criteria2 = example.createCriteria();
criteria2.andAgeBetween(22, 23); // age在22到23岁
// 用 or 连接
example.or(criteria2);
List list = mapper.selectByExample(example);
System.out.println(list);
} finally {
openSession.close();
}
}
最后的sql语句如下:
通过上面的例子,就可以举一反三,使用框架进行复杂的CRUD操作。
九、MyBatis-运行原理
1. 框架分层架构
2. SQLSessionFactory的初始化
// 获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
测试过程:
1)进入SqlSessionFactoryBuilder()
2)调用SqlSessionFactoryBuilder().build(),创建解析器parser,进入XMLConfigBuilder(),对配置文件进行解析
3)XMLConfigBuilder()中,调用parseConfiguration()方法
解析全局配置文件,其中this.mapperElement将会解析mapper映射配置文件
创建mapperParse解析器,然后调用parse()方法解析映射xml配置文件:
configuration对象保存了所有配置文件的详细信息:
3. openSession获取SqlSession对象
// 2.获取SqlSession实例,与数据库建立会话
SqlSession sqlSession = sqlSessionFactory.openSession();
4. getMapper获取到接口的代理对象
// 3. 获取接口的实现类对象
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
5. 查询实现
Employee emp = mapper.getEmpById(1001);
底层其实是JDBC操作,MyBatis对其进行封装
查询流程总结:重要
? 首先,创建一个代理对象进行增删改查操作,但实际上这个代理对象是用DefaultSqlSession进行增删改查,而这个DefaultSqlSession是通过Executor进行增删改查,而Executor会创建一个StatementHandler对象,用于处理sql语句、预编译和设置参数等相关工作。StatementHandler对象在进行处理工作的时候,创建了ParameterHandler和ResultSetHandler,前者是用来设置预编译参数用的,执行操作,此时,后者是用来处理执行返回的结果集。而设置参数和处理结果集执行,实际上是二者是通过TypeHandler进行设置参数并执行。而TypeHandler实际执行,是通过调用底层原生JDBC操作。
十、MyBatis-插件
? 拦截目标对象,用插件为目标对象创建一个动态代理,然后使用AOP的思想,通过动态代理对目标对象的目标方法进行拦截,同时对目标方法可以进行增强的操作,然后执行。从而实现控制sql语句执行的过程。
十一、MyBatis-扩展
1. PageHelper插件进行分页
MyBatis 分页插件 PageHelper
首先需要在全局配置文件中添加依赖:
测试方法:
@Test
public void testSelectAll() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Page
查询结果展示:
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll_COUNT]-[DEBUG] ==> Preparing: SELECT count(0) FROM info
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll_COUNT]-[DEBUG] ==> Parameters:
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll_COUNT]-[DEBUG] <== Total: 1
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll]-[DEBUG] ==> Preparing: select * from info LIMIT ?, ?
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll]-[DEBUG] ==> Parameters: 4(Long), 2(Integer)
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll]-[DEBUG] <== Total: 2
Employee{id=1005, name='Jack', age=23}
Employee{id=1006, name='James', age=26}
当前页码:3
总记录数:6
每页记录数:2
总页码:3
当前页码:3
总记录数:6
每页记录数:2
总页码:3
是否是第一页:false
连续显示的页码:
2
3
2. 批量操作
openSession(ExecutorType.BATCH); 开启一个批处理的session
@Test
public void testBatch() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 在创建sqlSession的时候,设置这是一个批处理的session
SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
long start = System.currentTimeMillis();
try {
EmployeeMapper mapper = (EmployeeMapper)openSession.getMapper(EmployeeMapper.class);
for (int i = 0; i < 10000; i++)
mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1"));
openSession.commit();
long end = System.currentTimeMillis();
System.out.println("+ (end - start));
} finally {
openSession.close();
}
}
? 非批处理:执行流程是,每进行一次插入语句,就 发送sql语句—》预编译设置参数—》执行 ,每一次的插入都是一次完成的数据库访问操作,消耗大量的预编译时间。
? 批处理:发一条sql语句给数据库,数据库预编译好之后,批处理不断传参数,数据库只需要等待这条sql语句需要执行多少遍的参数,然后再一次性执行。