MyBatis

一、框架概述

什么是框架

框架对通用的代码的封装,通过使用框架,提高开发效率,而不需要关心一些繁琐的、复杂的底层代码实现,把更多的经历用于所在需求的实现上。

框架可以理解为一个半成品,我们选用这个半成品,然后加上业务需求来最终实现整个功能。

软件开发的分层

在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一(单一原则)

单一原则:一个类或者一个方法,就只做一件事情,只管一个功能。这样就可以让类、接口、方法的复杂度更低,可读性更强、扩展性更好,也便于后期的维护。

以前我们写代码,从组成可以分成三个部分:

  • 数据访问:负责业务数据的维护操作
  • 逻辑处理:负责业务逻辑处理的代码
  • 请求处理:接受请求,给页面响应数据

在我们项目开发中,将代码分为三层:

  1. 前端发起的请求,由controller层接收,控制器响应数据给前端
  2. controller层调用service层进行逻辑处理,service层处理后,把处理结果返回给controller层
  3. dao层操作底层的数据,负责拿到数据返回给service层

分层就是分工,划分环节,通过分层架构的设计,使代码的职责分明,容易理解和维护,还能实现代码的重用,提高系统的整体性能和扩展性,同时各层之前的结构也很方便,提高代码的质量和稳定性。

通过分层更好的实现各个部分的职责,在每一层将再细化出不同的矿界,分别解决各层关注的问题。

分层开发下的常见框架

  • 表现层:指的是用户直接与应用进行交互的部分,负责接收用户请求、处理业务逻辑、返回响应结果。表现层框架的主要任务是将业务逻辑与用户进行交互,提供良好的用户体验。
  • 业务逻辑层:也称为服务层,负责处理应用的业务逻辑。它负责协调不同组件之间的工作,执行复杂的业务规则和计算。业务逻辑层框架的主要任务是封装业务逻辑,提供可重复使用的业务功能。
  • 数据访问层:也称为持久层,负责与数据库或其他数据存储进行交互。它负责执行数据库操作,如查询、插入、更新和删除数据。数据访问层框架的主要任务是提供统一的接口,隐藏底层数据库的细节,使业务逻辑层能够独立于具体的数据库实现。

二、Mybatis框架

什么是mybatis

MyBatis 是一款优秀的持久层框架。

采用ORM思想解决实体和数据库映射的问题,对JDBC 进行了封装,屏蔽了JDBC API底层访问细节,使我们不用与JDBC API打交道,就可以完成对数据库的持久化操作。

Mybatis本来是apache的一个开源项目IBatis,2010年的这个项目由apache改名MyBatis。

  • 持久层:指的是数据访问层dao,是用来操作数据库的
  • 框架:是一个半成品软件,再框架的基础上进行软件开发,更加高效,规范,可扩展。

ORM:对象关系映射,实现了面向对象编程语言中对象与关系型数据库中的表之间的映射,ORM框架允许开发者使用面向对象的方式来操作数据库,而无需关心底层的SQL语句。

具体来说:

  1. ORM框架将数据库中的表(table)映射为编程语言中的类(class)
  2. 表中的记录映射为类的实例
  3. 字段映射为对象的属性

JDBC编程的分析

  1. 数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
  2. 因为SQL语句的where条件不一定,可能多可能少,修改SQL还要i需改代码,系统不容易维护。
  3. 对结果集解析步骤相对繁琐。

Mybatis入门案例

1). pom.xml

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
<dependency>
<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>3.8.1</version>

</dependency>

<!--MySQL驱动-->
<dependency>
<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<version>5.1.46</version>

</dependency>

<!--MyBatis核心包-->
<dependency>
<groupId>org.mybatis</groupId>

<artifactId>mybatis</artifactId>

<version>3.5.5</version>

</dependency>

2). 编写实体类

1
2
3
4
5
6
public class Dept {
private int deptNo;
private String dname;
private String loc;
// 省略get/set及toString方法
}

3). 编写持久层接口

1
2
3
4
public interface IDeptDao {
// 查询所有部门信息
List<Dept> findAll();
}

XML配置文件实现

【1】创建XML映射文件

要求:

  • 创建位置必须与持久层接口在相同的包中
  • 名称:必须以持久层接口名称命名文件名

【2】编写XML映射文件

xml映射文件中的DTD约束,直接从mybatis官网复制即可,或者直接AI生成

1
2
3
4
<?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">

【3】配置

在mybatis-config.xml配置文件中,需要配置mapper映射文件的位置,告诉mybatis去哪里找到对应的SQL语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  <?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">

<!-- XML映射文件的namespace属性为dao接口全限名 包名.类名 -->
<mapper namespace="com.iweb.dao.IDeptDao">

<!-- 配置查询所有操作
id属性为方法名
resultType属性为返回值类型 报名.类名 区分大小
-->
<select id="findAll" resultType="com.iweb.bean.Dept">
select * from dept
</select>
</mapper>

【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
    <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<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
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
    package com.iweb.test;

import com.iweb.bean.Dept;
import com.iweb.dao.IDeptDao;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;

import java.io.InputStream;


public class TestMyBatis {

@Test
public void testFindAll() throws Exception{
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
//2.创建sqlSessionFactory构建者对象
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(in);
//3.使用SqlSessionFactory生成sqlSession对象
SqlSession session = build.openSession();
//4.使用session创建dao接口的代理对象
IDeptDao deptDao = session.getMapper(IDeptDao.class);
//5.使用代理对象执行查询所有的方法
for (Dept dept : deptDao.findAll()) {
System.out.println(dept.toString());
}
//6.释放资源
session.close();
in.close();
}
}

XML映射配置

mybatis的开发有两种方式:

  1. 注解
  2. XML

XML配置文件规范

使用mybatis的注解方式,主要是完成一些简单的增删改查功能,如果需要实现复杂的SQL功能,建议使用XML配置来配置映射语句,也就是将SQL语句写在XML配置文件中。

在mybatis中使用XML映射文件方式开发,需要符合一定的规范:

  1. XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)
  2. XML映射文件的namespace属性为Mapper接口全限定名一致
  3. XML映射文件中SQL语句的id属性值与Mapper接口中的方法名一致,并保持返回类型一致

总结
通过上述案例,我们发现使用mybatis是非常容易的事情,只需要编写Dao接口并且按照mybatis的要求编写两个配置文件,就可以实现功能。

基于注解的mybatis使用

1). 在持久层接口中添加注解

1
2
3
4
5
    public interface IDeptDao {
// 查询所有部门信息
@Select("select * from dept")
List<Dept> findAll();
}

2). 修改mybatis-config.xml
1
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;

@After // 在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
session.close();
in.close();
}

@Before // 在测试方法执行之前完成执行
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);
}


@Test
public void testFindByDeptId(){
Dept dept = deptDao.findByDeptId(3);
System.out.println(dept);
}

@Test
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
  @Test
public void testSaveDept(){
Dept dept=new Dept();
dept.setDname("市场部");
dept.setLoc("长沙");
// 执行保存方法
int i = deptDao.saveDept(dept);
System.out.println(i+"条受影响");
}

我们在实现增删该时一定要去控制事务的提交,那么mybatis是如何控制事务提交的?可以使用session.commit()来提交事务。

如何获取新增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
      @Test
    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
    @Test
    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
    @Test
    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
  @Test
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
@Test
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
 @Test
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
2
// 聚合函数
int findTotal();

映射配置文件

1
2
3
4
<!-- 聚合函数 -->
<select id="findTotal" resultType="int">
select count(*) from dept
</select>

测试
1
2
3
4
5
@Test
public void testFindTotal(){
int total = deptDao.findTotal();
System.out.println(total);
}

四、mybatis的参数深入

parameterType配置参数

SQL语句传参,使用标签的parameterType属性设置。属性的取值可以是基本类型,引用类型(String),还可以是实体类类型(POJO),同时也可以是实体类的包装类。

传递POJO包装对象

开发中通过POJO传递查询条件,查询条件是综合的查询条件,不仅包括用户查询条件还可以包括其他的查询条件(比如用户购买的商品信息也作为查询条件),这时可以使用包装类对象传递输入参数,POJO类中包含POJO

需求:根据部门名称查询部门查询,查询条件放到QueryVo的Dept属性中。

1). 编写paramVO

1
2
3
4
5
6
7
8
9
10
11
12
public 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
@Test
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
2
3
4
<resultMap id="唯一标识" type="指定查询结果映射的java类型">
<id property="实体类中主键属性名称" column="数据库表中主键字段的名称"/>
<result property="实体类中非主键属性名称" column="数据库表中非主键字段的名称"/>
</resultMap>
1
2
3
4
5
6
7
8
9
10
<!--  定义Dept实体类和数据库表中字段的对应关系
-->
<resultMap id="deptMap" type="com.iweb.bean.Dept">
<id property="deptNo" column="deptid"></id>

<result property="dname" column="deptname"></result>

<result property="loc" column="address"></result>

</resultMap>

属性说明

  • 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
@Test
public void testFindAll() throws Exception{
//5.使用代理对象执行查询所有的方法
List<Dept> depts = deptDao.findAll();
for(Dept dept : depts){
System.out.println(dept);
}
}

六、配置内容

在使用mybatis框架时,自定义别名可以简化XML配置文件和代码中的映射操作。

1
2
3
4
5
6
<!-- 单个别名定义 -->
<typeAliases>
<typeAlias type="com.iweb.bean.Dept" alias="Dept"/>
<!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大小写都可以) -->
<package name="com.iweb.bean"/>
</typeAliases>

在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
@Test
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
13
package 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
@Test
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
2
select 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
10
package 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
8
public class User {
private Integer id;
private String username;
private String sex;
private Date birthday;
private String address;
// 省略get/set..
}

4).创建mapper
1
2
3
public 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
<?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.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.xml
1
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
38
package com.iweb.test;
public class AccountTest {

private InputStream in;
private SqlSessionFactory build;
private SqlSession session;
private AccountDao accountDao;

@After // 在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
session.close();
in.close();
}

@Before // 在测试方法执行之前完成执行
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);
}

@Test
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
13
public 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
3
public 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
<?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.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.xml
1
<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
42
package com.iweb.test;

public class UserTest {

private InputStream in;
private SqlSessionFactory build;
private SqlSession session;
private UserDao userDao;

@After // 在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
session.close();
in.close();
}

@Before // 在测试方法执行之前完成执行
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);
}

@Test
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
@Test
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
@Test
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
@Test
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>标签用于遍历