一 : 什么是MyBatis
MyBatis是更简单完成程序和数据库交互的工具,也就是更简单的操作和读取数据库的工具.
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 去除了几乎所有的 JDBC 代码以及设置参数和获取结果集的动作 . MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通 式 Java 对象)为数据库中的记录 .
二 : MyBatis的作用
JDBC的工作流程非常繁琐 , 主要包括 :
1.创建数据库连接池DataSource
2.通过DataSource获取数据库连接Connection
3. 编写要执行带 ? 占位符的 SQL 语句
4. 通过 Connection 及 SQL 创建操作命令对象 Statement
5. 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值
6. 使用 Statement 执行 SQL 语句
7. 查询操作:返回结果集 ResultSet,更新操作:返回更新的数量
8. 处理结果集
9. 释放资源
而MyBatis可以帮助我们更方便、更快速的操作数据库 !
MyBatis组成
为什么要这样实现呢?
MyBatis要想实现增删改查,就必须要写SQL。写SQL就有两种选择:
- 写到Java的类里面;
- 放在xml里面。
如果选择第一种方式,那么你只能将SQL语句放在双引号中,如果你需要进行比较复杂的查询,比如联表查询,此时双引号中的字符串内容非常长,且里面包含大量的表名,字段名等等,这种做法出错的概率很高;
此时目光转到第二种方式。
三 : 学习重点
MyBatis 学习分为两部分:
- 配置 MyBatis 开发环境;
- 使用MyBatis模式和语法操作数据库 .
四 : 配置MyBatis开发环境
4.1 创建数据库和表
要使用MyBatis 的模式来读取用户表中的所有用户 , 需要先建库和建表 , 示范SQL语句如下 :
-- 创建数据库
drop database if exists myblog;
create database myblog DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use myblog;
-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(50) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1
) default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `myblog`.`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);
查询数据库中用户表信息 :
添加成功 !!!
4.2 创建项目
创建一个Spring MVC项目,并添加MyBatis框架支持.
4.3 设置数据库和MyBatis配置
4.3.1 配置数据库的连接信息(连接哪台数据库)
在resources文件夹下创建application.yml文件。
spring:
datasource:
url: jdbc:mysql://localhost:3306/myblog?characterEncoding=utf8
username: root
password: 111111
driver-class-name: com.mysql.cj.jdbc.Driver
4.3.2 配置MyBatis XML文件存放位置和命名规则
仍然在application.yml中进行操作。
此时同时需要在resources路径下创建mapper文件夹,如下图所示:
五:使用MyBatis实现增删改查功能
5.1 查询所有信息
首先回顾一下我们数据库中的内容 :
下面我们来实现,使用MyBatis进行查询。
实现步骤很简单,只要你理解了MyBatis的设计思路,其实不在话下。
步骤:
1.创建一个接口;
2.创建与上面接口对应的xml文件。
1.创建一个接口。
注意:一定要添加@Mapper注解,表示这是mybatis的接口!!!!!!
package com.example.demo.mapper;
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper //mybatis 接口
public interface UserMapper {
//查询所有的信息
public List<UserInfo> getAll();
}
2.创建与上面接口对应的xml文件。
第一个问题,这个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 namespace="">
</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">
<select id="getAll" resultType="com.example.demo.model.UserInfo">
select * from userinfo
</select>
</mapper>
到此为止,我们已经实现了MyBatis查询数据的功能。
那我们这个实现到底对不对呢?这里我们插入“单元测试”的内容,以检验代码的正确性。
有关单元测试的内容,详见这篇文章:
SpringBoot单元测试
5.2 传参查询
package com.example.demo.mapper;
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper //mybatis 接口
public interface UserMapper {
//查询所有的信息
List<UserInfo> getAll();
//传参查询
UserInfo getUserById(@Param("uid") Integer id);
}
此处需要使用一个注解:@Param
<?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">
<select id="getAll" resultType="com.example.demo.model.UserInfo">
select * from userinfo
</select>
<select id="getUserById" resultType="com.example.demo.model.UserInfo">
select * from userinfo where id=#{uid}
</select>
</mapper>
生成单元测试:
package com.example.demo.mapper;
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest//当前测试的上下文环境为springboot
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void getAll() {
List<UserInfo> list = userMapper.getAll();
for (UserInfo user : list) {
System.out.println(user.toString());
}
}
@Test
void getUserById() {
UserInfo userInfo = userMapper.getUserById(1);
System.out.println(userInfo.toString());
}
}
运行结果:
因为当前确实有id为1的一条记录,所以就成功查询出来了。
类比传参查询,我们就可以实现登录操作了,无非是传参时传递用户名和密码两个参数。
5.3 添加数据
前置工作,先创建一张文章表,代码如下:
-- 创建文章表
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';
创建文章的实体类
package com.example.demo.model;
import lombok.Data;
import java.util.Date;
@Data
public class ArticInfo {
private int id;
private String title;
private String content;
private Date createtime;
private Date updatetime;
private int uid;
private int rcount; // 访问量
private int state; // 状态(预览字段)
//..
}
注意创建位置:
在查询数据时,如果你创建的实体类的对象名和你表中的字段名不一致,那么查询会失败;而在插入数据时,即使你创建的实体类的对象名和表中的字段名不一致,比如表中是titile,你写成了private String t,此时在插入数据时,我们只需要在xml中将插入的对象名写为t,即可成功插入。
基于规范性,我们要求:实体类的对象名和表中的字段名要一一对应!
添加接口及xml实现
此处有2中方法,一种加注解,一种不加注解:
方法一:加注解
package com.example.demo.mapper;
import com.example.demo.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ArticleInfoMapper {
// 添加方法
public int add(@Param("articleInfo") ArticleInfo articleInfo);
}
<?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.ArticleInfoMapper">
<insert id="add">
insert into articleinfo(title,content,uid)
values(#{articleinfo.title},#{articleinfo.content},#{articleinfo.uid});
</insert>
</mapper>
进行单元测试:
方法二:不加注解
进行单元测试:
以上这两种写法下,控制台返回的都是受影响的行数:1。
5.4 添加得到用户的自增id
如何在添加同时得到用户的自增id呢?需要在xml的insert标签中添加两个键值对,具体操作方法如下:
单元测试:
运行结果:
你可能会疑惑,这里的自增id不应该是3吗 ?没错,事实是我在这中间测试时候添加了两条数据,之后又删除了它们,所以你会看到自增id是5。
5.5 删除数据
删除功能很简单,仍然沿袭前面的步骤:
- 添加方法;
- 修改xml文件;
- 编写单元测试。
同理也可将id=2的文章删除。
5.6 修改数据
单元测试:
我们上面的代码都是MyBatis的操作,但是没有提供相应的入口。下面我简单介绍一下如何提供入口,即创建控制器层和服务器层。以查询用户信息为例:
package com.example.demo.controller;
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController//标识其为控制器层
@RequestMapping("/user")//访问路由
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/getall")//访问路由
public List<UserInfo> getUser(){
return userService.getAll();//调用服务层的getAll方法
}
}
package com.example.demo.service;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Service//标识其为服务层
public class UserService {
@Autowired
private UserMapper userMapper;
public List<UserInfo> getAll() {
return userMapper.getAll();//调用接口的getAll方法
}
}
然后userMapper基于MyBatis,在数据库中查找相应的内容。启动DemoApplication类:
成功查询出所有信息!
六:参数占位符问题
前面在进行参数匹配时,我们使用了#{ } 。实际上我们还有另一个参数占位符,就是${ }。
- #{}:预编译处理。
- ${}:字符直接替换。
6.1 #{}
预编译处理是指:MyBatis 在处理#{}时,会将 SQL 中的 #{} 替换为?,使⽤ PreparedStatement的 set ⽅法来赋值。
#{}: 解析为SQL时,会将形参变量的值取出,并自动给其添加引号。 例如:当实参username="Amy"时,传入下Mapper映射文件后:
......
<select id="findByName">
SELECT * FROM user WHERE username=#{value}
</select>
....
SQL将解析为:
SELECT * FROM user WHERE username="Amy"
6.2 ${}
直接替换是指:MyBatis 在处理 ${} 时,就是把 ${} 替换成变量的值。
${}: 解析为SQL时,将形参变量的值直接取出,直接拼接显示在SQL中
例如:当实参username="Amy"时,传入下Mapper映射文件后:
......
<select id="findByName">
SELECT * FROM user WHERE username=${value}
</select>
....
SQL将解析如下:
SELECT * FROM user WHERE username=Amy
显然该SQL无法正常执行,故需要在mppaer映射文件中的${value}前后手动添加引号,如下所示:
......
<select id="findByName" parameterType="String" resultMap="studentResultMap">
SELECT * FROM user WHERE username='${value}'
</select>
....
SQL将解析为:
SELECT * FROM user WHERE username='Amy'
6.3 SQL注入
${}方式是将形参和SQL语句直接拼接形成完整的SQL命令后,再进行编译,所以可以通过精心设计的形参变量的值,来改变原SQL语句的使用意图从而产生安全隐患,即为SQL注入攻击。现举例说明:
当前我们数据库中有一条数据,其用户名和密码都是"admin"。正常情况下,我们使用#{}作为参数占位符号,此时正确输入用户名和密码,查询可以成功,代码如下:
1.添加方法:
//登录查询,需要提供用户名和密码
UserInfo getUserToLogin(@Param("username") String username,@Param("password")String password);
2.配置xml文件
<select id="getUserToLogin" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username=#{username} and password=#{password}
</select>
3.进行单元测试
@Test
void getUserToLogin() {
UserInfo userInfo = userMapper.getUserToLogin("admin","admin");
System.out.println(userInfo);
}
查询结果:
此时没有问题。
如果我们错误地输入了用户名或密码,当然不能查询出结果,如下所示:
现在我们使用${}作为参数占位符号,此时正确输入用户名和密码,查询可以成功,代码如下:
<select id="getUserToLogin" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username='${username}' and password='${password}'
</select>
查询结果:
如果我们错误地输入了用户名或密码,也不能查询出结果。但是,如果我们稍微设计一下输入的密码,那么即使用户名和密码都是错误地,仍然能够查询出结果,如下图所示:
@Test
void getUserToLogin() {
UserInfo userInfo = userMapper.getUserToLogin("admin","' or 1='1");
System.out.println(userInfo);
}
这就是使用${}作为参数占位符所存在的最大的问题,它是不安全的,存在SQL注入的风险。
那么${}这个占位符是否一无是处呢?有一种场景下是需要使用到 ${} 的。比如我们要实现对数据库中的数据进行排序,但是需要以传参指定排序的方式。代码如下:
先向数据库中插入一条数据,以便于观察结果:
// 查询所有的信息(根据排序条件进行排序)
public List<UserInfo> getAllByOrder(@Param("order") String order);
<select id="getAllByOrder" resultType="com.example.demo.model.UserInfo">
select * from userinfo order id by ${order}
</select>
@Test
void getAllByOrder() {
List<UserInfo> list = userMapper.getAllByOrder("desc");
for (UserInfo userInfo : list) {
System.out.println(userInfo);
}
}
运行结果:
当需要传入关键字时,我们就要用到${},因为如果使用#{}的话,在进行预处理阶段,desc这样的关键字会被加上双引号,直接解析为字符串了。
6.4 like查询
like 使⽤ #{} 报错。
<select id="findUserByName2" resultType="com.example.demo.model.User">
select * from userinfo where username like '%#{username}%';
</select>
相当于: select * from userinfo where username like ‘%‘username’%’;
此时可以考虑使用mysql的内置函数concat处理,代码如下:
<select id="findUserByName3" resultType="com.example.demo.model.User">
select * from userinfo where username like concat('%',#{usernam
e},'%');
</select>
6.5 多表查询
例如在articleinfo表查询出某篇文章的信息后,在userinfo表中查询出这篇文章的作者。
步骤如下:
单元测试:
查询此时数据库中表的信息:
查看单元测试结果:
6.6 字段重命名
程序中的属性和数据库中的字段名不一致时,可使用resultMap来解决。当你和数据库管理员分属于不同部门时,如果他使用了一些“魔法数字”作为字段名,比如将currentTime命名为ct,这显然不太符合规范。此时我们可以在程序中使用不同的字段名,并返回字典映射:resultMap。
package com.example.demo.model;
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {
private int author_id;
private String username;
private String password;
private String photo;
private Date createtime;
private Date updatetime;
private int state;
}
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<id column="id" property="author_id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
</resultMap>
<select id="getAll" resultMap="BaseMap">
select * from userinfo
</select>
单元测试:
@Test
void getAll() {
List<UserInfo> list = userMapper.getAll();
for (UserInfo user : list) {
System.out.println(user.toString());
}
}
七:MyBatis动态SQL
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
你可以参阅MyBatis的官方文档,进行动态SQL的学习。
7.1 < if >标签
在注册⽤户的时候,通常存在一些必填项字段和选填项字段,例如手机号、用户名、密码通常都是必填字段,而年龄、性别、生日等则是选填项字段。那如果在添加⽤户的时候有不确定的字段传⼊,程序应该如何实现呢?此时就需要使用到MyBatis的动态标签< if >了。
例如userinfo表中的photo字段就是一个非必填字段,具体实现如下:
添加方法:
//添加用户
public int add(@Param("username") String username,
@Param("password") String password,
@Param("photo") String photo);
配置xml文件:
<insert id="add">
insert into userinfo(username,
<if test="photo != null">
photo,
</if>
password)
values(#{username},
<if test="photo != null">
#{photo},
</if>
#{password})
</insert>
进行单元测试:
@Test
void add() {
int result = userMapper.add("zhangsan","123456",null);
System.out.println("受影响行数 " + result);
}
注意:
7.2 < trim >标签
之前的插⼊⽤户功能,只是有⼀个photo字段可能是选填项,如果所有字段都是⾮必填项,就考虑使⽤< trim >标签结合< if >标签,对多个字段都采取动态⽣成的⽅式。
调整 UserMapper.xml 的插⼊语句为:
<insert id="add">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username != null">
username,
</if>
<if test="photo != null">
photo,
</if>
<if test="password != null">
password,
</if>
</trim>
<trim prefix="values(" suffix=")" suffixOverrides=",">
<if test="username != null">
#{username},
</if>
<if test="photo != null">
#{photo},
</if>
<if test="password != null">
#{password},
</if>
</trim>
</insert>
进行单元测试:
@Test
void add() {
int result = userMapper.add("telier","156552","telier.png");
System.out.println("受影响行数 " + result);
}
解释:
7.3 < where >标签
传入的用户对象,根据属性做 where 条件查询,用户对象中属性不为 null 的,都为查询条件。
例如此根据用户名查询符合条件的用户,如果用户名不匹配,应该显示数据库中的所有用户:
// 根据用户姓名匹配查询
public List<UserInfo> getUserByName(@Param("username") String username);
<select id="getUserByName" resultType="com.example.demo.model.UserInfo">
select * from userinfo
<where>
<if test="username != null">
and username=#{username}
</if>
</where>
</select>
单元测试:
测试一:
@Test
void getUserByName() {
List<UserInfo> list = userMapper.getUserByName(null);
for (UserInfo user: list) {
System.out.println(user);
}
}
运行结果:
测试二:
void getUserByName() {
List<UserInfo> list = userMapper.getUserByName("zhangsan");
for (UserInfo user: list) {
System.out.println(user);
}
}
运行结果:
7.4 < set >标签
根据传入的用户对象属性来更新用户数据,可以使用标签来指定动态内容。
示例:UserMapper 接口中修改用户方法:根据传入的用户 id 属性,修改其他不为 null 的属性。
//根据用户id修改用户数据
int updateById(UserInfo userInfo);
<update id="updateById" parameterType="com.example.demo.model.UserInfo">
update userInfo
<set>
<if test="username != null">
username=#{username},
</if>
<if test="password != null">
password=#{password},
</if>
<if test="photo != null">
photo=#{photo},
</if>
<if test="createtime != null">
createtime=#{createtime},
</if>
<if test="updatetime != null">
updatetime=#{updatetime},
</if>
<if test="state != null">
state=#{state},
</if>
</set>
where id=#{id}
</update>
进行单元测试:
@Test
void updateById() {
UserInfo userInfo = userMapper.getUserById(7);
userInfo.setUsername("caixukun");
userInfo.setPhoto("jinitaimei.png");
int result = userMapper.updateById(userInfo);
System.out.println("受影响行数 " + result);
}
测试前用户表中的数据:
测试后用户表中的数据:
7.5 < for each >标签
对集合进行遍历时可以使用该标签。
示例:根据多个用户 id 来删除用户数据。
// 多条用户的删除
public int delByIds(List<Integer> ids);
<delete id="delByIds">
delete from userinfo where id in
<foreach collection="ids" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</delete>
进行单元测试:
@Test
void delByIds() {
List<Integer> list = new ArrayList<>();
list.add(6);
list.add(8);
list.add(9);
userMapper.delByIds(list);
}
已经成功删除了id为6,8,9号的用户。
以上就是MyBatis的全部内容。