虽然很早之前就已经学习过mybatis框架了,但是由于之前面试的时候被问到mybatis的一些相关的问题答不上来(mybatis的一级缓存和二级缓存),所以花了几天的时间进行复习和整理,写下了这篇博客,篇幅比较长,但基本涵盖了mybatis的重要知识点,希望能对大家有所帮助!
1.什么是Mybatis
MyBatis 是一款优秀的持久层框架,也是一个半自动化的ORM框架 (Object Relationship Mapping) -->对象关系映射;避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程,Mybatis就是帮助程序猿将数据存入数据库中 , 以及从数据库中取数据,虽然原生的jdbc也能完成这个操作,通过框架可以减少重复代码,提高开发效率 ;MyBatis 不仅简单易学、灵活,而且sql语句和java代码的分离,解除sql语句和java代码的耦合,提高了可维护性。
2. 创建一个Mybatis工程的完整步骤
项目结构图
- 创建一个标准的maven项目,并将src目录删除掉,使之成为父模块,然后在父模块下建子模块,这样的好处是:父模块中的依赖都能作用到子模块,这样就不用在每个子模块再导入依赖了。
导入相关依赖
<!--导入依赖-->
<dependencies>
<!--导入mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
如果使用了lombok插件,还需再导入lombok依赖
<!--lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
- 编写全局配置文件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>
<!--注意配置的顺序,否则会出错:properties-settings-typeAliases -->
<properties resource="db.properties"/>
<settings>
<!--开启日志-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!--开启驼峰命名转换-->
<setting name="mapUnderscoreToCamelCase" value="true"></setting>
</settings>
<!--开启别名的配置-->
<typeAliases>
<package name="com.kuang.pojo"></package>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!--标明每一个mapper.xml的路径-->
<mappers>
<!--<mapper resource="com/kuang/dao/UserMapper.xml"/>-->
<!--使用注解开发,那么mapper中映射绑定的就是接口类-->
<!--<mapper class="com.kuang.dao.UserMapper"/>-->
<package name="com.kuang.dao"/>
</mappers>
</configuration>
- 编写数据源相关的配置文件db.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8
username=root
password=123456
- 编写生产sqlsession的工具类(注意自动提交事务的设置)
package com.kuang.utils;
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 java.io.IOException;
import java.io.InputStream;
//编写mybatis的工具类
public class MybatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//获取SqlSession连接
public static SqlSession getSession() {
//return sqlSessionFactory.openSession();//默认是需要手动提交事务
return sqlSessionFactory.openSession(true);//设置为true之后就可以自动提交事务了
}
}
- 编写实体类和对应的mapper接口(代码省略)
- 编写接口对应的xml配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kuang.dao.UserMapper">
<select id="findUserById" resultType="user">
select * from mybatis.user where id=#{id};
</select>
</mapper>
补充:xml配置文件的头部可以直接使用全局配置文件的头部进行适当的修改;即将下面圈出的部分改成mapper即可!
7. 编写测试类
@Test
public void test(){
SqlSession session = MybatisUtils.getSession();
UserMapper userMapper = session.getMapper(UserMapper.class);
List<User> users = userMapper.selectUser();
for (User user:users) {
System.out.println(user);
}
session.close();
}
可能出现的问题:一般我们在resources目录下编写配置文件(.properties/.xml),启动项目时都会被加载到;但是如果配置文件是在java目录下编写而不是在resources目录下,那么配置文件是不会被加载到的,必须在build中配置resource,解决java目录下的资源导出失败的问题。
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
3.细节
- 在mapper接口对应的xml文件中编写sql语句需要注意:如果我们传入的参数是一个实体类,那么在sql语句中给字段赋值的参数名就必须要和实体类的属性一致,否则就会出现异常
例如:下面的例子中,User实体类中的属性有name和pwd,所以sql语句中的values值必须也为name和pwd,如果#{name}改成#{sname},那么就会报异常
<insert id="insertUser" parameterType="com.kuang.pojo.User">
insert into user(name,pwd) VALUES (#{name},#{pwd})
</insert>
4.万能Map
在实际开发中,我们经常会使用一个Map集合来接收参数,有人称之为“万能Map”,这样做有什么好处呢?
- 当需要传递多个参数时,直接使用一个Map集合,可以避免出现错误,因为Mybatis传递多个参数的话,需要对每个参数加上@Param注解,有时候我们会忘记加上这个参数,导致出现错误,而使用万能Map就可以避免这个错误的发生
不使用万能Map的情况(多个参数没有加上@Param注解)
//UserMapper的部分代码
public User findByIdAndName(Integer id,String name);
//UserMapper.xml的部分代码
<select id="findByIdAndName" resultMap="userMap">
select * from user where id=#{id} and name=#{name}
</select>
//测试代码
@Test
public void findByIdAndName(){
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.findByIdAndName(1, "狂神");
System.out.println(user);
}
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
不使用万能Map的情况(加上@Param注解)
public User findByIdAndName(@Param("id") Integer id,@Param("name") String name);
User(id=1, name=狂神, password=123456)
Process finished with exit code 0
需要注意的是:@Param注解括号里面声明的参数名要跟UserMapper.xml中SQL语句的参数名相同
使用万能Map的情况
//UserMapper的部分代码
public List<User> testLimit(Map<String,Integer> map);
//UserMapper.xml的部分代码
<select id="testLimit" parameterType="map" resultMap="userMap">
select * from user limit #{startIndex},#{pageSize}
</select>
//测试代码
@Test
public void testLimit(){
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
Map<String,Integer> map=new HashMap<>();
map.put("startIndex",0);
map.put("pageSize",2);
List<User> users = mapper.testLimit(map);
for(User u:users){
System.out.println(u);
}
}
User(id=1, name=狂神, password=123456)
User(id=2, name=张三, password=abcdef)
Process finished with exit code 0
由上面的例子可以看到,使用万能Map可以避免使用@Param注解;另外,我们可以发现万能Map的另一个好处是,使用万能Map作为参数的方法既可以用作是根据id查询,也可当做根据id和name查询,具体怎么用,取决于Map。
5.Mybatis关于多对一的处理
场景:一个老师对应多个学生,多个学生对应一个老师;站在学生的角度,学生与老师就是多对一的关系;
问题要求:查询所有学生信息,并且显示学生对应的老师信息
补充:
- 一对一:数据库表的外键可以建立在任何一方;
- 多对一:数据库表的外键建立在多的一方;
- 多对多:数据库表的外键通过建立一张中间表来实现
teacher表
student表(tid是外键,参考teacher表的id列)
//teacher实体类
public class teacher {
private Integer id;
private Integer age;
private String name;
}
//student实体类
public class student {
private Integer id;
private Integer age;
private String name;
private teacher t;
}
public interface studentMapper {
public student getStudent();
}
<mapper namespace="com.kuang.dao.studentMapper">
<!--按照结果嵌套处理(类似联表查询)-->
<resultMap id="studentMap1" type="student">
<id property="id" column="id"></id>
<result property="age" column="age"></result>
<result property="name" column="name"></result>
<!--多对一,即成员变量是引用类型的情况-->
<association property="t" column="tid" javaType="teacher">
<id property="id" column="id"></id>
<result property="age" column="age"></result>
<result property="name" column="name"></result>
</association>
</resultMap>
<select id="getStudent" resultMap="studentMap1">
select * from student s,teacher t where s.tid=t.id
</select>
</mapper>
关于resultMap的一些使用细节:
- resultMap可以处理数据库字段名与java类属性名不一致的问题,比如:假设数据库表的一个字段名是id,而实体类对应的属性名是sid,那么必须使用resultMap进行结果映射
<result property="sid" column="id"></result>
,否则从数据库表查询出来的字段信息将无法封装到对应的实体类上 - 如果sql语句指定了字段的别名,那么resultMap进行结果映射应该注意:“column”值对应的是数据库字段的别名,而不是原来的字段名
6.Mybatis关于一对多的处理
场景:一个老师对应多个学生,多个学生对应一个老师;站在老师的角度,老师与学生就是一对多的关系;
问题要求:查询所有老师信息,并且显示该老师对应的所有学生信息
//teacher实体类
public class teacher {
private Integer id;
private Integer age;
private String name;
private List<student> students;
}
//student实体类
public class student {
private Integer id;
private Integer age;
private String name;
private Integer tid;
}
public interface teacherMapper {
public List<teacher> getTeacher();
}
<mapper namespace="com.kuang.dao.teacherMapper">
<resultMap id="teacherMap" type="teacher">
<id property="id" column="tid"></id>
<result property="age" column="tage"></result>
<result property="name" column="tname"></result>
<collection property="students" ofType="student">
<id property="id" column="sid"></id>
<result property="age" column="sage"></result>
<result property="name" column="sname"></result>
<result property="tid" column="tid"></result>
</collection>
</resultMap>
<select id="getTeacher" resultMap="teacherMap">
select s.id sid,s.name sname,s.age sage,t.name tname,t.age tage,t.id tid from teacher t,student s where s.tid=t.id
</select>
</mapper>
补充:
- javaType:用来指定实体类中属性的类型
- ofype:用来指定映射到List或者集合中的pojo类型,泛型中的约束类型
注意:如果关联的两个表有相同的字段名,在编写sql语句应该给相同名字的字段名设置别名,否则可能出现意想不到的错误!
7.Mybatis关于多对多的处理
(多对多的处理,类似于一对多,实质就是在一方的实体类中建立一个关联另一方的集合,至于要在哪一方的实体类建立一个关联另一方的集合,关键看你的需求;比如你的需求是:查询所有用户信息,包括用户所关联的角色信息,那么应该在用户实体类创建一个角色集合;而如果需求是:查询所有角色信息,包括角色所关联的用户信息,那么应该在角色实体类创建一个用户集合)
场景:一个用户可以有多个角色,一个角色可以对应多个用户;那么不管是站在用户的角度还是角色的角度,都是多对多的关系;
user表
role表
u_r(中间表,保存user和role的外键)
①问题要求:查询所有用户信息,包括用户所关联的角色信息
//用户实体类
public class User {
private Integer id;
private String name;
private String pwd;
private List<Role> roles;
}
//角色实体类
public class Role {
private Integer id;
private String roleName;
}
public interface UserMapper {
//查询所有用户,包括用户所对应的角色信息
public List<User> getUser();
}
<mapper namespace="com.kuang.dao.UserMapper">
<resultMap id="userMap" type="user">
<id property="id" column="id"></id>
<result property="name" column="name"></result>
<result property="pwd" column="pwd"></result>
<collection property="roles" ofType="role">
<id property="id" column="rid"></id>
<result property="roleName" column="rname"></result>
</collection>
</resultMap>
<select id="getUser" resultMap="userMap">
select u.*,r.id as rid,r.role_name as rname from user u
left join u_r ur on u.id=ur.uid
left join role r on ur.rid=r.id
</select>
</mapper>
@Test
public void test1(){
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.getUser();
for(User u: users){
System.out.println(u);
}
session.close();
}
User(id=1, name=狂神, pwd=123456, roles=[Role(id=1, roleName=教师), Role(id=2, roleName=学生)])
User(id=2, name=张三, pwd=abcdef, roles=[Role(id=1, roleName=教师)])
User(id=3, name=李四, pwd=987654, roles=[])
User(id=4, name=我又来修改了!, pwd=123, roles=[])
User(id=5, name=哈哈, pwd=123, roles=[])
User(id=6, name=啊啊, pwd=123, roles=[])
②问题要求:查询所有角色信息,包括角色所关联的用户信息
//用户实体类
public class User {
private Integer id;
private String name;
private String pwd;
}
//角色实体类
public class Role {
private Integer id;
private String roleName;
private List<User> users;
}
public interface RoleMapper {
//查询所有角色,包括角色所对应的用户
public List<Role> getRole();
}
<mapper namespace="com.kuang.dao.RoleMapper">
<resultMap id="roleMap" type="role">
<id property="id" column="rid"></id>
<result property="roleName" column="rname"></result>
<collection property="users" ofType="user">
<id property="id" column="id"></id>
<result property="name" column="name"></result>
<result property="pwd" column="pwd"></result>
</collection>
</resultMap>
<select id="getRole" resultMap="roleMap">
select r.id rid,r.role_name as rname,u.* from role r
left join u_r ur on r.id=ur.rid
left join user u on ur.uid=u.id;
</select>
</mapper>
@Test
public void test1(){
SqlSession session = MybatisUtils.getSession();
RoleMapper mapper = session.getMapper(RoleMapper.class);
List<Role> roles = mapper.getRole();
for(Role r: roles){
System.out.println(r);
}
session.close();
}
Role(id=1, roleName=教师, users=[User(id=1, name=狂神, pwd=123456), User(id=2, name=张三, pwd=abcdef)])
Role(id=2, roleName=学生, users=[User(id=1, name=狂神, pwd=123456)])
Role(id=3, roleName=医生, users=[])
PS:如果sql语句是分成多行编写,记得在上一行的行尾或者换行后的行首多打一个空格,否则可能会出现错误!
8.动态SQL
什么是动态SQL?动态SQL就是指根据不同的条件生成不同的SQL语句
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架(Hibernate),你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
- if:假设多个if符合条件,那么会拼接多个if的参数
// 根据传入的参数进行查询
public List<Blog> queryBlogIF(Map map);
<select id="queryBlogIF" parameterType="map" resultType="blog">
/*where 1=1是为了满足sql语句的完整性,使用where标签可以完美的解决这个问题*/
select * from blog where 1=1
<if test="id!=null">and id=#{id}</if>
<if test="title!=null">and title=#{title}</if>
<if test="author!=null">and author=#{author}</if>
<if test="create_time!=null">and create_time=#{create_time}</if>
<if test="views!=null">and views=#{views} </if>
</select>
- where:只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。(主要用在查询语句)
<select id="queryBlogIF" parameterType="map" resultType="blog">
select * from blog
<where>
<if test="id!=null">id=#{id}</if>
<if test="title!=null">and title=#{title}</if>
<if test="author!=null">and author=#{author}</if>
<if test="create_time!=null">and create_time=#{create_time}</if>
<if test="views!=null">and views=#{views} </if>
</where>
</select>
- choose、when、otherwise:类似Java 中的 switch 语句,假设多个when符合条件,那么也只会拼接第一个when的参数
//根据传入的参数进行查询
public List<Blog> queryBlogChoose(Map map);
<select id="queryBlogChoose" parameterType="map" resultType="blog">
select * from blog
<where>
<choose>
<when test="id!=null">
id=#{id}
</when>
<when test="title!=null">
and title=#{title}
</when>
<when test="author!=null">
and author=#{author}
</when>
<when test="create_time!=null">
and create_time=#{create_time}
</when>
<otherwise></otherwise>
</choose>
</where>
</select>
- set:set 元素可以用于动态包含需要更新的列,忽略其它不更新的列(主要用在更新语句)
以往我们更新表信息传入的参数是一个实体类,这种方式带来的弊端是:如果你只需要更新某些字段的值,那么会给实体类对应的属性设置值,而不需要更新值的字段对应的属性没有赋值,那么会使得数据库表相应的字段值被置为null
现在使用万能Map与动态SQL语句就可以解决上述的问题,我们需要更新哪些字段的值,只需要将对应的值赋给一个map集合,那些不需要更新值的字段值则没有变化,还是原来的值
//根据传入的参数进行更新
public int updateBlog(Map map);
<update id="updateBlog" parameterType="map">
update blog
<set>
<if test="title">title=#{title},</if>
<if test="author">author=#{author},</if>
<if test="create_time">create_time=#{create_time},</if>
<if test="views">views=#{views}</if>
</set>
where id=#{id}
</update>
- foreach:对集合进行遍历(尤其是在构建 IN 条件语句的时候)。例如:select * from blog where id in(1,2,3);
public List<Blog> queryForeach(Map map);
<select id="queryForeach" parameterType="map" resultType="blog">
select * from blog where views IN
<foreach collection="lists" item="views" open="(" separator="," close=")">
#{views}
</foreach>
</select>
//测试根据views集合进行
@Test
public void test() {
SqlSession session = MybatisUtils.getSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
Map map = new HashMap();
List<Integer> list=new ArrayList<>();
list.add(100);
list.add(200);
list.add(300);
map.put("lists",list);
List<Blog> blogs = mapper.queryForeach(map);
for(Blog b: blogs){
System.out.println(b);
}
session.close();
}
9.Mybatis的缓存
什么是缓存?
缓存是指存在内存中的数据,将用户经常查询的数据放在缓存(内存)中,用户查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。
为什么使用缓存?
减少和数据库的交互,减少系统开销,提高系统效率。
什么样的数据能使用缓存?
经常查询并且不经常改变的数据。
一级缓存
一级缓存也叫本地缓存,sqlsession级别,是默认开启的;即缓存只在一个sqlsession会话级别生效;
缓存失效的情况:
- 增删改操作,可能会改变原来的数据,所以必定会刷新缓存(即使你增删改操作看起来并不跟缓存中的数据相关,也是会刷新缓存的)
- 查询不同的东西
- 查询不同的Mapper.xml
- 手动清理缓存(session.clearCache())
@Test
public void test(){
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user1 = mapper.findUserById(1);
System.out.println(user1);
System.out.println("==========================");
User user2 = mapper.findUserById(1);
System.out.println(user2);
session.close();
}
可以看到,两次查询相同的值,只有第一次是从数据库获取的,第二次会直接从缓存获取!
二级缓存
二级缓存也叫全局缓存,一级缓存的作用域太低了,所以出现了二级缓存;二级缓存是基于namespace级别的缓存,一个名称空间,对应一个二级缓存;也可以说二级缓存是基于一个Mapper.xml级别的
二级缓存的工作原理:
- 一个会话查询一条数据,这条数据就会被放在当前会话的一级缓存中
- 如果会话关闭了,那么这个会话对应的一级缓存就没了
- 也就是说,所有的数据都会先放在一级缓存中,只有当会话提交或关闭后,才会提交到二级缓存中
- 新的会话查询信息,就可以从二级缓存中获取内容
一级缓存是默认开启的,二级缓存是需要手动开启的,开启二级缓存的步骤为:
①在mybatis全局配置文件中显示的开启全局缓存(虽然该属性默认是为true,但是为了提高代码的可读性,让别人知道要开启二级缓存了,所以最好显示的开启)
<!--显示的开启全局缓存-->
<setting name="cacheEnabled" value="true"></setting>
②在要开启二级缓存的Mapper.xml配置文件中开启
注意:如果cache标签没有自定义参数,那么使用的实体类必须实现序列化接口,否则会出错。
<cache/>
也可以自定义参数
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
③测试
@Test
public void test2(){
SqlSession session1 = MybatisUtils.getSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.findUserById(1);
System.out.println(user1);
session1.close();
System.out.println("================================");
SqlSession session2 = MybatisUtils.getSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.findUserById(1);
System.out.println(user2);
session2.close();
}
可以看到,第一个sqlsession关闭后,第二个sqlsession还是能使用缓存获取数据,因为两个sqlsession创建的都是同一个mapper。
缓存原理
@Test
public void test3(){
SqlSession session1 = MybatisUtils.getSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.findUserById(1);
System.out.println(user1);
session1.close();//由于session1关闭了,所以id为1的用户被存到二级缓存了
System.out.println("=====================================");
SqlSession session2 = MybatisUtils.getSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.findUserById(1);//先从二级缓存查找,找到了,直接返回
System.out.println(user2);
System.out.println("=====================================");
User user3 = mapper2.findUserById(2);//由于当前的session2还没有关闭,所以id为2的用户被存在一级缓存,并没有存在二级缓存
System.out.println(user3);
System.out.println("=====================================");
User user4 = mapper2.findUserById(2);//二级缓存找不到id为2的用户,一级缓存找到了
System.out.println(user4);
System.out.println("=====================================");
User user5 = mapper2.findUserById(3);//第一次查找,二级缓存找不到,一级缓存也找不到,只能从数据库查找
System.out.println(user5);
System.out.println("=====================================");
session2.close();
}
10.补充
可以编写一个 生产随机ID的工具类,保证每个ID都是唯一的
public class IDUtils {
public static String getID(){
return UUID.randomUUID().toString().replaceAll("-","");
}
}
关于mybatis的学习文档:https://mybatis.org/mybatis-3/zh/dynamic-sql.html