https://www.bilibili.com/video/BV1wy4y1H7wu
为什么需要Mybatis
因为传统的JDBC代码很麻烦,我们就有了框架把它简化一下
- 简单易学
- 解除sql与程序代码的耦合
- 提供xml标签
- 提供映射标签
- 使用的人多
为什么是Mybatis
Mybatis优点:
-
学习成本低,上手快,当学完JDBC之后,再去学习Mybatis,上手时间要比Hibernate快。
-
SQL优化方便,MyBatis可以进行更为细致的SQL优化,可以减少查询字段。
Hibernate优点:
- Hibernate的DAO层开发比MyBatis简单,Mybatis需要维护SQL和结果映射。
- Hibernate学习成本很高,但是它功能强大,数据库无关性好,还有O/R映射能力很强。
- Hibernate对对象的维护和缓存要比MyBatis好,对增删改查的对象的维护要方便。
- Hibernate数据库移植性很好,MyBatis的数据库移植性不好,不同的数据库需要写不同SQL。
- Hibernate有更好的二级缓存机制,可以使用第三方缓存。MyBatis本身提供的缓存机制不佳。
第一个Mybatis程序
文件目录
搭建环境、导入依赖
创建maven项目,删除src文件夹,导入依赖
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<!-- lombok依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
添加配置文件
我们需要在src下的resources文件夹下创建名为mybatis-config.xml
的文件,然后根据官方提示,在文件中写入如下配置文件,在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"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"/>
<property name="username" value="root"/>
<property name="password" value="422518"/>
</dataSource>
</environment>
</environments>
<!--记得最后把Mapper配置文件放进来-->
<mappers>
<mapper resource="com/dao/UserMapper.xml"/>
</mappers>
</configuration>
Spring Boot的相关配置,dao层的接口加上@mapper
注解
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=422518
创建实体类
@Data
public class User {
String username;
String password;
}
编写dao接口
public interface UserDao {
List<User> findAll();
}
mapper接口实现类
我们需要建立xml配置文件,告诉SqlSession 和 Mapper 具体执行什么操作,配置文件可以写在任何地方,一般放在resources文件夹下,和接口位置同名的项目下(见文件目录)
注意,命名空间为要绑定的Dao(Mapper)接口,其中id
为对应的方法名
,可以右键copy reference
<?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">
<mapper namespace="com.dao.UserDao">
<select id="findAll" resultType="com.pojo.User">
select * from test.user
</select>
</mapper>
进行测试
import com.utils.MybatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
import com.pojo.User
public class UserDaoTest {
@Test
//获得sqlSession
//使用Mybatis第一步,获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//sqlSession可以理解为数据库连接
SqlSession sqlSession = sqlSessionFactory.openSession();
//执行sqSession
UserDao mapper = sqlSession.getMapper(UserDao.class);//获取对象
List<User> userList = mapper.findAll();//执行对象的方法
//userList.for即可得到这一部分代码
for (User user : userList) {
System.out.println(user.toString());
}
sqlSession.close();
}
使用插件
插件名:Free Mybatis plugin
在dao文件类名、方法名使用alt+enter可以自动生成mapper文件,支持xml和方法的跳转
数据库的增删改查
如果返回一个对象就使用类作为返回类型,这个返回值包含所有的信息,如果是字符串或者数字就指定类型,如果返回值是List只需要写原对象即可。
如果通过传入的id没找到就会返回null,传入的id不存在也会返回null
<select id="getUserById" resultType="com.demo.entity.User" parameterType="String">
select * from user where account=#{id}
</select>
注意:增删改需要提交事务,即调用close之前先commit
使用万能的Map进行添加
接口
int addUser2(Map<String,String> map);
mapper
<!-- user_account和user_pwd是map的key-->
<insert id="addUser" parameterType="Map">
insert into user (account,pwd) values (#{user_account},#{user_pwd});
</insert>
方法有多个参数时
我以为使用#{paramName}
即可高枕无忧,但是学到了方法有多个参数的时候,是用这种形式会报错,这时候应该在方法中进行操作,否则会变成avg1,avg2等等
User findByCondition(@Param("id") Integer id, @Param("username") String username);
配置文件
properties(从properties文件读取)
https://mybatis.org/mybatis-3/zh/configuration.html#properties
使用properties标签读取xml内容
settings(一些属性设置)
https://mybatis.org/mybatis-3/zh/configuration.html#settings
<!-- 开启驼峰命名 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"></setting>
</settings>
typeAliases(类型别名)
类的别名
<typeAliases>
<typeAlias type="com.demo.pojo.User" alias="User"/>
</typeAliases>
包的别名
<typeAliases>
<typeAlias package="com.demo.pojo"/>
</typeAliases>
environments(修改操作环境)
https://mybatis.org/mybatis-3/zh/configuration.html#environments
mappers
使用相对于类路径的资源引用
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>
将包内的映射器接口实现全部注册为映射器
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
注解开发
直接将SQL写在dao层的接口方法上即可,但不灵活,不是很常用,记得在配置文件中写接口的包
public interface UserDao {
@Select("select * from user where id=#{id}")
User findById(Integer id);
}
动态sql
<if>
满足test
的判断条件之后才会拼接sql,注意,test中如果需要使用方法参数,不需要加#{}
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
<trim>
清除或添加前缀或后缀,多种情况用|
分割,不要加空格,用的比较少
属性 | 含义 |
---|---|
prefixOverrides | 删除标签内前缀 |
suffixOverrides | 删除标签内后缀 |
prefix | 如果标签内有内容则添加前缀 |
suffix | 如果标签内有内容则添加后缀 |
综合案例:
<select id="findActiveBlogLike" resultType="User">
select * from user
<trim prefix="where" prefixOverrides="and|or">
<if test="id!=null">
id=#{id}
</if>
<!--id为null,username不为null的时候,可以清除and-->
<if test="username!=null">
and username=#{username}
</if>
</trim>
</select>
<where>
用的蛮多的,where标签等价于
<trim prefix="where" prefixOverrides="and|or"></trim>
使用案例
<select id="findActiveBlogLike" resultType="User">
select * from user
<where>
<if test="id!=null">
id=#{id}
</if>
<!--id为null,username不为null的时候,可以清除and-->
<if test="username!=null">
and username=#{username}
</if>
</where>
</select>
如果id和username都为null,则执行的sql为:**select * from user **
如果id为null,username不为null,则执行的sql为:**select * from user where username = ? **
<set>
当更新的时候,如果某些字段为null,则会直接设置为null,但实际我们希望应该这些字段不更新,这时候使用set标签就很方便了,可以使用set在sql拼接set字段,同时去掉后缀逗号
set标签等价于
<trim prefix="set" suffixOverrides=","></trim>
使用案例
<update id="updateById">
update user
<set>
<if test="username != null">
username=#{username},
</if>
<if test="age != null">
age=#{age},
</if>
<if test="address != null">
address=#{address},
</if>
</set>
where id=#{id}
</update>
如果调用方法时传入的User对象的id为2,username不为null,其他属性都为null则最终执行的sql为:UPDATE USER SET username = ? where id = ?
<foreach>
当我们查询的参数是一个数组的时候,我们期望动态的根据实际传入的数组的长度拼接SQL语句。例如传入长度为4个数组最终执行的SQL为:
select * from User where id in( ? , ? , ? , ?, ? )
例如方法定义如下
List<User> findByIds(@Param("ids") Integer[] ids);
则在xml映射文件中可以使用以下写法
<select id="findByIds" resultType="com.sangeng.pojo.User">
select * from User
<where>
<foreach collection="ids" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
- collection:表示要遍历的参数
- open:表示遍历开始时拼接的语句
- item:表示给当前遍历到的元素的取的名字
- separator:表示每遍历完一次拼接的分隔符
- close:表示最后一次遍历完拼接的语句
注意:如果方法参数是数组类型,默认的参数名是array,如果方法参数是list集合默认的参数名是list。建议遇到数组或者集合类型的参数统一使用@Param注解进行命名。
choose、when、otherwise
三者配套使用,when、otherwise都是放在choose标签里的,类似switch语法
使用场景如下:
- 如果user对象的id不为空时就通过id查询。
- 如果id为null,username不为null就通过username查询。
- 如果id和username都会null就查询id为3的用户
<select id="selectChose" resultType="com.sangeng.pojo.User">
select * from user
<where>
<choose>
<when test="id!=null">
id = #{id}
</when>
<when test="username!=null">
username = #{username}
</when>
<otherwise>
id = 3
</otherwise>
</choose>
</where>
</select>
-
choose类似于java中的switch
-
when类似于java中的case
-
otherwise类似于java中的dufault
一个choose标签中最多只会有一个when中的判断成立。从上到下去进行判断。如果成立了就把标签体的内容拼接到sql中,并且不会进行其它when的判断和拼接。如果所有的when都不成立则拼接otherwise中的语句。
SQL片段单独抽取
重复的片段可以单独抽取出来,提高复用性和简洁度
<sql id="baseSelect" >id,username,age,address</sql>
<select id="findAll" resultType="com.sangeng.pojo.User">
select <include refid="baseSelect"/> from user
</select>
resultMap的使用(自定义映射关系)
基本使用:
<!--
resultMap 用来自定义结果集和实体类的映射
属性:
id 相当于这个resultMap的唯一标识
type 用来指定映射到哪个实体类
id标签 用来指定主键列的映射规则
属性:
property 要映射的对象属性名
column 对应的列名
result标签 用来指定普通列的映射规则
属性:
property 要映射的属性名
column 对应的列名
-->
<resultMap id="orderMap" type="com.sangeng.pojo.Order" >
<id column="id" property="id"></id>
<result column="createtime" property="createtime"></result>
<result column="price" property="price"></result>
<result column="remark" property="remark"></result>
<result column="user_id" property="userId"></result>
</resultMap>
<!--使用我们自定义的映射规则-->
<select id="findAll" resultMap="orderMap">
SELECT id,createtime,price,remark,user_id FROM ORDERS
</select>
开启自动映射(只自定义映射某几个,相同的就不用设置了):
<!--默认情况下自动映射是打开的,可以在resultMap设置属性autoMapping关闭-->
<resultMap id="orderMap" type="com.sangeng.pojo.Order" >
<result column="user_id" property="userId"></result>
</resultMap>
<!--使用我们自定义的映射规则-->
<select id="findAll" resultMap="orderMap">
SELECT id,createtime,price,remark,user_id FROM ORDERS
</select>
继承映射关系:
<!--定义个父映射,供其他resultMap继承-->
<resultMap id="baseOrderMap" type="com.sangeng.pojo.Order" >
<id column="id" property="id"></id>
<result column="createtime" property="createtime"></result>
<result column="price" property="price"></result>
<result column="remark" property="remark"></result>
</resultMap>
<!--继承baseOrderMap,然后只需要写自己特有的映射关系即可-->
<resultMap id="orderMap" type="com.sangeng.pojo.Order" autoMapping="false" extends="baseOrderMap">
<result column="user_id" property="userId"></result>
</resultMap>
多表查询
实现多表查询大概有两种方式,一是写个多表查询的SQL,二是分步查询
但是在自动映射的时候可能没有合适的实体类做映射,我们可以使用resultMap做映射
关联查询
一对一(某个对象封装到属性)
方式一:使用ResultMap对所有字段进行映射
<!--Order和User关联的映射-->
<resultMap id="orderUserMap" type="com.sangeng.pojo.Order" autoMapping="false" extends="orderMap">
<id column="id" property="id"></id>
<result column="createtime" property="createtime"></result>
<result column="price" property="price"></result>
<result column="remark" property="remark"></result>
<result column="uid" property="user.id"></result>
<result column="username" property="user.username"></result>
<result column="age" property="user.age"></result>
<result column="address" property="user.address"></result>
</resultMap>
方式二:使用ResultMap中的association设置关联实体类的映射规则.
<resultMap id="baseOrderMap" type="com.sangeng.pojo.Order" >
<id column="id" property="id"></id>
<result column="createtime" property="createtime"></result>
<result column="price" property="price"></result>
<result column="remark" property="remark"></result>
</resultMap>
<resultMap id="orderMap" type="com.sangeng.pojo.Order" autoMapping="false" extends="baseOrderMap">
<result column="user_id" property="userId"></result>
</resultMap>
<!--Order和User关联的映射(使用association)-->
<resultMap id="orderUserMapUseAssociation" type="com.sangeng.pojo.Order" autoMapping="false" extends="orderMap">
<!--property是Order对象的user属性-->
<association property="user" javaType="com.sangeng.pojo.User">
<id property="id" column="uid"></id>
<result property="username" column="username"></result>
<result property="age" column="age"></result>
<result property="address" column="address"></result>
</association>
</resultMap>
一对多(某个对象集封装到属性)
因为期望User中还能包含该用户所具有的角色信息,所以可以在User中增加一个属性
//该用户所具有的角色
private List<Role> roles;
SQL语句如下
SELECT
u.`id`,u.`username`,u.`age`,u.`address`,r.id rid,r.name,r.desc
FROM
USER u,user_role ur,role r
WHERE
u.id=ur.user_id AND ur.role_id = r.id
AND u.id = 2
结果集
使用resultMap的collection
<!--定义User基本属性映射规则-->
<resultMap id="userMap" type="com.sangeng.pojo.User">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="age" property="age"></result>
<result column="address" property="address"></result>
</resultMap>
<resultMap id="userRoleMap" type="com.sangeng.pojo.User" extends="userMap">
<!--集合-->
<collection property="roles" ofType="com.sangeng.pojo.Role" >
<id column="rid" property="id"></id>
<result column="name" property="name"></result>
<result column="desc" property="desc"></result>
</collection>
</resultMap>
分步查询(嘶,有点麻烦哦)
场景:还是一对多的用户-角色的查询
- 首先查user
- 根据user.id查询角色信息
- 写两个方法,再写两个SQL即可
- 配置分步查询
分步查询配置
我们期望的效果是调用findByUsername方法查询出来的结果中就包含角色的信息。所以我们可以设置findByUsername方法的RestltMap,指定分步查询
<resultMap id="userMap" type="com.sangeng.pojo.User">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="age" column="age"></result>
<result property="address" column="address"></result>
</resultMap>
<!--
select属性:指定用哪个查询来查询当前属性的数据 写法:包名.接口名.方法名
column属性:设置当前结果集中哪列的数据作为select属性指定的查询方法需要参数
-->
<resultMap id="userRoleMapBySelect" type="com.sangeng.pojo.User" extends="userMap">
<collection property="roles"
ofType="com.sangeng.pojo.Role"
select="com.sangeng.dao.RoleDao.findRoleByUserId"
column="id">
<!--这里的column="id"是结果集返回的查询的参数-->
</collection>
</resultMap>
指定findByUsername使用我们刚刚创建的resultMap
<!--根据用户名查询用户-->
<select id="findByUsername" resultMap="userRoleMapBySelect">
select id,username,age,address from user where username = #{username}
</select>
分步查询设置按需加载
我们可以设置按需加载,这样在我们代码中需要用到关联数据的时候才会去查询关联数据,例如我们只想要用户信息,不想要角色信息。xml设置之后,当我们只需要user.id的时候,系统会智能的不去进行角色信息的查询
1、方式一:局部配置
设置resultMap
的fetchType
属性为lazy
<resultMap id="userRoleMapBySelect" type="com.sangeng.pojo.User" extends="userMap">
<collection property="roles"
ofType="com.sangeng.pojo.Role"
select="com.sangeng.dao.RoleDao.findRoleByUserId"
column="id" fetchType="lazy">
</collection>
</resultMap>
2、方式二:全局配置
在mybatis-config.xml
里设置lazyLoadingEnabled
为true
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
如何选择
分布式项目中分步查询可能会更好点,因为多表关联查询可能会很耗时,同时分步查询能进行按需加载,如果是小项目用关联查询也是OK的
而且在使用PageHelper的时候多表查询一对多查询,会出现关联数据不全的情况
分页查询——PageHelper
引入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.0.0</version>
</dependency>
配置Mybatis使用分页插件
<plugins>
<!-- 注意:分页助手的插件 配置在通用馆mapper之前 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<!-- 指定方言 -->
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
开始分页查询
//方式一:getmapper
UserDao mapper = sqlSession.getMapper(UserDao.class);//获取对象
//设置分页查询参数
PageHelper.startPage(1,2);//pagenumber、pagesize
List<User> users = mapper.findAll();
System.out.println(users.get(0));
如果需要获取总页数总条数等分页相关数据,只需要创建一个PageInfo对象,把刚刚查询出的返回值做为构造方法参数传入。然后使用pageInfo对象获取即可。
PageInfo<User> pageInfo = new PageInfo<User>(users);
System.out.println("总条数:"+pageInfo.getTotal());
System.out.println("总页数:"+pageInfo.getPages());
System.out.println("当前页:"+pageInfo.getPageNum());
System.out.println("每页显示长度:"+pageInfo.getPageSize());
一对多多表查询分页问题
我们在进行一对多的多表查询时,如果使用了PageHelper进行分页,会出现关联数据不全的情况。我们可以使用分步查询的方式解决该问题。
Mybatis缓存
Mybatis的缓存其实就是把之前查到的数据存入内存(map),下次如果还是查相同的东西,就可以直接从缓存中取,从而提高效率。
Mybatis有一级缓存和二级缓存之分,一级缓存(默认开启)是sqlsession级别的缓存。二级缓存相当于mapper级别的缓存。
一级缓存
几种不会使用一级缓存的情况
- 调用相同的方法但是传入的参数不同
- 调用相同的方法参数也相同,但是使用的是另外一个sqlSession
- 如果查询完后,对同一个表进行了增、删、改的操作,都会清空这sqlSession上的缓存
- 如果手动调用sqlSession的clearCache方法清除缓存了,后面也使用不了缓存
二级缓存
如果使用二级缓存,实体类需要实现Serializable接口。二级缓存在实际开发中基本不会使用,因为某个session修改数据后,另一个再去查缓存会导致数据混乱。
注意:只在sqlsession调用了close或者commit后的数据才会进入二级缓存,同时即使换了个sqlSession二级缓存也会存在。
第一步:全局开启,在Mybatis核心配置文件中配置
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
第二步:局部开启,在要开启二级缓存的mapper映射文件中设置 cache标签
<?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" >
<mapper namespace="com.sangeng.dao.RoleDao">
<cache></cache>
</mapper>
获取插入数据的主键
XML方式
返回自增主键:
<insert id="insert1" useGeneratedKeys="true" keyProperty="id">
insert into user(username,password) values(#{userName},#{password})
</insert>
返回非自增主键:
<insert id="insert2">
insert into user(username,password) values(#{userName},#{password})
<selectKey keyColumn="id" resultType="int" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
</insert>
注解方式
返回自增主键:
@Insert("insert into user(username,password) values(#{userName},#{password})")
@Options(keyColumn="id",keyProperty="id",useGeneratedKeys=true)
int insert(SysUser user);
返回非自增:
@Insert("insert into user(username,password) values(#{userName},#{password})")
@SelectKey(statement="SELECT LAST_INSERT_ID()",
keyProperty="id",
resultType=Integer.class,
before = false)
int insert(SysUser user);