目录
1.MyBatis是什么?
MyBatis是一款开源的基于Java的持久层框架,它可以帮助Java开发人员简化数据库操作,通过XML配置文件或注解,把Java对象映射到对应的数据库表中,实现对象关系和数据关系的互相转换,从而使得Java应用程序能够更简单的操作和读取数据库。
MyBatis相对于其他ORM框架最大的特点就是它提供了优秀的SQL编写和调试机制。通过MyBatis,我们可以直接编写出原生SQL语句,并通过MyBatis的一些工具,如参数映射、结果集映射等机制,把查询结果自动映射成Java对象或者List/Map等数据格式。同时,MyBatis还提供了懒加载、缓存和事务等高级特性,可以支持各种复杂的业务场景。
核心思想
将JavaBean中的数据和SQL语句中的数据进行映射,因此,在使用MyBatis时,我们需要定义一个Mapper接口,然后在XML中描述这个接口中每个方法对应的SQL语句,并指定参数和返回值类型,最后,让MyBatis自动生成这个Mapper接口的实现类,这样就可以方便地对数据库进行操作。
持久层
在 Java 程序开发中,数据通常被存储在关系型数据库或者非关系型数据库中,而持久层就是用来处理和管理这些数据存取操作的技术层次。持久层的作用是将应用程序中的对象转换成可存储的形式,然后将其存储到数据存储介质中,同时还要负责从数据存储介质中获取数据并将其转换成应用程序所需要的对象形式,这样就把数据存储和数据访问分开了,提高了程序的可维护性和扩展性。
持久层技术通常包括以下方面的内容:
JDBC编程的缺点
MyBatis可以解决这些问题.ORM框架可以将Java对象和数据库表自动映射,并提供了更加优雅和高效的API,让开发人员专注于业务逻辑,而不是繁琐的数据访问代码。同时,ORM框架还可以提供缓存、懒加载和批处理等高级特性,从而进一步提高程序的性能和可维护性。
MyBatis与JDBC
ORM框架
ORM是对象关系映射(Object-Relational Mapping)的缩写,是一种将对象模型和关系数据库模型进行转换、映射的技术。通过ORM框架,可以将Java对象映射到关系数据库表中的行,从而通过面向对象的方式来操作数据库,而无需编写大量的SQL语句。
Java的类,对象,对象属性映射到ORM框架中是什么
2.配置MyBatis环境
1.添加MyBatis框架支持
运行后会报错
2.设置MyBatis配置信息
1.设置数据库连接相关信息
2.配置xml保存路径,xml命名规则
至此,配置环境就完成了
3. MyBatis模式开发
先向数据库中插入一些数据
首先,创建一个实体类UserEntity,包含上述几个属性,必须对应。
接着,在Mapper接口中声明对UserEntity表进行操作的方法:
Mapper是MyBatis框架中的一个概念,用于描述一组将Java对象映射到SQL语句的规则。通常情况下,Mapper会包含一组接口和配置文件,用于定义Java对象和数据库表之间的映射关系。
然后,在Mapper.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="com.example.demo.mapper.UserMapper">
<select id="getAll" resultType="com.example.demo.entity.UserEntity">
select * from userinfo
</select>
</mapper>
按照标准分层,然后,创建Service层访问Mapper
Service层处理业务逻辑,并对外提供服务接口
创建Controller层
Controller层:负责接收用户请求,调用 Service 层处理请求,并通过视图呈现处理结果
启动项目,成功访问
向表中插入一条数据
刷新后,更新数据库中插入的新数据
上述是最简单的查询操作
4.单元测试
此处介绍一下单元测试的概念
SpringBoot已经内置了单元测试的依赖,不用添加支持
spring-boot-starter-test
是 Spring Boot 提供的一种测试 Starter,旨在提供各种常见的测试工具和框架,使开发人员能够更加轻松地编写、运行、管理 Spring Boot 应用程序的单元测试和集成测试
包含了以下几个主要组件:
- JUnit:一个广泛使用的 Java 单元测试框架,能够帮助开发人员快速构建单元测试用例,并自动执行这些测试用例;()我们此处使用的JUit5)
- Spring Test:一个专门为 Spring 应用程序设计的测试框架,能够帮助开发人员快速搭建 Spring 容器环境,在容器环境中执行测试用例,并验证应用程序的正确性;
- AssertJ:一个优秀的断言库,能够帮助开发人员编写简洁、易于理解、易于扩展的断言语句;
- Hamcrest:另一个流行的断言库,也提供了类似于 AssertJ 的各种断言方法;
- Mockito:一个强大的 Java 模拟框架,能够帮助开发人员快速创建模拟对象,并进行各种测试;
- JsonPath:一个用于处理 JSON 路径的库,能够帮助开发人员快速过滤、提取 JSON 数据。
优点主要有以下几个:
- 提升测试效率:借助
spring-boot-starter-test
Starter 提供的各种测试工具和框架,开发人员能够更加轻松地编写、运行、管理单元测试和集成测试,从而大大提升测试效率; - 方便集成 Spring Boot:
spring-boot-starter-test
Starter 与 Spring Boot 完美集成,可直接使用 Spring Boot 的自动配置机制和依赖注入机制,使得单元测试与实际应用程序开发更加无缝衔接; - 简化代码编写:借助
spring-boot-starter-test
Starter 提供的各种测试工具和框架,开发人员不需要编写大量的测试框架代码,只需要关注业务逻辑和测试用例的编写即可; - 多样化的测试支持:
spring-boot-starter-test
Starter 支持多种类型的测试:单元测试、集成测试、端到端测试等等; - 良好的社区支持与更新:
spring-boot-starter-test
Starter 是 Spring Boot 社区的核心组件之一,拥有庞大的用户群和活跃的维护者团队,能够及时更新和修复各种测试工具和框架的问题。
单元测试实现步骤
1.在要测试的类上右键Generate
会生成测试类
写测试信息.不要忘记添加注解!!
启动测试类
这是测试类的创建,向测试类中添加测试时,会报一个错误
意思是已经存在了这个测试类,是否向其中追加测试,直接点击Ok
单元测试就简单了解到这里
5.增删查改
下来看传参问题
当这两处,参数名称不一致,xml中应该写成什么
启动测试类会报错,参数id没找到
修改xml文件
此时单元测试运行成功了 .
所以xml文件的参数命名要和@Param("uid")一致..如果没有加这个注解,就要和原参数名相同
加了注解,生效的就是注解中的参数命名
1.${key}
${key} 是一种占位符语法,被称为字符串替换,用于将 SQL 语句中的参数占位符替换为实际的值。使用 ${} 语法会直接将参数替换为 SQL 字符串,而不是通过预编译语句来传递参数。在使用 ${key} 时,如果不正确地处理输入的参数,可能会导致 SQL 注入攻击。
可以打印出执行的生成的sql语句,就能看到效果了
需要先配置一下
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#配置mybatis xml 的文件路径,在resources/mapper 创建的所有表的 xml 文件
mybatis.mapper-locations=classpath:/mybatis/*Mapper.xml
#日志打印sql
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#日志打印级别(默认是info,需要设置位debug,才能显示)
logging.level.com.example.demo=debug
然后执行测试
可以看到这是直接将参数替换为 SQL 字符串
2.#{key}
在 MyBatis 中,#{key} 是一种占位符语法,被称为参数占位符,用于将 SQL 语句中的参数占位符替换为实际的值。它可以帮助我们在 SQL 语句中使用参数,使得 SQL 语句更加灵活和通用
具体来说,当应用程序调用 DAO 层的方法时,会传递一些参数给对应的 SQL 语句,这些参数可以通过 #{} 占位符来引用。MyBatis 会自动将 #{} 中的参数值进行预编译处理,保证输入数据的安全性,并将它们以安全的方式插入到 SQL 语句中,然后发送给数据库执行,从而防止了 SQL 注入攻击等安全问题
启动单元测试
这里的 #{uid} 是一个占位符,代表该位置需要动态替换为具体的参数值。在执行 SQL 时,MyBatis 会自动将 #{uid} 替换为实际传入的 uid 参数的值,并将其进行预编译处理,从而确保 SQL 查询语句的安全性。
日志打印看到的也不是sql语句,而是通过占位符模式进行内容的填充
这里数据是int类型的,我们换用String类型数据尝试
结果:
当将#{}替换为${} 再次进行执行.报错了
原因:${}是直接替换字符串的,你输入的是什么就完全替换
'${username}',才不会报错,不加单引号sql语句就会出现问题
这样只能保障不报错,但是会存在安全性问题
例如登陆场景下:sql注入问题
为了只返回一条数据,先将表中数据删除
mysql> select*from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 2 | lisi | lisi | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
1 row in set (0.00 sec)
正常行为:
攻击者发送如下输入:
mysql> select 1 = '1';
+---------+
| 1 = '1' |
+---------+
| 1 |
+---------+
1 row in set (0.00 sec)
那么该 SQL 查询语句将会被替换为:
SELECT * FROM users WHERE username = '' or '1'='1' AND password = '' or '1'='1'
使用不存在的username,不存在的password也能查到用户信息
这就是使用${key}时的sql注入攻击.
将${key}替换为#{key}
占位符的方式,不会出现安全性问题
${}在特定场景下,也有优点
此时使用#{key}会被错误地解析为字符串:
SELECT * FROM userinfo ORDER BY id "desc"
即使设置了 ord 参数的值为 ASC 或 DESC,也无法正确地替换该字符串
需要将该 SQL 语句更改为以下形式进行直接替换:
SELECT * FROM userinfo ORDER BY id ${ord}
#{key} 底层原理:
修改密码
运行单元测试
成功修改
mysql> select*from userinfo ;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 2 | lisi | 123456 | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
1 row in set (0.00 sec)
在单元测试情况下,我们是不愿意改动数据库的,也就是不污染数据库
默认情况下,是会持久化改动数据库的
这里就用到@Transactional注解了
正常执行,但不污染数据库,执行时开启事务,执行结束后进行回滚操作,就不会污染数据库
看看数据库中,没有改变,说明进行了回滚...并且成功验证了程序,返回修改:1
mysql> select*from userinfo ;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 2 | lisi | 123456 | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
1 row in set (0.00 sec)
下来学习删除操作
//删除用户,返回受影响行数
int deleteById(@Param("id")Integer id);
<delete id="deleteById">
<!-- 默认返回受影响的行数-->
delete from userinfo where id = #{id}
</delete>
@Transactional
@Test
void deleteById() {
int result = userMapper.deleteById(2);
System.out.println("删除: "+result);
}
执行结果
因为设置事务了,所以测试完成后进行了回滚,不会污染数据库
普通得增加模块,这里我们不进行回滚了,让数据插入到数据库中
//添加用户
int addUser(UserEntity user);
<insert id="addUser">
<!-- 默认返回受影响的行数-->
insert into userinfo(username,password) values(#{username},#{password})
</insert>
@Test
void addUser() {
UserEntity user = new UserEntity();
user.setUsername("zhangsan");
user.setPassword("123456");
int result = userMapper.addUser(user);
System.out.println(result);
}
执行结果,返回影响行数:1.数据库中新增了一条数据
==> Preparing: insert into userinfo(username,password) values(?,?)
==> Parameters: zhangsan(String), 123456(String)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3e4e4c1]
1
mysql> select*from userinfo ;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 2 | lisi | 123456 | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
| 3 | zhangsan | 123456 | | 2023-05-18 17:21:49 | 2023-05-18 17:21:49 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
2 rows in set (0.00 sec)
添加用户得到id
//添加用户得到ID
int addUserGetId(UserEntity user);
<insert id="addUserGetId" useGeneratedKeys="true" keyProperty="id">
<!--返回受影响的行数 和 id-->
<!--useGeneratedKeys="true"keyProperty="id"是否自动生成id,拿到id放到id字段中-->
insert into userinfo(username,password) values(#{username},#{password})
</insert>
@Test
void addUserGetId() {
UserEntity user = new UserEntity();
user.setUsername("wangwu");
user.setPassword("123456");
int result = userMapper.addUserGetId(user);
System.out.println("ID: "+user.getId());
}
执行结果
JDBC Connection [HikariProxyConnection@1775046789 wrapping com.mysql.cj.jdbc.ConnectionImpl@60bb7995] will not be managed by Spring
==> Preparing: insert into userinfo(username,password) values(?,?)
==> Parameters: wangwu(String), 123456(String)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@ef60710]
ID: 4
mysql> select*from userinfo ;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 2 | lisi | 123456 | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
| 3 | zhangsan | 123456 | | 2023-05-18 17:21:49 | 2023-05-18 17:21:49 | 1 |
| 4 | wangwu | 123456 | | 2023-05-18 17:36:28 | 2023-05-18 17:36:28 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
3 rows in set (0.00 sec)
模糊查询-concat
使用#{key}方式的查询结果会报错
因为传递参数为string类型,会被#{}解析为字符串,因此会自动加上'',那么占位符替换后变成
'%'li'%'
mysql> select*from userinfo where username like '%'li'%';
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'li'%'' at line 1
需要借助concat来进行拼接
mysql> select concat ('%','li','%');
+-----------------------+
| concat ('%','li','%') |
+-----------------------+
| %li% |
+-----------------------+
1 row in set (0.00 sec)
执行结果
JDBC Connection [HikariProxyConnection@1622899093 wrapping com.mysql.cj.jdbc.ConnectionImpl@40fa8766] will not be managed by Spring
==> Preparing: select * from userinfo where username like concat('%',?,'%')
==> Parameters: li(String)
<== Columns: id, username, password, photo, createtime, updatetime, state
<== Row: 2, lisi, 123456, , 2022-12-06 17:10:48, 2022-12-06 18:10:48, 1
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@56399b9e]
UserEntity(id=2, username=lisi, password=123456, photo=, createtime=2022-12-06T17:10:48, updatetime=2022-12-06T18:10:48, state=1)
由于DBA和后端开发人员代码风格不同,可能会出现不兼容的问题,比如
然后我们执行该单元测试
我们发现JDBC成功执行,但是由于password映射不上pwd,pwd的值为空
此时resultType="com.example.demo.entity.UserEntity"就失效了,因为表中字段和类的属性不一样了,无法一对一直接映射了
解决方法--<resultMap>
<resultMap> 是 MyBatis 中用于映射查询结果集的标签。它能够解决以下两个问题:
-
解决列名与属性名不一致的问题:在数据库中,有些列名可能并不符合 Java 对象中属性的命名规范,这时可以通过 <resultMap> 映射来将查询结果集中的列名与 Java 对象中属性名进行映射,使得结果集与对象之间能够正确匹配。
-
解决多表关联查询的问题:在进行多表关联查询时,查询结果集通常会包含多个表中的列,这时可以使用 <resultMap> 将不同表中的列映射到不同的 Java 对象属性中,从而完成对结果集的映射转换。
第一步,设置映射关系
<resultMap id="BaseMap" type="com.example.demo.entity.UserEntity">
<!--设置主键 property:属性,类中的 column:字段,数据库表中的 -->
<id property="id" column="id"></id>
<!--设置普通参数-->
<result property="pwd" column="password"></result>
<result property="username" column="username"></result>
<result property="createtime" column="createtime"></result>
<result property="photo" column="photo"></result>
<result property="updatetime" column="updatetime"></result>
<result property="state" column="state"></result>
</resultMap>
第二步,修改返回类型
<select id="getUserById" resultMap="BaseMap">
select * from userinfo where id = #{uid}
</select>
执行结果
还有比较简单的方式,在sql语句中设置重命名也能成功映射
<select id="getUserByName" resultType="com.example.demo.entity.UserEntity">
select id,username,password as pwd from userinfo where username = '${username}'
</select>
多表联查
mysql> update userinfo set id = 1 where username = 'lisi';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
文章信息表
mysql> select*from articleinfo;
+----+-------+----------+---------------------+---------------------+-----+--------+-------+
| id | title | content | createtime | updatetime | uid | rcount | state |
+----+-------+----------+---------------------+---------------------+-----+--------+-------+
| 1 | Java | Java正文 | 2023-05-15 09:12:59 | 2023-05-15 09:12:59 | 1 | 1 | 1 |
+----+-------+----------+---------------------+---------------------+-----+--------+-------+
1 row in set (0.01 sec)
为了对应,将lisi 的id修改为1
mysql> update userinfo set id = 1 where username = 'lisi';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select*from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | lisi | 123456 | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
| 3 | zhangsan | 123456 | | 2023-05-18 17:21:49 | 2023-05-18 17:21:49 | 1 |
| 4 | wangwu | 123456 | | 2023-05-18 17:36:28 | 2023-05-18 17:36:28 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
3 rows in set (0.00 sec)
创建文章表实体类
@Data
public class ArticleInfo {
private int id;
private String title;
private String content;
private LocalDateTime createtime;
private LocalDateTime updatrtime;
private int uid;
private int rcount;
private int state;
}
扩展类
package com.example.demo.entity.vo;
import com.example.demo.entity.ArticleInfo;
import lombok.Data;
@Data
public class ArticleInfoVO extends ArticleInfo {
private String username;
}
在Mapper中创建接口
package com.example.demo.mapper;
import com.example.demo.entity.vo.ArticleInfoVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ArticleMapper {
//查询文章详情
ArticleInfoVO getDetail(@Param("id")Integer id);
}
在mybatis包下创建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="com.example.demo.mapper.ArticleMapper">
<select id="getDetail" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select a.*,u.username from articleinfo a
left join userinfo u on u.id = a.uid
where a.id = #{id}
</select>
</mapper>
创建测试类
@SpringBootTest
class ArticleMapperTest {
@Autowired
private ArticleMapper articleMapper;
@Test
void getDetail() {
ArticleInfoVO articleInfoVO = articleMapper.getDetail(1);
System.out.println(articleInfoVO);
}
}
执行单元测试
JDBC执行是正确的,但是返回只有username,并没有查询到父类的属性
其实是lombok出现了问题,打印时没包含父类的属性
解决办法:手动设置toString
public class ArticleInfoVO extends ArticleInfo {
private String username;
@Override
public String toString() {
return "ArticleInfoVO{" +
"username='" + username + '\'' +
"} " + super.toString();
}
}
查看字节码
再次执行
JDBC Connection [HikariProxyConnection@166022233 wrapping com.mysql.cj.jdbc.ConnectionImpl@5dbb50f3] will not be managed by Spring
==> Preparing: select a.*,u.username from articleinfo a left join userinfo u on u.id = a.uid where a.id = ?
==> Parameters: 1(Integer)
<== Columns: id, title, content, createtime, updatetime, uid, rcount, state, username
<== Row: 1, Java, <<BLOB>>, 2023-05-15 09:12:59, 2023-05-15 09:12:59, 1, 1, 1, lisi
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3662bdff]
ArticleInfoVO{username='lisi'} ArticleInfo(id=1, title=Java, content=Java正文, createtime=2023-05-15T09:12:59, updatrtime=null, uid=1, rcount=1, state=1)
查询一个用户的所有文章
先将插入一条文章记录
mysql> select*from articleinfo;
+----+-------+-----------+---------------------+---------------------+-----+--------+-------+
| id | title | content | createtime | updatetime | uid | rcount | state |
+----+-------+-----------+---------------------+---------------------+-----+--------+-------+
| 1 | Java | Java正文 | 2023-05-15 09:12:59 | 2023-05-15 09:12:59 | 1 | 1 | 1 |
| 2 | mysql | mysql正文 | 2023-05-19 11:14:49 | 2023-05-19 11:14:49 | 1 | 1 | 1 |
+----+-------+-----------+---------------------+---------------------+-----+--------+-------+
2 rows in set (0.00 sec)
其他代码
//查询一个用户所有文章
List<ArticleInfoVO> getListByUid(@Param("uid")Integer uid);
<select id="getListByUid" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select a.*,u.username from articleinfo a
left join userinfo u on u.id = a.uid
where a.uid = #{uid}
</select>
@Test
void getListByUid() {
Integer uid =1;
List<ArticleInfoVO> list = articleMapper.getListByUid(uid);
//使用多线程的方式,顺序可能会不同
list.stream().parallel().forEach(System.out::println);
}
执行