前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >mybatis看这一篇就够了,简单全面一发入魂

mybatis看这一篇就够了,简单全面一发入魂

作者头像
全栈程序员站长
发布2022-09-14 10:39:14
3760
发布2022-09-14 10:39:14
举报
文章被收录于专栏:全栈程序员必看

大家好,又见面了,我是你们的朋友全栈君。

文章目录

Mybatis

概述

  1. mybatis是什么?有什么特点? 它是一款半自动的ORM持久层框架,具有较高的SQL灵活性,支持高级映射(一对一,一对多),动态SQL,延迟加载和缓存等特性,但它的数据库无关性较低
    • 什么是ORM? Object Relation Mapping,对象关系映射。对象指的是Java对象,关系指的是数据库中的关系模型,对象关系映射,指的就是在Java对象和数据库的关系模型之间建立一种对应关系,比如用一个Java的Student类,去对应数据库中的一张student表,类中的属性和表中的列一一对应。Student类就对应student表,一个Student对象就对应student表中的一行数据
    • 为什么mybatis是半自动的ORM框架? 用mybatis进行开发,需要手动编写SQL语句。而全自动的ORM框架,如hibernate,则不需要编写SQL语句。用hibernate开发,只需要定义好ORM映射关系,就可以直接进行CRUD操作了。由于mybatis需要手写SQL语句,所以它有较高的灵活性,可以根据需要,自由地对SQL进行定制,也因为要手写SQL,当要切换数据库时,SQL语句可能就要重写,因为不同的数据库有不同的方言(Dialect),所以mybatis的数据库无关性低。虽然mybatis需要手写SQL,但相比JDBC,它提供了输入映射和输出映射,可以很方便地进行SQL参数设置,以及结果集封装。并且还提供了关联查询动态SQL等功能,极大地提升了开发的效率。并且它的学习成本也比hibernate低很多

快速入门

只需要通过如下几个步骤,即可用mybatis快速进行持久层的开发

  1. 编写全局配置文件
  2. 编写mapper映射文件
  3. 加载全局配置文件,生成SqlSessionFactory
  4. 创建SqlSession,调用mapper映射文件中的SQL语句来执行CRUD操作

原生开发示例

总结

  1. 编写mapper.xml,书写SQL,并定义好SQL的输入参数,和输出参数
  2. 编写全局配置文件,配置数据源,以及要加载的mapper.xml文件
  3. 通过全局配置文件,创建SqlSessionFactory
  4. 每次进行CRUD时,通过SqlSessionFactory创建一个SqlSession
  5. 调用SqlSession上的selectOneselectListinsertdeleteupdate等方法,传入mapper.xml中SQL标签的id,以及输入参数

注意要点

上面其实是比较原始的开发方式,我们需要编写dao类,针对mapper.xml中的每个SQL标签,做一次封装,SQL标签的id要以字符串的形式传递给SqlSession的相关方法,容易出错,非常不方便;为了简化开发,mybatis提供了mapper接口代理的开发方式,不需要再编写dao类,只需要编写一个mapper接口,一个mapper的接口和一个mapper.xml相对应,只需要调用SqlSession对象上的getMapper(),传入mapper接口的class信息,即可获得一个mapper代理对象,直接调用mapper接口中的方法,即相当于调用mapper.xml中的各个SQL标签,此时就不需要指定SQL标签的id字符串了,mapper接口中的一个方法,就对应了mapper.xml中的一个SQL标签

基于Mapper代理的示例

全局配置文件和mapper.xml文件是最基本的配置,仍然需要。不过,这次我们不编写dao类,我们直接创建一个mapper接口

代码语言:javascript
复制
package com.yogurt.mapper;

import com.yogurt.po.Student;

import java.util.List;

public interface StudentMapper { 
   

	List<Student> findAll();

	int insert(Student student);

	int delete(Integer id);

	List<Student> findByName(String value);
}

而我们的mapper.xml文件如下

代码语言:javascript
复制
<?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.yogurt.mapper.StudentMapper">
    <select id="findAll" resultType="com.yogurt.po.Student">
        SELECT * FROM student;
    </select>

    <insert id="insert" parameterType="com.yogurt.po.Student">
        INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});
    </insert>

    <delete id="delete" parameterType="int">
        DELETE FROM student WHERE id = #{id};
    </delete>

    <select id="findByName" parameterType="string" resultType="student">
        SELECT * FROM student WHERE name like '%${value}%';
    </select>
</mapper>

mapper接口和mapper.xml之间需要遵循一定规则,才能成功的让mybatis将mapper接口和mapper.xml绑定起来

  1. mapper接口的全限定名,要和mapper.xml的namespace属性一致
  2. mapper接口中的方法名要和mapper.xml中的SQL标签的id一致
  3. mapper接口中的方法入参类型,要和mapper.xml中SQL语句的入参类型一致
  4. mapper接口中的方法出参类型,要和mapper.xml中SQL语句的返回值类型一致

测试代码如下

代码语言:javascript
复制
public class MapperProxyTest { 
   
	private SqlSessionFactory sqlSessionFactory;

	@Before
	public void init() throws IOException { 
   
		InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
		sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
	}

	@Test
	public void test() { 
   
		SqlSession sqlSession = sqlSessionFactory.openSession();
		StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
		List<Student> studentList = mapper.findAll();
		studentList.forEach(System.out::println);
	}
}

结果如下

image-20200525234945865
image-20200525234945865

这个mapper接口,mybatis会自动找到对应的mapper.xml,然后对mapper接口使用动态代理的方式生成一个代理类

基于注解的示例

如果实在看xml配置文件不顺眼,则可以考虑使用注解的开发方式,不过注解的开发方式,会将SQL语句写到代码文件中,后续的维护性和扩展性不是很好(如果想修改SQL语句,就得改代码,得重新打包部署,而如果用xml方式,则只需要修改xml,用新的xml取替换旧的xml即可)

使用注解的开发方式,也还是得有一个全局配置的xml文件,不过mapper.xml就可以省掉了,具体操作只用2步,如下

测试代码如下

代码语言:javascript
复制
public class PureMapperTest { 
   

	private SqlSessionFactory sqlSessionFactory;

	@Before
	public void init() throws IOException { 
   
		InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
		sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
	}

	@Test
	public void test() { 
   
		SqlSession sqlSession = sqlSessionFactory.openSession();
		PureStudentMapper mapper = sqlSession.getMapper(PureStudentMapper.class);
		mapper.insert(new Student(10,"Tomcat",120,60,0));
        sqlSession.commit();
		List<Student> studentList = mapper.findAll();
		studentList.forEach(System.out::println);
	}
}

结果如下

image-20200526000650920
image-20200526000650920

注:当使用注解开发时,若需要传入多个参数,可以结合@Param注解,示例如下

代码语言:javascript
复制
package org.mybatis.demo.mapper;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.mybatis.demo.po.Student;

import java.util.List;

public interface PureStudentMapper { 
   

	@Select("SELECT * FROM student WHERE name like '%${name}%' AND major like '%${major}%'")
	List<Student> find(@Param("name") String name, @Param("major") String major);
}

@Param标签会被mybatis处理并封装成一个Map对象,比如上面的示例中,实际传入的参数是一个Map对象,@Param标签帮忙向Map中设置了值,即它做了

代码语言:javascript
复制
Map<String,Object> map = new HashMap<>();
map.put("name", name);
map.put("major",major);

将方法形参中的name和major放到了map对象中,所以在@Select标签中可以用{name}和{major}取出map对象中的值。 ——————–(我是分割线)

上面我们见到了在全局配置文件中,两种配置mapper的方式,分别是

代码语言:javascript
复制
<!-- 在mapper接口中使用注解 -->
<mappers>
    <mapper class="com.yogurt.mapper.PureStudentMapper"/>
</mappers>

<!-- 普通加载xml -->
<mappers>
    <mapper resource="StudentMapper.xml"/>
</mappers>

而在实际工作中,一般我们会将一张表的SQL操作封装在一个mapper.xml中,可能有许多张表需要操作,那么我们是不是要在<mappers>标签下写多个<mapper>标签呢?其实不用,还有第三种加载mapper的方法,使用<package>标签

代码语言:javascript
复制
<mappers>
    <package name="com.yogurt.mapper"/>
</mappers>

这样就会自动加载com.yogurt.mapper包下的所有mapper,这种方式需要将mapper接口文件和mapper.xml文件都放在com.yogurt.mapper包下,且接口文件和xml文件的文件名要一致。注意,在IDEA的maven开发环境下,maven中还需配置<resources>标签,否则maven打包不会将java源码目录下的xml文件打包进去,见下文

三种加载mapper的方式总结

  • <mapper resource="" /> 加载普通的xml文件,传入xml的相对路径(相对于类路径)
  • <mapper class="" /> 使用mapper接口的全限定名来加载,若mapper接口采用注解方式,则不需要xml;若mapper接口没有采用注解方式,则mapper接口和xml文件的名称要相同,且在同一个目录
  • <package name="" /> 扫描指定包下的所有mapper,若mapper接口采用注解方式,则不需要xml;若mapper接口没有采用注解方式,则mapper接口和xml文件的名称要相同,且在同一目录

注意:用后两种方式加载mapper接口和mapper.xml映射文件时,可能会报错

image-20200527205657457
image-20200527205657457

仔细检查了一下,mapper接口文件和xml映射文件确实放在了同一个目录下,而且文件名一致,xml映射文件的namespace也和mapper接口的全限定名对的上。为什么会这样呢?

在这里插入图片描述
在这里插入图片描述

其实是因为,对于src/main/java 源码目录下的文件,maven打包时只会将该目录下的java文件打包,而其他类型的文件都不会被打包进去,去工程目录的target目录下看看maven构建后生成的文件

image-20200527210952423
image-20200527210952423

我们需要在pom.xml中的<build> 标签下 添加<resources> 标签,指定打包时要将xml文件打包进去

代码语言:javascript
复制
<build>
	<resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
</build>

此时再用maven进行打包,看到对应目录下有了xml映射文件(特别注意,这里配置了pom.xml下的resource标签后,可能会引发一些问题,例如原本src/main/resources资源目录下的文件没有被打包进来,参考我的这篇文章maven打包时的资源文件问题

image-20200527210835407
image-20200527210835407

此时再运行单元测试,就能正常得到结果了

应用场景

主键返回

通常我们会将数据库表的主键id设为自增。在插入一条记录时,我们不设置其主键id,而让数据库自动生成该条记录的主键id,那么在插入一条记录后,如何得到数据库自动生成的这条记录的主键id呢?有两种方式

测试代码如下

代码语言:javascript
复制
public class MapperProxyTest { 
   
	private SqlSessionFactory sqlSessionFactory;

	@Before
	public void init() throws IOException { 
   
		InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
		sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
	}

	@Test
	public void test() { 
   
		SqlSession sqlSession = sqlSessionFactory.openSession();
		StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
		Student student = new Student(-1, "Podman", 130, 15, 0);
		mapper.insert(student);
		sqlSession.commit();
		System.out.println(student.getId());
	}
}

结果如下

image-20200526204957207
image-20200526204957207

批量查询

主要是动态SQL标签的使用,注意如果parameterTypeList的话,则在标签体内引用这个List,只能用变量名list,如果parameterType是数组,则只能用变量名array

代码语言:javascript
复制
<select id="batchFind" resultType="student" parameterType="java.util.List">
        SELECT * FROM student
        <where>
            <if test="list != null and list.size() > 0">
                AND id in
                <foreach collection="list" item="id" open="(" separator="," close=")">
                    #{id}
                </foreach>
            </if>
        </where>
</select>
代码语言:javascript
复制
	@Test
	public void testBatchQuery() { 
   
		SqlSession sqlSession = sqlSessionFactory.openSession();
		StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
		List<Student> students = mapper.batchFind(Arrays.asList(1, 2, 3, 7, 9));
		students.forEach(System.out::println);
	}

结果

image-20200526210641300
image-20200526210641300

动态SQL

可以根据具体的参数条件,来对SQL语句进行动态拼接。

比如在以前的开发中,由于不确定查询参数是否存在,许多人会使用类似于where 1 = 1 来作为前缀,然后后面用AND 拼接要查询的参数,这样,就算要查询的参数为空,也能够正确执行查询,如果不加1 = 1,则如果查询参数为空,SQL语句就会变成SELECT * FROM student where,SQL不合法。

mybatis里的动态标签主要有

缓存

  • 一级缓存 默认开启,同一个SqlSesion级别共享的缓存,在一个SqlSession的生命周期内,执行2次相同的SQL查询,则第二次SQL查询会直接取缓存的数据,而不走数据库,当然,若第一次和第二次相同的SQL查询之间,执行了DML(INSERT/UPDATE/DELETE),则一级缓存会被清空,第二次查询相同SQL仍然会走数据库 一级缓存在下面情况会被清除
    • 在同一个SqlSession下执行增删改操作时(不必提交),会清除一级缓存
    • SqlSession提交或关闭时(关闭时会自动提交),会清除一级缓存
    • 对mapper.xml中的某个CRUD标签,设置属性flushCache=true,这样会导致该MappedStatement的一级缓存,二级缓存都失效(一个CRUD标签在mybatis中会被封装成一个MappedStatement)
    • 在全局配置文件中设置 <setting name="localCacheScope" value="STATEMENT"/>,这样会使一级缓存失效,二级缓存不受影响
  • 二级缓存 默认关闭,可通过全局配置文件中的<settings name="cacheEnabled" value="true"/>开启二级缓存总开关,然后在某个具体的mapper.xml中增加<cache />,即开启了该mapper.xml的二级缓存。二级缓存是mapper级别的缓存,粒度比一级缓存大,多个SqlSession可以共享同一个mapper的二级缓存。注意开启二级缓存后,SqlSession需要提交,查询的数据才会被刷新到二级缓存当中

缓存的详细分析可以参考我之前的文章 => 极简mybatis缓存

关联查询

使用<resultMap> 标签以及<association><collection> 子标签,进行关联查询,比较简单,不多说

延迟加载

延迟加载是结合关联查询进行应用的。也就是说,只在<association><collection> 标签上起作用

对于关联查询,若不采用延迟加载策略,而是一次性将关联的从信息都查询出来,则在主信息比较多的情况下,会产生N+1问题,导致性能降低。比如用户信息和订单信息是一对多的关系,在查询用户信息时,设置了关联查询订单信息,如不采用延迟加载策略,假设共有100个用户,则我们查这100个用户的基本信息只需要一次SQL查询

代码语言:javascript
复制
select * from user;

若开启了关联查询,且不是延迟加载,则对于这100个用户,会发出100条SQL去查用户对应的订单信息,这样会造成不必要的性能开销(其实我认为称之为1+N问题更为合适)

代码语言:javascript
复制
select * from orders where u_id = 1;
select * from orders where u_id = 2;
....
select * from orders where u_id = 100;

当我们可能只关心id=3的用户的订单信息,则很多的关联信息是无用的,于是,采用延迟加载策略,可以按需加载从信息,在需要某个主信息对应的从信息时,再发送SQL去执行查询,而不是一次性全部查出来,这样能很好的提升性能。

另外,针对N+1问题,除了采用延迟加载的策略按需进行关联查询。如果在某些场景下,确实需要查询所有主信息关联的从信息。在上面的例子中,就是如果确实需要把这100个用户关联的订单信息全部查询出来,那怎么办呢?这里提供2个解决思路。

1是采用连接查询,只使用1条SQL即可,如下

代码语言:javascript
复制
select * from user as u left join orders as o on u.id = o.u_id;

但使用连接查询查出来的结果是两表的笛卡尔积,还需要自行进行数据的分组处理

2是使用两个步骤来完成,先执行一条SQL,查出全部的用户信息,并把用户的id放在一个集合中,然后第二条SQL采用IN关键字查询即可。这种方式也可以简化为子查询,如下

代码语言:javascript
复制
select * from orders where u_id in (select id from user);

现在说回来,mybatis的延迟加载默认是关闭的,可以通过全局配置文件中的<setting name="lazyLoadingEnabled" value="true"/>来开启,开启后,所有的SELECT查询,若有关联对象,都会采用延迟加载的策略。当然,也可以对指定的某个CRUD标签单独禁用延迟加载策略,通过设置SELECT标签中的fetchType=eager,则可以关闭该标签的延迟加载。

(还有一个侵入式延迟加载的概念,在配置文件中通过<setting name="aggressiveLazyLoading" value="true">来开启,大概是说,访问主对象中的主信息时,就会触发延迟加载,将从信息查询上来,这其实并不是真正意义的延迟加载,真正意义上的延迟加载应该是访问主对象中的从信息时,才触发延迟加载,去加载从信息,侵入式延迟加载默认是关闭的,一般情况下可以不用管他)

注意,延迟加载在关联查询的场景下才有意义。需要配合<resultMap>标签下的<association><collecction> 标签使用

代码语言:javascript
复制
<!-- StudentMapper.xml -->
<resultMap id="studentExt" type="com.yogurt.po.StudentExt">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="score" column="score"/>
        <result property="age" column="age"/>
        <result property="gender" column="gender"/>
		<!-- 当延迟加载总开关开启时,resultMap下的association和collection标签中,若通过select属性指定嵌套查询的SQL,则其fetchType默认是lazy的,当在延迟加载总开关开启时,需要对个别的关联查询禁用延迟加载时,才有必要配置fetchType = eager -->
    	<!-- column用于指定用于关联查询的列 property用于指定要封装到StudentExt中的哪个属性 javaType用于指定关联查询得到的对象 select用于指定关联查询时,调用的是哪一个DQL -->
        <association property="clazz" javaType="com.yogurt.po.Clazz" column="class_id" select="com.yogurt.mapper.ClassMapper.findById" fetchType="lazy"/>

    </resultMap>

    <select id="findLazy" parameterType="string" resultMap="studentExt">
        SELECT * FROM student WHERE name like '%${value}%';
    </select>
代码语言:javascript
复制
<!-- com.yogurt.mapper.ClassMapper -->
<select id="findById" parameterType="int" resultType="com.yogurt.po.Clazz">
        SELECT * FROM class WHERE id = #{id}
</select>
代码语言:javascript
复制
/** 用于封装关联查询的对象 **/
public class StudentExt{ 
   

	private Integer id;

	private String name;

	private Integer score;

	private Integer age;

	private Integer gender;

    /** 关联对象 **/
	private Clazz clazz;
    
   	//getter/setter
}

逆向工程

mybatis官方提供了mapper自动生成工具mybatis-generator-core来针对单表,生成PO类,以及Mapper接口和mapper.xml映射文件。针对单表,可以不需要再手动编写xml配置文件和mapper接口文件了,非常方便。美中不足的是它不支持生成关联查询。一般做关联查询,就自己单独写SQL就好了。

基于IDEA的mybatis逆向工程操作步骤如下

执行日志如下

image-20200527203940817
image-20200527203940817

生成的文件如下

image-20200527204043910
image-20200527204043910

能看到mybatis-generator除了给我们生成了基本的PO类(上图的Student和Product),还额外生成了Example类。Example类是为了方便执行SQL时传递查询条件的。使用的示例如下

代码语言:javascript
复制
public class GeneratorTest { 
   

	private SqlSessionFactory sqlSessionFactory;

	@Before
	public void init() throws IOException { 
   
		InputStream resourceAsStream = Resources.getResourceAsStream("mysql8-config.xml");
		sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
	}

	@Test
	public void test() { 
   
		SqlSession sqlSession = sqlSessionFactory.openSession();
		StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
		StudentExample example = new StudentExample();
		StudentExample.Criteria criteria = example.createCriteria();
		criteria.andNameLike("%o%");
		List<Student> students = mapper.selectByExample(example);
		students.forEach(System.out::println);
	}
}

结果如下

image-20200527211115356
image-20200527211115356

PageHelper分页插件

使用该插件,快速实现查询结果的分页,使用步骤如下

Mybatis Plus

mybatis虽然非常方便,但也需要编写大量的SQL语句,于是mybatis plus就应运而生了。它是一个mybatis增强工具,为了简化开发,提高效率。搭配Spring-Boot食用简直不要太爽。

可以参考我的这篇文章 mybatis-plus一发入魂 ,或者mybatis-plus官网,以及慕课网的入门教程进阶教程

(完)

注:该文是一篇较为全面详细的笔记,内容篇幅很长。当对mybatis的使用较为熟练后,可以查看这篇极为简短的 mybatis精髓总结,从整体架构和源码层面上把握mybatis。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/158947.html原文链接:https://javaforall.cn

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • Mybatis
    • 概述
      • 快速入门
        • 原生开发示例
        • 基于Mapper代理的示例
        • 基于注解的示例
      • 应用场景
        • 主键返回
        • 批量查询
        • 动态SQL
        • 缓存
        • 关联查询
        • 延迟加载
        • 逆向工程
        • PageHelper分页插件
        • Mybatis Plus
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档