1 简介
1.1 什么是Mybatis
- MyBatis 是一款优秀的持久层框架
- 它支持定制化 SQL、存储过程以及高级映射。
- MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
- MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。
- MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了[google code](https://baike.baidu.com/item/google code/2346604),并且改名为MyBatis 。
- 2013年11月迁移到Github。
获取Mybatis
-
maven仓库
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency>
-
github : https://github.com/mybatis/mybatis-3/releases
-
中文文档:https://mybatis.org/mybatis-3/zh/index.html
1.2 特点
- 简单易学
- 本身就很小且简单。
- 没有任何第三方依赖,最简单安装只要两个jar文件+配置几个sql映射文件易于学习
- 易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现。
- 灵活:
- mybatis不会对应用程序或者数据库的现有设计强加任何影响。
- sql写在xml里,便于统一管理和优化。通过sql语句可以满足操作数据库的所有需求。
- 松耦合
- 解除sql与程序代码的耦合
- 通过提供DAO层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。
- sql和代码的分离,提高了可维护性。
- 提供标签
- 提供映射标签,支持对象与数据库的orm字段关系映射
- 提供对象关系映射标签,支持对象关系组建维护
- 提供xml标签,支持编写动态sql。
1.3 持久层
持久化:将数据从瞬时状态转化为持久状态。
之所以有持久层,就是将数据能断电后存储。一些其他原因,比如内存太贵
Dao层就是一个持久层,层的概念是为了解耦合。
MyBatis 是一款优秀的持久层框架
1.4 为什么使用Mybatis
- 非常方便将数据库存入到数据库中
- 框架,简化JDBC,自动化
- 不使用Mybatis也能持久化,但是它更容易、更方便上手
- 优点:简单易学、灵活、sql与代码分离、可维护性、标签
- 最重要的一点:使用的人多
2 第一个Mybatis程序
pojos
package com.zl.pojos;
//对应数据库的一张表结构
public class Students {
private int id;
private int age;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
...
}
DAO接口
public interface StudentsMapper {
public List<Students> getStudents();
}
映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace对应一个mapper接口-->
<mapper namespace="com.zl.dao.StudentsMapper">
<select id="getStudents" resultType="com.zl.pojos.Students">
select * from students
</select>
</mapper>
核心配置文件
<?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>
<!--环境,可以配置多个-->
<environments default="development">
<environment id="development">
<!--事务类型-->
<transactionManager type="JDBC"/>
<!--type:线程池-->
<dataSource type="POOLED">
<!--四个配置项-->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/jdbc_test?useUnicode=true&characterEncoding=utf8&useSSL=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--绑定映射文件-->
<mapper resource="com/zl/dao/StudentsMapper.xml"/>
</mappers>
</configuration>
工具类
public class Mybatis_utils {
//SqlSessionFactory建立单例,可以重复使用,创建SqlSession
private static SqlSessionFactory sqlSessionFactory;
static{
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
//SqlSessionFactoryBuilder在创建sqlSessionFactory即可删除
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}
}
测试
public class Test {
@org.junit.Test
public void test1(){
//SqlSession不是线程安全的,每次使用完之后应该关闭
try(SqlSession session = Mybatis_utils.getSqlSession()){
StudentsMapper students = session.getMapper(StudentsMapper.class);
List<Students> list = students.getStudents();
for(Students s: list){
System.out.println(s);
System.out.println("");
}
}
}
}
3 CRUD
更新
<insert id="addStudents" parameterType="com.zl.pojos.Students" >
insert into students (`name`,`age`) values (#{name}, #{age})
</insert>
<update id="updateStudents" parameterType="com.zl.pojos.Students">
update students set `name` = #{name},`age` = #{age} where `id` = #{id}
</update>
<delete id="deleteStudents" parameterType="com.zl.pojos.Students">
delete from `students` where `id` = #{id}
</delete>
测试
public void updateTest(){
try(SqlSession session = Mybatis_utils.getSqlSession()){
StudentsMapper mapper = session.getMapper(StudentsMapper.class);
Students students = new Students(0, 12, "Jane");
int line = mapper.addStudents(students);
System.out.println("line:" + line);
students.setId(1);
line = mapper.updateStudents(students);
System.out.println("line:" + line);
students.setId(2);
line = mapper.deleteStudents(students);
System.out.println("line:" + line);
session.commit();
List<Students> list = mapper.getStudents();
for(Students s: list){
System.out.println(s);
System.out.println("");
}
}
}
parameterType扩展
- map 使用键值对传参
- 单个传参查询,无需指定parameterType
- 模糊查询
<select id="getStudentsById" resultType="com.zl.pojos.Students">
select * from `students` where `id` = #{id}
</select>
<select id="getStudentsByName" parameterType="map" resultType="com.zl.pojos.Students">
select * from `students` where `name` = #{sname}
</select>
<select id="getStudentsLikeName" resultType="com.zl.pojos.Students">
select * from `students` where `name` like #{name}
</select>
测试
try(SqlSession session = Mybatis_utils.getSqlSession()){
StudentsMapper students = session.getMapper(StudentsMapper.class);
List<Students> list = students.getStudentsById(3);
showList(list);
Map<String, Object> map = new HashMap<>();
map.put("sname", "Tom"); //通过键值对,可以多个
list = students.getStudentsByName(map);
showList(list);
list = students.getStudentsLikeName("_o%");
showList(list);
}
4 配置解析
研究核心配置文件中的属性配置
4.1 环境配置(environments)
MyBatis 可以配置成适应多种环境,比如开发环境、上线环境、测试环境等。
尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
<!--default选择默认环境-->
<environments default="development">
事务管理器(transactionManager)
<transactionManager type=""[JDBC|MANAGED]"">
-
JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域,常用
-
MANAGED - 让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)
如果使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。
数据源(dataSource)
<dataSource type="[UNPOOLED|POOLED|JNDI]">
- UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。
- POOLED– 常用类型选择,这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。
- JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用。在EJB用,目前很少用
4.2 属性(properties)
这些属性可以在外部进行配置,并可以进行动态替换。
<properties resource="org/mybatis/example/config.properties">
<!--以下的property也可以不写,会自动加载配置文件中的内容-->
<property name="username" value="root"/>
<property name="password" value="123456"/>
</properties>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
配置文件
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc_test?useUnicode=true&characterEncoding=utf8&useSSL=true
username=root
password=123456
注意点:核心配置中,各个配置有严格的顺序
The content of element type "configuration" must match "(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?)".
4.3 设置(settings)
这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。
常用的setting配置
<settings>
<!--全局性地开启或关闭所有映射器配置文件中已配置的任何缓存-->
<setting name="cacheEnabled" value="true"/>
<!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--允许 JDBC 支持自动生成主键,需要数据库驱动支持-->
<setting name="useGeneratedKeys" value="false"/>
<!--是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn-->
<setting name="mapUnderscoreToCamelCase" value="false"/>
<!--指定 MyBatis 所用日志的具体实现,未指定时将自动查找-->
<setting name="logImpl" value="JDK_LOGGING "/>
</settings>
4.4 类型别名(typeAliases)
类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。
<typeAliases>
<package name="com.zl.pojos"/>
</typeAliases>
<typeAliases>
<typeAlias type="com.zl.pojos.Students" alias="stu"/>
</typeAliases>
有两种方式:
- 指定包名,使用时直接使用类名,Mybatis会在包下搜索需要的Jave Bean
- 直接为单一类型增加别名,使用时使用别名即可
<!--已指定包名,直接使用students,首字母也可以不用大写-->
<select id="getStudents" resultType="students">
select * from students
</select>
<!--使用stu-->
<select id="getStudents" resultType="stu">
select * from students
</select>
4.5 mappers(映射器)
作用:告诉Mybatis到哪里去找寻SQL语句。
方式一:
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
方式二:因为是指定类名,所以资源名要与类名同名、且在同一目录下。
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
方式三:同方式二,因为是指定类名,所以资源名要与类名同名、且在同一目录下。
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
4.6 其他配置
4.7 生命周期和作用域
不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。
-
SqlSessionFactoryBuilder
- 一旦创建了 SqlSessionFactory,就不再需要它了
- 最佳作用域是方法作用域
-
SqlSessionFactory
- SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在
- 没有任何理由丢弃它或重新创建另一个实例
- 它维护一个连接池,供SqlSession获取连接
-
SqlSession
- 每个线程都应该有它自己的 SqlSession 实例
- SqlSession 的实例不是线程安全的,因此是不能被共享的
- 最佳的作用域是请求或方法作用域,即使用完就关闭
4.8 结果映射
结果集可以使用一个map接收,也可以使用JavaBean或者POJO接收,之前的例子都是使用POJO接收。
使用map接收:
<select id="getStudents" resultType="map">
select id, username, hashedPassword from students
</select>
使用Pojo接收:
<select id="getStudents" resultMap="students">
select * from students
</select>
问题:使用Pojo接收时,数据库列名与对象类的属性名不一致怎么办?如下 “name”与“sName”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xBVqw4pF-1613116023827)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208160905129.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8J0m73ly-1613116023829)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208160927986.png)]
解决:使用结果映射
<!--创建一个resultMap结果映射-->
<resultMap id="myMap" type="students">
<!--colume代表数据库列名,property代表要映射到的类的属性名-->
<result property="sName" column="name"/>
<!--未明确标出的映射,会自动安装列名与属性名相同映射-->
</resultMap>
<select id="getStudents" resultMap="myMap">
select * from students
</select>
结论:
-
resultMap
元素是 MyBatis 中最重要最强大的元素。 - 它可以让你从 90% 的 JDBC
ResultSets
数据提取代码中解放出来 - ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
- 如果这个世界总是这么简单就好了。对于复杂的SQL语句,就没那么简单了,在后面继续介绍。
5 日志
5.1 日志工厂STDOUT_LOGGING
在核心配置文件中配置setting即可,无需外加载jar包
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ZpbesY3-1613116023833)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208163732473.png)]
5.2 log4j
log4J是java常用日志工具,全称是log for java
介绍:
- 通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器等
- 可以控制每一条日志的输出格式
- 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
- 最重要的是,可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
配置:
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
需要引入jar包,maven引入
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
控制日志的配置文件
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file
#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n
#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/zl.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n
#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
MyBatis使用输出示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zl2jrHvV-1613116023835)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208170633107.png)]
自用使用示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zB3ZyCnO-1613116023837)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208171225481.png)]
6 分页
为什么使用分页
- 分页有利于提取少量数据,提高响应速度
- 减少系统负担
6.1 使用Limit分页
数据库分页的本质都是包含limit的SQL语句分页。
mybatis分页示例:
<select id="getStudentsLimit" parameterType="map" resultType="students">
select * from `students` limit #{start},#{size}
</select>
测试:
@org.junit.Test
public void testPage(){
try (SqlSession session = Mybatis_utils.getSqlSession()){
StudentsMapper mapper = session.getMapper(StudentsMapper.class);
int start = 0;
int size = 4;
int page = 1;
Map<String,Integer> map = new HashMap<>();
map.put("start", start);
map.put("size",size);
List<Students> list = mapper.getStudentsLimit(map);
while(list.size() > 0) {
showList(list);
System.out.println(page + " page");
start = page * size;
page++;
map.put("start", start);
list = mapper.getStudentsLimit(map);
}
}
}
注意点:如果使用传两个参数(基本类型和String)的方法,需要使用@Param注解,参数是列名。
import org.apache.ibatis.annotations.Param;
selectAlbum(@Param("userid") Integer userid, @Param("albumName") String albumName);
6.2 RowBonds分页
以前的代码使用的比较多,目前较少使用,看到老代码不慌!
6.3 分页插件
比较流行的PageHelper
7 使用注解
提一句:面向接口编程,接口是定义一系列的行为,是对象与对象交互的协议,面向接口编程最大的好处就是解耦合,从而带来可扩展性、可维护性、分层开发、分模块开发等。
7.1 注解示例
首先,在方法上定义sql语句的注解
public interface UserMapper {
@Select("select * from user")
List<User> getUsers();
}
因为没有xml文件了,所以在核心配置文件中,不能通过映射mapper文件来绑定,可以通过之前介绍的后两种方法:指定类名和指定包
<mappers>
<!--指定类名-->
<mapper class="com.zl.dao.UserMapper"/>
</mappers>
测试
@Test
public void annotationTest(){
try(SqlSession session = Mybatis_utils.getSqlSession()){
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.getUsers();
for (User user : users) {
System.out.println(user);
}
}
}
**特别说明:**使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
7.2 本质
- 反射
- 动态代理
7.3 流程解析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nIUtvB2w-1613116023838)(C:\Users\zl\Downloads\1.png)]
7.4 使用注解CRUD
@Select("select * from user")
List<User> getUsers();
@Insert("insert into `user` (`name`,`pwd`) values(#{name}, #{pwd})")
int addUsers(User user);
@Update("update `user` set `name`=#{name} where `id` = #{id}")
int updateUserName(@Param("id") int id, @Param("name") String name);
@Delete("delete from `user` where `id` = #{id}")
int deleteUser(@Param("id") int id);
测试方法和用xml映射是一样的。
#{} ${}的区别:
- #{}是预编译,${}是字符拼接
- 使用#{}防止SQL注入,提高系统的安全性。
8 Lombok
Lombok是一种 Java实用工具,可用来帮助开发人员消除Java的冗长,尤其是对于简单的Java对象(POJO), 它通过注释实现这一目的。
安装步骤:
- IDEA中下载插件
- 导入Jar包(maven仓库)
Lombok注解
- @Data:注解在类上,将类提供的所有属性都添加get、set方法,并添加、equals、canEquals、hashCode、toString方法
- @Setter:注解在类上,为所有属性添加set方法、注解在属性上为该属性提供set方法
- @Getter:注解在类上,为所有的属性添加get方法、注解在属性上为该属性提供get方法
- @NotNull:在参数中使用时,如果调用时传了null值,就会抛出空指针异常
- @Synchronized 用于方法,可以锁定指定的对象,如果不指定,则默认创建一个对象锁定
- @Log作用于类,创建一个log属性
- @Builder:使用builder模式创建对象
- @NoArgsConstructor:创建一个无参构造函数
- @AllArgsConstructor:创建一个全参构造函数
- @ToString:创建一个toString方法
- @Accessors(chain = true)使用链式设置属性,set方法返回的是this对象。
- @RequiredArgsConstructor:创建对象, 例: 在class上添加@RequiredArgsConstructor(staticName = “of”)会创建生成一个静态方法
- @UtilityClass:工具类
- @ExtensionMethod:设置父类
- @FieldDefaults:设置属性的使用范围,如private、public等,也可以设置属性是否被final修饰。
- @Cleanup: 关闭流、连接点。
- @EqualsAndHashCode:重写equals和hashcode方法。
- @Cleanup: 用于流等可以不需要关闭使用流对象.
使用:
import lombok.Data;
//将类提供的所有属性都添加get、set方法,并添加、equals、canEquals、hashCode、toString方法
@Data
public class User {
private int id;
private String name;
private String pwd;
private String phone;
}
Lombok的争议:
Lombok为java代码的精简提供了一种方式,但是**所有的源代码很多时候是用来阅读的,只有很少的时间是用来执行的。**Lombok会降低代码的可读性,而且对于升级和扩展带来了一定潜在难度。
9 关联查询
当一个表需要同时关联另一个表查询时,需要用到association关键字。
比如,查询一个学生表,附带其中个的老师信息,学生表的属性是(id,name,tid),tid指向老师表的id。
create table students
(
id int auto_increment comment 'ID' primary key,
name varchar(100) not null comment '姓名',
tid int not null
)
create table teacher
(
id int auto_increment primary key,
name varchar(20) not null
)
学生表的POJO
import lombok.Data;
@Data
public class Student {
private int id;
private String name;
private Teacher teacher; //Teacher类
}
有两种方式查询,一种是连接查询,一种是子查询
<!--连接查询-->
<resultMap id="sMap" type="student">
<!--property是student类属性名,column代表查询出的表的列名-->
<result column="sname" property="name"/>
<result column="sid" property="id"/>
<!--javaType需要映射到的类-->
<association property="teacher" javaType="Teacher">
<!--这里表示的是JavaType的映射-->
<result column="tid" property="id"/>
<result column="tname" property="name"/>
</association>
</resultMap>
<select id="getStudents2" resultMap="sMap">
select s.id sid, s.name sname, t.id tid, t.name tname
from `students` as s
left join `teacher` as t
on s.tid = t.id
</select>
<!--子查询-->
<resultMap id="stMap" type="student">
<result column="id" property="id"/>
<result column="name" property="name"/>
<!--select代表子查询,查询的结果映射到javaType中,column代表传递给子查询的参数-->
<association property="teacher" column="tid" javaType="Teacher" select="getTeacherById"/>
</resultMap>
<select id="getStudents" resultMap="stMap">
select * from `students`
</select>
<select id="getTeacherById" resultType="Teacher">
select * from `teacher` where `id` = #{id}
</select>
结果:
Student(id=3, name=李四, teacher=Teacher(id=1, name=Janey))
Student(id=5, name=Tom, teacher=Teacher(id=2, name=wen))
Student(id=24, name=zhangsan, teacher=Teacher(id=1, name=Janey))
Student(id=25, name=lisi, teacher=Teacher(id=2, name=wen))
10 集合查询
当一个pojo类型中有一个集合时,需要通过collection关键字查询。
比如,老师表中有一个学生集合
import lombok.Data;
import java.util.List;
@Data
public class Teacher {
private int id;
private String name;
private List<Student> studentList; //学生集合
}
同样,有两种查询方式,一种是连接查询,一种是子查询。相对来说,连接查询时推荐的方式,更接近SQL查询的思想,更易于理解和排除错误。
<!--连接查询-->
<resultMap id="map1" type="Teacher">
<!--id与result一样,只是id用于标记出ID字段,有利于性能提高-->
<id property="id" column="tid"/>
<result property="name" column="tname"/>
<!--ofType代表集合中的元素类型-->
<collection property="studentList" ofType="student">
<!--这里表示元素中的每一个对象-->
<id property="id" column="sid"/>
<result property="name" column="sname"/>
</collection>
</resultMap>
<select id="getTeachers" resultMap="map1">
select t.id tid, t.name tname, s.id sid, s.name sname
from `teacher` as t
left join `students` as s
on t.id = s.tid
</select>
<!--子查询-->
<resultMap id="map2" type="teacher">
<!--通过select查询获取javaType类型,传参是column-->
<collection property="studentList" ofType="student" javaType="ArrayList" select="getSudentsbyTid" column="id"/>
</resultMap>
<select id="getTeachers2" resultMap="map2">
select * from `teacher`
</select>
<select id="getSudentsbyTid" resultType="student">
select * from `students` where `tid` = #{tid}
</select>
建议: 最好逐步建立结果映射。从最简单的形态开始,逐步迭代。每次都使用单元测试保证每次迭代的正确性。
数据库面试高频:
- MySQL引擎
- InnoDB底层
- 索引
- 索引优化
11 动态SQL
动态 SQL 是 MyBatis 的强大特性之一。
动态SQL就是根据不同条件拼接SQL语句,MyBatis提供一套标签在xml映射文件中使用的条件判断。
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
11.1 if语句
使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。
<!--parameterType传递一个条件类-->
<select id="getUsers" parameterType="UserCondition" resultType="user">
select * from `user`
<!--were标签会根据需要去掉and或者or,或者去掉where-->
<where>
<if test="name != null">
and `name` = #{name}
</if>
<if test="hasPhone == true">
and ( `phone` is not null and `phone` != "")
</if>
</where>
</select>
以上可以匹配getUser的两个重载方法:
public interface UserMapper {
List<User> getUsers();
List<User> getUsers(UserCondition user);
}
11.2 choose
choose类似于java中的switch语句,只有一个case能够执行,配合when、otherwise标签,每个when相当于一个case,otherwise相当于default。
<select id="getUsers2" parameterType="UserCondition" resultType="User">
select * from `user`
<choose>
<when test="name != null">
where `name` = #{name}
</when>
<when test="hasPhone == true">
where ( `phone` is not null and `phone` != "")
</when>
<otherwise>
</otherwise>
</choose>
</select>
11.3 trim
trim可以用于自定义元素的功能,如内置的where,如下所示(注意:此例中的空格是必须的)。
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
比如if例子中,最后拼接的是 select * from user
where and name
= ?,此处就会把“and ”去掉,如果有的话。
内置的set是如下设置,此处是在结尾删除多余的“,”
<trim prefix="SET" suffixOverrides=",">
...
</trim>
更新语句示例:
<update id="updateUser" parameterType="UserCondition">
update `user`
<set>
<if test="name != null">
`name` = #{name},
</if>
<if test="phone != null">
`phone` = #{phone}
</if>
</set>
where `id` = #{id}
</update>
上面也可以写成:
<update id="updateUser" parameterType="UserCondition">
update `user`
<!--如果trim内部条件都不满足,什么都不返回。
整个trim以prefix开头,以suffix结尾,中间拼接的部分的最后去掉“,”-->
<trim prefix="set" suffix=" where id = #{id} " suffixOverrides=",">
<if test="name != null"> `name` = #{name} , </if>
<if test="phone != null"> `phone` = #{phone} , </if>
</trim>
</update>
翻译:如果trim内部条件都不满足,什么都不返回。如果存在满足条件,整个trim以prefix开头,以suffix结尾,中间拼接的部分的最后去掉“,”
11.4 SQL片段
可以通过sql标签将sql代码片段提取出来,减少重复SQL语句,提高复用。
<sql id="if-name-phone">
<if test="name != null"> `name` = #{name} , </if>
<if test="phone != null"> `phone` = #{phone} , </if>
</sql>
<update id="updateUser" parameterType="UserCondition">
update `user`
<trim prefix="set" suffix=" where id = #{id} " suffixOverrides=",">
<!--直接引用sql片段-->
<include refid="if-name-phone"/>
</trim>
</update>
一般只将简单的if语句提取出来,连表查询部分,复用性不高,一般不提取,where、set标签等一般也不提取。
11.5 foreach
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。
<!--拼接select * from `user` where id in (1, 2, 3, 4)-->
<select id="getUsersForeach" parameterType="ArrayList" resultType="User">
select * from `user`
<where>
<if test="ids.size() > 0">
<!--collection集合名、open代表拼接的开头,separator为每一项之间的分隔符,close是结尾字符,item代表每一项的变量名-->
<foreach collection="ids" open="id in (" separator="," close=")" item="id">
#{id}
</foreach>
</if>
</where>
</select>
11.6 总结
动态SQL的本质就是拼接字符串,字符串最后会形成一个SQL语句,所以在写动态SQL时,首先应该写出完整的SQL语句,并在数据库中执行成功后,再写Mybatis的拼接语句。
12 缓存
12.1 简介
what: 什么是缓存?
- 存在缓存中的数据
- 数据库一般是存在读取速度较慢的磁盘中,而将经常查询的数据放在内存中,从而提高查询效率,解决高并发系统的性能问题。
why:为什么使用缓存?
- 减少与数据库的交互,提升访问速度,提升系统效率。
when:什么时候使用缓存?
- 当经常查询的数据经常保持不变时,应该使用缓存
12.2 主从复制、读写分离
简单的来说,就是有两个或两个以上的数据库,其中一个当做主数据库,其他当做从数据库,主数据库负责更新数据,从数据库负责查询,当主数据库更新时,会更新到所有从数据库。就实现了主从复制、读写分离的设计。
优点:
- 当其中一个数据库出问题时,其他数据库可以工作,保证数据不易丢失,且稳定工作
- 写操作更耗时,将读写分离,更新操作不会影响到查询操作。
12.3 Mybatis缓存
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。
MyBatis定义了两级缓存:会话缓存和二级缓存
- 默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。(在一个SqlSession中缓存,调用close即缓存结束)
- 二级缓存需要手动开启和配置,在映射文件中使用标签,作用域是所在namespace内。
- 二级缓存也可以扩展Cache接口定义
12.4 一级缓存
- 一级缓存是默认开启的
- 作用域是获取SQLSession到close关闭
- 期间的查询会被缓存
- 期间所有的更新操作(insert、delete、update)会清除缓存
- 默认采用LRU(最近最少使用算法)
- 采用读/写缓存,即缓存不是共享的,可以安全的被调用者修改。
通过Log4j日志输出,可以查看到select语句只执行了一次,如果中间有更新操作,select会再次执行。
[com.zl.dao.UserMapper.getUsers]-==> Preparing: select * from `user`
[com.zl.dao.UserMapper.getUsers]-==> Parameters:
[com.zl.dao.UserMapper.getUsers]-<== Total: 3
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=1234567890)
User(id=5, name=Sam, pwd=789, phone=5566)
======================
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=1234567890)
User(id=5, name=Sam, pwd=789, phone=5566)
======================
[com.zl.dao.UserMapper.updateUser]-==> Preparing: update `user` set `phone` = ? where id = ?
[com.zl.dao.UserMapper.updateUser]-==> Parameters: 5698(String), 4(Integer)
[com.zl.dao.UserMapper.updateUser]-<== Updates: 1
[com.zl.dao.UserMapper.getUsers]-==> Preparing: select * from `user`
[com.zl.dao.UserMapper.getUsers]-==> Parameters:
[com.zl.dao.UserMapper.getUsers]-<== Total: 3
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=5698)
User(id=5, name=Sam, pwd=789, phone=5566)
12.5 二级缓存
一级缓存的作用域太小,诞生了二级缓存,二级缓存的作用域是基于namespace,只需要加入标签即可,当然,在核心配置文件中可以增加setting设置,显视的表示开启全局缓存,虽然默认是开启的。
首先查询保存在一级缓存中,当close关闭时,会将一级缓存保存到二级缓存,使用了java对象序列化。在另一个连接查询中,读取缓存时,会从一级缓存和二级缓存中读取。
<mapper namespace="com.zl.dao.UserMapper">
<!--开启二级缓存,标签内也可以进行详细配置-->
<cache/>
<select id="getUsers" parameterType="UserCondition" resultType="user">
select * from `user`
<where>
<if test="name != null">
and `name` = #{name}
</if>
<if test="hasPhone == true">
and ( `phone` is not null and `phone` != "")
</if>
</where>
</select>
</mapper>
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
注意:二级缓存使用了序列化,所以类必须可以序列化
测试:
@Test
public void testCached(){
try(SqlSession session1 = Mybatis_utils.getSqlSession()){
UserMapper mapper = session1.getMapper(UserMapper.class);
List<User> users = mapper.getUsers();
for (User user : users) {
System.out.println(user);
}
System.out.println("=====================");
}
try(SqlSession session1 = Mybatis_utils.getSqlSession()){
UserMapper mapper = session1.getMapper(UserMapper.class);
List<User> users = mapper.getUsers();
for (User user : users) {
System.out.println(user);
}
System.out.println("=====================");
}
}
下面结果显示,两次获取连接,只查询了一次 select * from user
[com.zl.dao.UserMapper]-Cache Hit Ratio [com.zl.dao.UserMapper]: 0.0
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Opening JDBC Connection
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Created connection 1912850431.
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7203c7ff]
[com.zl.dao.UserMapper.getUsers]-==> Preparing: select * from `user`
[com.zl.dao.UserMapper.getUsers]-==> Parameters:
[com.zl.dao.UserMapper.getUsers]-<== Total: 3
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=1234567890)
User(id=5, name=Sam, pwd=789, phone=5566)
=====================
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7203c7ff]
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7203c7ff]
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Returned connection 1912850431 to pool.
[org.apache.ibatis.io.SerialFilterChecker]-As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
[com.zl.dao.UserMapper]-Cache Hit Ratio [com.zl.dao.UserMapper]: 0.5
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=1234567890)
User(id=5, name=Sam, pwd=789, phone=5566)
=====================
12.6 缓存原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9pXoP1Xr-1613116023840)(C:\Users\zl\Downloads\未命名文件.png)]
12.7 自定义缓存
可以在引用自定义缓存。
步骤就是在java中继承cache类,然后设置在标签中。
<cache type="com.domain.something.MyCustomCache"/>
自定义cache需要实现的cache接口,不过目前有大量开源的数据库缓存框架,你确定能做的比他们好,再自定义缓存吧,-_-||
public interface Cache {
String getId();
int getSize();
void putObject(Object key, Object value);
Object getObject(Object key);
boolean hasKey(Object key);
Object removeObject(Object key);
void clear();
}
也可以引入开源的缓存框架,如ehcache(需要导包,具体查看网上)
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
不过,一般会使用redies数据库做缓存