Mybatis
01、Mybatis简介
什么是Mybatis
-
MyBatis 是一款优秀的持久层框架
-
MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程
-
MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 实体类 【Plain Old Java Objects,普通的 Java对象】映射成数据库中的记录。
-
MyBatis 本是apache的一个开源项目ibatis, 2010年这个项目由apache 迁移到了google code,并且改名为MyBatis 。
Mybatis官方文档 : http://www.mybatis.org/mybatis-3/zh/index.html
GitHub : https://github.com/mybatis/mybatis-3
持久化
持久化是将程序数据在持久状态和瞬时状态间转换的机制。
- 即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML数据文件中等等。
- JDBC就是一种持久化机制。文件IO也是一种持久化机制。
简单来说,就是把内存里的写到硬盘里
为什么需要持久化服务呢?那是由于内存本身的缺陷引起的
-
内存断电后数据会丢失,但有一些对象是无论如何都不能丢失的,比如银行账号等,所以需要写到硬件设备中
-
内存过于昂贵,与硬盘、光盘等外存相比,内存的价格要高2~3个数量级,而且维持成本也高。所以即使对象不需要永久保存,也会因为内存的容量限制不能一直呆在内存中,需要持久化来缓存到外存。
持久层
-
完成持久化工作的代码块 , ----> dao层 (Data Access Object数据访问对象)
-
持久化的实现过程则大多通过各种关系数据库来完成(当然也有非关系型数据库)。
-
不过这里有一个字需要特别强调,也就是所谓的“层”。对于应用系统而言,数据持久功能大多是必不可少的组成部分。之所以要独立出一个“持久层”的概念,而不是“持久模块”,“持久单元”,也就意味着,我们的系统架构中,应该有一个相对独立的逻辑层面,专注于数据持久化逻辑的实现。
与系统其他部分相对而言,这个层面应该具有一个较为清晰和严格的逻辑边界。(说白了就是用来操作数据库存在的!)
为什么需要Mybatis
-
Mybatis就是帮助程序猿将数据存入数据库中 , 和从数据库中取数据 。
-
传统的jdbc操作 , 有很多重复代码块 ,比如 : 数据取出时的封装 , 数据库的建立连接等等,通过框架可以减少重复代码,提高开发效率
-
MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) -->对象关系映射
-
所有的事情,不用Mybatis依旧可以做到,只是用了它,所有实现会更加简单,减少了自己写jdbc的复杂操作
-
解除sql与程序代码的耦合:通过提供DAO层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性。
-
提供xml标签,支持编写动态sql。
02、Mybatis的基本使用
预先准备工作
- 导入相关jar包
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<!--偷懒必备-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
- 资源过滤配置
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
</build>
配置mybatis-config
- mybatis-config.xml
<?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>
<!--添加配置文件,将连接数据库的一些信息比如url,username,password,driver写在配置文件中-->
<properties resource="db.properties"/>
<typeAliases>
<!--
给实体类起个别名
<typeAlias type="com.sinan.pojo.User" alias="User"/>
也可以在实体类上使用注解 @Alias("别名") 来起别名
-->
<!--给包下的所有实体类起别名,默认别名就是类名(懒人用法)-->
<package name="com.sinan.pojo"/>
</typeAliases>
<!--可以有多套环境,但是每次只能选择一个-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<!--配置数据源信息-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<environment id="other">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="123.com"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--
通过xml路径来关联接口和xml配置
<mapper resource="com/sinan/mapper/UserMapper.xml"/>
通过mapper路径来关联接口和xml配置,需要保证接口名和xml配置名相同
<mapper class="com.sinan.mapper.UserMapper"/>
-->
<!--直接扫描包下的所有接口和xml配置文件,需要保证接口名和xml配置名相同(懒人用法)-->
<package name="com.sinan.mapper"/>
</mappers>
</configuration>
- db.properties
# url有时候需要加上时区(mysql版本8以上)
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf-8
username=root
password=123.com
编写接口和相应xml
- UserMapper
package com.sinan.mapper;
import com.sinan.pojo.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
public interface UserMapper {
//查询所有用户
List<User> getAllUser();
//查询指定用户
//@Param("id"),在xml的sql语句中使用#{id},不能使用#{userId}
User getUserById(@Param("id") int userId);
//插入用户
int addUser(User user);
//插入用户map用法
int addUser2(Map<String, Object> map);
//修改用户信息
int updateUser(User user);
//删除用户
int deleteUserById(int id);
}
- UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--这里需要绑定对应的接口-->
<mapper namespace="com.sinan.mapper.UserMapper">
<!--表里的字段是name,但是实体类里的属性是username,需要映射一下,在sql语句的resultType写上userMap
就可以映射到这里了
-->
<resultMap id="userMap" type="User">
<!-- id为主键 -->
<id column="id" property="id"/>
<!-- column是数据库表的列名 , property是对应实体类的属性名 -->
<result column="name" property="username"/>
<result column="pwd" property="password"/>
</resultMap>
<!--
id 需要和接口里的方法名相同
resultType表示返回值类型,对象或对象数组(也就是List<对象>)都写返回对象
-->
<select id="getAllUser" resultType="User">
select * from user
</select>
<!--parameterType表示参数类型,int会自动装箱为Integer
当只有一个参数的时候,可以不写parameterType
-->
<select id="getUserById" resultType="User" parameterType="int">
select * from user where id = #{id}
</select>
<insert id="addUser" parameterType="User" >
insert into user (id, name, sex, age) values (#{id}, #{name}, #{sex}, #{age})
</insert>
<!--使用map作为参数,直接根据map中的键(key)取值
对于一些需要修改较少信息的sql,使用map就可以了,不需要重新new一个实体类,然后再来update
返回值为基本类型,可以不写
-->
<insert id="addUser2" parameterType="map">
insert into user (id, name, sex, age) values (#{id}, #{name}, #{sex}, #{age})
</insert>
<update id="updateUser" parameterType="User">
update user set name = #{name}, sex = #{sex}, age = #{age} where id = #{id}
</update>
<delete id="deleteUserById" parameterType="int" >
delete from user where id = #{id}
</delete>
</mapper>
测试
- 测试类
import com.sinan.mapper.UserMapper;
import com.sinan.pojo.User;
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 java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
public class MyTest {
public static void main(String[] args) throws IOException {
//下面这一段可以打包为工具类,是固定代码
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//参数为true表示自动提交事务,否则需要加上 sqlSession.commit()来进行事务提交(不包括查数据)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// //查询所有用户
// List<User> users = userMapper.getAllUser();
// for (User user : users) {
// System.out.println(user);
// }
//
// //根据id查询指定用户
// User user = userMapper.getUserById(1);
// System.out.println(user);
//
// //插入新用户
// int i = userMapper.addUser(new User(5, "菠萝吹雪", 10, "水果"));//返回结果非0表示插入成功
//
// //map插入新用户
// HashMap<String, Object> map = new HashMap<>();
// map.put("id",6);
// map.put("name","成刘翔");
// map.put("age",6);
// map.put("sex","橙子");
// userMapper.addUser2(map);
//
// //更新用户信息
// userMapper.updateUser(new User(5, "菠萝吹牛", 10, "水果"));
//
// //根据id删除用户
// int i = userMapper.deleteUserById(8);//返回结果非0表示删除成功
//用完不要忘了关闭
sqlSession.close();
}
}
总结:
-
所有的增删改操作都需要提交事务!
-
接口所有的普通参数,尽量都写上@Param参数,尤其是多个参数时,必须写上!
-
有时候根据业务的需求,可以考虑使用map传递参数!
-
为了规范操作,在SQL的配置文件中,我们尽量将Parameter参数和resultType都写上!
配置文件(mybatis-config)
mybatis-config中可以配置的内容如下:
configuration(配置)
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识)
mappers(映射器)
<!-- 注意元素节点的顺序!顺序不对会报错 -->
作用域(Scope)和生命周期
下面是Mybatis的执行流程图:其实就是在测试类的那段固定代码
-
SqlSessionFactoryBuilder 的作用在于创建 SqlSessionFactory,创建成功后,SqlSessionFactoryBuilder 就失去了作用,所以它只能存在于创建 SqlSessionFactory 的方法中,而不要让其长期存在。因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
-
SqlSessionFactory 可以被认为是一个数据库连接池,它的作用是创建 SqlSession 接口对象。因为 MyBatis 的本质就是 Java 对数据库的操作,所以 SqlSessionFactory 的生命周期存在于整个 MyBatis 的应用之中,所以一旦创建了 SqlSessionFactory,就要长期保存它,直至不再使用 MyBatis 应用,所以可以认为 SqlSessionFactory 的生命周期就等同于 MyBatis 的应用周期。
-
由于 SqlSessionFactory 是一个对数据库的连接池,所以它占据着数据库的连接资源。如果创建多个 SqlSessionFactory,那么就存在多个数据库连接池,这样不利于对数据库资源的控制,也会导致数据库连接资源被消耗光,出现系统宕机等情况,所以尽量避免发生这样的情况。
-
**因此在一般的应用中我们往往希望 SqlSessionFactory 作为一个单例,让它在应用中被共享。**所以说 SqlSessionFactory 的最佳作用域是应用作用域。
-
如果说 SqlSessionFactory 相当于数据库连接池,那么 SqlSession 就相当于一个数据库连接(Connection 对象),你可以在一个事务里面执行多条 SQL,然后通过它的 commit、rollback 等方法,提交或者回滚事务。所以它应该存活在一个业务请求中,处理完整个请求后,应该关闭这条连接,让它归还给 SqlSessionFactory,否则数据库资源就很快被耗费精光,系统就会瘫痪,所以用 try…catch…finally… 语句来保证其正确关闭。
03、属性名和字段名不一致
<select id="getUserById" resultType="User" parameterType="int">
select * from user where id = #{id}
</select>
# User的属性为id、username、password
#表字段为id、name、pwd
自动映射
<select id="getUserById" resultType="User" parameterType="int">
select id, pwd, name from user where id = #{id}
</select>
这种方式很拉,不建议使用
ResultMap
在上面的编写接口与相应xml部分中已经介绍了这类的解决方案
<!--表里的字段是name,但是实体类里的属性是username,需要映射一下,在sql语句的resultType写上userMap
就可以映射到这里了
-->
<resultMap id="userMap" type="User">
<!-- id为主键 -->
<id column="id" property="id"/>
<!-- column是数据库表的列名 , property是对应实体类的属性名 -->
<result column="name" property="username"/>
<result column="pwd" property="password"/>
</resultMap>
<!--这里的resultType引用上面的userMap-->
<select id="getUserById" resultType="userMap" parameterType="int">
select * from user where id = #{id}
</select>
as起别名
<select id="getUserById" resultType="User" parameterType="int">
select id, pwd as password, name as username from user where id = #{id}
</select>
直接在sql里给他起个别名
开启驼峰命名转换
<settings>
<!--开启驼峰自动转换-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
# User的属性为id、userName、userEmail
#表字段为id、user_name、user_email
开启驼峰自动转换,即可自动将表字段中的名称转化为驼峰命名
04、日志工厂
我们在测试SQL的时候,如果能够在控制台输出 SQL 的话,就能够有更快的排错效率
-
如果一个数据库相关的操作出现了问题,我们可以根据输出的SQL语句快速排查问题。
-
对于以往的开发过程,我们会经常使用到debug模式来调节,跟踪我们的代码执行过程。
-
但是现在使用Mybatis是基于接口,看不到配置文件的源代码执行过程。因此,我们必须选择日志工具来作为我们开发,调节程序的工具。
Mybatis内置的日志工厂提供日志功能,具体的日志实现有以下几种工具:
- SLF4J
- Apache Commons Logging
- Log4j 2
- Log4j
- JDK logging
具体选择哪个日志实现工具由MyBatis的内置日志工厂确定。它会使用最先找到的(按上文列举的顺序查找)。如果一个都未找到,日志功能就会被禁用。
标准日志实现
最简单的,只需要设置一下即可
<settings>
<!--标准的日志工厂的实现-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
这样在执行sql的时候就会显示sql执行的细节。
Log4j
简介:
-
Log4j是Apache的一个开源项目
-
通过使用Log4j,我们可以控制日志信息输送的目的地:控制台,文本,GUI组件…
-
我们也可以控制每一条日志的输出格式;
-
通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
使用:
- 导入log4j的包
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- log4j.properties配置文件
#将等级为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/sinan.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
- setting设置
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
05、分页
这一块很简单,就是一个map~~
为什么需要分页:
在学习mybatis等持久层框架的时候,会经常对数据进行增删改查操作,使用最多的是对数据库进行查询操作,如果查询大量数据的时候,我们往往使用分页进行查询,也就是每次处理小部分数据,这样对数据库压力就在可控范围内。
使用limit实现分页
#语法
SELECT * FROM table LIMIT stratIndex,pageSize
SELECT * FROM table LIMIT 5,10; // 从下标5开始,每一页10行数据
#如果只给定一个参数,它表示返回最大的记录行数目:
SELECT * FROM table LIMIT 5; //检索前 5 个记录行
- 修改Mapper文件
<select id="selectUser" parameterType="map" resultType="User">
select * from user limit #{startIndex},#{pageSize}
</select>
没了。
06、使用注解开发
面向接口编程
-
大家之前都学过面向对象编程,也学习过接口,但在真正的开发中,很多时候我们会选择面向接口编程
-
根本原因 : 解耦 , 可拓展 , 提高复用 , 分层开发中 , 上层不用管具体的实现 , 大家都遵守共同的标准 , 使得开发变得容易 , 规范性更好
-
在一个面向对象的系统中,系统的各种功能是由许许多多的不同对象协作完成的。在这种情况下,各个对象内部是如何实现自己的,对系统设计人员来讲就不那么重要了;
-
而各个对象之间的协作关系则成为系统设计的关键。小到不同类之间的通信,大到各模块之间的交互,在系统设计之初都是要着重考虑的,这也是系统设计的主要工作内容。面向接口编程就是指按照这种思想来编程。
关于接口的理解
-
接口从更深层次的理解,应是定义(规范,约束)与实现(名实分离的原则)的分离。
-
接口的本身反映了系统设计人员对系统的抽象理解。
-
接口应有两类:
- 第一类是对一个个体的抽象,它可对应为一个抽象体(abstract class);
- 第二类是对一个个体某一方面的抽象,即形成一个抽象面(interface);
-
一个体有可能有多个抽象面。抽象体与抽象面是有区别的。
三个面向区别、
- 面向对象是指,我们考虑问题时,以对象为单位,考虑它的属性及方法 .
- 面向过程是指,我们考虑问题时,以一个具体的流程(事务过程)为单位,考虑它的实现 .
- 接口设计与非接口设计是针对复用技术而言的,与面向对象(过程)不是一个问题.更多的体现就是对系统整体的架构
注解开发
主要注解是:
- @select ()
- @update ()
- @Insert ()
- @delete ()
有了注解,就不需要mapper对应的xml了,但是,但是啊,使用注解只能写一些简单的sql,对于稍微复杂一点的sql,还是需要xml。
具体实现:
public interface UserMapper {
//使用注解开发的增删改查,注解开发只能做最简单的SQL,对于项目还是需要使用xml开发
@Select("select * from user")
List<User> getUserList();
@Select("select * from user where id = #{uid}")
User getUserById(@Param("uid") int id);//这里的参数注解相当于起的参数别名,在select注解中需要使用别名
@Insert("insert into user (id,name,sex,age) values (#{id},#{name},#{sex},#{age})")
int addUser(User user);
@Update("update user set name = #{name},sex = #{sex},age = #{age} where id = #{id}")
int updateUser(User user);
@Delete("delete from user where id = #{id}")
int deleteUser(int id);
}
#与$的区别:
-
#{} 的作用主要是替换预编译语句(PrepareStatement)中的占位符? (推荐使用)
-
${} 的作用是直接进行字符串替换(小心sql注入啊)
关于@Param:
-
@Param注解用于给方法参数起一个名字。
-
在方法只接受一个参数的情况下,可以不使用@Param。
-
在方法接受多个参数的情况下,建议一定要使用@Param注解给参数命名。
-
如果参数是 JavaBean , 则不能使用@Param。
07、一对多和多对一处理
多对一的处理
多对一的理解:
- 多个学生对应一个老师
- 如果对于学生这边,就是一个多对一的现象,即从学生这边关联一个老师!
学生和老师的实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private int id;
private String name;
private Teacher teacher;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
private int id;
private String name;
}
数据库表格式:
create table student(
id int primary key auto_increment,
name varchar(20),
tid int
);
create table teacher(
id int primary key auto_increment,
name varchar(20)
);
studentMapper接口:
package com.sinan.mapper;
import com.sinan.pojo.Student;
import java.util.List;
public interface StudentMapper {
//查询所有学生以及其对应老师信息(子查询)
public List<Student> getStudent1();
//联表查询
public List<Student> getStudent2();
}
子查询方式
<!--结果映射至StudentTeacher1-->
<select id="getStudent1" resultMap="StudentTeacher1">
select * from student
</select>
<resultMap id="StudentTeacher1" type="Student">
<result column="id" property="id"/>
<result column="name" property="name"/>
<!--将查询结果tid关联属性teacher,进行再查询-->
<association property="teacher" column="tid" select="getTeacher" />
</resultMap>
<select id="getTeacher" resultType="Teacher">
select * from teacher where id = #{tid};
</select>
联表查询
<!--进行联表查询-->
<select id="getStudent2" resultMap="StudentTeacher2">
select s.id as sid, s.name as sname, t.id as tid, t.name as tname from student as s, teacher as t where s.tid = t.id;
</select>
<resultMap id="StudentTeacher2" type="Student">
<result column="sid" property="id"/>
<result column="sname" property="name"/>
<!--关联teacher表,直接将联表查询的列赋予teacher的属性-->
<association property="teacher" javaType="Teacher">
<result column="tid" property="id"/>
<result column="tname" property="name"/>
</association>
</resultMap>
一对多的处理
一对多的理解:
- 一个老师拥有多个学生
- 如果对于老师这边,就是一个一对多的现象,即从一个老师下面拥有一群学生(集合)!
Teacher类更改:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher2 {
private int id;
private String name;
private ArrayList<Student> students;
}
TeacherMapper接口更改:
public interface TeacherMapper2 {
//嵌套查询
Teacher2 getTeacherById1(@Param("tid") int id);
//子查询
Teacher2 getTeacherById2(@Param("tid") int id);
}
结果嵌套处理
<!--结果嵌套处理-->
<select id="getTeacherById1" resultMap="TeacherStudent1" parameterType="int">
select t.id tid,t.name tname,s.id sid,s.name sname
from teacher t,student s
where t.id = #{tid} and t.id = s.tid;
</select>
<resultMap id="TeacherStudent1" type="Teacher2">
<result property="id" column="tid"/>
<result property="name" column="tname"/>
<!--通过collection对多行数据进行合并-->
<collection property="students" javaType="ArrayList" ofType="Student2">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<result property="tid" column="tid"/>
</collection>
</resultMap>
查询嵌套处理
<!--查询嵌套处理-->
<select id="getTeacherById2" parameterType="int" resultMap="TeacherStudent2">
select * from teacher where id = #{tid}
</select>
<resultMap id="TeacherStudent2" type="Teacher2">
<result property="id" column="id"/>
<collection property="students" column="id" ofType="Student" select="getStudent"/>
</resultMap>
<select id="getStudent" resultType="Student2">
select * from student where tid = #{id}
</select>
总结:
-
关联-association
-
集合-collection
-
association是用于一对一和多对一,而collection是用于一对多的关系
-
JavaType和ofType都是用来指定对象类型的
- JavaType是用来指定pojo中属性的类型,属性是老师,JavaType就是Teacher,属性是数组,JavaType就是ArrayList
- ofType指定的是映射到list集合属性中pojo的类型。
08、动态SQL
创建Blog实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Blog {
private String id;
private String title;
private String author;
private Date createTime;
private int views;
}
BlogMapper接口:
package com.sinan.mapper;
import com.sinan.pojo.Blog;
import java.util.List;
import java.util.Map;
public interface BlogMapper {
//查询博客,要求,参数有title就查title,有author就查author,都有就都查
List<Blog> queryBlogByIf(Map map);
//类似于switch
List<Blog> queryBlogByChoose(Map map);
//使用set标签更新数据
int updateBlog(Map map);
//使用foreach查询
List<Blog> queryBlogByForeach(Map map);
}
if语句和where标签
<!--
这个where标签会知道如果它包含的标签中有返回值的话,它就插入一个‘where’。
此外,如果标签返回的内容是以AND 或OR 开头的,则它会剔除掉。
-->
<select id="queryBlogByIf" resultType="Blog" parameterType="map">
select * from blog
<where>
<if test="author != null">
author = #{author}
</if>
<if test="create != null">
and create = #{create}
</if>
</where>
</select>
set标签和sql代码块
<!--SQL片段,最好只对于一张表使用sql片段,而且,这样sql的维护性会变差-->
<sql id="updataBlog-sqlPart">
<if test="title != null">
title = #{title}
</if>
<if test="author != null">
author = #{author}
</if>
<if test="views != null">
views = #{views},
</if>
</sql>
<!--这里的set和上面的where差不错-->
<update id="updateBlog" parameterType="map">
update blog
<set>
<include refid="updataBlog-sqlPart"></include>
</set>
where id = #{id}
</update>
choose、when和otherwise
<!--有时候,我们不想用到所有的查询条件,只想选择其中的一个,查询条件有一个满足即可,
使用 choose 标签可以解决此类问题,类似于 Java 的 switch 语句-->
<select id="queryBlogByChoose" parameterType="map" resultType="Blog">
select * from blog
<where>
<choose>
<when test="title != null">
title = #{title}
</when>
<when test="author != null">
author = #{author}
</when>
<otherwise>
views = #{views}
</otherwise>
</choose>
</where>
</select>
foreach
<!--map<id, list<id>>,这里需要id对应的值是一个数组,然后通过foreach来进行访问
效果为:select * from blog where (id = #{id1} or id = #{id2} or id = #{id3})
-->
<select id="queryBlogByForeach" parameterType="map" resultType="Blog">
select * from blog
<where>
<foreach collection="ids" item="id" open="(" separator="or" close=")">
id = #{id}
</foreach>
</where>
</select>
总结:
动态SQL就是一个拼接的活
09、缓存
缓存简介
-
什么是缓存(Cache)
- 存在内存中的临时数据。
- 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。
-
为什么使用缓存?
- 减少和数据库的交互次数,减少系统开销,提高系统效率。
-
什么样的数据能使用缓存?
- 经常查询并且不经常改变的数据。
Mybatis缓存
-
MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
-
MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
- 二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
- 为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
一级缓存
一级缓存也叫本地缓存:
- 与数据库同一次会话期间查询到的数据会放在本地缓存中。
- 以后如果需要获取相同的数据,直接从缓存中拿,没必须再去查询数据库;
<select id="getUserById" resultType="User" parameterType="int">
select * from user where id = #{id}
</select>
对于上述sql,两次查询的结果是相同的(hashcode是一样的)
一级缓存失效的四种方法
一级缓存是SqlSession级别的缓存,是一直开启的,我们关闭不了它;
- sqlSession不同,不同的sqlSession有自己的一级缓存区
- sqlSession相同,但通过UserMapper.class拿到了两个mapper实现类
- sqlSession相同,两次查询之间执行了增删改操作,执行增删改操作后对数据产生了影响,缓存消失
- sqlSession相同,手动清除一级缓存
sqlSession.clearCache();//手动清除一级缓存
二级缓存
-
二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存
-
基于namespace级别的缓存,一个名称空间,对应一个二级缓存;
-
工作机制
-
一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
-
如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中;
-
新的会话查询信息,就可以从二级缓存中获取内容;
-
不同的mapper查出的数据会放在自己对应的缓存(map)中;
-
使用方法:
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
在每个mapper.xml中配置:
<!--在config配置文件中,默认开启全局缓存-->
<!--如果不开启只读,那么实体类就需要序列化-->
<!--
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,
而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
-->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
总结:
- 查出的数据都会被默认先放在一级缓存中
- 只有会话提交或者关闭以后,一级缓存中的数据才会转到二级缓存中
- 只要开启了二级缓存,我们在同一个Mapper中的查询,可以在二级缓存中拿到数据
- 进行增删改操作同样会让二级缓存失效