MyBatis中文文档参考地址 http://www.mybatis.org/mybatis-3/zh/index.html
首先到 https://github.com/mybatis/mybatis-3/releases 去下载jar包 除了mybatis的jar包之外还需要连接MySQL的jar包
所有的框架要连接数据库都需要驱动包,除非框架内部已经集成
mybatis解压之后就是这样一个目录,我们只需要拷贝mybatis.jar 和 mysql-connector-java-6.0.6.jar 就可以了
lib目录中的包可以暂时不需要
环境配置
Mybatis中有两种配置文件:
全局配置文件 : 放在classPath的根路径下, 取名myBatis-config.xml / myBatis.xml等
全局配置文件内容包括:1、全局配置信息 2、属性配置信息3、插件配置信息 4、环境配置信息(事务+连接池)5、关联映射文件
myBatis映射文件 : 放在Mappper接口的位置,取名一般使用 xxxMapper.xml、UserMapper.xml以示是哪个对象的配置信息
myBatis映射文件内容 : 1、编写增删改查的sql语句 2、对象属性和数据库字段映射关系配置 3、缓存配置
新建resources目录,在该目录下创建全局配置文件 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="">
<environment id="my">
<!-- 配置连接池事务等 -->
</environment>
<environment id="test">
<!-- 配置连接池事务等 -->
</environment>
<environment id="produ">
<!-- 配置连接池事务等 -->
</environment>
</environments>
</configuration>
mybatis可以配置多个环境,通过修改 environments 的default属性来改变环境
比如我现在配置三个环境,一个是我自己开发用、一个是测试环境、一个是生产环境,我只需要把default的属性设置为my
那么现在的环境就会使用我自己配置的my 的 连接池和事务等配置
transactionManager的Type设置是告诉MyBatis使用事务
dataSource的Type设置是告诉MyBatis用它自带的连接池
基本查询
编写UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
namespace:表示该Mapper的唯一标识
-->
<mapper namespace="com.xiaoww.myBatis.UserMapper">
<!--
select 标签标识这是一条查询语句
id:标识该sql的唯一标识
parameterType:传入的参数类型。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
resultType:返回的数据类型(把结果集封装成什么对象)
#{id}:传入的参数名
-->
<select id="getById" parameterType="Long" resultType="com.xiaoww.myBatis.User">
SELECT id,name,salary FROM user WHERE id = #{id}
</select>
</mapper>
测试代码
public class App {
public static void main(String[] args) throws IOException {
//从classPath读取配置文件
InputStream is = App.class.getClassLoader().getResourceAsStream("mybatis-
config.xml");
//用来创建SqlSessionFactory
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
//用来创建SqlSession 需要传入一个流对象,也就是mybatis-config.xml
SqlSessionFactory sessionFactory = factoryBuilder.build(is);
//类似于connection对象
SqlSession session = sessionFactory.openSession();
//查询一个对象使用 UserMapper.xml 的 namespace再加上 select语句的id
User selectOne = session.selectOne("com.xiaoww.myBatis.UserMapper.getById", 1L);
System.out.println(selectOne);
//资源关闭
session.close();
}
}
这样就完成了最基本的查询操作,可能会出现的问题:mysql驱动包版本错误
而如果我想查询所有 只需要在UserMapper中添加
<select id="listAll" resultType="com.xiaoww.myBatis.User">
SELECT * FROM user
</select>
测试代码改为
List<User> list= session.selectList("com.xiaoww.myBatis.UserMapper.listAll");
for (User user : list) {
System.out.println(user);
}
日志配置
myBatis的日志操作也非常方便,只需要在全局配置中声明使用哪个日志实现就可以了
在mybatis-config.xml中 configuration 元素下面添加一个
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
使用log4j,然后拷贝log4j-1.2.17.jar到项目中去,在resources目录下添加log4j.properties 配置日志输出格式等
可以参考我的另外一篇文章https://blog.csdn.net/qq_39205291/article/details/82146656
然后将日志级别改为 DEBUG或者更低,再次运行就会看到DEBUG级别日志(包括执行的sql、查询到的结果集等)
#{id}所表达的意思是什么
在Mapper.xml中使用#{id}来传入参数,那么#{id}到底代表的是什么?
这种写法叫OGNL,OGNL是Object-Graph Navigation Language的缩写,它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。
假如有一个员工对象
employee{
id:1,
name:阿黄,
dept:{
id:10,
name:应用开发部
}
}
当前上下文中获取该员工名称就可以使用#{name} ,而获取员工部门则可以使用#{dept.name}。
如果当前上下文对象是javaBean,则#{属性名}取值
如果当前上下文对象是map,则#{key}取值
如果当前上下文对象是简单类型,则直接取值 和 #{xxxx}里面的xxxx无关 所以where id = #{xx} 也可以查询出来
获取SqlSession工具类
SqlSessionFactoryBuilder对象只是为了创建SqlSessionFactory,在创建好之后便不会再使用;
SqlSessionFactory在整个应用中应该只存在一个(类似于DataSource),所以我们要将它定义为static
SqlSession对象是线程不安全的,所以无法使用单例模式,每个请求或方法都需要新创建一个SqlSession
public class MyBatisUtil {
private static SqlSessionFactory sessionFactory = null;
static{
InputStream in = MyBatisUtil.class.getClassLoader().getResourceAsStream("mybatis-
config.xml");
sessionFactory = new SqlSessionFactoryBuilder().build(in);
}
public static SqlSession getSqlSession(){
return sessionFactory.openSession();
}
}
修改
@Test
public void update(){
User u = new User();
u.setId(1L);
u.setName("安其拉");
u.setSalary(new BigDecimal("1111"));
SqlSession session = MyBatisUtil.getSqlSession();
session.update("com.xiaoww.myBatis.UserMapper.update", u);
session.commit();
session.close();
}
UserMapper.xml
<update id="update" parameterType="com.xiaoww.myBatis.User">
UPDATE user SET name = #{name},salary=#{salary} WHERE id = #{id}
</update>
删除
@Test
public void delete(){
SqlSession session = MyBatisUtil.getSqlSession();
session.update("com.xiaoww.myBatis.UserMapper.delete", 1L);
session.commit();
session.close();
}
<delete id="delete" >
DELETE FROM user WHERE id = #{id}
</delete>
修改、保存 和删除是需要提交事务的,由于 sessionFactory.openSession() 默认是不自动提交事务,需要手动提交 session.commit();
保存
@Test
public void save(){
User u = new User();
u.setName("安其拉");
u.setSalary(new BigDecimal("1111"));
System.out.println(u);//此时的id为null
SqlSession session = MyBatisUtil.getSqlSession();
session.update("com.xiaoww.myBatis.UserMapper.save", u);
session.commit();
session.close();
System.out.println(u);//此时的id为数据库返回的id
}
<!--
useGeneratedKeys表示是否需要返回数据库生成的主键
keyProperty 表示该对象的哪个属性是主键
-->
<insert id="save" parameterType="com.xiaoww.myBatis.User" useGeneratedKeys="true"
keyProperty="id">
INSERT INTO user (name,salary) values (#{name},#{salary})
</insert>
自定义别名
在UserMapper.xml中的resultType都是全限定名称 com.xiaoww.myBatis.User ,这样每次写起来都比较麻烦
可以在全局配置mybatis-config.xml中来配置一个别名 type为全限定名,alias为别名
如果使用的是package元素,则是为本包中所有类都取别名,别名名称为该类名首字母小写
<typeAliases>
<typeAlias type="com.xiaoww.myBatis.User" alias="User"/>
</typeAliases>
或者
<typeAliases>
<package name="com.xiaoww.myBatis"/>
</typeAliases>
这样就可以在UserMapper.xml中使用User作为resultType或paramterType了
注意:configuration元素的子元素是有先后顺序的,如果把typeAliases元素写在settings元素前面就会报错
别名不区分大小写
一些java中的数据类型在MyBatis中的别名
别名 | 映射的类型 |
---|---|
_byte | byte |
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
_boolean | boolean |
string | String |
byte | Byte |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
object | Object |
map | Map |
hashmap | HashMap |
list | List |
arraylist | ArrayList |
collection | Collection |
iterator | Iterator |
例如: 查询一个表的总数据量 此时必须设置 resultType 根据上面的别名可以写作integer或者int或者_integer或者_int
@Test
public void count(){
SqlSession session = MyBatisUtil.getSqlSession();
int a = session.selectOne("com.xiaoww.myBatis.UserMapper.count");
System.out.println(a);
session.close();
}
<select id="count" resultType="integer">
SELECT count(id) FROM user
</select>
资源引用
如何从properties文件中读取这些参数配置
在全局配置mybatis-config中添加 <properties resource="db.properties"/> 引入properties文件
将配置提取到properties文件中即可
resultMap映射
当对象的属性与数据库的字段名不一致应该怎么办?
根据我们以前学过的知识,可以在查询sql中 使用字段别名让其与属性名保持一致
MyBatis提供了对象的映射关系,可以在xml中便捷处理这种映射
注意:resultMap中还有个id的标签用来描述对象的id映射关系可以提高映射效率,最终的映射关系应该是
使用Mapper接口
在前面测试方法时我们的代码是这样的
可以看到String类型很容易写错 在编译时期也无法被检测出来
而参数为类型为Object类型, 在编译时期也同样无法被检测出来
这时候就需要有Mapper接口来限制 首先分包
将UserMapper.xml的namespace改为UserMapper接口的全限定名
在接口中定义方法
在Mapper.xml中定义同名xml查询方法
将测试方法改为
这样就跟以前调用Dao接口是一样的方式了,而这两个问题也都解决了
再定义个getById方法
多参数处理
很多的Dao方法都会涉及到多个参数的处理, session的CRUD方法也都是只允许传入一个参数,那么遇到多个参数应该怎么处理
首先: 可以将多个参数封装为一个对象,以登录为例
其次: 可以将多个参数封装为一个Map,注意Key值要和#{value}中的value一致
还可以使用MyBatis的方式,该方式的原理就是将参数封装到Map中,与方式二一样,只是写法不同而已
#和$的区别
在Mapper.xml中写sql 取值的时候 使用#{} 其实也可以使用${}
select * from user where username = ${username} and password = ${password}
那么这两个有什么区别呢
#和$符号都可以把值取出来,但是#{}会把传入的数据作为参数 并且会为参数加上单引号
select * from user where username = ? and password = ?
而${}会把取出的值作为sql的一部分 并且不会为参数加上单引号
select * from user where username = 123 and password = 456
如果你传入的参数中包含sql关键字,比如order by 、desc、group 等 则必须使用${} 其他情况一律使用#{}
简单来说 只有当使用#{}无效时,才会考虑使用${}
注解开发myBatis (不推荐)
注解开发,就等于把之前xml里面写的sql、设置的属性搬到java代码里面来了
既然没了Xml,唯一可以写代码的地方就是Mapper接口里面了
首先,在全局配置文件中将UserMapper.xml注释掉,然后添加一个class对应我们的Mapper接口
然后直接将代码写到方法上,贴上注解insert ,注意:要将主键返回需要设置两个属性useGeneratedKeys、keyProperty
贴上注解delete
贴上注解update
贴上注解select、select我们需要配置对象映射关系 使用@Results注解进行配置
那么如果我需要查询所有 是不是也得重新写一遍映射配置呢
我们知道在xml中的resultMap可以设置一个id,@Results也可以设置一个id
所以最正确的写法应该是在@Results上设置一个id
下面用到的地方可以直接使用resultMap引用
条件语句
先将表结构改为
if 条件语句 与java中的if使用方式一样
跟据user表中的type查询结果集,如果为null,则当前参数无效
但是,如果现在 type==0 而 lastLoginTime 不为null 则sql 变成了
select id,username,pwd,type,lastLoginTime from user and lastLoginTime >= ? 没有了where关键字
这个问题在开发中也是经常遇到
myBatis提供了解决方案
使用<where>标签 将条件语句包含在内部
这样在生成sql 的时候,如果检测到没有where 关键字 则在条件之前添加where 关键字
如果检测到条件语句以and 或者 or 开头 则会用where 替换第一个 and 或者 or
所以我们的sql只需要写 and 就可以了
choose条件语句
相当于 if else 语句
写法
set标签
我们之前的update语句一般是这么写的
如果我在修改lastLoginTime的时候password为null,或者我想修改 password 的时候lastLoginTime 为null 。那么实际上是不应该把null该写到数据库中的,所以我们会加个判断
会变成这个样子
但是这样如果 password 为null ,sql则会变成
update user set password = #{password}, where id = #{id} 注意#{password}, 会多出来一个逗号
所以set标签就出现了,使用set标签之后,它可以在所有条件前面添加一个set,并且会把最后一个逗号给去掉
测试传入lastLoginTime值为null 从日志可以看出来是对的
set 和 where这两个标签是怎么实现的呢
其实他们就是trim标签的实现
我可以把上面的update语句改写成这样
那么trim 到底是怎么一回事
<trim prefix="set" prefixOverrides="" suffix="" suffixOverrides=",">
trim标签的四个属性
prefix:类似于SpringMvc的WEB-INF/pages前缀 、在sql前面加上个 where、set
prefixOverrides:如果sql前面是以AND或者OR打头,则把他替换掉 , 而替换他的值就是 prefix 属性值
例子: <trim prefix="ABC" >
123456
</trim>会被拼接为ABC123456
<trim prefix="ABC" prefixOverrides="123">
123456
</trim>会被拼接为ABC456
同理 suffix="" suffixOverrides="," 为后缀、后缀替换属性
迭代 foreach标签
当需要一次性删除多个元素的时候 delete from user where id in(?,?,?); 这时 (?,?,?) 形式的参数 就需要用foreach来拼接
还有mysql批量插入的时候 insert into user(id,name) values (?,?),(?,?),(?,?); 这样可以一次性插入三条数据
<sql>和<include> 标签的使用
一对多的对象关系处理
先来张部门的表 department 字段 id,name
再来张员工的表 employee 字段 id,name,dept_id 一个部门可以包含多个员工,所以首先先保存一个部门和两个员工
public interface DepartmentMapper {
public void save(Department dept);
}
<mapper namespace="com.xiaoww.myBatis.mapper.DepartmentMapper">
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into department (name) values (#{name})
</insert>
</mapper>
public interface EmployeeMapper {
public void save(Employee e);
}
<mapper namespace="com.xiaoww.myBatis.mapper.EmployeeMapper">
<insert id="save">
insert into employee (name,dept_id) values(#{name},#{dept.id})
</insert>
</mapper>
测试的时候我们需要先保存部门再保存员工,因为员工需要先设置部门才能保证数据的正确性
@Test
public void save(){
SqlSession sqlSession = MyBatisUtil.getSqlSession();
DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
Department dept = new Department();
dept.setName("java部门");
mapper.save(dept);
EmployeeMapper mapper2 = sqlSession.getMapper(EmployeeMapper.class);
Employee e = new Employee();
e.setName("张三");
e.setDept(dept);
Employee e2 = new Employee();
e2.setName("李四");
e2.setDept(dept);
mapper2.save(e);
mapper2.save(e2);
sqlSession.commit();
}
保存完之后我们来查询一个部门员工
员工里面包含了部门信息
那么如果使用 select id,name,dept_id from employee where id = #{id} 查出来之后部门信息一定是null
此时我可以使用resultMap将dept_id 赋值 给 dept.id
查询出结果为
可以看到已经有了部门,但是部门名字还是没有查出来
最简单的方式无非就是直接用id把部门查出来再设置给Employee对象
myBatis的处理方式 使用association关联复杂查询
properties 就是Employee对象中的属性名称
javaType 关联的对象类型
select 使用哪条查询语句 、使用下图这条查询语句
column 传递的参数
连起来就是 使用select * from department where id = #{id} 传入参数为 Employee的dept_id 查询返回一个Department对象 封装到Employee 的dept属性中去,这样就解决了对象的关联问题
思考: 这样每次查询等于要发两条sql?
如果使用select * from employee 会是什么样的情况
测试过后发现,如果部门都是不一样的,就会每次都去查询 ,如果有一样的,则不用再次发送sql。
也就是说:如果员工很多,且每个部门都不一样,就会发送 员工数(每个员工查一次部门)+1(查询所有员工)条sql去查询
这样是很影响效率的
解决 : 使用关联查询 我们查询多张表一般是使用sql关联 来查询
使用association来关联映射
一对多的查询
还是使用刚才的员工和部门的关系,使用部门作为主表的时候,部门里面可以存在多个员工,就变成了一对多关系
在部门对象中加入员工集合 初始化list的原因是 使用elps添加的时候不会报空指针,也可以不初始化
员工类
跟上面一对一查询差不多,设置一下查询sql、传入参数、属性名
另外一种方式,使用关联查询
注意:一对多使用的是collection (类型属性ofType) 、 一对一是 association(类型属性javaType)
延迟加载
上面说到一对一和一对多的关联时,第一种查询方式其实就是查询了两次
第一次查询员工,第二次使用员工的部门id去查询部门
第一次查询部门,第二次使用部门的id去查询部门下面的所有员工
也就是说我只要查询员工,就会把所在部门查出来,只要查询部门就会把所有员工一起查出来(非常累赘)
我使用部门的时候不一定要看该部门下的员工,这样会每次浪费数据库资源去查询大量的无用sql
mybatis提供了延迟加载的方式来处理这一问题 :
当需要使用该关联属性的时候才加载该关联属性
跟他所匹配的还有一个属性叫
lazyLoadingEnabled开启时,会启用延迟加载。但是aggressiveLazyLoading也需要设置为false;
aggressiveLazyLoading的意思是,虽然是延迟加载了,但是你无论调用对象的哪个属性,我都会马上加载关联属性。
此时测试,调用对象的getName只发送了一条查询sql
但是打印整个对象还是发送了两条sql(此时toString方法并没有getEmployees)
这是因为还有一个配置lazyLoadTriggerMethods 他配置了使用哪个方法时触发延迟加载方法
我们将它配置为 clone 就可以了 , 只有在调用clone方法时才会触发
缓存
mybatis的缓存分为一级和二级缓存
一级缓存 : 也称之为本地缓存,作用域为sqlSession 默认开启且不能关闭 可以使用sqlSession.clearCache();清除缓存
随便找一个测试方法,使用相同的参数再查一次之前查过的对象,会发现没有再次发送sql
我这里重写了toString方法,如果没有重写的话你会发现 对象的hashCode都是同一个,这说明缓存直接保存的就是Object
而不是通过Json等格式转换的对象,其实就是sqlSession中有个hashMap,保存一个Key 和 对象 当map中存在则直接取出
由于每次查询我们都会开启一个新的sqlSession,所以一级缓存对性能的提升非常有限
二级缓存:mapper级别的缓存,同一个namespace共用一个缓存对象,不同的sqlSession也共享数据
如何使用:首先在全局设置中开启二级缓存(默认开启)
1、<setting name="cacheEnabled" value="true"/>
2、在需要缓存的mapper文件中 加上 <cache />
3、需要缓存的实体类实现Serializable接口 (缓存需要配置大小,如果超出这个大小则会往磁盘中写,所以需要实现序列化)
查询两次如果分别看到命中率为0.0 和0.5 说明缓存已经开启成功
在mapper的<cache/>元素中也可以配置相关属性
eviction:缓存回收策略 可用的值如下图 默认LRU(推荐)
flushInterval:刷新策略 以毫秒为单位 多少毫秒不使用将清空该缓存
size:缓存大小,默认1024(推荐)
readOnly:是否只读 只读对象不能被修改(效率高) 默认false
列表查询必须参数一致才会使用缓存,所以一般不推荐使用。浪费内存 如果有多条件列表查询不需要使用缓存就直接在select标签上加上useCache="false"
默认情况下,执行insert、update、delete都会刷新缓存,如果想阻止缓存刷新,可以在标签上加flushCache="false"