Spring MyBatis 在 Spring 中是一个非常重要的知识,将前端传递的数据存储起来,或者查询数据库⾥⾯的数据;简单来说MyBatis 是更简单完成程序和数据库交互的⼯具,也就是更简单的操作和读取数据库⼯具。
MyBatis 是一款优秀的ORM 持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
这里有个 MyBatis 的官方文档 MyBatis文档
对于程序来说有两个重要的组成部分:
1、后端程序
2、数据库
这两个重要部分需要连接通讯,都会依赖的数据库连接工具,比如说之前文章写过的 JDBC(点击这个连接 JDBC 跳转),但是 JDBC 步骤繁杂麻烦,于是 Spring 就想出用更简单的,更方便,更快捷的方式来 操作数据库,那么 MyBatis 就产生了。
基本上掌握好这两大步骤,MyBatis 就过关了。
1、引入 MyBatis 依赖在 Spring 项目中,不管是用什么方式引入。
a)、可以在新项目中添加 MyBatis 框架
重要的就是在 MyBatis 创建的时候要添加两个依赖,一个是数据库的映射,因为数据库不止有SQL这一个语法,还有其他数据库的语法,一个就是 JDBC 驱动。
b)、在老项目中添加依赖,社区版需要添加一个插件 Edit Starters,在 pom.xml 中右键选择就行了。
依赖添加完后需要手动在 pom.xml 刷新一下让依赖加载进项目中,点击在最右边的 Maven 在 Lifecycle 文件夹下有一个 install ,点击install 等待加载。
在新建的 MyBatis 项目中,如果没有配置数据库的连接信息,那么项目是启动不起来的。
1、配置数据库连接信息:
这里的配置比 JDBC 多了一个操作,有一个属性为 driver-class-name
因为这只是一个映射连接,在连接数据库的时候要做出是连接那个数据库类型;我这里用的就是 MySQL 的类型,就在属性的位置写 MySQL 的类型即可。
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8&useSSL=true
username: root
password: 012345678
driver-class-name: com.mysql.jdbc.Driver
注意:
1、还有一个注意的地方就是 url 最后的加密useSSL,如果启动程序提示是SSL有报错信息,那么就修改为false 就行了。有些时候可能电脑或者说程序不支持加密就会报错,改为false即可。
2、driver-class-name 这个属性中 MySQL8之前使用 com.mysql.jdbc.Driver ,如果是 MySQL8之后 com.mysql.cj.jdbc.Driver
2、配置 MyBatis 中的 XML 保存路径
首先在看看 MyBatis 在整个框架中的定位框架流程图
前端通过 Ajax 发送给后端,后端通过 Controller 检验,通过后调用服务层 Service 来找对应的接口,在进行调用来到了持久层,Mapper 是持久层中的实现之一;持久层里面又分为两小步,interFace 这里只是一个声明,.xml是对这个声明对应的实现接口,并不是普通Spring xml 的配置,这个xml里面写的都是 SQl 代码,总结下来,一个声明对应一个 .xml文件,最中生成 SQL 调用 JDBC 来操作数据库。
而我们这里就需要在 .xml 中配置文件路径(MyBatis 运行时使用):
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
classpath: 当前文件里面创建 mapper 文件夹,这个文件夹可以取任何的名字配置路径的时候记得把你取的文件名写在上面 mapper不是固定的,最后是**Mapper.xml,也可以直接不要Mapper,只要你mapper文件夹里面没有放其他的文件,如果是文件夹里面有其他文件 通配符后缀加上 Mapper 容易区分。
用后端开发的流程思路,来用 MyBatis 程序连接数据库查询用户的功能流程代码,要先有已经准备好的数据库:
1、首先添加实体类(自行创建 model 文件,添加实体类);
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int state;
}
2、添加 Controller(验证参数的操作)
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@ResponseBody
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/getUser") // localhost:8080/getUser
public UserInfo getUserById(Integer id) {
UserInfo userInfo = null;
if (id != null && id > 0) {
userInfo = userService.getUserByid(id);
}
return userInfo;
}
}
3、添加 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;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public UserInfo getUserByid(Integer id) {
return userMapper.getUserById(id);
}
}
4、添加 mapper 接口,数据持久层接口的定义,也就是在 interface 里面声明方法
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper //一定要加的注解
public interface UserMapper {
public UserInfo getUserById(Integer id); // 这只是一个声明,具体实现在 xml 里面
}
5、在 .xml 中写 interface 声明的具体实现,这里先说一下 MyBatis配置文件 xml 的具体格式
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xxxxx">
mapper>
具体实现代码:
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="getUserById" resultType="com.example.demo.model.UserInfo">
select * from userinfo
select>
mapper>
以上的图是 MyBatis 写代码基础的流程图。以下是项目目录以及数据库的连接操作:
以上是使用 MyBatis 的整个流程,从创建到配置,配置好了过后再试代码流程,以上虽然基础且是重要的,所谓地基不牢,最后地洞山摇。
java 里面有个单元测试,单元测试指的是在一个项目里面对某一个最小的单元进行测试。
单元测试的代码:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest // 必须加,表示是用 SpringBoot 类型的测试
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void getUserById() {
System.out.println(userMapper.getUserById(1)); // 测试这个方法是否运行正常
}
}
最后执行的结果是 绿色的√ 说明程序没有问题。
为什么介绍单元测试,因为后边将会进行 增删改 操作,需要用单元测试验证每一步的操作有没有成功。
他们之间对应的 MyBatis 标签如下:
1、普通的添加方法,返回受影响的行数
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper //一定要加的注解
public interface UserMapper {
// 添加操作,受影响的行数
public int addUser(String username, String password);
}
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">
<insert id="addUser">
insert into userinfo(username,password) values (#{username},#{password})
insert>
mapper>
这里添加参数的方式前建议使用 # 号,最好不要使用 $ 符号。
@SpringBootTest // 必须加,表示是用 SpringBoot 类型的测试
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void addUser() {
int flag = userMapper.addUser("java","987");
System.out.println("受影响的行数:"+ flag); // 返回受影响的行数
}
}
2、使用传递对象,返回自增的id。(当必须传递的参数过多的时候可以选择传递对象)
当前表中只有俩个id:
+----+----------+----------+-------+-------------+------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+------------+
| 1 | admin | admin | | XXXXXXXXXXX | XXXXXXXXXXX| 1 |
| 2 | java | 987 | | XXXXXXXXXXX | XXXXXXXXXXX| 1 |
+----+----------+----------+-------+---------------------+------------+
修改的代码:
在具体实现的 .xml 中有两个重要的属性:
在测试中,Userinfo 对象已经自动拿到了自动新增的属性 id,获取新增属性id即可。
注意区分:
1、keyProperty:程序里面的属性(自增最后赋值给那个属性),,keyColumn:表中的字段(表里面的那个属性进行自增的
2、property:在 MyBatis 中都是与当前类有关,Column:与当前表有关
当前表中就有3个id:
+----+----------+----------+-------+------------+-----------+-------+
| id | username | password | photo | createtime | updatetime| state |
+----+----------+----------+-------+------------------------+-------+
| 1 | admin | admin | | xxxxxxxxxx | xxxxxxxxx | 1 |
| 2 | java | 987 | | xxxxxxxxxx | xxxxxxxxx | 1 |
| 3 | python | 345 | | xxxxxxxxxx | xxxxxxxxx | 1 |
+----+----------+----------+-------+---------------------+----------+
修改功能也是一样的操作
1、Interface 声明方法:
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper //一定要加的注解
public interface UserMapper {
// 修改操作,
public int upData(UserInfo userInfo);
}
2、.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">
<!--namespace:是实现接口的完整的包名+接口名,你的 interface 完整路径-->
<mapper namespace="com.example.demo.mapper.UserMapper">
<update id="upData">
update userinfo
set username=#{username},password=#{password}
where id = #{id};
</update>
</mapper>
3、单元测试代码:
@SpringBootTest// 必须加,表示是用 SpringBoot 类型的测试
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void upData() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("MySQL");
userInfo.setPassword("888");
userInfo.setId(3);
int result = userMapper.upData(userInfo);
System.out.println("修改的结果是:"+result);
}
}
始终就是这三个步骤,后面的删除操作也是一样的。
方法的声明——>.xml方法的具体实现——>单元测试是否通过
在传递参数的时候有两种传法,一种是#{} 一种是 ${},他们之间的区别:
1、正因为如此,在 String 类型底下,$ 符号是会报错的;
2、${} 符号有他另外的用途,当数据库需要排序的时候 asc /desc , 就不能添加双引号。所以当用的关键字的场景 #{} 是不能实现的。
但是这里有个严重的问题就是使用 ${} 存在安全隐患问题.
因为既然 ${} 是直接替换,
有的人就会想那不直接在外面加上 ‘${}’ 不也是一样的?所说理论上是行得通的,实际操作也是行得通的
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="sel" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username = '${username}' and '${password}'
select>
mapper>
通过测试代码是可以执行通过并能查询到的;但是有一些黑客会利用这种漏洞,使用一些特殊的语法来破解,本来是要输入正确的username和password 你才能访问信息,黑客就会用特殊语法来攻破你的语法来访问信息,就比如这个语法: ‘’ or 1 =‘1’ 。(这种行也叫做 SQL 注入)
select * from userinfo where username = "java" and password = '' or 1 ='1';
按逻辑和道理来说这个语法是登录不进去查询不到用户信息的,可是当在 MySQL 输入的时候确实能查询的。
最后的结论:
使用 ${} 存在安全隐患(SQL 注入问题),而 #{} 不存在安全隐患问题。
在学习了 MyBatis 的时候通常都会这样写 sql 语句:
select * from userinfo where username like "%#{username}%"
但是like 使用 #{} 是会报错的,因为进行预处理后就变成了中间的信息自动加上引号:
select * from userinfo where username like "%"java"%"
在 MySQL 中这样的语法是会报错的;那 # 号不行,按道理说 $ 是可以运行的。
select * from userinfo where username like "%${username}%"
实际运行了一下也确实可以,但是上文就说了,$ 符号他是有安全隐患的,所以在模糊匹配 like 中需要使用 MySQL 中的内置函数 concat()
来处理,concat()
就是起到连接作用。
mysql> select concat("1","4","3");
+---------------------+
| concat("1","4","3") |
+---------------------+
| 143 |
+---------------------+
1 row in set (0.00 sec)
所以正确的写法是:
select * from userinfo where username like concat('%',#{username},'%')
在 like 模糊查询中就 concat()连接方法需要注意一下。
在 MyBatis 里面不存在多对多的概念,只存在一对一,或者是一对多,在MyBatis 中一般最多是双方的关系,多对多的话中间会有一个中间表,解决方案只能一张一张的查询最后在程序里做组装。
多表查询中一张表里面有属性是一个对象(另一张表),那么使用 resultType 就会让它的属性(另一张表)为null(但是程序不会报错);所以在多表的查询中就得使用 < resultMap > 和 < association > 标签
一对一类似于,一篇文章为这一个作者写的。
<resultMap id="BaseMap" type="com.example.demo.model.ArticleInfo">
<id property="id" column="id">id>
<result property="title" column="title">result>
<result property="content" column="content">result>
<result property="createtime" column="createtime">result>
<result property="updatetime" column="updatetime">result>
<result property="uid" column="uid">result>
<result property="rcount" column="rcount">result>
<result property="state" column="state">result>
<association property="userInfo"
resultMap="com.example.demo.mapper.UserMapper.BaseMap"
columnPrefix="u_">association>
resultMap>
resultMap id 的名字可以随便取,取的名字需要再 select 里面resultMap 属性里面一致;
type:就是映射表的路径
主键定情况下开头就是属性的名字,非主键就是 result
property:是对象里面对应的字段名称
column:是数据库里面与程序对象里面映射的属性,当两个属性名称不一样的时候按照上诉方法映射
associatetion:这个标签是用来连接其他对象的,就是一张表里面有其他对象就需要用
associatetion->resultMap:由于 associatetion 标签里面只有 resultMap 属性,故还需要在另外userInfo的xml 里面设置 resultMap
columnPrefix:这个属性很重要,这个对应的是连个对象的字段,比如说,两个对象都有id ,如果你不标注谁是谁的id 那么,就会存在误差性,可能会把UserInfo的id覆盖给 Articleinfo id,在属性前面加上前缀就防止字段误差性
查询具体代码实现:
结果:(如果使用 resultType 属性,UserInfo 则为 null)
ArticleInfo(id=1, title=Java, content=Java正?, createtime=2023-08-16T10:00:38, updatetime=2023-08-16T10:00:38, uid=6, rcount=1, state=1, userInfo=UserInfo(id=6, username=admin, password=admin, photo=null, createtime=null, updatetime=null, state=0))
整个流程梳理:
一对多:一个作者写了多篇文章,这次的标签用的是 < collection >
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<id property="id" column = "id">id>
<result property="username" column="username">result>
<result property="password" column="password">result>
<result property="photo" column="photo">result>
<result property="createtime" column="createtime">result>
<result property="updatetime" column="updatetime">result>
<result property="state" column="state">result>
<collection property="artList"
resultMap="com.example.demo.mapper.ArticleInfoMapper.BaseMap"
columnPrefix="a_">collection>
resultMap>
在一对多的关系中,就变为了 < collection >
标签,其他 < collection >
里面的属性都是一样的道理,其他额外需要注意的就是主键和普通字段。
查询语句:
<select id="getUserById2" resultMap="BaseMap">
select u.*,a.id a_id,a.title a_title,a.content a_content
from userinfo as u left join
articleinfo as a on u.id=a.uid and u.id=#{id}
select>
流程图与 一对一的一致,需要就是自己准备数据库操作。
动态 SQL 是 MyBatis 的强大特性之一。相信大家都遇到在填表的时候,某几项是必填的项目,某几项是非必填的项目,程序在添加不确定字段的时候就需要使用动态 SQL 进行实现。
< if > 标签就是用来执行必填字段和非必填字段的。(需要注意的就是标签里面的逗号应该写在哪里)
<insert id="addUser3">
insert into userinfo(username,
<if test="photo != null">
photo,
if>
password) values (#{username}
<if test="photo!=null">
,#{photo}
if>
,#{password})
insert>
以上代码中,是把不确定的标签放在中间,如果说不放在中间放在两边的话,就需要考虑 < if > 标签里面的逗号放在哪里的问题;因为非必填参数 sql 语句变化可能性会增加,逗号该不该出现,出现了放在那个位置会不影响 sql 语句?这些就增加了程序错误性,为了更好的解决次问题就出现了 < trim > 标签.
< trim > 标签 和 < if > 标签是搭配着用的,< trim > 标签有如下属性:
<insert id="addUser4">
insert into userinfo(username,
<trim suffix=")" suffixOverrides=",">
<if test="password!=null">
password,
if>
<if test="photo!=null">
photo,
if>
<if test="state!=null">
state,
if>
trim>
values(#{username},
<trim suffix=")" suffixOverrides=",">
<if test="password!=null">
#{password},
if>
<if test="photo!=null">
#{photo},
if>
<if test="state!=null">
#{state},
if>
trim>
insert>
有了< trim > 标签后,就不用担忧后面的逗号或者“ )”了,直接利用< trim > 标签的四个属性进行灵活变换。
当 SQL 使用 where 关键字条件查询的时候,当输入条件的时候是非必选项的时候就可以用 < where > 标签,也是和 < if > 标签搭配使用。
<select id="getListByTitleOrUId" resultMap="BaseMap">
select * from articleinfo
<where>
<if test="title!=null">
title = #{title}
if>
<if test="uid!=null">
and uid = #{uid}
if>
where>
select>
< where > 标签的功能是很强大的,内涵了一个相当于< trim > 的功能,会把无效的内容去掉,当你只是传递 title 的时候, < where > 标签会自动过滤到后面的 and uid=?,当传递 uid 的时候,and也会自动过滤掉。
标签 < set > 的用法也是一样的,使用 < set > 标签来指定动态内容。
<update id="upArticle">
update articleinfo
<set>
<if test="content!=null">
content = #{content},
if>
<if test="rcount!=null">
rcount = #{rcount},
if>
<if test="title!=null">
title = #{title},
if>
set>
where id = #{id}
update>
< set > 与 < where > 标签都会把无效的内容自动去掉,也是搭配 < if > 标签搭配使用。
单元测试代码:
@SpringBootTest
class ArticleInfoMapperTest {
@Test
void upArticle() {
int result = articleInfoMapper.upArticle(2,"xxxx", null, "MyBatis");
System.out.println("受影响行数" + result);
}
}
对集合进行遍历是可以使用该标签。< foreach > 标签有如下重要的属性:
示例:使用文章 id 来删除数据库的文章信息
1、首先也是先要声明;
// foreach 循环删除说明
public int delIds(List<Integer> ids);
2、在UserByIdMapper.xml 里面实现具体的声明;
<delete id="delIds">
delete from userinfo where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
foreach>
delete>
3、最终测试;
@Test
void delIds() {
List<Integer> list = new ArrayList<>();
list.add(12);
list.add(13);
list.add(14);
int result = userMapper.delIds(list);
System.out.println("删除结果:" + result);
}
结果展示: