1.MyBatis 是什么?
MyBatis(前身IBatis)定义:MyBatis 是一款优秀的持久层框架(ORM框架)
MyBatis支持的操作方式:
1.支持XML的操作方式(MyBatis 3.1 之前)【主流的操作方式】。
2.使用注解实现数据的操作(MyBatis 3.1 之后)
简单来说 MyBatis 是更简单完成程序和数据库交互的工具,也就是更简单的操作和读取数据库工具。
2.学习目标:
- 搭建一个MyBatis开发环境(建立在Spring Boot 和Spring MVC 的基础上)。
- 基于MyBatis的开发模式,操作数据库。
3.搭建MyBaits开发环境
3.1 创建数据库和测试数据
-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use mycnblog;
-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(32) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1
) default charset 'utf8mb4';
-- 创建文章表
drop table if exists articleinfo;
create table articleinfo(
id int primary key auto_increment,
title varchar(100) not null,
content text not null,
createtime datetime default now(),
updatetime datetime default now(),
uid int not null,
rcount int not null default 1,
`state` int default 1
)default charset 'utf8mb4';
-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(
vid int primary key,
`title` varchar(250),
`url` varchar(1000),
createtime datetime default now(),
updatetime datetime default now(),
uid int
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);
-- 文章添加测试数据
insert into articleinfo(title,content,uid)
values('Java','Java正文',1);
-- 添加视频
insert into videoinfo(vid,title,url,uid) values(1,'java title','http://www.baidu.com',1);
3.2在项目中添加Mybatis框架
3.2.1新项目添加Mybatis
s框架
耐心等待项目加载完成即可。
3.2.2原有项目添加Mybatis
前置条件:添加插件Editstarters:
之后导入包即可。
注意事项:
经过以上操作,就创建了SSM(Spring Boot/Spring MVC/MyBatis)项目,但此时因为没有配置连接的数据库服务器地址,如果此时启动项目,就会报错
3.3 配置连接字符串和MyBatis的XML报错路径
此步骤需要进行两项设置,数据库连接字符串设置和 MyBatis 的 XML 文件配置。
3.3.1 配置连接字符串
如果是 application.yml 添加如下内容:
#配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1/mycnblog?characterEncoding=utf8mb4
username: root
password: 12345678
driver-class-name: com.mysql.cj.jdbc.Driver
3.3.2 配置MyBatis中XML保存路径(使用XML方式操作MyBatis)
#配置MyBatis中XML保存路径
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
4.使用MyBatis模式实现增删改查(CRUD)操作
MyBatis模式就是两个文件:@Mapper文件(定义方法,没有方法实现)+XML会实现@Mapper的方法体
4.1 创建@Mapper接口文件(方法声明,没有方法实现)
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
//查询方法定义完成
public UserInfo getUserById(Integer id);
}
4.2 使用XML实现@Mapper接口的方法
<?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.example.demo.mapper.UserMapper">
<!-- 查询单条数据(根据id) -->
<select id="getUserById" resultType="com.example.demo.model.UserInfo">
<!-- select * from userinfo where id=#{id}-->
select * from userinfo where id=#{id}
</select>
</mapper>
关于IDEA专业版@Autowired报错但不影响执行的问题原因分析:
@Autowired属于Spring注解,而@Mapper属于MyBatis的注解,Spring中的@Autowired不能识别MyBatis的@Mapper注解,所以会报错。
解决方案:
将Service里的Spring的@Autowireg更换为JDK的@Resource
4.3添加功能实现
方法实现:
<!-- 用户添加(返回受影响的行数) -->
<insert id="add">
insert into userinfo(username,password)
values(#{username},#{password})
</insert>
<!-- 用户添加(返回自增 ID) -->
<insert id="add2" useGeneratedKeys="true" keyProperty="id">
insert into userinfo(username,password) values(#{username},#{password})
</insert>
方法声明:
//添加用户(返回受影响行数)
public int add(String username,String password);
// 添加用户 2(返回的自增 ID)
public void add2(UserInfo userInfo);
4.4修改功能实现
方法声明:
//修改操作(根据id修改username,返回受影响的行数)
public int update(@Param("id") int id, @Param("username") String username);
方法实现:
<!-- 根据 id 修改 username(返回受影响的行数) -->
<update id="update">
update userinfo set username=#{username} where id=#{id}
</update>
4.5 删除用户操作
方法声明:
//删除方法(返回受影响的行数)
public int del(@Param("id") int id);
方法实现:
<!-- 根据 id 进行删除(返回受影响的行数) -->
<delete id="del">
delete from userinfo where id=#{id}
</delete>
扩展功能:单元测试之后,将测试的数据进行回滚,也就是使用单元测试不污染数据库的数据
@Transactional 注解
@Transactional // 单元测试的类或者方法上加上此注解,表示当前测试的数据不会影响数据库
@Test
void del() {
int result = userMapper.del(3);
Assertions.assertEquals(1, result);
}
}
5.查询操作
参数占位符 #{} 和 ${}
#{}:预编译处理,不存在安全问题。
${}:字符直接替换,存在安全问题。
${}使用场景:当传递的是一个SQL语句时(而非一个某个参数的值),那么只能使用 ${}的形式,比如传递排序的 desc 或者是 asc 的时候,他是一个SQL语句,而非某一个参数的值。
<!-- 根据时间排序查询所有的用户 -->
<select id="getAllOrderByCreateTime" resultType="com.example.demo.model.UserInfo">
select * from userinfo order by createtime ${order}
</select>
预编译处理是指:MyBatis 在处理#{}时,会将 SQL 中的 #{} 替换为?号,使用 PreparedStatement 的set 方法来赋值。
直接替换:是MyBatis 在处理 ${} 时,就是把 ${} 替换成变量的值。
${} 优点
使用 ${sort} 可以实现排序查询,而使用 #{sort} 就不能实现排序查询了,因为当使用 #{sort} 查询时,如果传递的值为 String 则会加单引号,就会导致 sql 错误。
SQL 注入问题
方法声明:
//登录方法
public UserInfo login(@Param("username") String username,
@Param("password") String password);
方法实现(使用${}):
<!-- 登录方法 -->
<select id="login" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username='${username}' and password='${password}'
</select>
结论:用于查询的字段,尽量使用 #{} 预查询的方式。 |
扩充知识:打印MyBatis最终执行的SQL
如图配置即可:
like 查询
1.like 使用 #{} 报错
<!-- 根据用户名称进行模糊查询 -->
<select id="getUserByLikeName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like '%#{username}%'
</select>
2.使用 ${}
<!-- 根据用户名称进行模糊查询 -->
<select id="getUserByLikeName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like '%${username}%'
</select>
可以实现功能,但是存在安全风险。
3.可以考虑使用 mysql 的内置函数 concat() 来处理,实现代码如下:
<!-- 根据用户名称进行模糊查询 -->
<select id="getUserByLikeName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like concat('%',#{username},'%')
</select>
5.2多表查询
对于 查询标签来说至少需要两个属性:
- id 属性:用于标识实现接口中的那个方法;
-
结果映射属性:结果映射有两种实现标签:resultMap和 resultType
5.2.1返回类型:resultType
绝大数查询场景可以使用 resultType 进行返回,它的优点是使用方便,直接定义到某个实体类即可。
5.2.2返回字典映射:resultMap
resultMap 使用场景:
- 字段名称和程序中的属性名不同的情况,可使用 resultMap 配置映射;
- 多表关联查询(一对一和一对多)关系可以使用 resultMap 映射并查询数据。
字段名和属性名不同的情况
程序中属性如下:
mapper.xml 代码如下:
<select id="getAll" resultType="com.example.demo.model.ArticleInfo">
select * from articleinfo
</select>
查询出来name=NULL;
resultMap 的使用如下:
resultMap 基础使用:
5.2.3多表查询
5.2.3.1 一对一的表映射
一对一映射要使用 association标签,具体实现如下(一篇文章只对应一个作者):
ArticleMapper.xml实现代码:
<resultMap id="BaseMap" type="com.example.demo.model.ArticleInfo">
<id column="id" property="id"></id>
<result column="title" property="name"></result>
<result column="content" property="content"></result>
<result column="createtime" property="createtime"></result>
<result column="updatetime" property="updatetime"></result>
<result column="uid" property="uid"></result>
<result column="rcount" property="rcount"></result>
<result column="state" property="state"></result>
<!-- 一对一关系的实现 -->
<association property="userInfo"
resultMap="com.example.demo.mapper.UserMapper.BaseMap"
columnPrefix="u_">
</association>
</resultMap>
<select id="getAll2" resultMap="BaseMap">
select a.*,u.id u_id,u.username u_username,u.password u_password from articleinfo a left join userinfo u on
a.uid=u.id
</select>
以上使用 association标签,表示一对一的结果映射:
- property 属性:指定 Article 中对应的属性,即用户。
- resultMap 属性:指定关联的结果集映射,将基于该映射配置来组织用户数据。
- columnPrefix 属性:绑定一对一对象时,是通过columnPrefix+association.resultMap.column 来映射结果集字段。
association.resultMap.column是指 标签中 resultMap属性,对应的结果集映射中,column字段。
userMapper.xml代码:
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="photo" property="photo"></result>
<result column="createtime" property="createtime"></result>
<result column="updatetime" property="updatetime"></result>
<result column="state" property="state"></result>
<collection property="alist"
resultMap="com.example.demo.mapper.ArticleMapper.BaseMap"
columnPrefix="a_">
</collection>
</resultMap>
实体类:
5.2.3.2一对多:一个用户多篇文章案例
一对多需要使用 collection 标签,用法和 association相同,如下所示:
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="photo" property="photo"></result>
<result column="createtime" property="createtime"></result>
<result column="updatetime" property="updatetime"></result>
<result column="state" property="state"></result>
<!-- 一对多关系的实现 -->
<collection property="alist"
resultMap="com.example.demo.mapper.ArticleMapper.BaseMap"
columnPrefix="a_">
</collection>
</resultMap>
<select id="getAll2" resultMap="BaseMap">
select u.*,a.id a_id,a.title a_title,a.content a_content
from userinfo u
left join articleinfo a
on u.id=a.uid
</select>
6.动态SQL使用
动态 sql 是Mybatis的强大特性之一,能够完成不同条件下不同的 sql 拼接。
6.1 < if> 标签
注册分为两种字段:必填字段和非必填字段,那如果在添加用户的时候有不确定的字段传入,程序应该如何实现呢?
这个时候就需要使用动态标签 < if> 来判断了,比如添加的时候性别 sex 为非必填字段,具体实现如下:
<!-- 用户添加(使用动态 sql if -->
<insert id="add3">
insert into userinfo(username,
<if test="photo!=null">
photo,
</if>
password)
values(#{username},
<if test="photo!=null">
#{photo},
</if>
#{password})
</insert>
6.2 < trim> 标签
之前的插入用户功能,只是有一个 sex 字段可能是选填项,如果有多个字段,一般考虑使用 < trim> 标签结合 < if> 标签,对多个字段都采取动态生成的方式。
trim 标签中有如下属性:
- prefix:表示整个语句块,以prefix的值作为前缀
- suffix:表示整个语句块,以suffix的值作为后缀
- prefixOverrides:表示整个语句块要去除掉的前缀
- suffixOverrides:表示整个语句块要去除掉的后缀
<!-- 用户添加(使用动态 sql trim + if -->
<insert id="add4">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username!=null">
username,
</if>
<if test="password!=null">
password,
</if>
<if test="photo!=null">
photo
</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username!=null">
#{username},
</if>
<if test="password!=null">
#{password},
</if>
<if test="photo!=null">
#{photo}
</if>
</trim>
</insert>
在以上 sql 动态解析时,会将第一个 trim 部分做如下处理:
- 基于 prefix 配置,开始部分加上 (
- 基于 suffix 配置,结束部分加上 )
- 多个 组织的语句都以 , 结尾,在最后拼接好的字符串还会以 , 结尾,会基于suffixOverrides 配置去掉最后一个 ,
- 注意 <if test="username!=null> 中的 username是传入对象的属性
6.3 < where> 标签
<!-- 根据名称或密码查询用户列表(使用动态 sql where) -->
<select id="getListByNameOrPwd" resultMap="BaseMap">
select * from userinfo
<where>
<if test="username!=null">
username=#{username} and
</if>
<if test="password!=null">
password=#{password}
</if>
</where>
</select>
情景一:当不传递任何参数时,传递的SQL如下:
情景二:传递前面一个参数
使用以下代码可以解决这个问题:
<!-- 根据用户名称进行模糊查询 -->
<select id="getUserByLikeName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like concat('%',#{username},'%')
</select>
<!-- 根据名称或密码查询用户列表(使用动态 sql where) -->
<select id="getListByNameOrPwd" resultMap="BaseMap">
select * from userinfo
<where>
<if test="username!=null">
username=#{username}
</if>
<if test="password!=null">
and password=#{password}
</if>
</where>
</select>
情景三:传递后面一个参数
<!-- 根据用户名称进行模糊查询 -->
<select id="getUserByLikeName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like concat('%',#{username},'%')
</select>
<!-- 根据名称或密码查询用户列表(使用动态 sql where) -->
<select id="getListByNameOrPwd" resultMap="BaseMap">
select * from userinfo
<where>
<if test="username!=null">
username=#{username}
</if>
<if test="password!=null">
and password=#{password}
</if>
</where>
</select>
也可以使用< trim>标签进行该写代码:
<!-- 根据名称或密码查询用户列表(使用动态 sql where) -->
<select id="getListByNameOrPwd" resultMap="BaseMap">
select * from userinfo
<trim prefix="where" prefixOverrides="and">
<if test="username!=null">
username=#{username}
</if>
<if test="password!=null">
and password=#{password}
</if>
</trim>
</select>
6.4< set>标签
根据传入的用户对象属性来更新用户数据,可以使用< set>标签来指定动态内容。
<update id="updateById">
update userinfo
<set>
<if test="username!=null">
username=#{username},
</if>
<if test="password!=null">
password=#{password},
</if>
<if test="photo!=null">
photo=#{photo}
</if>
</set>
where id=#{id}
</update>
执行的SQL:
set标签已经自动将最后一个逗号删除掉
使用< trim>标签也可以实现< set>的功能:
<update id="updateById">
update userinfo
<trim prefix="set" suffixOverrides=",">
<if test="username!=null">
username=#{username},
</if>
<if test="password!=null">
password=#{password},
</if>
<if test="photo!=null">
photo=#{photo}
</if>
</trim>
where id=#{id}
</update>
6.5 < foreach>标签
对集合进行遍历时可以使用该标签。< foreach>标签有如下属性:
- collection:绑定方法参数中的集合,如 List,Set,Map或数组对象
- item:遍历时的每一个对象
- open:语句块开头的字符串
- close:语句块结束的字符串
- separator:每次遍历之间间隔的字符串
接口方法:
public int delByIds(List<Integer> ids);
删除SQL:
<delete id="delByIds">
delete from userinfo where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>