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)
- 表中的记录映射为类的实例
- 字段映射为对象的属性
JDBC编程的分析
- 数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
- 因为SQL语句的where条件不一定,可能多可能少,修改SQL还要i需改代码,系统不容易维护。
- 对结果集解析步骤相对繁琐。
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的要求编写两个配置文件,就可以实现功能。
基于注解的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()
来提交事务。
如何获取新增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);
}
}
六、配置内容
在使用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
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>
标签用于遍历