MyBatis学习笔记
MyBatis
一、框架概述
什么是框架
框架对通用的代码的封装,通过使用框架,提高开发效率,而不需要关心一些繁琐的、复杂的底层代码实现,把更多的经历用于所在需求的实现上。
框架可以理解为一个半成品,我们选用这个半成品,然后加上业务需求来最终实现整个功能。
软件开发的分层
在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一(单一原则)
单一原则:一个类或者一个方法,就只做一件事情,只管一个功能。这样就可以让类、接口、方法的复杂度更低,可读性更强、扩展性更好,也便于后期的维护。
以前我们写代码,从组成可以分成三个部分:
- 数据访问:负责业务数据的维护操作
- 逻辑处理:负责业务逻辑处理的代码
- 请求处理:接受请求,给页面响应数据
在我们项目开发中,将代码分为三层:
- 前端发起的请求,由controller层接收,控制器响应数据给前端
- controller层调用service层进行逻辑处理,service层处理后,把处理结果返回给controller层
- dao层操作底层的数据,负责拿到数据返回给service层
分层就是分工,划分环节,通过分层架构的设计,使代码的职责分明,容易理解和维护,还能实现代码的重用,提高系统的整体性能和扩展性,同时各层之前的结构也很方便,提高代码的质量和稳定性。
通过分层更好的实现各个部分的职责,在每一层将再细化出不同的矿界,分别解决各层关注的问题。
分层开发下的常见框架
- 表现层:指的是用户直接与应用进行交互的部分,负责接收用户请求、处理业务逻辑、返回响应结果。表现层框架的主要任务是将业务逻辑与用户进行交互,提供良好的用户体验。
- 业务逻辑层:也称为服务层,负责处理应用的业务逻辑。它负责协调不同组件之间的工作,执行复杂的业务规则和计算。业务逻辑层框架的主要任务是封装业务逻辑,提供可重复使用的业务功能。
- 数据访问层:也称为持久层,负责与数据库或其他数据存储进行交互。它负责执行数据库操作,如查询、插入、更新和删除数据。数据访问层框架的主要任务是提供统一的接口,隐藏底层数据库的细节,使业务逻辑层能够独立于具体的数据库实现。
二、Mybatis框架
什么是mybatis
MyBatis 是一款优秀的持久层框架。
采用ORM思想解决实体和数据库映射的问题,对JDBC 进行了封装,屏蔽了JDBC API底层访问细节,使我们不用与JDBC API打交道,就可以完成对数据库的持久化操作。
Mybatis本来是apache的一个开源项目IBatis,2010年的这个项目由apache改名MyBatis。
- 持久层:指的是数据访问层dao,是用来操作数据库的
- 框架:是一个半成品软件,再框架的基础上进行软件开发,更加高效,规范,可扩展。
ORM:对象关系映射,实现了面向对象编程语言中对象与关系型数据库中的表之间的映射,ORM框架允许开发者使用面向对象的方式来操作数据库,而无需关心底层的SQL语句。
具体来说:
- ORM框架将数据库中的表(table)映射为编程语言中的类(class)
- 表中的记录映射为类的实例
- 字段映射为对象的属性
Mybatis框架自定义sql语句
- 什么情况下需要自定义sql语句
情况一:需要实现的功能超出了BaseMapper父接口的能力范围
框架提供的BaseMapper父接口中只实现了【单表】和【CRDU】操作,支持条件构造器和分页器功能,但是要是想实现复杂功能比如
- 多表关联查询
- 复杂统计
- 特殊SQL
等等情况二:项目使用的是基础版本的Mybatis,而不是MP
基础版本的Mybatis没有提供BaseMapper,MP版本的在基础版本的功能基础上提供了BaseMapper,所以MP版本的Mybatis更方便。
如果是基础版本的mybatis,单表操作都需要自己写sql一个自动化高,一个半自动灵活性高
JDBC编程的分析
- 数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
- 因为SQL语句的where条件不一定,可能多可能少,修改SQL还要i需改代码,系统不容易维护。
- 对结果集解析步骤相对繁琐。
自定义sql写在哪里呢?
SQL语句不写在java代码中,而是写在XML映射文件中。
xml放在resources资源目录中
其xml文件名需要和对应的接口同名,目录与接口的包同名
Mybatis入门案例
1). pom.xml
1 | <dependency> |
2). 编写实体类
1 | public class Dept { |
3). 编写持久层接口
1 | public interface IDeptDao { |
XML配置文件实现
【1】创建XML映射文件
要求:
- 创建位置必须与持久层接口在相同的包中
- 名称:必须以持久层接口名称命名文件名
【2】编写XML映射文件
xml映射文件中的DTD约束,直接从mybatis官网复制即可,或者直接AI生成1
2
3
4
【3】配置
在mybatis-config.xml配置文件中,需要配置mapper映射文件的位置,告诉mybatis去哪里找到对应的SQL语句。
1 |
|
【4】配置mybatis-config.xml配置文件
核心配置文件主要用于配置数据库的环境以及mybatis的全局配置信息,存放的位置src/main/resources目录下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<configuration>
<!-- 配置mybatis的环境 -->
<environments default="mysql">
<!-- 配置mysql的环境 -->
<environment id="mysql">
<!-- 配置事务的类型 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置数据库信息,用的是连接池的数据源 -->
<dataSource type="POOLED">
<!--配置连接池需要的参数-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/db"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 告诉mybatis映射文件的位置 -->
<mappers>
<mapper resource="com/iweb/dao/DeptDao.xml"></mapper>
</mappers>
</configuration>
以后,这个配置文件可以省略
【测试】
1 | package com.iweb.test; |
XML映射配置
mybatis的开发有两种方式:
- 注解
- XML
XML配置文件规范
使用mybatis的注解方式,主要是完成一些简单的增删改查功能,如果需要实现复杂的SQL功能,建议使用XML配置来配置映射语句,也就是将SQL语句写在XML配置文件中。
在mybatis中使用XML映射文件方式开发,需要符合一定的规范:
- XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)
- XML映射文件的namespace属性为Mapper接口全限定名一致
- XML映射文件中SQL语句的id属性值与Mapper接口中的方法名一致,并保持返回类型一致
总结
通过上述案例,我们发现使用mybatis是非常容易的事情,只需要编写Dao接口并且按照mybatis的要求编写两个配置文件,就可以实现功能。
接口和xml文件是如何绑定的呢
接口与XML文件绑定是通过xml文件的根标签的namespace属性一致。
接口方法与xml文件中的SQL语句的id属性值一致。
基于注解的mybatis使用
1). 在持久层接口中添加注解1
2
3
4
5 public interface IDeptDao {
// 查询所有部门信息
List<Dept> findAll();
}
2). 修改mybatis-config.xml1
2
3
4
5 <!-- 告诉mybatis映射文件的位置 -->
<mappers>
<mapper class="com.iweb.dao.IDeptDao"></mapper>
</mappers>
注意事项
在使用基于注解的mybatis配置时,需要移除xml的映射配置
mybatisX的使用
mybatis是一款基于DIEA的快速开发mybatis的插件,为效率而生。
xml与接口互跳
我们点击dao接口方法左侧的图标可以直接跳转到dao.xml对应的SQL实现,在dao.xml点击左侧图标也可以直接跳转到dao接口中对应的方法。
日志技术
日志就用来记录程序运行信息,状态信息,错误信息的。
mybatis日志框架
mybatis没有直接依赖具体的日志实现,而是通过内置的日志抽象层来桥接不同的日志框架。
mybatis支持的日志框架(优先级别):
- SLF4J
- LOG4J 2
- LOG4J
- JDK
- LOG4J
LOG4J是一个流行的日志框架,提供了灵活的配置选项,支持多种输出目标。
【1】引入log4j的依赖1
2
3
4
5<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
【2】在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择日志实现工具1
2
3<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
【3】添加日志控制文件1
2
3
4
5
6
7
8# 配置全局的日志输出级别debug->info->warn->error
# 设置日志输出源 stdout 输出到控制台
log4j.rootLogger=debug, stdout
# 配置日志相关的信息
log4j.logger.org.mybatis.example.BlogMapper=TRACE
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
代理Dao实现CRUD操作
查询
需求:依据部门编号查询部门信息
1)在持久层添加findByDeptId方法1
2// 根据部门编号查询部门信息
Dept findByDeptId(int deptNo);
2)在deptDao.xml的映射配置见配置查询方法1
2
3<select id="findByDeptId" resultType="com.iweb.bean.Dept" parameterType="int">
select * from dept where deptno = #{deptNo}
</select>
属性说明:
- resultType属性:用于指定返回结果集的类型
- parameterType属性:用于指定传入参数的类型
- sql语句种使用#{}:在mybatis中我们可以通过占位符#{..}来占位,在调用findByDeptId方法时,传递参数只,最终会替换占位符。
3)测试1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 package com.iweb.test;
public class TestMyBatis {
private InputStream in;
private SqlSessionFactory build;
private SqlSession session;
private IDeptDao deptDao;
// 在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
session.close();
in.close();
}
// 在测试方法执行之前完成执行
public void init() throws Exception{
//1.读取配置文件
in = Resources.getResourceAsStream("mybatis-config.xml");
//2.创建sqlSessionFactory构建者对象
build = new SqlSessionFactoryBuilder().build(in);
//3.使用SqlSessionFactory生成sqlSession对象
session = build.openSession();
//4.使用session创建dao接口的代理对象
deptDao = session.getMapper(IDeptDao.class);
}
public void testFindByDeptId(){
Dept dept = deptDao.findByDeptId(3);
System.out.println(dept);
}
public void testFindAll() throws Exception{
//5.使用代理对象执行查询所有的方法
for (Dept dept : deptDao.findAll()) {
System.out.println(dept.toString());
}
}
}
添加
1). 在持久层中添加新增方法1
2// 新增部门
int saveDept(Dept dept);
2). 在映射配置文件中配置新增方法1
2
3
4
5 <!-- 添加 -->
<insert id="saveDept" parameterType="com.iweb.bean.Dept">
insert into dept(dname,loc) values(#{dname},#{loc})
</insert>
parameterType属性:代表参数的类型,因为我们要传入的是一个类的对象,所以类型就写全类名
#{}中内容的写法:由于我们保存方法的参数是一个Dept对象,此处需要写Dept对象中的属性名称。它用的是OGNL表达式:
OGNL表达式:
它是apache提供的一种表达式语言,全程是对象图导航语言
#{dept.dname}它会先去找dept对象,然后在找到dept对象中的dname属性,并调用getDname()方法把值取出来。但是我们在parameterType属性上指定了实体类名称,所以可以省略dpet.,而直接写dname即可。1
2
3
4
5
6
7
8
9
public void testSaveDept(){
Dept dept=new Dept();
dept.setDname("市场部");
dept.setLoc("长沙");
// 执行保存方法
int i = deptDao.saveDept(dept);
System.out.println(i+"条受影响");
}
我们在实现增删该时一定要去控制事务的提交,那么mybatis是如何控制事务提交的?可以使用session.commit()来提交事务。
#{}和${}的区别
#{}表示一个占位符通过#{}可以实现PreparedStatement向占位符中设置值,自动进行Java类型和Jdbc类型转换,#{}可以有效防止SQL注入。#{}可以接受简单类型值和POJO属性值,如果parameterType传输单个简单类型值,#{}扩种可以是value或者其他名字。${}表示拼接SQL通过${}可以将parameterType传入的内容拼接在SQL中且不进行Jdbc类型转换,${}可以接受简单类型值或者POJO属性值,如果parameterType传输单个简单类型值,${}括号只能是value。
说人话就是#{}是mybatis提供的占位符,可以有效防止SQL注入,而${}是java提供的占位符,使用的是拼接方法,不能有效防止SQL注入。
如何获取新增id的返回值?
新增部门后,同时还有返回当前新增部门的id值,因为id属性值是由数据库的自动增长来实现的,所以就相当于我们要在新增后将自动增长的值返回1
2
3<insert id="saveDept" useGeneratedKeys="true" keyProperty="deptNo" parameterType="com.iweb.bean.Dept">
insert into dept(dname,loc) values(#{dname},#{loc})
</insert>
属性说明:
- useGeneratedKeys=true: 在执行添加记录之后可以获取到数据库自动生成的主键ID,在自动获取到主键后,需要设置返回的主键对象。
- keyProperty:设置的值为java对象主键的属性,指定主键id值存放的属性。
1
2
3
4
5
6
7
8
9
public void testSaveDept(){
Dept dept=new Dept();
dept.setDname("市场部");
dept.setLoc("长沙");
// 执行保存方法
deptDao.saveDept(dept);
System.out.println("插入的主键--->"+dept.getDeptNo());
}修改
1
2// 修改部门信息
int updateDept(Dept dept);测试1
2
3
4
5<!-- 修改 -->
<update id="updateDept" parameterType="com.iweb.bean.Dept">
update dept set dName=#{dname},loc=#{loc} where detpNo=#{deptNo}
</update>1
2
3
4
5
6
7
8
9
public void testUpdateDept(){
Dept dept=new Dept();
dept.setDeptNo(5);
dept.setDname("销售部");
dept.setLoc("广州");
int result = deptDao.updateDept(dept);
System.out.println(result+"行受影响");
}删除
1
2// 删除
int delDeptByDeptNo(int deptNo);测试1
2
3
4<!-- 删除 -->
<delete id="delDeptByDeptNo" parameterType="int">
delete from dept where deptno=#{deptNo}
</delete>1
2
3
4
5
public void testDelDeptByDeptNo(){
int i = deptDao.delDeptByDeptNo(5);
System.out.println(i+"条受影响");
}模糊查询
【方式一】
在dao接口中直接使用#{}1
2// 模糊查询方式一
List<Dept> findByDeptName(String dname);
映射文件配置1
2
3
4<!-- 模糊查询一 -->
<select id="findByDeptName" resultType="com.iweb.bean.Dept" parameterType="string">
select * from dept where dname like #{dname}
</select>
测试方法1
2
3
4
5
6
7
public void testLikeByDeptName(){
List<Dept> depts = deptDao.findByDeptName("%部%");
for (Dept dept : depts) {
System.out.println(dept);
}
}
我们在配置文件中没有加入%来作为模糊查询的条件,所以在传入字符串实参时,就需要给定模糊查询的标识符%。
【方式二】
在xml中使用concat函数
如果你不想在java代码中拼接字符串,可以在xml映射文件中的SQL中使用concat函数来拼接百分比符号和参数。1
2
3
4
5<!-- 模糊查询二 -->
<select id="findByDeptName" parameterType="string" resultType="com.iweb.bean.Dept">
select * from dept where dname like concat('%',#{deptName},'%')
</select>
测试1
2
3
4
5
6
7
public void testLikeByDname(){
List<Dept> depts = deptDao.findByDeptName("运营");
for (Dept dept : depts) {
System.out.println(dept);
}
}
【方式三】
使用${}进行拼接(不推荐)
虽然可以使用${}进行字符串拼接以实现like查询,但是这种方式容易导致SQL注入攻击,因此不推荐使用。1
2
3
4 <!-- 模糊查询三 -->
<select id="findByDeptName" parameterType="string" resultType="com.iweb.bean.Dept">
select * from dept where dname like '%${value}%'
</select>
我们在上面将原来的#{}占位符改成了${value},如果用这种方式的模糊查询,那么${value}的写法是固定的,不能改成其他的名字。
测试1
2
3
4
5
6
7
public void testLikeByDname(){
List<Dept> depts = deptDao.findByDeptName("部");
for (Dept dept : depts) {
System.out.println(dept);
}
}
面试题 #{} vs ${}区别?
#{}表示一个占位符通过#{}可以实现PreparedStatement向占位符中设置值,自动进行Java类型和Jdbc类型转换,#{}可以有效防止SQL注入。#{}可以接受简单类型值和POJO属性值,如果parameterType传输单个简单类型值,#{}扩种可以是value或者其他名字。${}表示拼接SQL通过${}可以将parameterType传入的内容拼接在SQL中且不进行Jdbc类型转换,${}可以接受简单类型值或者POJO属性值,如果parameterType传输单个简单类型值,${}括号只能是value。
聚合查询
1 | // 聚合函数 |
映射配置文件1
2
3
4<!-- 聚合函数 -->
<select id="findTotal" resultType="int">
select count(*) from dept
</select>
测试1
2
3
4
5
public void testFindTotal(){
int total = deptDao.findTotal();
System.out.println(total);
}
四、mybatis的参数深入
parameterType配置参数
SQL语句传参,使用标签的parameterType属性设置。属性的取值可以是基本类型,引用类型(String),还可以是实体类类型(POJO),同时也可以是实体类的包装类。
传递POJO包装对象
开发中通过POJO传递查询条件,查询条件是综合的查询条件,不仅包括用户查询条件还可以包括其他的查询条件(比如用户购买的商品信息也作为查询条件),这时可以使用包装类对象传递输入参数,POJO类中包含POJO
需求:根据部门名称查询部门查询,查询条件放到QueryVo的Dept属性中。
1). 编写paramVO1
2
3
4
5
6
7
8
9
10
11
12public class ParamVO {
private Dept dept;
public Dept getDept() {//这里是类中嵌入一个类
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
2). 在持久层接口中添加方法1
2// 包装类作为参数
List<Dept> findByVo(ParamVO vo);
3). 映射配置1
2
3<select id="findByVo" resultType="com.iweb.bean.Dept" parameterType="com.iweb.bean.ParamVO">
select * from dept where dname like #{dept.dname}
</select>
4). 测试包装类作为参数1
2
3
4
5
6
7
8
9
10
11
public void testFindParamVo(){
ParamVO paramVO=new ParamVO();
Dept d=new Dept();
d.setDname("%部%");
paramVO.setDept(d);
List<Dept> depts = deptDao.findByVo(paramVO);
for (Dept dept: depts) {
System.out.println(dept);
}
}
五、输出结果封装
resultType配置结果
resultType的主要作用是告诉mybatis如何将数据库查询的结果集(resultSet)转换为java对象,它告诉mybatis每一行结果应该映射到哪个类型的java对象,还有一个要求,实体类属性名和数据库表中的返回的字段名一致,mybatis才会自动封装。
如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。
特殊情况示例
实体类属性和数据库表的列名已经不一致了
resultMap结果类型
resultMap标签可以建立查询的列名和实体类的属性名不一致时建立对应关系,从而实现封装。
在select标签中使用resultMap属性指定引用即可。同时resultMap可以实现将查询结果映射为复杂的POJO,比如在查询结果映射对象中包括POJO和List实现一对一查询或者一对多查询。
1). 定义resultMap
1 | <resultMap id="唯一标识" type="指定查询结果映射的java类型"> |
1 | <!-- 定义Dept实体类和数据库表中字段的对应关系 |
属性说明:
- id属性:唯一的标识,将来是给查询select标签去引用的
- type属性:指定查询结果映射的java类型
- id标签:用于指定主键字段
- property属性:指定实体类中属性名称
- column属性:指定数据库表中字段的名称
- result标签:用于指定非主键字段
2). 映射配置1
2
3<select id="findAll" resultMap="deptMap">
select * from dept
</select>
3)测试1
2
3
4
5
6
7
8
public void testFindAll() throws Exception{
//5.使用代理对象执行查询所有的方法
List<Dept> depts = deptDao.findAll();
for(Dept dept : depts){
System.out.println(dept);
}
}
resultMap和resultType的区别
resultType是自动映射查询结果
- resultType是select标签的属性,用于指定映射查询结果的类型
- resultType的取值是自定义的java类型
- 自动映射:框架自动将查询结果映射到java对象,无需编写映射规则
- 适用于单表查询,列名和属性名一致
- 不适用可与一对多连表查询,命名不一致
resultMap是手动映射查询结果 - resultMap是select标签的另一个属性,用于指定映射查询结果的规则
- resultMap需要额外编写映射规则,resultType不需要
- resultMap的取值是映射规则的ID名字,必须是唯一的
- 支持手动执行负责映射规则
- 适用于列名和属性名不一致的情况
- 可以实现一对一查询和一对多查询
自动映射级别
NONE(最低级别)
- 完全关闭自动映射,只映射指定了映射规则的字段
PARTIAL(默认级别)
- 自动映射简单属性
- 不自动映射嵌套对象或者集合
FULL(最高级别)
- 完全自动映射,包含嵌套的对象和集合也自动映射
- 容易产生误映射
总结:
- 单表查询或者统计结果可以用resultType自动映射(前提是要符合自动映射的命名规范)
- 连表查询(一对一,一对多)使用resultMap手动映射
在配置文件中我们可以对其进行修改级别
一对一,一对多的映射标签是什么
- 一对一指的是主表中的一行数据,在对应的中只有一行匹配连接的数据
- 实体类中嵌套一个对象
- 在xml文件中用
标签映射
- 一对多指的是主表中的一行数据,在对应的中有多行匹配连接的数据
- 实体类中嵌套一个集合
- xml文件中用
标签映射
六、配置内容
在使用mybatis框架时,自定义别名可以简化XML配置文件和代码中的映射操作。
- 告诉框架entity包的路径,mybatis就可以自动扫描包下的类,为每个类生成一个别名
- 可以在dao.xml文件中直接使用别名来引用java类
- 多个包路径之间使用逗号分隔
- 配置后在xml文件中映射查询结果,直接写类名即可
1 | <!-- 单个别名定义 --> |
在dao.xml文件使用别名
定义了别名,就可以在dao.xml文件中直接使用别名来引用java类1
2
3
4<select id="findAll" resultType="dept">
select * from dept
</select>
七、mybatis连接池与事务
连接池技术
在我们前面Web课程中也学习过类似的连接池技术,而mybatis中也有连接池技术,在之前的案例中mybatis通过mybatis-config.xml配置文件中的<dataSource type="POOLED">来实现mybatis连接池的配置。
连接分类
MyBatis内置了连接池技术,dataSource标签的type属性有3个取值:
- POOLED 使用连接池
- UNPOOLED 不使用连接池
- JNDI 使用JNDI实现连接池
在这三种数据源中,我们一般都是采用POOLED 数据源,数据库连接只有在我们用到的时候,才会获取并打开连接,当我们用完了就立即将数据库连接归还给连接池。
八、mybatis的动态SQL
通常情况下,静态SQL是预先定义好的SQL语句,动态SQL允许根据程序运行时的条件和需求动态的生成SQL语句,从而提供更高的灵活性和可重用性。
在mybatis框架中,提供了一系列的标签可以让我们实现动态SQL配置。
if标签
<if>标签用于进行条件判断,类似于java中的if语句,通过标签可以有选择的加入SQL语句的片段。
需求:
根据实体类的不同取值,使用不同的SQL语句进行查询,比如deptName不为空时可以根据deptName查询,如果address不为空时还要加入部门位置作为条件,这种情况在我们的多条件组合查询中经常会碰到。1
2// 多条件查询
List<Dept> findByDept(Dept dept);1
2
3
4
5
6
7
8
9
10
11<select id="findByDept" resultMap="deptMap" parameterType="dept">
select * from dept where 1=1
<if test="dname!=null and dname!=''">
and deptname like #{dname}
</if>
<if test="loc!=null and loc!=''">
and address like #{loc}
</if>
</select>
在构建动态SQL时,我们可能会根据不同的条件来拼接查询语句,如果没有任何条件,直接拼接一个where子句会导致SQL语句格式错误,使用where 1=1以确保无论是否添加其他条件,都不会出现语法错误。1
2
3
4
5
6
7
8
9
10
public void testFindByDept(){
Dept d=new Dept();
// d.setDname("%部%");
// d.setLoc("%州%");
List<Dept> depts = deptDao.findByDept(d);
for (Dept dept : depts) {
System.out.println(dept);
}
}
where标签
<where>标签替换where,能够自动处理查询条件,智能的处理多余的where、and、or关键字。
需求:为了简化上面where1=1的条件拼接,可以采用<where>标签来简化开发。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<!-- sql标签封装SQL语句 id给SQL去定义 -->
<sql id="queryAll">select * from dept</sql>
<select id="findByDept" resultMap="deptMap" parameterType="dept">
<include refid="queryAll"/>
<where>
<if test="dname!=null and dname!=''">
and deptname like #{dname}
</if>
<if test="loc!=null and loc!=''">
and address like #{loc}
</if>
</where>
</select>
foreach标签
<foreach>标签可以在SQL中配置迭代集合类型参数,用于in语句查询某一范围内的数据。
需求:
传入多个deptId查询部门信息,这样我们在进行范围查询时,就要将一个集合中的值,作为参数动态添加进来
1). 在Vo中加入一个List集合用于封装参数1
2
3
4
5
6
7
8
9
10
11
12
13package com.iweb.bean;
public class QueryVo {
private List<Integer> ids;
public List<Integer> getIds() {
return ids;
}
public void setIds(List<Integer> ids) {
this.ids = ids;
}
}
映射配置1
2
3
4
5
6
7
8
9
10
11
12
13
14<select id="findInIds" parameterType="queryVo" resultMap="deptMap">
<include refid="queryAll"/>
<where>
<if test="ids!=null and ids.size()>0">
<foreach collection="ids" open="deptid in(" item="deptId" separator="," close=")">
#{deptId}
</foreach>
</if>
</where>
</select>
SQL说明:
- ollection:代表要遍历的集合
- open:前缀,表示该语句以什么开头
- item:代表遍历集合的每一个元素别名
- separator:表示每次迭代元素之间以什么作为分隔符
- close:是后缀,表示以什么结束
测试1
2
3
4
5
6
7
8
9
10
11
12
13
public void testFindByIds(){
QueryVo vo=new QueryVo();
List<Integer> ids=new ArrayList<>();
ids.add(1);
ids.add(3);
ids.add(9);
vo.setIds(ids);
List<Dept> depts = deptDao.findInIds(vo);
for (Dept dept : depts) {
System.out.println(dept);
}
}
九、多表查询
我们之间学习都是基于单表操作的,而实际开发中,随着业务难度的加深,肯定需要多表操作。
多表分类
- 一对一:在任意一方建立外键,关联对方的主表
- 一对多:在多的一方建立外键,关联一的一方的主键
- 多对多:借助中间表,中间表至少两个字段,分别关联两张表的主键
【一对一】
需求:查询用户,同时还要获取当前账户的所属用户信息因为一个账户信息只能给一个用户使用,所以从查询账户信息触发关联查询用户信息是一对一查询。如果从用户信息出发查询用户下的账户信息为一对多查询,因为一个用户可以有多个账户
1).数据库设计1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20-- 用户表
create table user(
id int auto_increment primary key,
username varchar(10) not null,
sex varchar(4),
birthday date,
address varchar(200)
)
insert into user values(1,'jack','男','2005-1-1','南京'),
(2,'lucy','女','2000-11-12','扬州');
-- 账户表
create table account(
id int auto_increment primary key,
money decimal(10,2),
uid INT, -- 外键
foreign key(uid) references user(id)
);
insert into account values(10,2000,1);
insert into account values(11,1500,2);
insert into account values(12,3000,1);
实现查询账户信息时对应用户的信息SQL如下:1
2select s.*,a.`id` as aid,a.`money` from
account a,user s where a.`uid`=s.`id`
2).账户实体类1
2
3
4
5
6
7
8
9
10package com.iweb.bean;
public class Account {
private Integer id;
private Integer uid;
private Double money;
// 查询账户信息以及对应的用户信息,需要建立一对一或者一对多映射关系,将查询结果封装到user属性中
private User user;
// 省略get/set..
}
3).用户信息实体类1
2
3
4
5
6
7
8public class User {
private Integer id;
private String username;
private String sex;
private Date birthday;
private String address;
// 省略get/set..
}
4).创建mapper1
2
3public interface AccountDao {
List<Account> findAll();
}
5).创建mapper映射文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<mapper namespace="com.iweb.mapper.AccountDao">
<!-- 建立对应关系 -->
<resultMap id="accountMap" type="account">
<!-- 封装account对象 -->
<id column="aid" property="id"></id>
<result column="uid" property="uid"></result>
<result column="money" property="money"></result>
<!-- 用于映射关联查询用户的信息
property 实体类对应的属性名,查询完之后将结果封装到property属性所指定的实体bean熟悉中
javaType 实体类对应的全类名,用于指定关联查询的结果封装到哪个实体类中
-->
<association property="user" javaType="user">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="sex" property="sex"></result>
<result column="birthday" property="birthday"></result>
<result column="address" property="address"></result>
</association>
</resultMap>
<!-- 配置查询所有操作,同时关联用户信息 -->
<select id="findAll" resultMap="accountMap">
select s.*,a.`id` as aid,a.`money` from account a,user s where a.`uid`=s.`id`
</select>
</mapper>
6).将mapper映射文件,添加到mybatis-config.xml1
2
3
4
5
6<!-- 告诉mybatis映射文件的位置 -->
<mappers>
<mapper resource="com/iweb/mapper/AccountDao.xml"></mapper>
</mappers>
7).测试1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38package com.iweb.test;
public class AccountTest {
private InputStream in;
private SqlSessionFactory build;
private SqlSession session;
private AccountDao accountDao;
// 在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
session.close();
in.close();
}
// 在测试方法执行之前完成执行
public void init() throws Exception{
//1.读取配置文件
in = Resources.getResourceAsStream("mybatis-config.xml");
//2.创建sqlSessionFactory构建者对象
build = new SqlSessionFactoryBuilder().build(in);
//3.使用SqlSessionFactory生成sqlSession对象
session = build.openSession();
//4.使用session创建dao接口的代理对象
accountDao = session.getMapper(AccountDao.class);
}
public void testFindAll(){
List<Account> accounts = accountDao.findAll();
for (Account account : accounts) {
System.out.println("------------账户信息-------------");
System.out.println(account.getId()+"\t"+account.getMoney());
System.out.println("------------所属用户-------------");
System.out.println(account.getUser().getId()+"\t"+account.getUser().getUsername());
}
}
}
【一对多】
需求:查询用户信息及用户关联的账户信息
1).修改用户表1
2
3
4
5
6
7
8
9
10
11
12
13public class User {
private Integer id;
private String username;
private String sex;
private Date birthday;
private String address;
// 实现查询用户的同时关联账户信息 一对多关联
// 查询用户的信息对应的账户信息之间建立一对多关系,主表为用户表,实体类中需要包含从表实体类的集合引用
private List<Account> accounts;
// 省略get/set方法
}
2).用户持久层添加查询方法1
2
3public interface UserDao {
List<User> findAll();
}
3).创建对应mapper文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<mapper namespace="com.iweb.mapper.UserDao">
<resultMap id="userMap" type="user">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="sex" property="sex"></result>
<result column="birthday" property="birthday"></result>
<result column="address" property="address"></result>
<!-- collection 是用于建立一对多集合属性的对应关系
property="accounts" 关联查询的结果集存储在Users对象上的哪个属性中
ofType="account" 指定关联查询的结果集中的对象类型,即List中的对象类型
-->
<collection property="accounts" ofType="account">
<id column="aid" property="id"></id>
<result column="uid" property="uid"></result>
<result column="money" property="money"></result>
</collection>
</resultMap>
<!-- 配置查询所有操作,同时关联账户信息 -->
<select id="findAll" resultMap="userMap">
select u.*,a.`id` as aid,a.`uid`,a.`money` from user u left join account a on u.`id`=a.`uid`
</select>
</mapper>
4).将mapper映射文件,添加到mybatis-config.xml1
<mapper resource="com/iweb/mapper/UserDao.xml"></mapper>
5).测试1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42package com.iweb.test;
public class UserTest {
private InputStream in;
private SqlSessionFactory build;
private SqlSession session;
private UserDao userDao;
// 在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
session.close();
in.close();
}
// 在测试方法执行之前完成执行
public void init() throws Exception{
//1.读取配置文件
in = Resources.getResourceAsStream("mybatis-config.xml");
//2.创建sqlSessionFactory构建者对象
build = new SqlSessionFactoryBuilder().build(in);
//3.使用SqlSessionFactory生成sqlSession对象
session = build.openSession();
//4.使用session创建dao接口的代理对象
userDao = session.getMapper(UserDao.class);
}
public void testFindAll(){
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println("-----------用户信息------------");
System.out.println(user.getId()+"\t"+user.getUsername());
System.out.println("-----------账户信息-----------");
List<Account> accounts = user.getAccounts();
for (Account account : accounts) {
System.out.println(account.getId()+"\t"+account.getMoney());
}
}
}
}
十、Mybatis缓存
缓存(cache)是一种临时存储数据的机制,用于提供数据访问速度,在计算机系统中,缓存通常存储频繁访问的数据副本,避免每次访问都从原始数据源(数据库)获取,从而减轻IO操作和数据库的压力。
大多数的持久层框架都提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提供性能。
一级缓存
默认开启,一级缓存是SqlSession级别的缓存,当Session flush或者close后,该session中的一级缓存将被清空。1
2
3
4
5
6
public void testFindByDeptId(){
Dept dept = deptDao.findByDeptId(3);
System.out.println("第一次查询:"+dept);
System.out.println("第二次查询:"+dept);
}
我们可以发现虽然上面的代码中我们查询了两次,但是最后只执行了一次数据库操作,这就是mybatis提供给我们的一级缓存起了作用,因为一级缓存的存在,导致第二次查询id为3的记录时,并没有发起SQL语句从数据库查询数据,而是从一级缓存中查询。
一级缓存分析
1.一级缓存是SqlSession范围的缓存。
2.第一次发起部门id为3的部分信息,先去缓存中找是否有id为3的部门信息,如果没有从数据库查询部门信息,得到部门信息后,将部门信息存储到一级缓存中。
3.如果sqlSession去执行commit操作(执行插入,更新,删除),会清空SqlSession中的一级缓存,是直接从数据库查询的数据。这样做的目的是为了让缓存中存储的是最新的数据,避免脏读。即在一个会话中,对数据库的增删改操作,均会使一级缓存失效。
4.第二次发起查询部门id为3的部门信息,先去缓存中找是否有id为3的部门信息,缓存中有,直接从缓存中获取信息。
测试清空一级缓存1
2
3
4
5
6
7
8
9
public void testFindByDeptId(){
Dept dept = deptDao.findByDeptId(3);
System.out.println("第一次查询:"+dept);
session.clearCache();// 此方法清空缓存
Dept dept1 = deptDao.findByDeptId(3);
System.out.println("第二次查询:"+dept1);
System.out.println(dept==dept1);
}
mybatis的二级缓存
默认关闭,需要手动开启。
作用域是sqlSessionFactory级别。
开启二级缓存
1).在mybatis-config.xml中开启二级缓存1
2<!-- 因为cacheEnabled的取值默认是true,false代表不开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
2).在对应的mapper配置文件中声明使用二级缓存1
2
3
4
5 <mapper namespace="com.iweb.dao.IDeptDao">
<!-- 开启二级缓存的支持 -->
<cache/>
....省略其他配置信息...
</mapper>
3).实体类必须实现Serializable接口1
public class Dept implements Serializable {}
4).配置statement上面的1
2
3
4<!-- useCache="true" 要使用二级缓存,针对每次要查询的数据是最新的,设置为false,禁用二级缓存 -->
<select id="findByDeptId" useCache="true" parameterType="int" resultMap="deptMap">
<include refid="queryAll"/> where deptid=#{deptNo}
</select>
5).测试二级缓存1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void testFindByDeptId(){
SqlSession sqlSession1 = build.openSession();
IDeptDao deptDao1 = sqlSession1.getMapper(IDeptDao.class);
Dept dept1 = deptDao1.findByDeptId(3);
System.out.println(dept1);
sqlSession1.close();// 清除一级缓存
SqlSession sqlSession2 = build.openSession();
IDeptDao deptDao2 = sqlSession2.getMapper(IDeptDao.class);
Dept dept2 = deptDao2.findByDeptId(3);
System.out.println(dept2);
sqlSession2.close();// 清除一级缓存
System.out.println(dept1==dept2);
}
经过上面的测试,我们发现执行的两次查询,并且在第一次查询后,我们关闭了一级缓存,再去执行第二次查询时,我们发现并没有对数据库发起SQL语句,所以此时的数据就是来源我们所说的二级缓存。
==注意==
当我们使用二级缓存时,所缓存的类一定要实现Serializable接口这种就可以使用序列化来保存对象。
一级缓存和二级缓存的使用和区别
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
<foreach>标签用于遍历
MyBatisPlus
这玩意神的一批啊,有这个和小辣椒搭配,我上面写的这上千行就和没写一样
SpringBoot整合MyBatisPlus
创建SpringBoot项目
1).使用Spring Initializr创建SpringBoot项目,选择Spring Web和MyBatis Plus依赖
2).在项目中添加MyBatis Plus的依赖
3).在项目中添加MyBatis Plus的配置
当然上述过程最快的部署方式,如果我们出现了一些小问题,那我们可以自行在pom.xml中添加依赖
依赖要补充这些(示例,请自行参考自己的环境选择合适的版本)1
2
3
4
5
6<!-- MyBatis Plus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
配置文件说明(yml写法)1
2
3
4
5
6# MyBatis Plus配置
mybatis-plus:
# 配置mapper.xml文件的位置
mapper-locations: classpath:/mapper/*.xml
# 配置实体类所在的包
type-aliases-package: com.iweb.bean
记得匹配数据库配置,不然mybatis-plus会报错(自行匹配链接)1
2
3
4
5
6spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
创建实体类
创建一个entity包,用于存放实体类,这里以User为例为例
这里的参数和字段得和数据库表相对应,以便可以正常映射1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 用户实体类
*/
//这个是除了全参构造的全部写好
//这个写了全参构造
public class User {
private Integer id;
private String name;
private Integer age;
public String email;
}
这个上面使用了小辣椒lombok注解,来简化代码的,如果你也想省了get,set,toString方法,你可以使用
小辣椒lombok依赖引入1
2
3
4
5
6<!-- Lombok依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
我们编写实体类有一些东西需要介绍下:
框架会通过反射实体类的字节码,阅读字节码中的属性名和注解的信息,动态拼接增删改查的sql语句
- 有注解的属性,会自动拼接sql语句
- 没有注解的属性,不会拼接sql语句
编写Mapper接口
先创建mappar包或者写为DAO,创建每张表的Mapper接口(这里以UserMapper为例)1
2
3
4
5
6
7
8
9/**
* UserMapper接口用于代替UserDAO
* 用于操作user表的增删改查
* 需要继承mybits框架的BaseMapper接口
* 指定实体范形
*/
public interface UserMapper extends BaseMapper<User> {
}
很简单吧,你只要继承官方提供的基础父接口就生命都不用动了,说白了这就是给你项目中加一个目录锚点,方法早就写好了
家产继承这一块,少走了多少弯路啊,增删改查的功能全给你写好了
不信吗?你编译一下看看就知道了,全弄好了都
在启动类上加上扫描注解
在启动类上我们得添加扫描注解,来扫描我们的Mapper接口包
- @MapperScan注解用于扫描Mapper接口包,后面括号中的参数是Mapper接口包的路径,告诉框架这个包在哪里
- @SpringBootApplication注解用于开启Spring Boot应用,自动配置Spring Boot的环境
配好之后框架会根据我们指定的Mapper包路径来扫描该包中的所有接口,这个矿建会基于cglib动态代理模式,动态的实现接口中的每一个方法
也就是说,我们缩写的Mapper接口继承BaseMapper接口,里面所有的抽象方法,框架已经帮助我们实现,但是你如果想要半自动写法,请看上面的在xml中配置sql的写法
1 |
|
组合使用Mapper接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UserController {
UserMapper userMapper;
public Object getAllUsers(){
List<User> userList = userMapper.selectList(null);
return userList;
}
public Object createUser( User user){
return userMapper.insert(user);
}
}
上述代码是在控制层包中编写的一个简单的实际应用,他主要有这几个部分构成:
- @Autowired注解用于自动注入UserMapper接口的实现类
- @GetMapping注解用于映射GET请求,返回所有用户
- @PostMapping注解用于映射POST请求,创建用户
- @RequestBody注解用于将请求体中的JSON数据转换为User实体类
关注BaseMapper有哪些核心功能
官方提供的BaseMapper接口将成为每个Mapper接口的基础,它包含了增删改查的基本方法,以及一些高级方法,如批量操作、分页查询等
控制器层中,我们可以直接调用BaseMapper接口的方法,来实现对数据库的增删改查操作
mybatis条件构造器和分页器的使用
条件构造器的分类
- 按照功能分
- 查询条件构造器
- 生成where子句
- 更新条件构造器
- 生成where子句
- 生成set子句
- 删除条件构造器
- 新增条件构造器
- 查询条件构造器
- 按照语法分
- 普通条件构造器
- 列名需要用字符串硬编码写死,写错不容易被发现,写错编译也可以通过
- Lambda条件构造器
- 支持方法引用【::方法名】语法,写错立马出红线,不能编译通过
- 普通条件构造器
常见的条件构造器名单就不在这里描述了,见站外文档
分页器部署和安装
在Mybatisatis中,分页器是用于分页查询的工具类,它可以帮助我们快速实现分页查询的功能,该分页器需要单独配置依赖才可以使用
分页器的依赖引入1
2
3
4
5
6<!-- 分页器依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.1</version>
</dependency>
配置分页器插件
在项目中的config(自定义设置)中添加相关配置类,注意导包和路径命名问题1
2
3
4
5
6
7
8
9
10
11
12
13
public class MybatisPlusConfig {
/**
* 添加分页插件
*/
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加
// 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
return interceptor;
}
}
如果你是在配置文件中设置,在yml中如下配置1
2
3
4
5
6
7
8
9
10
11spring:
application:
name: springboot-mybits
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
pagehelper:
helper-dialect: mysql
reasonable: true
params: countSql
support-methods-arguments: true
使用分页器
在Mapper接口中,我们可以直接调用PageHelper类的方法,来实现分页查询,我们需要创建传参对象
高效的方式是创建一个BaseDTO类,来封装分页查询的参数,有分页页码、每页数量、查询条件等属性1
2
3
4
5
6
public class BaseDTO {
//这两个是分页参数名称
private Integer current;
private Integer size;
}
其他类继承BaseDTO类,来封装分页查询的参数,比如UserDTO类
分页器的成员结构分析
| 成员 | 作用 | 说明 |
| —- | —- | —- |
| records | 分页查询的结果集 | 分页查询的结果集 ,是一个List集合,每个元素都是一个实体类对象,分页器会允许select from user limit ?,?,其集合中的内容就是查询结果 |
| total | 总记录数 | 总记录数,用于分页查询的分页信息,如果没有查询条件,是所有数据的总行数,如果有查询条件,是符合条件的总行数,分页器会先运行select count() from user,再根据查询条件进行分页查询 |
| size | 每页数量 | 用客户端传回来的size作为返回的size |
| current | 当前页码 | 用客户端传回来的current作为返回的current |
| pages | 总页数 | pages是total/size的向上取整的值 |
分页器的底层运行流程
1.用select count() from user查询总记录数,根据总行数算出总页数
2.用select from user limit ?,?查询当前页数据,并将结果集解析为list,用名为records的list集合存储
3.将total、records、size、current、pages这5个属性封装到一个PageResult对象中,返回给客户端
分页参数current和size的作用分别是什么?
分页器会使用current和size参数,计算limit后面的两个参数
第一个参数是查询的行号偏移量 = (current - 1) * size
第二个参数是查询的行号 = size
如果客户端没有传current和size参数,他们不会有什么影响,会使用默认值完成,默认值分别是1和10
1
2
3
4
5
6 Page<User> page;
if (userDTO.getCurrent()!=null && userDTO.getSize()!=null) {
page = new Page<>(userDTO.getCurrent(), userDTO.getSize());
}else {
page = new Page<>();
}
