Mybatis
其实就是根据官方文档来进行学习。
mybatis – MyBatis 3 | 入门 进不去的挂梯子试试
文章目录
简介
- MyBatis 是一款优秀的持久层框架;
- 它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
什么是持久层?
Dao层、Service层、Controller层
- 完成持久化工作的代码块
- 层界限十分明显
为什么需要MyBatis?
-
帮助程序员将数据存入到数据库中
-
方便
-
传统的JDBC代码太复杂了,简化,框架,自动化
-
不用MyBatis也可以,技术没有高低之分
-
优点:
- 简单易学
- 灵活
- sql和代码的分离,提高了可维护性。
- 提供映射标签,支持对象与数据库的orm字段关系映射
- 提供对象关系映射标签,支持对象关系组建维护
- 提供xml标签,支持编写动态sql
开始搭建
使用Maven 导入
使用前先确认自己的maven是不是配置正确(具体参照前面的JavaWeb
github有开源的mybatis maven仓库中也有,使用Maven直接导入依赖安装即可。
pom.xml文件依赖
最新版的junit依赖安装时会出现org.opentest4j的错误,所以。。我就不安装最新版的了。
<dependencies>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<!--junit 5.8.1版本的爆红 后来发现是因为我创建的时候,默认选择了自带的Maven 在上面的图中进行修改-->
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
首先在mysql数据库中创建表
CREATE TABLE `user`(
`id` INT(20) NOT NULL PRIMARY KEY,
`name` VARCHAR(30) DEFAULT NULL,
`pwd` VARCHAR(30) DEFAULT NULL
)ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `user`(`id`,`name`,`pwd`)VALUES
(1,'狂神','123456'),
(2,'张三','123456'),
(3,'李四','123890')
-
新建一个普通的没用模板的Maven项目,然后建议删掉src文件夹,因为后面将要作为父工程。
-
然后导入依赖,如果爆红就多刷新几次.或者检查maven配置
-
最后创建一个module作为子工程(这个应该都会吧
-
在新建的子工程的sources文件夹中新建一个 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>
<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>
</environments>
<!--狂神将这里的mappers删除了 弹幕说后面还会加回来。。。-->
<!--好了,我在后面回来了,后面狂神会遇到一个error然后神秘兮兮的说我之前删了一个地方,就是这个地方。。导致报错-->
<!--每一个mapper.xml必须在mybatis的核心配置文件中注册-->
<!--如果mapper.xml和核心文件一样都在resource的文件夹下,不用写路径直接写文件名即可-->
<mappers>
<mapper resource="com/song/dao/UserMapper.xml"/>
</mappers>
</configuration>
- 然后在idea中连接上数据库,连接过程中有可能会遇上驱动问题或者时区报错 这时可以在网上搜索永久或者临时的解决方案。
弹幕中提供了临时方法:在自己的url后面加上 ?serverTimezone=GMT 应该是一种临时的修改方法。 – 我没试过哈
注意:useSSL=false 解决 Mysql在高版本需要指明是否进行SSL连接。 狂神版本不同,写的为true。
& ; xml中也就是转义相当于 AND
数据库驱动选择"com.mysql.cj.jdbc.Driver 不然后面会爆红(如果选择了新版驱动)
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
- 在main的java文件夹下创建 两个包 类似于 com.song.dao 和 com.song.utils 两个包。然后在utils中创建 MybatisUtils.java 文件 写入帮助文档已经写死了必须写的三句代码。
public class MybatisUtils {
//编写工具类
// 提升作用域 提升到全局变量 static只执行一次
private static SqlSessionFactory sqlSessionFactory ;
static {
try {String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//注意这里不要写 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//会出现空指针的错误,这个地方狂神在后面会提到
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。
//SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}
}
- java文件夹下创建com.song.pojo包,然后在里面写User.java文件.在里面定义变量要和自己的数据库中的名称相同。记得通过快捷键进行定义函数等。
public class User {
private int id;
private String name;
private String pwd;
public User (){
}
public int getId() {
return id;
}
// 快捷键 ALT+INSERT 插入
public User(int id, String name, String pwd) {
this.id = id;
this.name = name;
this.pwd = pwd;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
}
- 在com.song.dao 包中创建 UserDao 接口interface (等价于之后学的mapper
public interface UserDao {
//查询全部用户的方法。以链表的形式返回
List<User>getUserList();
}
- 在com.song.dao 包中创建一个文件 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">
<!--namepace 绑定了一个 对应的Dao/Mapper接口 -->
<!--namespace 中的包名要和Dao/mapper 接口的包名一致!!-->
<mapper namespace="com.song.dao.UserDao">
<!--xml中select语句就是查询语句的意思-->
<!-- mybatis就是自己创建的数据库 user是自己创建的表 User是自己的写的实体类-->
<select id="getUserList" resultType="com.song.pojo.User">
select * from mybatis.user
</select>
</mapper>
测试
- 在test(测试类)的java文件夹中创建包结构和上面所创建的相对应
一个快捷创建测试类的方法。。。。Dao接口里面右键。选择go to,再点test,然后create new test
public class UserDaoTest {
@Test
public void test(){
//第一步:获得sqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
//方式一: getMapper
//class 可以获取本项目的任意一个实体类的对象
UserDao userDao = sqlSession.getMapper(UserDao.class);
List<User> userList = userDao.getUserList();
//方式二:不建议使用了
// List<User> userList = sqlSession.selectList("com.song.dao.UserDao.getUserList");
for(User user:userList){
System.out.println(user);
}
//官方建议为了防止忘记关闭sql,建议将这一大段test代码放到try和finall中。
//关闭SQL SqlSession
sqlSession.close();
}
}
运行会有两个常见的错误,狂神在这里一一演示了。。就是之前的伏笔,除此之外还有一个UTF错误
- mapper.xml忘记了在mybatis的核心配置文件中进行注册
Type interface com.song.dao.UserDao is not known to the MapperRegistry.
解决方法就是上面的核心配置文件中进行注册
- java中的xml格式文件导出失败,运行后Test自动生成的target文件夹中的test-class目录下的dao目录没有xml
Could not find resource com/song/dao/UserMapper.xml
- 解决方法就是在Maven的pom.xml中加入下面一段代码。 导入之后一定一定要刷新Maven!!!不然没作用
- (或者手动将 xml 文件复制到target对应的目录下
- (或者将该xml文件该路径从java到同路径下的resources文件夹中
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<!--弹幕中有人说 将filtering 改为false 。。。建议别这样改-->
<filtering>true</filtering>
</resource>
</resources>
</build>
(3条消息) maven - filtering标签_The Secret-CSDN博客_filtering
- 显示:1 字节的 UTF-8 序列的字节 1 无效。
(弹幕给了很多方法:去掉所有中文注释、将开头的UTF-8改为UTF8、将filtering标签改为 false 等等。。
根本原因是因为xml文件字符编码为utf-8 、idea编辑器字符编码为GBK,两者不一致所导致的。
所以要不就将xml开头的编码改为GBK要不就将idea编辑器字符编码改为UTF-8。记得改完clean并刷新一下Maven。
增删改查实现
注意:在这里狂神将上面的UserDao.java文件改名为了UserMapper.java 文件
(新版idea好像只要改名直接自动将你代码中用到这个java文件名的地方全部更换。
下面的代码并没有改正为UserMapper.java 仍然使用UserDao.java文件
一定要细心!!!多带了一个参数。。就报了一个很模糊的错误结果找了好久的bug。。。
- 关于parameter
parameterType在查询全部方法不需要这个参数,也就是没有
parameterType在添加方法里,返回的是实体类的全类名
parameterType在更新方法里,这个参数返回的也是实体类的全类名
parameterType在删除方法里,这个参数返回的也是实体类的Integer,就是成功影响一条数据的记录表示成功
增、删、改一定要最后提交事务!!
(不加提交事务也成功的应该都是标签写错了,写成了select,但是有的就会爆null错误
当出现错误时,读取错误信息要从后往前读 – 来自狂神
UserDao.java文件
public interface UserDao {
List<User>getUserList();
//根据id返回用户
User getUserById(int id);
//增加一个用户
int addUser(User user);
//修改用户
int updateUser(User user);
//删除用户
int deleteUser(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.song.dao.UserDao">
<select id="getUserList" resultType="com.song.pojo.User">
select * from mybatis.user
</select>
<!--namespace 就是指向接口-->
<!--id就是对应接口的方法-->
<!--resultType sql 语句的返回值-->
<!--parameterType 参数的类型-->
<select id="getUserById" parameterType="int" resultType="com.song.pojo.User" >
select * from mybatis.user where id=#{id}
</select>
<!--注意改为了insert 标签 并且没有返回类型了-->
<!--对象中的属性,可以直接取出来-->
<insert id="addUser" parameterType="com.song.pojo.User">
insert into mybatis.user (id,name,pwd)values(#{id},#{name},#{pwd});
</insert>
<update id="updateUser" parameterType="com.song.pojo.User">
update mybatis.user set name=#{name}, pwd=#{pwd} where id=#{id};
</update>
<delete id="deleteUser" parameterType="int">
delete from mybatis.user where id=#{id};
</delete>
</mapper>
UserDaoTest.java 文件 点击每个函数前的小箭头就可以一个一个的运行了
public class UserDaoTest {
@Test
public void test(){
//就是上面那个测试的代码
}
@Test
public void test_getUserById(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
User user=mapper.getUserById(1);
System.out.println(user);
sqlSession.close();
}
@Test
public void test_addUser(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
//创建数据库表的时候如果设定了id 自增可以选择不写id
mapper.addUser(new User(4,"haha","qwe123"));
//增删改除必须要提交事务!!
sqlSession.commit();
sqlSession.close();
}
@Test
public void test_updateUser (){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
mapper.updateUser(new User(4,"hehe","asd123"));
sqlSession.commit();
sqlSession.close();
}
@Test
public void test_deleteUser(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
mapper.deleteUser(4);
sqlSession.commit();
sqlSession.close();
}
}
关于下一节查错建议自己看视频
Map和模糊查询
假设,我们的实体类,或者数据库中的表,字段或者参数过多,我们应当考虑使用Map!
在公司里面很常用,方便,但是不正规!
Map传递参数,直接在sql中取出key即可!【parameterType=“map”】
对象传递参数,直接在sql中取对象的属性即可!【parameterType=“Object”】
只有一个基本类型参数的情况下,可以直接在sql中取到!可以不写 parameterType
多个参数用Map更加方便,或者注解!(后面学
UserDao.java
int addUser2(Map<String,Object> map);
UserMapper.xml文件
<!--利用map传参时,userid 这个名称可以根据自己的需要设定,保证后面java文件一致就行-->
<insert id="addUser2" parameterType="map">
insert into mybatis.user (id,name,pwd) values (#{userid},#{username},#{password})
</insert>
UserDaoTest.java 文件
@Test
public void addUser2(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
Map<String,Object> map = new HashMap<String, Object>();
//注意这里的键要和xml文件中的相对应
//值就是要插入的数据
map.put("userid",6);
map.put("username","szg");
map.put("password","123321");
mapper.addUser2(map);
sqlSession.commit();
sqlSession.close();
}
思考题:模糊查询怎么写?
两种方法应该都不会有sql注入,狂神说这里有注入也讲的不明白,弹幕一大堆反驳的 。。。
- java代码执行的时候,传递通配符% %
List<User> userList = mapper.getUserLike("%李%");
2.在sql拼接中使用通配符!
select * from mybatis.user where name like "%"#{value}"%"
配置解析
新建一个子工程,mybatis_02 。其内容大约就是将上一个子工程中都复制过来。
当然,自己再敲一遍是最好的,不容易出各种报错。
这一次就把UserDao改UserMapper吧,然后将除了增删改查主要的方法之外其余的几个都删掉吧。
核心配置文件
- 命名为mybatis-config.xml
- Mybatis的配置文件包含了会深深影响MyBatis行为的设置和属性信息。
笔记中没有的自己查看官方文档
这13个属性的顺序一定不能错
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
环境配置
以下来自官网
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。
不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单:
<environments default="development"> <!--设置默认环境-->
<environment id="development"> <!--一个环境的开头,设定环境的id,便于选择-->
<transactionManager type="JDBC"/><!--设置事务管理器,有两种!!另一种不常用-->
<dataSource type="POOLED"><!--设置连接池(数据源),这个就是之前学的-->
- POOLED 采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现
- UNPOOLED 采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
- JNDI 采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。
属性
注意顺序: 属性标签要在第一个位置
我们可以通过properties属性标签来实现引用配置文件
这些属性都是可外部配置且可动态替换的,既可以在典型的Java属性文件中配置,亦可通过properties元素的子元素来传递。【db.properties】
在resources文件夹中编写一个配置文件 db.properties 注意不用写 & ;了
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=UTF-8
pwd=123456
在核心配置文件mybatis-config.xml中映入
<properties resource="db.properties">
<property name="username" value="root"/>
<property name="pwd" value="123123"/>
</properties>
记得在核心配置文件下面将dataSource标签下的value改成相应的值
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${pwd}"/>
</dataSource>
观察上面外部配置文件和内部配置文件都对pwd进行了赋值那么结果会选择哪个值呢?(不考虑在此的适用性,只是在讲述这个问题
使用db.property中的值!!
下面来自官方文档:
如果一个属性在不只一个地方进行了配置,那么,MyBatis 将按照下面的顺序来加载:
-
首先读取在 properties 元素体内指定的属性。
-
然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。
-
最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。
-
因此,通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的则是 properties 元素中指定的属性。
因此,最后读取的就是优先级最高的!!!
类型别名
类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。
<!--可以给实体类起别名-->
<typeAliases>
<typeAlias type="com.song.pojo.User" alias="User" />
</typeAliases>
这时,两个字符串意义等价
也可以指定一个包名,MyBatis会在包名下面搜索相应的Java Bean,比如:
扫描包下的实体类,它们的默认别名就为这个类首字母小写的非限定类名!(
<!--可以给包起别名-->
<typeAliases>
<package name="com.kuang.pojo"/>
</typeAliases>
实体类若有注解Alias,则别名锁定为其注解值。
@Alias("author_test")
public class Author {
...
}
在实体类比较少的时候,使用第一种方式。
如果实体类十分多,建议使用第二种。
常见的 Java 类型内建的类型别名。它们都是不区分大小写的,注意,为了应对原始类型的命名重复,采取了特殊的命名风格。
别名 | 映射的类型 |
---|---|
_byte | byte |
_long | long |
_short | short |
_int | int |
_integer | int |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
object | Object |
map | Map |
hashmap | HashMap |
list | List |
arraylist | ArrayList |
映射器
MapperRegistry:注册绑定我们的Mapper文件;
方式一:【推荐使用】
<mappers>
<mapper resource="com/song/dao/UserMapper.xml"/>
</mappers>
方式二和方式三:
使用class文件绑定注册 AND 使用包扫描进行注入
都有注意点:
接口和他的Mapper配置文件必须同名并且在同一个包下。
<mappers>
<mapper class="com.song.dao.UserMapper"/>
<package name="com.kuang.dao"/>
</mappers>
方式四:完全限定资源定位符 (URL
来自官方文件;
==优先级:==package->resource->url->class
如果同时指定了这四种方式,只有package方式生效。如果同时指定了resource、url、class,那么这三种都不会生效,而是抛出异常。因为在解析resource、url、class这三种方式中任何一种方式时,都要求另外两种必须为null,如果不为null,则不会解析。 --来自大佬(3条消息) MyBatis的四种资源加载方式以及优先级_君战-CSDN博客
作用域和生命周期
声明周期和作用域是至关重要的,因为错误的使用会导致非常严重的并发问题。
- SqlSessionFactoryBuilder:
- 一旦创建了SqlSessionFactory,就不再需要它了
- 是一个局部变量
- SqlSessionFactory:
- 说白了就可以想象为:数据库连接池
- SqlSessionFactory一旦被创建就应该在应用的运行期间一直存在,==没有任何理由丢弃它或重新创建一个实例。==否则就不是一个好的代码
- 因此SqlSessionFactory的最佳作用域是应用作用域(ApplocationContext)。
- 最简单的就是使用单例模式或静态单例模式。
- SqlSession:
- 连接到连接池的一个请求
- SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
- 用完之后需要赶紧关闭,否则资源被占用!
有点类似租赁公司的运作模式。
ResulMap结果集映射
当出现编写的实体类的代码变量名称和对相应的数据库中变量名称不一样时,会出现查询失败的问题
例如:你在数据库中密码name=‘pwd’ 而在user.java实体类中确定义了一个passward的String变量,在查询时就会出现错误,显示密码为NULL
- 此时可以在查询语句加上AS passward 作为别名来识别。
select id,name,pwd AS passward from mybatis.user where id= #{id};
- 另一种解决方法 就是标题,采用resultmap
<resultMap id="UserMap" type="User">
<!--column数据库中的字段,property实体类中的属性-->
<result column="pwd" property="password"></result>
</resultMap>
<select id="getUserById" resultMap="UserMap">
select * from mybatis.user where id= #{id};
</select>
- resultMap 元素是 MyBatis 中最重要最强大的元素。
- ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
- ResultMap 的优秀之处——你完全可以不用显式地配置它们。
日志工厂
如果一个数据库操作出现了异常,我们需要排错。日志就是最好的助手!
曾经:sout、debug
现在:日志工厂!
标签setting 属性为logImpl 相应属性值有:
- SLF4J
- LOG4J 【掌握】
- LOG4J2
- JDK_LOGGING
- COMMONS_LOGGING
- STDOUT_LOGGING【掌握】【标准】
- NO_LOGGING
使用STDOUT_LOGGING
在mybatis-config.xml文件中配置 然后随便运行一个测试方法/
<settings>
<!-- 标准输出日志 一定一定要注意后面不要多出来一个空格 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
日志会详细的列出本程序执行中进行的操作,更有利于查错。
应该大部分语句都看得懂吧。。
使用LOG4J
什么是Log4j?
- Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件
- 我们也可以控制每一条日志的输出格式;
- 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
- 可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
先在pom.xml文件中导入log4j的依赖包
这个版本的包是已经存在于idea中了,我这是直接导入的比较老的版本,新的版本已经更新到 2.14 了
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>
在resources文件夹下建立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
#输出信息同步到log下的文件中
log4j.appender.file.File = ./log/kuang.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
配置log4j为属性值
<setting name="logImpl" value="log4j"/>
然后随便运行一个test方法,观察分析输出信息[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2l9W6lgf-1634713477177)(Mybatis.assets/image-20211012214845286.png)]
简单使用log4j
在Test文件夹下新建一个UserLog4j.java文件用来简单使用log4j
package com.song.dao;
//一定注意挡自动导入包的时候,会显示两个一个是java.util的,一个是apache的,要apache的,不要导错了
import org.apache.log4j.Logger;
import org.junit.jupiter.api.Test;
public class UserLog4j {
//日志对象,参数为当前类的class
static Logger logger =Logger.getLogger(UserLog4j.class);
@Test
public void test(){
//参数代表级别
logger.info("info:进入了testLog4j");
logger.debug("DEBUG:进入了testLog4j");
logger.error("erro:进入了testLog4j");
}
}
可以用于代替sout(System.out.println() )
分页
使用limit函数实现分页
sql语句:
SELECT * from user limit startIndex,pageSize
分步骤:
首先在UserMapper.java中添加接口
List<User> getUserByLimit(Map<String, Integer> map);
然后在UserMapper.xml中注册 (注意这里要考虑需不需要做结果集映射)
<select id="getUserByLimit" parameterType="map" resultType="com.song.pojo.User">
select * from mybatis.user limit #{startIndex},#{pageSize};
</select>
然后进行测试
@Test
public void test_getUserByLimit(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper =sqlSession.getMapper(UserMapper.class);
HashMap <String,Integer> map=new HashMap<String, Integer>();
map.put("startIndex",0);
map.put("pageSize",2);
//hashmap作为参数导入接口
List<User> list = mapper.getUserByLimit(map);
for (User user :list){
System.out.println(user);
}
sqlSession.close();
}
使用rowbounds实现分页
分步骤:
首先在UserMapper.java中添加接口
List<User> getUserByRowBounds();
然后在UserMapper.xml中注册 (注意这里要考虑需不需要做结果集映射)
<select id="getUserByRowBounds" resultType="com.song.pojo.User">
select * from mybatis.user
</select>
然后进行测试
@Test
public void getUserByRowBounds(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
//RowBounds实现
RowBounds rowBounds = new RowBounds(0, 2);
List<User> userList = sqlSession.selectList("com.song.dao.UserMapper.getUserByRowBounds",null,rowBounds);
//直接通过Java代码层面实现分页
//List<User> userList = sqlSession.selectList("com.song.dao.UserMapper.getUserByRowBounds");
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}
分页插件
pageHelper
注解开发
面向接口开发
- 接口从更深层次的理解,应是定义(规范,约束)与实现(名实分离的原则)的分离。
- 接口的本身反映了系统设计人员对系统的抽象理解。
接口应有两类:
- 第一类是对一个个体的抽象,它可对应为一个抽象体(abstract class);
- 第二类是对一个个体某一方面的抽象,即形成一个抽象面(interface) ;-一个体有可能有多个抽象面。抽象体与抽象面是有区别的。
三个面向区别
- 面向对象是指,我们考虑问题时,以对象为单位,考虑它的属性和方法;
- 面向过程是指,我们考虑问题时,以一个具体的流程(事务过程)为单位,考虑它的实现;
- 接口设计与非接口设计是针对复用技术而言的,与面向对象(过程)不是一个问题,更多的体现就是对系统整体的架构;
使用注解开发
在mybatis中,复杂的项目一定要使用xml配置文件开发,而简单的项目可以使用注解进行开发,否则维护成本大。
和xml开发不同之处在于,没有与接口相对应的xml文件而是直接在接口的定义上面写注解
注解的形式就是sql语句。
@Select("select * from mybatis.user")
List<User> getUsers();
在使用mapper映射时,使用的class映射
<mappers>
<mapper class="com.song.dao.UserMapper"/>
</mappers>
然后就可以编写测试代码测试了
本质:反射机制实现
底层:动态代理
注解CRUD
//方法存在多个参数(限定为基本数据类型的),所有的参数前面必须加上@Param("自定义参数名")注解
//Param中的参数名称如果在后面用到,必须保持一致
//下面的注解 和下面的xml注册选择一个使用就行。
@Select("select * from mybatis.user where pwd = #{ipwd} AND name=#{name}")
User getUserByID(@Param("ipwd") String pwd, @Param("name") String name);
<select id="getUserByID" resultType="com.song.pojo.User">
select * from mybatis.user where pwd = #{ipwd} AND name=#{name};
</select>
关于@Param( )注解
- 基本类型的参数或者String类型,需要加上
- 引用类型不需要加
- 如果只有一个基本类型的话,可以忽略,但是建议大家都加上
- 我们在SQL中引用的就是我们这里的@Param()中设定的属性名
能用#{}就尽量不用${}可以防止注入。
Mybatis执行流程解析
(4条消息) mybatis的执行流程_落叶飘零z的博客-CSDN博客_mybatis执行流程
Lombok
一个帮助偷懒的插件,通过一行注解来代替大量的java模板代码,更加简洁,易于维护。
使用步骤:
首先在idea中导入Lombok插件
File->Settings->plugins
如果这里插件中心不显示东西,连不上网的话,plugins的右上角有个小齿轮点击选择修改http设置,然后重启
如果还不行,垃圾移动校园网!!!换网络!
然后使用maven在工程的pom中导入依赖
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
然后就可以使用了(可以省略大篇幅的代码了
@Data : 注解在类上, 为类提供读写属性, 此外还提供了 equals()、hashCode()、toString() 方法
@Builder : 注解在类上, 为类提供一个内部的 Builder
@Synchronized : 注解在方法上, 为方法提供同步锁
@Log4j : 注解在类上, 为类提供一个属性名为 log 的 log4j 的日志对象
@Slf4j : 注解在类上, 为类提供一个属性名为 log 的 log4j 的日志对象
==@AllArgsConstructor:==会生成一个包含所有变量的构造函数,该注解的作用域也是只有在实体类上,参数的顺序与属性定义的顺序一致。
==@NoArgsConstructor:==生成一个无参构造函数
@Getter @Setter @ToString 生成单个函数的注解 要加在对应的变量上面.
多表查询
搭建环境
在sqlyog中创建两个表
CREATE TABLE `teacher` (
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
INSERT INTO teacher(`id`, `name`) VALUES (1, '秦老师');
CREATE TABLE `student` (
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
`tid` INT(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fktid` (`tid`),
CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
INSERT INTO `student` (`id`, `name`, `tid`) VALUES (1, '小明', 1);
INSERT INTO `student` (`id`, `name`, `tid`) VALUES (2, '小红', 1);
INSERT INTO `student` (`id`, `name`, `tid`) VALUES (3, '小张', 1);
INSERT INTO `student` (`id`, `name`, `tid`) VALUES (4, '小李', 1);
INSERT INTO `student` (`id`, `name`, `tid`) VALUES (5, '小王', 1);
先放整体截图:
然后创建一个新的module pom中导入lombok依赖,记得刷新哦 !
在java文件夹中还是创建三个包,将上一个module里的java文件夹中的MybatisUtils复制过来
将上一个module里的resources里的文件全部复制到新的module里面,里面的映射mapper删掉(后面还会再写的)
至此,一个干净的工程环境就准备好了。
然后在pojo中创建两个实体类student和teacher
@Data
public class Teacher {
private int id;
private String name;
}
@Data
public class Student {
private int id;
private String name;
//学生需要关联一个老师
private Teacher teacher;
}
在java文件夹中的 dao文件中创建两个接口TeacherMapper StudentMapper
然后在resources目录下创建com/song/dao包(文件夹)然后在里面创建TeacherMapper.xml和StudentMapper.xml两个xml文件
在这里弹幕提示resources建包采用’/’ 分开而不是 ‘.’ 不然包后面不能展开,只是作为一个包存在而已。
在这里,我建好之后和在java文件夹中的dao包的标志不同,没有小白点。。。
编写TeacherMapper.xml等两个文件时,基本固定语句建议和官方文档保持一致;(狂神自己写的漏下了一个东西
关于视频中的注解和xml
在视频中,狂神在配置xml文件时,并没有写完整,没有写sql语句,而是在接口文件中采用了注解,导致在后面mybatis-config.xml文件中mapper标签映射时,只能采用class的映射方法.
还是那个原则,简单的sql语句采用注解sql的方式,复杂的sql语句采用xml的方式.
记得select注解和xml文件里的select语句 两者只能存在一个,不然会报错!!!
// TeacherMapper 接口文件
public interface TeacherMapper {
@Select("select * from mybatis.teacher where id=#{id} ")
Teacher getTeacherByid( @Param("id") int id);
}
<!--TeacherMapper 的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.song.dao.TeacherMapper">
<select id="getTeacherByid" resultType="com.song.pojo.Teacher" parameterType="int">
select * from mybatis.teacher where id=#{id};
</select>
</mapper>
<!--关于mybatis-config.xml文件里的mapper映射 两种对应方式选择其一即可-->
<mappers>
<!--<mapper resource="com/song/dao/TeacherMapper.xml"></mapper>-->
<mapper class="com.song.dao.TeacherMapper"></mapper>
</mappers>
public class TestTeacher { //测试函数
@Test
public void getTeacherByid(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = mapper.getTeacherByid(1);
System.out.println(teacher);
sqlSession.close();
}
}
当上面的都全部正常运行时,说明环境完成…
多对一关系
例如:多个学生都有同一个老师
public interface StudentMapper {
// 查询所有信息,包括每个学生信息和他们的老师的信息
public List<Student> getStudent1();
public List<Student> getStudent2();
}
记得mapper映射
<mappers>
<mapper resource="com/song/dao/StudentMapper.xml"></mapper>
</mappers>
存在两种查询方式:
<?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.song.dao.StudentMapper">
<!--按照查询嵌套处理 (子查询)
思路:
1. 查询所有的学生信息
2. 根据查询出来的学生的tid寻找特定的老师 (子查询)
-->
<select id="getStudent1" resultMap="StudentTeacher">
select * from mybatis.student
</select>
<resultMap id="StudentTeacher" type="com.song.pojo.Student">
<result property="id" column="id"/>
<result property="name" column="name"/>
<!--复杂的属性,我们需要列单独出来 对象:association属性 集合:collection属性-->
<collection property="teacher" column="tid" javaType="com.song.pojo.Teacher" select="getTeacher"/>
</resultMap>
<select id="getTeacher" resultType="com.song.pojo.Teacher">
<!--这里#{} 括号中随便写因为会自动匹配-->
select * from mybatis.teacher where id = #{sadid}
</select>
<!--=====================按照结果嵌套处理(连表查询)==============-->
<select id="getStudent2" resultMap="StudentTeacher2">
<!--加上t.id 使结果正常显示老师的id -->
select s.id sid , s.name sname, t.name tname,t.id tid
from mybatis.student s,mybatis.teacher t
where s.tid=t.id
</select>
<!--结果封装,将查询出来的列封装到对象属性中-->
<resultMap id="StudentTeacher2" type="com.song.pojo.Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<association property="teacher" javaType="com.song.pojo.Teacher">
<result property="name" column="tname"></result>
<result property="id" column="tid"></result>
</association>
</resultMap>
</mapper>
狂神的代码中第二种方法查询的老师id为0,因为代码中并没有查询id.
查询是一条一条执行的,当执行到tid的时候去调用映射,映射有去子查询,所以#{} 里面自动填充了column的值,所以#{}里面随意填写 --来自一个红色弹幕大佬
一对多关系
例如:一个老师教授一个班的学生
重新建立一个module 创建实体类Teacher和Student。
Teacher 增加一个private List<Student>students;
来存储其班里的学生的id
Studnet类 将老师变量改为private int tid;
对应老师的id
其他的该删的删 ,保留整体的框架就行
在TeacherMapper中
<!--按结果嵌套查询-->
<select id="getTeacher" resultMap="StudentTeacher" parameterType="int">
SELECT s.id sid, s.name sname,t.name tname,t.id tid FROM mybatis.student s, mybatis.teacher t
WHERE s.tid = t.id AND tid = #{tid} <!--这里不强求必须是tid-->
</select>
<resultMap id="StudentTeacher" type="com.song.pojo.Teacher">
<result property="id" column="tid"/>
<result property="name" column="tname"/>
<!--复杂的属性,我们需要单独处理 对象:association 集合:collection 此处用集合
JavaType用来指定POJO中属性的类型,而ofType指定的是映射到List集合中POJO的类型
-->
<collection property="students" ofType="com.song.pojo.Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<result property="tid" column="tid"/>
</collection>
</resultMap>
记得mapper注册一下,然后写一个测试函数测试
@Test
public void test(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = mapper.getTeacher(1);
System.out.println(teacher);
sqlSession.close();
}
面试高频
- Mysql引擎
- InnoDB底层原理
- 索引
- 索引优化
动态SQL
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
什么是动态SQL:动态SQL就是根据不同的条件生成不同的SQL语句
所谓的动态SQL,本质上还是SQL语句,只是我们可以在SQL层面,去执行一个逻辑代码
搭建环境
CREATE TABLE `blog` (
`id` varchar(50) NOT NULL AUTO_INCREMENT COMMENT '博客id',
`title` varchar(30) NOT NULL COMMENT '博客标题',
`author` varchar(30) NOT NULL COMMENT '博客作者',
`create_time` datetime(0) NOT NULL COMMENT '创建时间',
`views` int(30) NOT NULL COMMENT '浏览量',
PRIMARY KEY (`id`)
)
创建一个新的module,导入lombok的依赖,然后创建该有的文件结构,再然后将Blog类写出来。
@Data
public class Blog {
private String id;
private String title;
private String author;
//需要开启驼峰命名自动转换 因为属性名和数据库里的列名不同
private Date createTime;
private int views;
}
MybatisUtils.xml文件在上个工程直接复制过来,复制到utils包下。添加一个标签开启驼峰命名自动转换
从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
然后dao包里写Blog的接口和对应的xml文件 (BlogMapper 和BlogMapper.xml)(这里狂神又都写到dao包里了,之前还说写到resources里比较好)
将resources中的配置文件复制过来,记得改mapper映射
在utils包中创建一个Idutils.java文件
public class IdUtils {
public static String getId(){
//得到随机id
String s = UUID.randomUUID().toString();
//去掉随机id中的横杠
return s.replace("-","");
}
@Test
public void test()
{
//导入不了Test的,查看pom中的junit范围<scope>test</scope>改为<scope>compile</scope> 记得刷新
System.out.println(getId());
}
}
测试一下,尝试插入数据
public interface BlogMapper {
void addBlog(Blog blog);
}
<insert id="addBlog" parameterType="com.song.pojo.Blog" >
insert into mybatis.blog(id, title, author, create_time, views)
values (#{id},#{title},#{author},#{createTime},#{views})
</insert>
@Test
public void addBlogTest() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
Blog blog = new Blog();
blog.setId(IdUtils.getId());
blog.setTitle("Mybatis");
blog.setAuthor("狂神说");
blog.setCreateTime(new Date());
blog.setViews(9999);
mapper.addBlog(blog);
blog.setId(IdUtils.getId());
blog.setTitle("Java");
mapper.addBlog(blog);
blog.setId(IdUtils.getId());
blog.setTitle("Spring");
mapper.addBlog(blog);
blog.setId(IdUtils.getId());
blog.setTitle("微服务");
mapper.addBlog(blog);
//别忘了提交事务
sqlSession.commit();
sqlSession.close();
}
IF标签
加1=1 是为了保证当不输入参数的时候,也能够从数据库中查出值(所有的数据)
<select id="queryBlogs" parameterType="Map" resultType="com.song.pojo.Blog">
select * from mybatis.blog where 1=1
<if test="title !=null">
<!--后面不能加分号否则这条sql语句就结束了,后面的and会报错!! -->
and title =#{title}
</if>
<if test="author !=null">
and author =#{author}
</if>
</select>
HashMap map=new HashMap();
map.put("title","Java");
map.put("author","狂神说");
List<Blog> blogList=mapper.queryBlogs(map);
for (Blog blog : blogList) {
System.out.println(blog);
}
Where标签
就是为了去掉上面的1=1
显得更加专业
若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。但不能起到自动添加and的作用!!!
<select id="queryBlogs" parameterType="Map" resultType="com.song.pojo.Blog">
select * from mybatis.blog
<where>
<if test="title !=null">
title =#{title}
</if>
<if test="author !=null">
and author =#{author};
</if>
</where>
</select>
Choose标签
类似于java里的switch
在这里因为当出现了一条语句符合就会跳出choose匹配,所以不用加and。
但是如果有多个choose时,后面choose里的sql语句前都需要加上and。
<select id="queryBlogs" parameterType="Map" resultType="com.song.pojo.Blog">
select * from mybatis.blog
<where>
<choose>
<when test="title!=null">
title=#{title}
</when>
<when test="author!=null">
author=#{author}
</when>
<otherwise>
views=#{views}
</otherwise>
</choose>
<choose>
<when test="author!=null">
and author=#{author}
</when>
<otherwise>
and views=#{views}
</otherwise>
</choose>
</where>
</select>
set标签
适用于更新语句update
<update id="UpdateBlog" parameterType="Map">
update mybatis.blog
<set>
<if test="title!=null">
<!-- 这里的逗号一定不要忘记,set会自动删掉无用的逗号,但不会自动添加
也别手滑而写成分号-->
title=#{title},
</if>
<if test="views!=null">
views=#{views},
</if>
<if test="author!=null">
author=#{author}
</if>
</set>
where id =#{id}
</update>
HashMap map=new HashMap();
map.put("title","Javayyds");
map.put("author","szg");
map.put("views",100);
map.put("id","267fda04e2c648fdb2076159d4f0d93d");
mapper.UpdateBlog(map);
sqlSession.commit();
sql语段
当出现一段相同的sql语句,在xml配置文件中十多次复用的时候,就可以使用sql标签将其提取出来,每次使用只需要include进去就行
尽量只包含if判断 where字句啥的留在原位置。
<sql id="set_sql">
<set>
<!--假装有一大堆代码-->
</set>
</sql>
<update id="UpdateBlog" parameterType="Map">
update mybatis.blog
<include refid="set_sql"> </include>
where id =#{id}
</update>
Foreach标签
方便起见,通过可视化手动将数据库中博客的id改成从一开始的连续数字
<select id="queryBlogsForeach" parameterType="Map" resultType="com.song.pojo.Blog">
<!--select * from mybatis.blog where 1=1 and( id=#{id} or id =#{id} or id =#{id} ...)-->
select * from mybatis.blog
<where>
<foreach collection="ids" item="id" open="and (" close=")" separator="or">
id= #{id}
</foreach>
</where>
</select>
HashMap map=new HashMap();
ArrayList<Integer> ids = new ArrayList<>();
ids.add(1);ids.add(3);ids.add(5);
map.put("ids",ids);
List<Blog> blogList = mapper.queryBlogsForeach(map);
for (Blog blog : blogList) {
System.out.println(blog);
}
上面是狂神的例子,使用的是在map容器中又放置了list链表
然后自己试了一下直接将接口的参数改为list,然后不经过map容器也是可以的。
缓存
简介
- 什么是缓存[Cache]?
存在内存中的临时数据。
将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题 - 为什么使用缓存?
减少和数据库的交互次数,减少系统开销,提高系统效率 - 什么样的数据可以使用缓存?
经常查询并且不经常改变的数据 【可以使用缓存】
Mybatis缓存
- MyBatis包含一个非常强大的查询缓存特性,它可以非常方便的定制和配置缓存,缓存可以极大的提高查询效率。
- MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启(SqlSession级别的缓存,也称为本地缓存)
- 二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
- 为了提高可扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来定义二级缓存。
- 缓存肯定会被清除,清除算法查询文档
一级缓存
一级缓存也叫本地缓存:SqlSession,与数据库同一次会话期间查询到的数据会放在本地缓存中。
一级缓存往往默认自动开启。
测试就创建一个新的子工程,然后接口就定义一个UserMapper 其他该复制的都复制了
- 首先设置按照标准的日志输出即可
- 然后写java语句测试
@Test
public void test(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.getUserById(1);
System.out.println(user1);
User user2 = mapper.getUserById(1);
System.out.println(user2);
System.out.println(user1==user2);
sqlSession.close();
}
发现查询了两次相同的id却只访问了一次sql,说明第一次访问的结果存在了一级缓存中。
当查询不同的id就会访问两次sql 了
刷新缓存的情况:
- 增删改操作,可能会改变原来的数据,所以必定会刷新缓存
- 查询的不同的Mapper
- 手动清理了缓存
sqlSession.clearCache();
一级缓存就是一个Map,存放着每次查询的结果。
二级缓存
二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存
基于namespace级别的缓存,一个名称空间,对应一个二级缓存
工作机制:
开启缓存之后,会话关闭了,一级缓存中的数据被保存到二级缓存中,当新的会话查询信息,就可以从二级缓存中获取内容。
不同的mapper查询出的数据会放在自己对应的缓存(map)中
便于理解,二级缓存需要显示的开启和配置
也就是说,其实二级缓存是默认开启的。(当然,我只是简单的实验得出的结论,网上大部分都是说的默认关闭
为了提高可扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来定义二级缓存。
测试:
- mybatis_config显示的开启全局缓存
<setting name="cacheEnabled" value="true"/>
- 在Mapper.xml中使用缓存 (把下面这句话加到xml中 来自官方的 参数可以自己修改的
上面图片来自MyBatis 二级缓存全详解 - 程序员cxuan - 博客园 (cnblogs.com)
<!--在当前Mapper.xml中使用二级缓存
使用FIFO回收策略
每隔60s刷新缓存
最多存放512条信息
只读
-->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
测试:
@Test
public void test(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
SqlSession sqlSession2 = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.getUserById(1);
System.out.println(user1);
sqlSession.close();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.getUserById(1);
System.out.println(user2);
System.out.println(user1==user2);
sqlSession2.close();
}
结果很明显,虽然开启了两个不同的sqlsession来select同一个id,但是只启用了一次sql查询,
错误:Cause: java.io.NotSerializableException: com.song.pojo.User
昂出现这个错误,两种解决方法,一个是将cache标签的参数readOnly="true"
要加上
另一种是将实体化对象进行序列化public class User implements Serializable {
存放在共享缓存中数据进行序列化操作和反序列化操作 ,因此数据对应实体类必须实现序列化接口
。
两种解决方法第二种是官方要求的,因此应以第二种为准。
关于序列化
程序在运行时实例化出对象,这些对象存在于内存中,随着程序运行停止而消失,但如果我们想把某些对象(一般都是各不相同的属性)保存下来或者传输给其他进程,在程序终止运行后这些对象仍然存在,可以在程序再次运行时读取这些对象的信息,或者在其他程序中利用这些保存下来的对象信息恢复成实例对象。这种情况下就要用到对象的序列化和反序列化。说白了就是为了在不同时间或不同平台的JVM之间共享实例对象。
序列化可以将一个对象转化成一段字符串编码,传输或者做存储处理,使用时再进行反序列,而字符串不用序列化的原因是字符串在源码中已经实现了Serializable接口的,所以它已经是序列化了的。
当cache标签使用默认的配置时,readOnly默认为 false,需要通过序列化返回缓存对象的拷贝。所以pojo里的User实体类需要实现Serializable接口,使其支持序列化。
关于序列化后比较user1==user2时出现false的解释:
-
==
这个操作符对于引用来说,是判断它们的地址是否一样。
你反序列出来的对象其实是重新创建了一个对象, 它的地址当然跟前面那个对象的地址不一样。 -
要想返回同一个对象,sqlsessionfactory得实现单例模式。并且还要写readResolve方法。(实现浅复制,指向同一个地址
-
如果换成equals判断,则返回的是true(没有重写过的equals应该比较的地址,也应该是false啊??!
小结:
- 只要开启了二级缓存,在同一个Mapper下就有效(在同一个namesqace下,即使不同mapper也有效
- 所有的数据都会放在一级缓存中
- 只有sql语句类型是“SELECT”的时候,使用二级缓存里拿值,其他操作,刷新缓存里的值。
mybatis二级缓存的使用场景及脏数据的产生 - 沈叶唐 - 博客园 (cnblogs.com)
缓存原理
-
开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询。
-
当开启二级缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
-
只有当前会话提交,或者关闭的时候,一级缓存的信息才会提交到二级缓存中