文章目录
相关依赖
<!--mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!--为了之后能方便测试-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
编写配置文件
<?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>
<settings>
<!--懒加载,没有真正用到XXX的时候,不会只是相关的SQL语句-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--对resultMap做一个映射子查询的自动映射-->
<!--<setting name="autoMappingBehavior" value="FULL"/>-->
</settings>
<!-- 为 JavaBean 起类别名 -->
<typeAliases>
<!-- 指定一个包名起别名,将包内的 Java 类的类名作为类的类别名 -->
<package name="cn.edu.guet.pojo" />
</typeAliases>
<!-- 配置 mybatis 运行环境 -->
<environments default="development">
<environment id="development">
<!-- type="JDBC" 代表直接使用 JDBC 的提交和回滚设置 -->
<transactionManager type="JDBC" />
<!-- POOLED 表示支持 JDBC 数据源连接池 -->
<!-- 数据库连接池,由 Mybatis 管理,数据库名是 mybatis,MySQL 用户名 root,密码为空 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=gbk&autoReconnect=true&failOverReadOnly=false&serverTimezone=UTC" />
<property name="username" value="root" />
<property name="password" value="123456" />
</dataSource>
</environment>
</environments>
<mappers>
<!-- 通过 mapper 接口包加载整个包的映射文件,通俗来讲我们需要告诉 MyBatis 到哪里去找到这些语句 -->
<package name="cn.edu.guet.mapper" />
</mappers>
</configuration>
# 测试要的环境
# Global logging configuration
log4j.rootLogger=DEBUG, stdout
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
搭建环境和编写映射sql
- 实例化一个工厂建造类读取配置文件用来产生一个工厂类
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- 从 SqlSessionFactory 中获取 SqlSession,SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。
SqlSession session = sqlSessionFactory.openSession()
- 编写要映射sql语句
<?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="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
- 创建一个BlogMapper的接口和方法名selectBlog的方法,用Session加载接口创建代理对象并调用方法,如下就会执行相关sql
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
用单例模式改造基础环境
package cn.edu.guet.singleton;
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;
/**
* @author pangjian
* @ClassName SessionFactory
* @Description 构建操作mybatis的单例对象
* @date 2021/7/5 10:49
*/
public class SessionFactory {
private SqlSessionFactory sqlSessionFactory;
private SqlSession sqlSession;
/**
* @Description:在程序运行时就为变量赋值,保证了该类只有一个实例,调用构造方法的同时也给sqlSessionFactory赋值,也保证了这个sqlSessionFactory也只有一个
* @date 2021/7/5 11:22
*/
private static SessionFactory instance = new SessionFactory();
/**
* @Description:构造器私有化,调用该方法就是给该类的sqlSessionFactory成员变量赋值
* @return
* @date 2021/7/5 10:56
*/
private SessionFactory(){
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
/**
* @Description:通过实例调用该方法返回一个Session对象
* @return org.apache.ibatis.session.SqlSession
* @date 2021/7/5 11:24
*/
public SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}
/**
* @Description:返回该单例
* @return cn.edu.guet.singleton.SessionFactory
* @date 2021/7/5 11:24
*/
public static SessionFactory getInstance(){
return instance;
}
}
简单测试
实体类
package cn.edu.guet.pojo;
/**
* @author pangjian
* @ClassName Dog
* @Description TODO
* @date 2021/7/5 11:55
*/
@Data
public class Dog {
private String name;
private Integer age;
}
数据库访问层接口
package cn.edu.guet.mapper;
import cn.edu.guet.pojo.Dog;
import java.util.List;
/**
* @author pangjian
* @Interface DogMapper
* @Description TODO
* @date 2021/7/5 11:58
*/
public interface DogMapper {
List<Dog> selectAllDog();
// 不同类型的多参数处理,name会映射到xml文件中的占位符,做为形参传过给xml
Dog selectOneDog(@Param("name") String name,@Param("age") Integer age);
void insertOneDog(Dog dog);
}
编写映射sql
<?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="cn.edu.guet.mapper.DogMapper">
<select id="selectAllDog" resultType="Dog">
select * from t_dog
</select>
<select id="selectOneDog" resultType="Dog">
select * from t_dog where name=#{name} and age=#{age}
</select>
<insert id="insertOneDog" parameterType="Dog">
insert into t_dog(name,age) values (name,age);
</insert>
</mapper>
- namespace是匿名空间,id是方法名,为了就是找到是哪一个类下的方法,给它做一个映射
- resultType是返回类型,只能是简单数据类型或者引用类型,对应对象有着对象属性,则不能使用这个类型
- parameterType是参数类型,如果是javabean,会把它的属性做一个映射到sql语句中的占位符,默认是属性名做形参
测试代码
package cn.edu.guet;
import cn.edu.guet.mapper.DogMapper;
import cn.edu.guet.pojo.Dog;
import cn.edu.guet.singleton.SessionFactory;
import org.apache.ibatis.session.SqlSession;
import java.util.List;
public class Test {
@org.junit.Test
public void test1(){
DogMapper dogMapper = SessionFactory.getInstance().getSqlSession().getMapper(DogMapper.class);
List<Dog> list = dogMapper.selectAllDog();
for (Dog dog : list){
System.out.println(dog.getName());
}
}
@org.junit.Test
public void test2(){
DogMapper dogMapper = SessionFactory.getInstance().getSqlSession().getMapper(DogMapper.class);
Dog dog = dogMapper.selectOneDog("阿拉斯加",1);
System.out.println(dog.getName());
}
@org.junit.Test
public void test3(){
SqlSession sqlSession = SessionFactory.getInstance().getSqlSession();
DogMapper dogMapper = sqlSession.getMapper(DogMapper.class);
Dog dog = new Dog("田园犬",2);
dogMapper.insertOneDog(dog);
sqlSession.commit();
}
}
复杂测试(ResultMap的使用)
- 当你查询出来的结果集字段不能一一映射到实体类的属性,那就要用到ResultMap了
实体类
数据库学生表里面存的是班级的id做为外键,而不是一个班级实例,我们就要连表查询,得到的结果集字段我们不仅仅要做映射到我们自定义的javabean中,首先我们要分析他们的关系。
package cn.edu.guet.pojo;
/**
* @author pangjian
* @ClassName Student
* @Description 学生类,有一个班级属性
* @date 2021/7/5 11:27
*/
@Data
public class Student {
private Integer id;
private String username;
private String password;
private Clazz clazz; // 一个学生会属于一个班级
}
package cn.edu.guet.pojo;
import java.util.List;
/**
* @author pangjian
* @ClassName Class
* @Description 班级类,有一个学生列表属性
* @date 2021/7/5 11:27
*/
@Data
public class Clazz {
private Integer id;
private String className;
private String area;
private List<Student> students; // 一个班级会有很多个学生
}
有一个的问题(association)
/**
* @Description:查询所以学生信息,每一个学生的班级信息也要包括
* @return java.util.List<cn.edu.guet.pojo.Student>
* @date 2021/7/5 16:06
*/
List<Student> selectAllStudent();
第一种方法
- 学生肯定是属于一个班级,也就是多对一的问题,对于复杂属性类型,我们得用association去处理,查询出学生信息,包括了学生所在班级的信息,映射sql如下
<select id="selectAllStudent" resultMap="stu-cla">
select * from t_student
</select>
<resultMap id="stu-cla" type="Student">
<!--property是实体类的属性名,column是查询结果集字段名,可以是别名-->
<id property="id" column="t_student.id"></id>
<result property="username" column="username"></result>
<result property="password" column="password"></result>
<!--数据库的外键处理,property是javabean的属性名,也就是对象属性名clazz,column对应学生表的外键,javaType:实体类中的属性类型-->
<association property="clazz" column="classid" javaType="Clazz" select="selectClass"></association>
</resultMap>
<!--子查询用的参数是通过association标签定义的classid值传过来的-->
<select id="selectClass" resultType="Clazz">
select * from t_class where id=#{classid}
</select>
但这样会造成N+1问题,也就是对于select * from t_student执行一次,然后对结果集(结果集为N条记录)关联的查询会执行N次,我们可以采取懒加载去解决这个问题,当没有用到class信息,则不会执行子查询。也可以换一种方式去编写映射sql和resultMap。
<setting name="lazyLoadingEnabled" value="true"/>
第二种方法
<select id="selectAllStudent" resultMap="stu-cla">
select *
from t_student t
left join t_class c
on classid=c.id
</select>
<resultMap id="stu-cla" type="Student">
<!--property是实体类的属性名,column是查询结果集字段名,可以是别名-->
<id property="id" column="t_student.id"></id>
<result property="username" column="username"></result>
<result property="password" column="password"></result>
<!--数据库的外键处理,property是javabean的属性名,也就是对象属性名clazz,column对应学生表的外键,javaType:实体类中的属性类型-->
<association property="clazz" javaType="Clazz">
<!--对关联属性对象指定结果集字段映射-->
<id property="id" column="t_class.id"></id>
<result property="clazzName" column="name"></result>
<result property="area" column="area"></result>
</association>
</resultMap>
有多个的问题(collection)
/**
* @Description: 根据id查询班级的信息,包括在这些班的所有学生
* @Param id: 班级id
* @return java.util.List<cn.edu.guet.pojo.Clazz>
* @date 2021/7/5 15:54
*/
List<Clazz> selectOneClazz(@Param("id") Integer id);
第一种方法
<select id="selectOneClazz" resultMap="cla-stu">
select *
from t_class c
left join t_student s
on c.id=s.classid
where c.id=#{id}
</select>
<resultMap id="cla-stu" type="Clazz">
<id property="id" column="t_class.id"></id>
<result property="className" column="classname"></result>
<result property="area" column="area"></result>
<!--由于属性类型是一个List,且泛型为Student,集合我们用ofType去表明List属性的泛型-->
<collection property="students" ofType="Student" javaType="List">
<result property="id" column="t_student.id"></result>
<result property="username" column="username"></result>
<result property="password" column="password"></result>
</collection>
</resultMap>
第二种方法
<select id="selectOneClazz" resultMap="cla-stu">
select * from t_class where id=#{id}
</select>
<resultMap id="cla-stu" type="Clazz">
<id property="id" column="t_class.id"></id>
<result property="className" column="classname"></result>
<result property="area" column="area"></result>
<!--由于属性类型是一个List,且泛型为Student,集合我们用ofType去指定-->
<collection property="students" javaType="List" column="id" select="getAllStudentbyClass"></collection>
</resultMap>
<select id="getAllStudentbyClass" resultType="Student">
<!--id形参是collection中column指定的名传过来的-->
select * from t_student where classid=#{id}
</select>
#{} 和 ${}
区别:#{}是预编译,${}是拼接sql,#{}能防止sql注入
sql注入
利用了拼接sql的缺点,修改其执行sql语句的结构
1.登录如果用拼接sql形式去查询账号和密码是否正确
select * from `user` where
username = ${} and password = ${}
- 预期开发人员逻辑:输入用户名和密码查询
select * from `user` where
username = 'admin' and password = '123456'
- 黑客攻击站点sql注入逻辑(账号填写 ’ or 1 = 1 # )
# 并集有一个为true,就会查询出全部人的信息,这是我们不允许的
select * from `user` where
username = '' or 1 = 1 #' and password = '123456'
2.预编译sql形式去查询账号和密码是否正确
select * from `user` where
username = #{} and password = #{}
你可以理解为预编译为:指令和参数区别开来,在参数传过来之前预编译指令,告诉了DBMS这个sql已经确定好做什么了,你再传sql注入传指令,系统不会识别,而是当作成整个参数
# 并集有一个为true,就会查询出全部人的信息,这是我们不允许的
select * from `user` where
username = '' or 1 = 1 #' and password = '123456'
- 当黑客再一次填写账号为 ’ or 1 = 1 #,系统会把这个做为参数去执行查询,而不会识别 or 这个关键字去改变原有的sql结构
#{}其实是用了PreparedStatement去预编译执行sql,而${}是用了Statement去拼接并执行sql
缓存
- 什么是缓存
存在内存中的临时数据,将用户经常查询到的数据放到缓存中,用户去查询数据就不用从磁盘上查找了,直接从缓存上查询,提高了效率。
- 为什么使用缓存
减少和数据库的交互次数,减少系统开销,提高系统效率
- 什么数据能使用缓存
经常查询并且不经常改变的数据
一级缓存
默认情况只有一级缓存开启。(SqlSession级别,也就是一次会话期间,如果一次会话(创建到Close期间)进行相同的两次查询,第二次查询会走缓存)
@GetMapping("/queryUserList")
@Transactional
public List<User> queryUserList() {
List<User> userList = userMapper.queryUserList();
userList.stream().forEach(
user -> System.out.println("第一次查询" + user)
);
List<User> userList1 = userMapper.queryUserList();
userList1.stream().forEach(
user -> System.out.println("第二次查询" + user)
);
return userList;
}
一定要加事务,由于使用了数据库连接池,默认每次查询完之后自动commite,这就导致两次查询使用的不是同一个sqlSessioin,根据一级缓存的原理,它将永远不会生效。当我们开启了事务,Spring将两次查询都在同一个sqlSession中,将事务边界上升到业务层,从而让第二次查询命中了一级缓存。
只走了一次sql
二级缓存
- 工作机制
一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
如果当前会话关闭了,这个会话对应的一级缓存就没了;会话关闭了,一级缓存中的数据被保存到二级缓存中;新的会话查询信息,就可以从二级缓存中获取内容;不同的mapper查出的数据会放在自己对应的缓存(map)中;
1.先看二级缓存有没有想要的数据
2.再看一级缓存有没有想要的数据
3.查询数据库
- 注意点
实体类要实现Serializable接口
# 在.xml文件中配置则是开启二级缓存
<cache/>
测试
@GetMapping("/queryUserList")
public List<User> queryUserList() {
List<User> userList = userMapper.queryUserList();
userList.stream().forEach(
user -> System.out.println("第一次查询" + user)
);
return userList;
}
@GetMapping("/queryUserList1")
public List<User> queryUserList1(){
List<User> userList1 = userMapper.queryUserList();
userList1.stream().forEach(
user -> System.out.println("第二次查询" + user)
);
return userList1;
}
进行两次查询,不同sqlSession,但也只进行了一次查询
自定义缓存
Mybatsi默认缓存对象PerpetualCache,是本地缓存,本地缓存不适合分布式环境,因为不同jvm环境不能共享。
手动注入redis操作实例
package com.pang.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author pangjian
* @ClassName ApplicationContextHolder
* @Description TODO
* @date 2021/8/28 18:49
*/
@Slf4j
@Component
public class ApplicationContextHolder implements ApplicationContextAware, DisposableBean {
private static ApplicationContext applicationContext;
/**
* 获取存储在静态变量中的 ApplicationContext
*
* @return
*/
public static ApplicationContext getApplicationContext() {
if (applicationContext == null) {
throw new RuntimeException("applicationContext 属性未注入");
}
return applicationContext;
}
/**
* 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
*
* @param name
* @param <T>
* @return
*/
public static <T> T getBean(String name) {
if (applicationContext == null) {
throw new RuntimeException("applicationContext 属性未注入");
}
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
if (applicationContext == null) {
throw new RuntimeException("applicationContext 属性未注入");
}
return applicationContext.getBean(clazz);
}
/**
* 实现 DisposableBean 接口,在 Context 关闭时清理静态变量
* @throws Exception
*/
@Override
public void destroy() throws Exception {
log.debug("清除 SpringContext 中的 ApplicationContext: {}", applicationContext);
applicationContext = null;
}
/**
* 实现 ApplicationContextAware 接口,注入 applicationContext 到静态变量中
* @param applicationContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextHolder.applicationContext = applicationContext;
}
}
采用redis自定义Mybatis的cache
package com.pang.cache;
import com.pang.config.ApplicationContextHolder;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.ibatis.cache.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author pangjian
* @ClassName MyBatisCache
* @Description 在mybatis完成查询后,将数据缓存起来
* @date 2021/8/28 17:49
*/
@Slf4j
public class MyBatisCache implements Cache {
/**
* @Description:id唯一标识,指的就是namespace
*/
private String id;
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private RedisTemplate<String, Object> redisTemplate;
public MyBatisCache() {
}
/**
* @Description:项目启动时扫描XXXMapper.xml文件时,识别出<Cache type="xxx.xxx.Cache.MyBatisCache"></>
* 就会调用有参构造,把namespace通过构造方法注入到MyBatisCache实例的id私有属性上
*/
public MyBatisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
/**
* @Description:Mybatis需要时会getId拿到该缓存实例的namespace
*/
@Override
public String getId() {
return this.id;
}
/**
* @Description: 当查询执行后,将结果缓存起来,我们自定义缓存实现用redis,所以把该数据自定义实现存储到redis里面
* @Param k:此次查询的标识,其中包含sql语句(String)
* @Param v:此次查询的结果数据
*/
@Override
public void putObject(Object k, Object v) {
RedisTemplate<String, Object> redisTemplate = getRedisTemplate();
redisTemplate.opsForValue().set(k.toString(), v);
}
/**
* @Description: 通过key取到缓存数据
* @Param k:
*/
@Override
public Object getObject(Object k) {
RedisTemplate<String, Object> redisTemplate = getRedisTemplate();
Object cache = redisTemplate.opsForValue().get(k.toString());
if (cache == null) {
log.debug("缓存未命中");
return null;
}
return cache;
}
/**
* @Description: 删除某一个缓存数据
* @Param o:
*/
@Override
public Object removeObject(Object k) {
try {
RedisTemplate<String, Object> redisTemplate = getRedisTemplate();
redisTemplate.delete(k.toString());
log.debug("Remove cached query result from redis");
} catch (Throwable t) {
log.error("Redis remove failed", t);
}
return null;
}
@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.execute((RedisCallback) connection -> {
connection.flushDb();
return null;
});
log.debug("Clear all the cached query result from redis");
}
@Override
public int getSize() {
return 0;
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
}
return redisTemplate;
}
}
# 全局开启二级缓存
mybatis:
configuration:
cache-enabled: true
# Mapper文件开启缓存
<cache type="com.pang.cache.MyBatisCache"/>
此次如果你的服务器宕机了,但redis缓存服务器没有宕机,那缓存依旧是存在的