简介
在当下MyBatis已经是最主流的持久层框架,具体一点就是操作数据库的框架;MyBatis非常轻量,只需要通过简单的XML或者注解就可以完成数据映射和操作数据。很多大公司(比如阿里巴巴)都采用MyBatis作为Java的特久层框架,主要的原因在于它可以灵活的自定义SQL又兼具ORM框架的特性。
ORM
ORM对象关系映射(英语:Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),是一种程序设计技术用于实现面向对象编程语言里不同类型系统的数据之间的转换
DO对象规则
所有的ORM框架都需要一个java对象来映射数据库的表,并且一一对应,一般把这类对象成为DO对象,对象名称规范是表名+DO,例如user表对象名称就是UserDO
DO对象包规则
通常把DO对象存放在dataobject包下
DO对象数据类型
与普通POJO并无不同,但数据类型要与数据库类型进行匹配:
DAO层
数据层服务称为DAO层,DAO层包含对数据库操作的接口和实现类
创建DAO层
- 创建包
com.mybatis.dao
- 创建DAO接口
//放在dao包中
package com.mybatis.dao;
import org.apache.ibatis.annotations.Mapper;
//这个接口特殊在于添加@Mapper注解
@Mapper
public interface UserDAO {
}
- 引用DAO
完成MyBatis DAO的定义后,Spring启动会自动加载这个接口并动态创建Spring Bean,只需要按照Spring Bean的方式完成资源注入即可,比如创建一个UserController用于处理用户web服务:
package com.mybatis.control;
import com.mybatis.dao.UserDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Autowired
private UserDAO userDAO;
}
MyBatis-注解CRUD
MyBatis的强大之处在于和SQL语句的天然集成
查询
- 添加接口方法
package com.mybatis.dao;
import org.apache.ibatis.annotations.Mapper;
import UserDO;
import java.util.List;
@Mapper
public interface UserDAO {
//添加了findAll方法,查询多条记录时一般使用List作为返回类型
public List<UserDO> findAll();
}
- 添加
@Select注解
@Select("SELECT id,user_name as userName,pwd,nick_name as nickName,avatar,gmt_created as gmtCreated,gmt_modified as gmtModified FROM user")
List<UserDO> findAll();
这里使用SQL的别名是为了完成数据映射,MyBatis转化数据的时候是按照名称一一对应的,表字段与DO字段名称不一样时,数据映射失败
插入
在MyBatis中同样支持插入,可以使用@Insert注解,包路径为: org.apache.ibatis.annotations.Insert
- 添加接口方法
执行SQL插入语句时,会返回行数,一般成功返回1,所以设置返回类型为int,判断是否插入成功可以通过返回值>0来判断
package com.mybatis.dao;
import UserDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserDAO {
int insert(UserDO userDO);
@Select("SELECT id,user_name as userName,pwd,nick_name as nickName,avatar,gmt_created as gmtCreated,gmt_modified as gmtModified FROM user")
List<UserDO> findAll();
}
- 添加@Insert注解
@Insert("INSERT INTO user (user_name, pwd, nick_name,avatar,gmt_created,gmt_modified) VALUES(#{userName}, #{pwd}, #{nickName}, #{avatar},now(),now())")
int insert(UserDO userDO);
Mybatis获取动态值的方式为#{变量名}执行的时候会自动从上下文参数获取变量的值比如:
#{userName}实际上是执行userDO.getUserName() 这个方法来获取userName变量值,MyBatis会自动更新生成正式的SQL语句到数据库里执行,完成了动态数据的存储
修改
使用@Update注解完成SQL语句:
@Update("update user set nick_name=#{nickName},gmt_modified=now() where id=#{id}")
这里根据id主键修改了nick_name字段,但任何数据的修改都需要同步修改gmt_modified字段,这样可以知道数据在什么时候被修改了.
删除
删除一般都是通过主键来进行删除.执行SQL语句时会返回删除的行数,所以方法为int型
int delete(long id);
MyBatis除了支持DO对象传递参数之外,还可以接受普通参数,如String,int等,为了SQL语句中完成普通参数的解析要对参数增加一个注解 @Param
@Delete("delete from user where id=#{id}")
public interface UserDAO {
int delete(@Param("id") long id);
}
为了验证删除成功可以用return userDAO.delete(id) > 0;判断删除是否成功
findByUserName方法通过传递自定义方法参数userName来获取UserDO数据
public interface UserDAO {
@Select("select id,user_name as userName,pwd,nick_name as nickName,avatar,gmt_created as gmtCreated,gmt_modified as gmtModified from user where user_name=#{userName} limit 1")
UserDO findByUserName(@Param("userName") String name);
}
Mybatis XML语句
配置
要使用MyBatis的XML,首先得在application.properties文件中添加配置mybatis.mapper-locations,这个配置用于指定MyBatis Mapper XML文件路径,一般来说这个路径和DAO包路径一致,又因为代码以外的文件存放在resources文件目录下:
com.mybatis.dao为包名
mybatis.mapper-locations=classpath:src/main/resources/com/mybatis/dao/*.xml
MyBatis XML Mapper
一个DAO类对应一个DAO.xml文件,比如UserDAO.java对应UserDAO.xml
- 头信息创建完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">
- 有头信息后添加mapper节点
<?xml version="1.0" encoding="UTF-8"?>
namespace这个是命名空间的含义,一般是mapper所对应的DAO接口的全称
- resultMap( 在mapper根节点内 ) 用于处理表和DO对象的属性映射,确保表中每个字段都有属性可以匹配
<?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.mybatis.dao.UserDAO">
<resultMap id="userResultMap" type="com.mybatis.dataobject.UserDO">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
</resultMap>
</mapper>
- id: 唯一标示一般命名规则是xxxResultMap用于确保唯一
- type:对应的DO类完整路径
子节点
- id设置数据库主键字段信息,column属性对应的表的字段名称,property对于的是DO属性名称
- result设置数据库其他字段信息,column属性对应的表的字段名称,property对于的是DO属性名称
Insert
- 首先先新增一个add方法
package com.comment.dao;
import com.comment.dataobject.UserDO;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserDAO {
int add(UserDO userDO);
}
然后打开UserDAO.xml文件添加insert语句,在mapper节点下添加insert节点:
<insert id="add" parameterType="com.comment.dataobject.UserDO" >
INSERT INTO user (user_name, pwd, nick_name,avatar,gmt_created,gmt_modified)
VALUES(#{userName}, #{pwd}, #{nickName}, #{avatar},now(),now())
</insert>
- id同DAO类的方法名,同一个xml内要唯一
- parameterType用于传递参数类型,一般和方法的参数类型一致
Update/Delete
在xml文件中添加update节点:
<update id="update" parameterType="com.youkeda.comment.dataobject.UserDO">
update user set nick_name=#{nickName},gmt_modified=now() where id=#{id}
</update>
在xml文件中添加delete节点:
<delete id="delete">
delete from user where id=#{id}
</delete>
int delete(@Param("id") long id);
这里的delete节点并没有配置parameterType属性,这个方法的参数是由@Param注解组成,MyBatis默认的parameterType类型就是Map,所以可以省略
Select
总结基于XML模式的开发顺序:
- 创建DO对象
- 创建DAO对象,配置@Mapper注解
- 创建XML文件,完成resultMap配置
- 创建DAO接口方法
- 创建相应的XML语句
在xml文件增加select节点
<select id="findAll" resultMap="userResultMap">
select * from user
</select>
<select id="findByUserName" resultMap="userResultMap">
select * from user where user_name=#{userName} limit 1
</select>
这里多使用了一个属性resultMap这个值一般配置为该XML文件下的resultMap节点的id值
MyBatis XML条件语句
if
用于条件判断,test属性用于指定判断条件,为了拼接条件在SQL语句后强行添加
where 1=1恒成立的条件,比如在update节点中,一般会结合条件语句进行判断再执行:
<select id="selectBy" resultType="Users">
select * from users where 1=1
<if test="uid != 0">
and uid = #{uid}
</if>
<if test="username != null and username != ''">
and username = #{username}
</if>
<if test="password != null and password !='' ">
and password =#{password}
</if>
<if test="city != null and city !='' ">
and city =#{city}
</if>
</select>
<update id="update" parameterType="com.comment.dataobject.UserDO">
update user set
<if test="nickName != null">
nick_name=#{nickName},gmt_modified=now()
</if>
where id=#{id}
</update>
set
- 满足条件时,会自动添加set关键字
- 要注意加逗号set会去除set子句中的多余的逗号
- 不满足条件时不会生成
set关键字
修改操作一定要更新时间,set就可以避免所有列值都为null是引起的语法错误,使用set系统会自动去掉最后一个
,
<update id="update" parameterType="com.comment.dataobject.UserDO">
update user
<set>
<if test="nickName != null">
nick_name=#{nickName},
</if>
<if test="avatar != null">
avatar=#{avatar},
</if>
gmt_modified=now()
</set>
where id=#{id}
</update>
if+select
查询条件一般是动态的,比如模糊查询某个时间后注册的用户:
List<UserDO> search(@Param("keyWord")String keyWord,@Param("time")LocalDateTime time);
<select id="search" resultMap="userResultMap">
select * from user where
<if test="keyWord != null">
user_name like CONCAT('%',#{keyWord},'%')
or nick_name like CONCAT('%',#{keyWord},'%')
</if>
<if test="time != null">
and gmt_created <![CDATA[ >= ]]> #{time}
</if>
</select>
>=、<、<=、>、>=、&这类的表达式会导致MyBatis解析失败,所以需要使用<![CDATA[ key ]]>来包围住,当参数为LocalDateTime时,需要添加注解@DateTimeFormat用于把字符串转化为日期类型
where
- 如果没有条件,不会生成where关键字
- 如果有条件会自动生成
- 如果条件中有and,自动去除,但不会自动加
<select id="search" resultMap="userResultMap">
select * from user
<where>
<if test="keyWord != null">
user_name like CONCAT('%',#{keyWord},'%')
or nick_name like CONCAT('%',#{keyWord},'%')
</if>
<if test="time != null">
and gmt_created <![CDATA[ >= ]]> #{time}
</if>
</where>
</select>
choose when otherwise
这是一套标签,功能类似于if...else if...else if...else,也就是单条件查询只取其一
when只取其中一个不会追加多个条件,如果有多个条件,只取最前面的那个,如果都不满足不查数据
<select id="selectBy" resultType="Users">
select * from users
<where>
<choose>
<when test="uid != 0">
uid=#{uid}
</when>
<when test="username != null and username != ''">
username = #{username}
</when>
<when test="password != null and password != ''">
password = #{password}
</when>
<otherwise>
city = #{city}
</otherwise>
</choose>
</where>
</select>
bind
对数据加工,可用于模糊查询
<!--模糊查询方式一:使用bind标签对city中的数据作字符串拼接加工-->
<select id="selectLike" resultType="Users">
<bind name="city" value="'%'+city+'%'"/>
select * from users where city like #{city}
</select>
这里相当于在查询之前给city前后加了%,再进行模糊查询
也可以使用concat()函数进行字符串拼接
<!--模糊查询方式二-->
<select id="selectLike2" resultType="Users">
select * from users where city like concat('%',#{city},'%')
</select>
MyBatis XML 循环语句
MyBatis很好的支持批量插入,使用foreach即可满足
首先创建DAO方法
int batchAdd(@Param("list") List<UserDO> userDOs);
配置XML
<insert id="batchAdd" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (user_name, pwd, nick_name,avatar,gmt_created,gmt_modified)
VALUES
<foreach collection="list" item="it" index="index" separator =",">
(#{it.userName}, #{it.pwd}, #{it.nickName}, #{it.avatar},now(),now())
</foreach >
</insert>
foreach相当于执行力java的for循环,他的属性:
- collection指定集合的上下文参数名称比如这里的
@Param("list")
- item指定遍历的每一个数据的变量,一般叫it,可以使用
it.userName来获取具体的值
- index集合的索引值,从0开始
- separator遍历每条记录并添加分隔符
除了批量插入,使用SQL in查询多个用户时也会使用:
List<UserDO> findByIds(@Param("ids") List<Long> ids);
<select id="findByIds" resultMap="userResultMap">
select * from user
<where>
id in
<foreach item="item" index="index" collection="ids"
open="(" separator="," close=")">
#{item}
</foreach>
</where>
</select>
- open: 表示的是节点开始时自定义的分隔符
- close: 表示是节点结束时自定义的分隔符
执行后:select * from user where id in (?,?,?)
多表关联查询
业务装配实现多表查询
假设现有两表他们通过
cid进行关联
调用mapper层,先查询所有学生,在根据班级信息手动进行组装,称之为业务装配。
//查询学生的信息及其所在的班级信息
@Test
public void findAll(){
List<Student> students = studentMapper.findAll();
for (Student stu: students) {
long cid = stu.getCid();
Clazz clazz = clazzMapper.fingByCid(cid); //手动转配
stu.setClazz(clazz); //每个学生中都有班级信息了
}
System.out.println(students);
}
resultMap的N+1方式实现多表查询 (多对一)
在中关联xml语句映射文件可以使用package
<mappers>
<package name="com.mybatis.mapper"/>
</mappers>
通过<resultMap>定义映射关系,并通过<association>指定对象属性的映射关系。
<mapper namespace="com.mybatis.mapper.StudentMapper">
<resultMap id="studentResultMap" type="Student">
<id property="sid" column="sid"></id>
<!--如果是外键pojo中的属性名与数据库中的列名一致,也不可以省略-->
<result property="sname" column="sname"></result>
<result property="age" column="age"></result>
<result property="cid" column="cid"></result>
<association property="clazz" select="com.mybatis.mapper.ClazzMapper.findByCid" javaType="Clazz" column="cid">
<!--如果是外键pojo中的属性名与数据库中的列名一致,也不可以省略-->
</association>
</resultMap>
<select id="fingAll" resultMap="studentResultMap">
select * from student
</select>
</mapper>
- property是student类中持有的班级属性名
- select指定使用哪个查询语句
- javaType返回值类型
- column表示根据哪个列查
resultMap 的关联方式实现多表查询(多对一)
<mapper namespace="com.mybatis.mapper.StudentMapper">
<resultMap id="studentResultMap" type="Student">
<!--无论pojo实体类中的属性名与数据库列名是否同名,都得写上映射关系-->
<id property="sid" column="sid"></id>
<result property="sname" column="sname"></result>
<result property="age" column="age"></result>
<result property="cid" column="cid"></result>
<association property="clazz" javaType="Clazz">
<!--无论pojo实体类中的属性名与数据库列名是否同名,都得写上映射关系-->
<id property="cid" column="cid"></id>
<result property="cname" column="cname"></result>
<result property="room" column="room"></result> </association>
</resultMap>
<select id="fingAll" resultMap="studentResultMap">
select s.sid,s.sname,s.age,s.cid,c.cname,c.room from student s join clazz c on s.cid = c.cid
</select>
</mapper>
通过定义映射关系, 并通过指定对象属性的映射关系. 可以把看成一个使用. javaType 属性表示当前对象, 可以写全限定路径或别名.
resultMap 的 N+1 方式实现多表查询 (一对多)
实体类:
在班级类中定义一个学生集合, 用于存放该班级的所有学生信息.
ClassMapper查询所有班级信息,
StudentMapper根据班级编号查询学生信息,在
ClassMapper中使用<collection>设置装配。
用于关联一个集合
- property: 实体类属性
- select: 设定要继续引用的查询, namespace+id
- column: 查询时需要传递的列
<mapper namespace="com.mybatis.mapper.ClazzDao">
<resultMap id="m1" type="Clazz">
<!--除了主键列之外,其他的同名列的映射关系可以省略-->
<id property="cid" column="cid"></id>
<collection property="students" select="com.mybatis.mapper.StudentDao.selByCid" column="cid">
</collection>
</resultMap>
<select id="selBy" resultType="Clazz">
select * from clazz where cname=#{param1}
</select>
</mapper>
resultMap 的关联方式实现多表查询 (一对多)
- 在 ClazzMapper.xml 中定义多表连接查询 SQL 语句, 一次性查到需要的所有数据, 包括对应学生的信息。
<select id="selBy2" resultMap="m2">
select c.cid,c.cname,c.room,s.sid,s.sname,s.age from clazz c join student s on c.cid = s.cid where cname=#{param1}
</select>
- 通过定义映射关系, 并通过指定集合属性泛型的映射关系. 可以把看成一个使用. ofType 属性表示集合的泛型, 可以写全限定路径或别名.
<resultMap id="m2" type="Clazz">
<!--属性名与数据库列的映射关系不能省略-->
<id property="cid" column="cid"></id>
<result property="cname" column="cname"></result>
<result property="room" column="room"></result>
<!--关联Student中的各属性;property是Clazz类中持有的属性名,javaType是集合的别名,ofType是集合的泛型类型-->
<collection property="students" javaType="list" ofType="Student">
<!--属性名与数据库列的映射关系不能省略-->
<id property="sid" column="sid"></id>
<result property="sname" column="sname"></result>
<result property="age" column="age"></result>
<result property="cid" column="cid"></result>
</collection>
</resultMap>
MyBatis 进阶
MyBatis可以通过插件来很好的支持分页查询,目前最成熟的方案是
pagehelper这个第三方插件
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.13</version>
</dependency>
改造UserController.getAll方法:
@GetMapping("/users")
@ResponseBody
public List<UserDO> getAll() {
// 设置当前页数为1,以及每页3条记录
Page<UserDO> page = PageHelper.startPage(1, 3).doSelectPage(() -> userDAO.findAll());
return page.getResult();
}
在
doSelectPagelambda方法中执行查询方法,会自动执行分页逻辑,并返回分页对象Page
startPage第一个参数是指定页数,第二个参数是指定每页的记录数
返回类型Page对象时MyBatis封装的分页模型,一般会额外封装一个通用的分页模型Paging用于处理返回值:
package com.comment.model;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分页模型
*/
@Data
public class Paging<R> implements Serializable {
private static final long serialVersionUID = 522660448543880825L;
/**
* 页数
*/
private int pageNum;
/**
* 每页数量
*/
private int pageSize = 15;
/**
* 总页数
*/
private int totalPage;
/**
* 总记录数
*/
private long totalCount;
/**
* 集合数据
*/
private List<R> data;
public Paging() {
}
}
getAll最后改为:
@GetMapping("/users")
@ResponseBody
public Paging<UserDO> getAll() {
// 设置当前页数为1,以及每页3条记录
Page<UserDO> page = PageHelper.startPage(1, 3).doSelectPage(() -> userDAO.findAll());
return new Paging<>(page.getPageNum(), page.getPageSize(), page.getPages(), page.getTotal(), page
.getResult());
}