What is Mybatis?
mybatis是支持普通SQl查询,存储过程和高级映射的优秀持久层框架,半自动ORM框架。
- 消除了几乎所有JDBC代码和参数的手工设置以及搜索结果集的检索;
- 使用简单的xml或注解用于配置和原始映射,将接口和Java的对象映射成数据库中的记录。
本质
mybatis的本质就是解决Java和mysql之间的协调作用,Java通过面向对象的方式去操作mysql。
框架使用步骤
添加依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
添加配置文件 mybatis-config.xml
这个配置文件中包含了MyBatis系统的核心设置;
- environments:事务管理和连接池的配置
- TransactionManager:事务作用域和控制方式的事务管理器
- DataSource:获取数据库连接实例的数据源
- mappers:包含一组mapper映射器
这些mapper的XML文件包含了SQL代码和映射定义信息;
用来连接数据库
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<!--配置了mybatis连接数据库的信息-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="westos"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--指定映射文件的位置-->
<mapper resource="mapper/StudentMapper.xml"/>
</mappers>
</configuration>
提供一个 映射文件
用来管理sql语句,描述了sql语句跟数据库之间的映射关系;
mapper/StudentMapper.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(命名空间) 用来防止sql的命名冲突的-->
<mapper namespace="mapper.StudentMapper">
<!--#{} 用来获取student参数对象中的sname属性-->
<!--parameterType用来指定获取参数的类型,
因为student属性参数类型不一致,所以直接使用student类型-->
<!--useGeneratedKeys告诉mybatis要使用数据库产生的主键值,默认是不使用;
keyProperty逐渐对应的属性名-->
<insert id="abc" parameterType="domain.Student"
useGeneratedKeys="true" keyProperty="sid">
insert into student(sid,sname,birthday,sex)
values (null ,#{sname},#{birthday},#{sex})
</insert>
<delete id="delete" parameterType="int">
delete from student where sid= #{sid}
</delete>
<!--这里的列名要和student类的属性名一致-->
<select id="select" resultType="domain.Student">
select sid,sname,birthday,sex from student
</select>
<select id="selectById" resultType="domain.Student" parameterType="int">
select * from student where sid=#{sid}
</select>
<select id="update" parameterType="domain.Student">
update student
<set>
<if test="sname!=null">
sname=#{sname},
</if>
<if test="birthday!=null">
birthday=#{birthday},
</if>
<if test="sex!=null">
sex=#{sex}
</if>
</set>
where sid=#{sid}
</select>
<!-- m , n
java.util.Map -> map
java.util.List -> list
java.lang.String -> string
map.put("m", 0);
map.put("n", 5);
-->
<select id="findByPage" parameterType="map" resultType="domain.Student">
select * from student limit #{m},#{n}
</select>
</mapper>
调用mybatis api使用映射文件真正执行增删改查
每一个MyBatis的应用都是以一个SqlSessionFactory的实例为中心的,SqlSessionFactory的实例可以通过SqlSessionFactoryBuilder获得,而SqlSessionFactoryBuilder则可以从XML配置文件或一个预先定制的Configuration的实力构建出SqlSessionFactory的实例;
- SqlSessionFactoryBuilder:一旦创建了SqlSessionFactory就不再需要它了;最佳作用域是应用程序作用域;
- SqlSessionFactory 用来创建SqlSession的工厂类,应该避免多次创建,最好使用单例模式或静态单例模式创建;最佳作用域是应用程序作用域;
- SqlSession:每个线程都应该有它自己的SqlSession实例,实例不是线程安全的,最佳作用域是请求或方法作用域。
public class TestStudentMapper {
static SqlSessionFactory factory;
static {
try {
//读取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
//创建sqlSession工厂类
factory = new SqlSessionFactoryBuilder().build(in);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
//读取配置文件
public void testInsert() throws IOException {
//创建sqlSession ,这里的session更类似于jdbc中的Connection
SqlSession sqlSession = factory.openSession();
//执行新增
Student stu = new Student();
stu.setSname("小林");
stu.setBirthday(new Date());
stu.setSex("女");
//参数一:namespace+sql_id 参数二:传递sql语句需要的java对象
sqlSession.insert("mapper.StudentMapper.abc",stu);
//执行增删改 没有启用自动提交事务
sqlSession.commit();
//关闭资源
sqlSession.close();
}
@Test
public void testDelete(){
SqlSession sqlSession = factory.openSession();
sqlSession.delete("mapper.StudentMapper.delete",1009);
sqlSession.commit();
sqlSession.close();
}
@Test
public void testFindAll(){
SqlSession sqlSession = factory.openSession();
List<Student> stu = sqlSession.selectList("mapper.StudentMapper.select");
sqlSession.close();
for (Student student : stu) {
System.out.println(student);
}
}
@Test
public void testFindById(){
SqlSession sqlSession = factory.openSession();
Student stu = sqlSession.selectOne("mapper.StudentMapper.selectById",1002);
sqlSession.close();
System.out.println(stu);
}
@Test
public void testUpdate(){
SqlSession sqlSession = factory.openSession();
Student student = new Student();
student.setSid(1008);
student.setSex("女");
sqlSession.update("mapper.StudentMapper.update",student);
sqlSession.commit();
sqlSession.close();
}
@Test
public void findByPage(){
SqlSession sqlSession = factory.openSession();
HashMap<String, Integer> map = new HashMap<>();
map.put("m",0);
map.put("n",6);
List<Student> list = sqlSession.selectList("mapper.StudentMapper.findByPage",map);
sqlSession.close();
for (Student student : list) {
System.out.println(student);
}
}
}
通过日志工具监控Mybatis生成的sql语句
logback
添加依赖
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
添加一个logback.xml到resources文件夹;
<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
<!-- 输出控制,格式控制-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{32} - %m%n </pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志文件名称 -->
<file>logFile.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天产生一个新的日志文件 -->
<fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 保留 15 天的日志 -->
<maxHistory>15</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%date{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{17} - %m%n </pattern>
</encoder>
</appender>
<!-- 用来控制查看那个类的日志内容(对mybatis name 代表命名空间) -->
<logger name="mapper.StudentMapper" level="DEBUG" additivity="false">
<!--输出到控制台-->
<appender-ref ref="STDOUT"/>
<!--输入到文件-->
<appender-ref ref="FILE"/>
</logger>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
输出日志信息
动态sql生成
- foreach 循环
循环执行sql语句
案例1:给定多个id同时删除
<!--list是集合,x相当于临时变量-->
<!--open:sql语句以什么开头
close:sql语句以什么关闭
separator:语句中间分隔符-->
<delete id="deleteByIds" parameterType="list">
delete from student where sid in
<foreach collection="list" item="x" open="(" close=")" separator=",">
#{x}
</foreach>
</delete>
- if where 生成动态标签;
案例2:给定多个条件进行查询
用来抵消多余and带来的影响;
<select id="findByCondition" parameterType="map" resultType="domain.Student">
select sid,sname,birthday,sex from student
where 1=1
<if test="sname!=null">
and sname=#{sname}
</if>
<if test="birthday!=null">
and birthday=#{birthday}
</if>
<if test="sex!=null">
and sex=#{sex}
</if>
</select>
where标签去除多余的and;
<select id="findByCondition" parameterType="map" resultType="domain.Student">
select sid,sname,birthday,sex from student
<where>
<if test="sname!=null">
and sname=#{sname}
</if>
<if test="birthday!=null">
and birthday=#{birthday}
</if>
<if test="sex!=null">
and sex=#{sex}
</if>
</where>
</select>
- 方法一:“<“符号在xml中有特殊含义,所以可以用<转义字符代替,但是可读性不太好;
方法二:xml中的CDATA块中的特殊字符不用额外的转义;
<select id="findByLt" parameterType="int" resultType="domain.Student">
<![CDATA[
select * from student where sid<1004
]]>
</select>
- #{ }与${ } 的区别
- #{ }:先把#{ }占位符用?替换,在后续代码中给?赋值;只能占位一个值,不能占位表名或列名以及关键字;
- ${ }:直接把值拼接到sql语句中;由于底层拼接字符串所以可能存在sql注入攻击的安全隐患;
- 尽量使用#{},只有当某些功能不能使用#{} 实现时,采用${}
Mapper接口
配置文件中
<mappers>
<!--指定映射接口的位置-->
<mapper class="mapper.StudentMapper"/>
</mappers>
@Insert 包括 @option
@Update
@Delete
@select
案例:
package mapper;
import domain.Student;
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Map;
public interface StudentMapper {
@Insert("insert into student(sid,sname,birthday,sex) values(null,#{sname},#{birthday},#{sex})")
@Options(useGeneratedKeys = true,keyProperty = "sid")
void insert(Student student);
//需要动态生成sql语句
void update(Student student);
@Delete("delete from student where sid=#{sid}")
void delete(int sid);
@Select("select * from student")
List<Student> findAll();
@Select("select * from studnet where sid=#{sid}")
Student findById(int sid);
@Select("select * from student limit #{m} #{n}")
List<Student> findByPage(Map map);
}
测试类
public class StudentMapperTest {
static SqlSessionFactory factory;
static {
try {
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
factory = new SqlSessionFactoryBuilder().build(in);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void testInsert() {
SqlSession sqlSession = factory.openSession();
//获取接口
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student stu = new Student();
stu.setSname("小林");
stu.setBirthday(new Date());
stu.setSex("女");
mapper.insert(stu);
sqlSession.commit();
sqlSession.close();
}
}
原理:
mapper的类型,这个类是使用了jdk的动态代理技术生成的类,在代码运行期间生成;所以这个接口在调用mapper时在底层被实现;
public class $Proxy10 imlements StudentMapper{
private Sqlsession sqlsessionl
public $proxy10(Sqlsession sqlsession){
this.sqlSession=sqlsession;
}
//其中sql语句从@Insert获得,参数对象就是student
public void insert(Student student){
sqlSession.insert(sql,参数对象)
}
}
缺点
-
接口方法不能直接应用多个方法参数;
解决方法:
1) 使用map集合来传递参数,每个参数对应map中的一个键值对;
2 ) 用@Param注解 -
Mapper接口中不能有方法的重载;
-
使用Mapper接口方式实现动态方式比较复杂;接口和xml文件结合使用;
案例:动态更新数据库
在接口中定义方法,因为需要使用xml,所以不用注解;
void update(Student student);
创建与接口同名包同名文件的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(命名空间) 用来防止sql的命名冲突的-->
<mapper namespace="mapper.StudentMapper">
<delete id="deleteByIds" parameterType="list">
delete from student where sid in
<foreach collection="list" item="x" open="(" close=")" separator=",">
#{x}
</foreach>
</delete>
<update id="update" parameterType="domain.Student">
update student
<set>
<if test="sname!=null">
sname=#{sname},
</if>
<if test="birthday!=null">
birthday=#{birthday},
</if>
<if test="sex!=null">
sex=#{sex}
</if>
</set>
where sid=#{sid}
</update>
</mapper>
需要同步的地方
复杂sql映射
高级映射
案例1:表名和属性名不一致时 如何映射(除了起别名)例如属性名为sname,表列名为name;
解决方法:
resultMap是在需要复杂映射时,自己来定义更详细的映射方式;告诉Mybatis数据表列和对象属性的对应关系;
如果属性名和列名一致,则可以不用显示映射,但是如果像案例2那样存在嵌套映射则不能省略,可以添加``
<!--自定义的映射关系-->
<resultMap id="b" type="domain.Student">
<id column="id" property="id"></id>
<result column="sname" property="name"></result>
</resultMap>
案例2:查询一级模块,每个一级模块又有自己对应的二级子模块;
注意新增了接口,要在配置文件中添加该接口
<mapper class="mapper.ModuleMapper"/>
表信息
+----+----------+-----+--------------------+
| id | name | pid | code |
+----+----------+-----+--------------------+
| 1 | 系统管理 | 0 | |
| 2 | 订单管理 | 0 | |
| 3 | 商品管理 | 0 | |
| 11 | 邮件设置 | 1 | /system/email |
| 12 | 短信设置 | 1 | /system/sms |
| 13 | 用户管理 | 1 | /system/user |
| 14 | 权限分配 | 1 | /system/role |
| 21 | 查询订单 | 2 | /order/search |
| 22 | 退单处理 | 2 | /order/refund |
| 23 | 统计分析 | 2 | /order/stat |
| 31 | 查询商品 | 3 | /product/search |
| 32 | 上下架 | 3 | /product/onoff |
| 33 | 统计分析 | 3 | /product/stat |
| 34 | 库存管理 | 3 | /product/inventory |
+----+----------+-----+--------------------+
方法一:
先查询一级模块,再根据一级module的id查新二级Module;N+1次查询
<!--namespace(命名空间) 用来防止sql的命名冲突的-->
<mapper namespace="mapper.ModuleMapper">
<select id="selectModule" resultMap="b">
select id,name,pid,code from rbac_module where pid=0
</select>
<!--自定义的resultMap映射map
id是select生命的map名
type为查找出来的对象-->
<resultMap id="b" type="domain.Module">
<!--主键:column:一级module表的列名
property:一级module对象id-->
<id column="id" property="id"></id>
<!--其他列-->
<result column="name" property="name"></result>
<result column="pid" property="pid"></result>
<result column="code" property="code"></result>
<!--一级的children属性是二级module集合-->
<!--property:一级module的children属性
select:查找二级module的select标签id
column:根据一级mudule的id查找二级module-->
<collection property="children" select="findChildren" column="id">
</collection>
</resultMap>
<!--定义查找二级module的select标签-->
<!--一级二级对应关系:二级的pid对象一级的id-->
<select id="findChildren" parameterType="int" resultType="domain.Module">
select id,name,pid,code from rbac_module where pid=#{pid}
</select>
</mapper>
方法二:
使用表连接,将所有模块都一次性查询出来,再根据列名和属性进行映射;
连接后的表
+----+----------+-----+------+-----+----------+------+--------------------+
| id | name | pid | code | bid | bname | bpid | bcode |
+----+----------+-----+------+-----+----------+------+--------------------+
| 1 | 系统管理 | 0 | | 11 | 邮件设置 | 1 | /system/email |
| 1 | 系统管理 | 0 | | 12 | 短信设置 | 1 | /system/sms |
| 1 | 系统管理 | 0 | | 13 | 用户管理 | 1 | /system/user |
| 1 | 系统管理 | 0 | | 14 | 权限分配 | 1 | /system/role |
| 2 | 订单管理 | 0 | | 21 | 查询订单 | 2 | /order/search |
| 2 | 订单管理 | 0 | | 22 | 退单处理 | 2 | /order/refund |
| 2 | 订单管理 | 0 | | 23 | 统计分析 | 2 | /order/stat |
| 3 | 商品管理 | 0 | | 31 | 查询商品 | 3 | /product/search |
| 3 | 商品管理 | 0 | | 32 | 上下架 | 3 | /product/onoff |
| 3 | 商品管理 | 0 | | 33 | 统计分析 | 3 | /product/stat |
| 3 | 商品管理 | 0 | | 34 | 库存管理 | 3 | /product/inventory |
+----+----------+-----+------+-----+----------+------+--------------------+
<!--连接表查询,好处:只查询一次-->
<select id="findAll" resultMap="c">
select a.*,b.id bid,b.name bname,b.pid bpid,b.code bcode
from rbac_module a inner join rbac_module b on a.id=b.pid
</select>
<resultMap id="c" type="domain.Module">
<!--映射一级module的属性-->
<id column="id" property="id"></id>
<result column="name" property="name"></result>
<result column="pid" property="pid"></result>
<result column="code" property="code"></result>
<!--ofType查询集合的类型-->
<collection property="children" ofType="domain.Module">
<!--映射二级module属性-->
<id column="bid" property="id"></id>
<result column="bname" property="name"></result>
<result column="bpid" property="pid"></result>
<result column="bcode" property="code"></result>
</collection>
</resultMap>
缓存
一级缓存
SqlSession自带缓存功能,缓存中没有才回去数据库中查询,否则直接返回缓存结果,称为一级缓存;缓存生命周期,SqlSession建立的时候开始缓存,直到SqlSession关闭,缓存清空;
每个SqlSession有自己独立的缓存并且互不干扰;
二级缓存
二级缓存是全局的,作用域整个SqlSession,需要额外设置,但可以允许多个SqlSession共享缓存数据;
好处:可以较大提升查询效率,避免了频繁访问数据库,在频繁增删改时不适合使用;
缺点:如果其他SqlSession修改了数据库的记录的时候缓存失效;
往二级缓存中存储时需要将对象序列化,取出来的时候需要反序列化;
- 在xml mapper里加上
<cache></cache>
标签,表示启动二级缓存; - 接口mapper 二级缓存:在接口上添加一个@CacheNameSpace注解;
学习资料
深入学习Mybatis:
- mybatis简介中文文档;(偏向基础)
- 阅读Mybatis源代码
- Mybatis-plus