02-mybatis

一 Mybatis介绍

1 什么是框架

框架即一个半成品软件。开发者从头开发一个软件需要花费大量精力,于是有一些项目组开发出半成品软件,开发者在这些软件的基础上进行开发,这样的软件就称之为框架。

如果将开发完成的软件比作是一套已经装修完毕的新房,框架就好比是一套已经修建好的毛坯房。用户直接购买毛坯房,保证建筑质量和户型合理的同时可以进行风格的自由装修。

使用框架开发的好处:

  • 省去大量的代码编写、减少开发时间、降低开发难度。

  • 限制程序员必须使用框架规范开发,增强代码的规范性,降低程序员之间沟通及日后维护的成本。

  • 将程序员的注意力从技术中抽离出来,更集中在业务层面。

使用框架就好比和世界上最优秀的软件工程师共同完成一个项目,并且他们完成的还是基础、全局的工作。

2 什么是ORM框架

在这里插入图片描述

ORM(Object Relationl Mapping),对象关系映射,即在数据库和对象之间作映射处理。

之前我们使用JDBC操作数据库,必须手动进行数据库和对象间的数据转换。

// 新增方法,将对象转为sql语句字段
public void AddUser(User user) throws Exception {
	Class.forName("com.mysql.jdbc.Driver");
	Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","root", "root");
	String sql = "INSERT INTO user values (null,?,?,?,?)";
    PreparedStatement preparedStatement = connection.prepareStatement(sql);  
	preparedStatement.setString(1,user.getName()); 
	preparedStatement.setInt(2,user.getAge()); 
	preparedStatement.setString(3,user.getAddress());  
	preparedStatement.setString(4,user.getSex());
	preparedStatement.executeUpdate();
	// 省略资源关闭...
}

// 查询方法,将数据库结果集转为对象
public List<User> findAllUser() throws Exception {
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","root", "root");
	PreparedStatement preparedStatement = connection.prepareStatement("select * from
user");
	ResultSet resultSet = preparedStatement.executeQuery();
   //遍历查询结果集
    List<User> users = new ArrayList<>();
    while(resultSet.next()){
        // 拿到每一列数据
        int id = resultSet.getInt("id");
        String name = resultSet.getString("name");
        int age = resultSet.getInt("age");
        String address =resultSet.getString("address");
        String sex = resultSet.getString("sex");
        // 将数据封装到对象中
        User user = new User();
        user.setId(id);
        user.setName(name);
        user.setAge(age);
        user.setAddress(address);
        user.setSex(sex);
        users.add(user);
   }  
  // 省略资源关闭...
   return users;
 }                                                                                       

这段代码中,数据库数据与对象数据的转换代码繁琐、无技术含量。而使用ORM框架代替JDBC后,框架可以帮助程序员自动进行转换,只要像平时一样操作对象,ORM框架就会根据映射完成对数据库的操作,极大的增强了开发效率。

3 什么是MyBatis

MyBatis是一个半自动的ORM框架,其本质是对JDBC的封装。使用MyBatis不需要写JDBC代码,但需要程序员编写SQL语句。之前是apache的一个开源项目iBatis,2010年改名为MyBatis。

补充:
Hibernate也是一款持久层ORM框架,多年前的市场占有率很高,但近年来市场占有率越来越低。


MyBatis与Hibernate的比较:

	1.MyBatis是一个半自动的ORM框架,需要手写SQL语句。
	
	2.Hibernate是一个全自动的ORM框架,不需要手写SQL语句。
	
	3.使用MyBatis的开发量要大于Hibernate。
	

为什么Hibernate市场占有率越来越低:

	1.对于新手学习Hibernate时间成本比MyBatis大很多,MyBatis上手很快。
	
	2.Hibernate不需要写SQL语句是因为框架来生成SQL语句。对于复杂查询,开发者很难控制生成的SQL语句,这就导致SQL调优很难进行。
	
	3.之前的项目功能简单,数据量小,所以使用Hibernate可以快速完成开发。而近年来项目的数据量越来越大,而互联网项目对查询速度要求也很高,这就要求我们一定要精细化的调整SQL语句。此时灵活性更强,手动编写SQL语句的MyBatis慢慢代替了Hibernate使用。

	4.在高并发、大数据、高性能、高响应的互联网项目中,MyBatis是首选的持久框架。而对于对性能要求不高的比如内部管理系统等可以使用Hibernate。

二 Mybatis入门案例

1 环境搭建

  • 将SQL文件导入数据库
/*
SQLyog Ultimate v12.09 (64 bit)
MySQL - 5.7.35-log : Database - mybatis
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`mybatis` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `mybatis`;

/*Table structure for table `classes` */

DROP TABLE IF EXISTS `classes`;

CREATE TABLE `classes` (
  `cid` int(11) NOT NULL AUTO_INCREMENT,
  `className` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`cid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

/*Data for the table `classes` */

insert  into `classes`(`cid`,`className`) values (1,'三年一班'),(2,'三年二班');

/*Table structure for table `classes_teacher` */

DROP TABLE IF EXISTS `classes_teacher`;

CREATE TABLE `classes_teacher` (
  `cid` int(11) NOT NULL,
  `tid` int(11) NOT NULL,
  PRIMARY KEY (`cid`,`tid`) USING BTREE,
  KEY `tid` (`tid`) USING BTREE,
  CONSTRAINT `classes_teacher_ibfk_1` FOREIGN KEY (`cid`) REFERENCES `classes` (`cid`),
  CONSTRAINT `classes_teacher_ibfk_2` FOREIGN KEY (`tid`) REFERENCES `teacher` (`tid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

/*Data for the table `classes_teacher` */

insert  into `classes_teacher`(`cid`,`tid`) values (1,1),(1,2),(2,2),(2,3);

/*Table structure for table `student` */

DROP TABLE IF EXISTS `student`;

CREATE TABLE `student` (
  `sid` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `sex` varchar(255) DEFAULT NULL,
  `classId` int(11) DEFAULT NULL,
  PRIMARY KEY (`sid`) USING BTREE,
  KEY `classId` (`classId`) USING BTREE,
  CONSTRAINT `student_ibfk_1` FOREIGN KEY (`classId`) REFERENCES `classes` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

/*Data for the table `student` */

insert  into `student`(`sid`,`name`,`age`,`sex`,`classId`) values (1,'张三',10,'男',1),(2,'李四',10,'女',1),(3,'尚学堂',10,'男',2),(4,'百战',11,'男',2),(5,'王五',10,'男',2);

/*Table structure for table `teacher` */

DROP TABLE IF EXISTS `teacher`;

CREATE TABLE `teacher` (
  `tid` int(11) NOT NULL AUTO_INCREMENT,
  `tname` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`tid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

/*Data for the table `teacher` */

insert  into `teacher`(`tid`,`tname`) values (1,'王老师'),(2,'李老师'),(3,'张老师');

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `sex` varchar(255) DEFAULT NULL,
  `address` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

/*Data for the table `user` */

insert  into `user`(`id`,`username`,`sex`,`address`) values (1,'北京百战','男','北京'),(2,'上海百战','男','上海'),(3,'广州百战','女','广州'),(4,'北京尚学堂','男','北京'),(5,'太原尚学堂','男','太原'),(6,'西安尚学堂','男','西安');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

  • 创建maven工程,引入依赖
    <dependencies>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>
        <!-- log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <!--  junit      -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>

    </dependencies>
  • 创建mybatis核心配置文件SqlMapConfig.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="mysql">
        <environment id="mysql">
            <!-- 事务类型   -->
            <transactionManager type="JDBC"></transactionManager>
            <!--  数据源    -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 注册映射文件 -->
    <mappers>
        <mapper resource="com/itbaizhan/mapper/UserMapper.xml"></mapper>
    </mappers>

</configuration>
  • 将log4j.properties文件放入resources中,让控制台打印SQL语句。
# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE            debug   info   warn error fatal
log4j.rootCategory=debug, CONSOLE

# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
#log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=[%d{MM/dd HH:mm:ss}] %-6r [%15.15t] %-5p %30.30c %x - %m\n


  • 创建实体类
public class User {

    private int id;
    private String username;
    private String sex;
    private String address;
    // 省略getter/setter/构造方法/toString方法
}

2 创建持久层接口和映射文件

  • 在java目录创建持久层接口
public interface UserMapper {
    List<User> findAll();
}

  • 在resource目录创建映射文件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.itbaizhan.mapper.UserMapper">
    <select id="findAll" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user;
    </select>
</mapper>
  • 将映射文件配置到mybatis核心配置文件SqlMapConfig.xml中
    <!-- 注册映射文件 -->
    <mappers>
        <mapper resource="com/itbaizhan/mapper/UserMapper.xml"></mapper>
    </mappers>

映射文件注意事项:

  1. 映射文件要和接口名称相同。

  2. 映射文件要和接口的目录结构相同。

在这里插入图片描述

  1. 映射文件中namespace属性要写接口的全类名。

  2. 映射文件中标签的id属性是接口方法的方法名。

  3. 映射文件中标签的resultType属性是接口方法的返回值类型。

  4. 映射文件中标签的parameterType属性是接口方法的参数类型。

  5. 映射文件中resultType、parameterType属性要写全类名,如果是集合类型,则写其泛型的全类名。

3 测试持久层接口方法

package com.itbaizhan.mapper;

import com.itbaizhan.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 org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class UserMapperTest {

    @Test
    public void testFindAll() throws Exception {
        // (1) 读取核心配置文件
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // (2) 创建SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // (3) SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        // (4) SqlSessionFactory对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // (5) SqlSession对象获取代理对象
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        // (6) 代理对象执行方法
        List<User> users = userMapper.findAll();
        users.forEach(System.out::println);
        // (7) 释放资源
        sqlSession.close();
        inputStream.close();
    }

}

4 MyBatis核心对象及工作流程

在这里插入图片描述

MyBatis核心对象

  • SqlSessionFactoryBuilder

SqlSession工厂构建者对象,使用构造者模式创建SqlSession工厂对象。

  • SqlSessionFactory

SqlSession工厂,使用工厂模式创建SqlSession对象。

  • SqlSession

该对象可以直接操作数据库,也可以使用动态代理模式创建持久层接口的代理对象操作数据库。

  • Mapper

持久层接口的代理对象,他具体实现了持久层接口,用来操作数据库。

MyBatis工作流程

  1. 创建SqlSessionFactoryBuilder对象

  2. SqlSessionFactoryBuilder对象构建了SqlSessionFactory对象:构造者模式

  3. SqlSessionFactory对象生产了SqlSession对象:工厂模式

  4. SqlSession对象创建了持久层接口的代理对象:动态代理模式

  5. 代理对象操作数据库

5 使用SqlSession操作数据库

除了代理对象能够操作数据库,SqlSession也能操作数据库。只是这种方式在开发中使用的较少,接下来我们使用SqlSession操作数据库:

    @Test
    public void testFindAll2() throws Exception {
        // (1) 读取核心配置文件
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // (2) 创建SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // (3) SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        // (4) SqlSessionFactory对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // (5) SqlSession直接操作数据库
        List<User> users = sqlSession.selectList("com.itbaizhan.mapper.UserMapper.findAll");
        users.forEach(System.out::println);
        // (6) 释放资源
        sqlSession.close();
        inputStream.close();
    }

6 Mapper动态代理原理

接下来我们通过源码,了解MyBatis的Mapper对象究竟是怎么生成的,他又是如何代理接口的方法。

获取代理对象

点开测试类的 getMapper方法,查看该方法最终调用了什么方法。

在这里插入图片描述

当看到Proxy.newProxyInstance时,可以确定 getMapper 方法最终调用的是JDK动态代理方法,且使用MapperProxy类定义代理方式

查看代理方式

点开MapperProxy类,查看invoke方法,查看代理对象是如何工作的。

在这里插入图片描述

可以看到,MapperProxy调用了MapperMethod的execute方法定义了代理方式,且底层调用的是SqlSession的方法,根据映射文件标签不同调用不同的SqlSession方法。

结论:

  • SqlSession的getMapper方法,最终是调用的是JDK动态代理方法,生成一个代理对象,类型就是传入的接口类型。

  • MapperProxy对象通过调用MapperMethod的execute方法定义了代理方式,该方法的底层调用的SqlSession的方法。

三 MyBatis增删改查

1 MyBatis新增

新增用户

  • 持久层接口添加方法
 void add(User user);
  • 映射文件添加标签
   <insert id="add" parameterType="com.itbaizhan.pojo.User">
        insert into user(username, sex, address)
        values (#{username}, #{sex}, #{address});
    </insert>
  • 编写测试方法
@Test
public void testAdd() throws Exception {
 		// (1) 读取核心配置文件
        inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // (2) 创建SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // (3) SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        // (4) SqlSessionFactory对象获取SqlSession对象
        sqlSession = sqlSessionFactory.openSession();
        // (5) SqlSession对象获取代理对象
        userMapper = sqlSession.getMapper(UserMapper.class);
    	User user = new User("程序员", "男", "上海");
    	userMapper.add(user);
    	// 提交事务
    	sqlSession.commit();
    	// 关闭资源
    	sqlSession.close();
    	inputStream.close();
}

注意

  1. 当接口方法的参数类型为POJO类型时,SQL语句中绑定参数时使用 #{POJO的属性名} 即可。

  2. MyBatis事务默认手动提交,所以在执行完增删改方法后,需要手动调用SqlSession对象的事务提交方法,否则数据库将不发生改变。

2 MyBatis修改

优化测试类

我们发现MyBatis的测试方法在操作数据库前都需要获取代理对象,操作数据库后都需要释放资源,可以利用Junit的前置后置方法,优化测试类代码。

    InputStream inputStream = null;
    SqlSession sqlSession = null;
    UserMapper userMapper = null;

    @Before
    public void before() throws IOException {
        // (1) 读取核心配置文件
        inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // (2) 创建SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // (3) SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        // (4) SqlSessionFactory对象获取SqlSession对象
        sqlSession = sqlSessionFactory.openSession();
        // (5) SqlSession对象获取代理对象
        userMapper = sqlSession.getMapper(UserMapper.class);
    }

    @After
    public void after() throws IOException {
        // 释放资源
        sqlSession.close();
        inputStream.close();
    }

这样Junit就会自动执行获取代理对象和释放资源的方法。

修改用户

  • 持久层接口添加方法
 void update(User user);
  • 映射文件添加标签
    <update id="update" parameterType="com.itbaizhan.pojo.User">
        update user
        set username=#{username},
            sex=#{sex},
            address=#{address}
        where id = #{id};
    </update>
  • 编写测试方法
    @Test
    public void testUpdate() {
        User user = new User(9, "程序员2", "女", "昆明");
        userMapper.update(user);
        sqlSession.commit();
    }

3 MyBatis删除

删除用户

  • 持久层接口添加方法
 void delete(int id);
  • 映射文件添加标签
   <delete id="delete" parameterType="int">
        delete
        from user
        where id = #{id};
    </delete>

注意:当方法的参数类型是简单数据类型时,#{}中可以写任意名称,

简单数据类型:基本数据类型、字符串等

  • 编写测试方法
    @Test
    public void testDelete() {
        userMapper.delete(8);
        sqlSession.commit();
    }

4 MyBatis根据id查询

根据ID查询用户

  • 持久层接口添加方法
  User findById(int id);
  • 映射文件添加标签
   <select id="findById" parameterType="int" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user
        where id = #{id};
    </select>
  • 编写测试方法
    @Test
    public void testFindById() {
        User user = userMapper.findById(1);
        System.out.println(user);
    }

5 MyBatis模糊查询

使用#定义参数

  • 持久层接口添加方法
 List<User> findByNameLike1(String username);
  • 映射文件添加标签
   <select id="findByNameLike1" parameterType="string" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user
        where username like #{username};
    </select>
  • 编写测试方法
    @Test
    public void testFindByNameLike1() {
        List<User> users = userMapper.findByNameLike1("%学%");
        users.forEach(System.out::println);
    }

我们看到在映射文件中,parameterType的值为string 而没有写java.lang.String ,这是为什么呢?

参数/返回值类型为基本数据类型/包装类/String等类型时,我们可以写全类名,也可以写别名。

在这里插入图片描述

使用$定义参数

模糊查询如果不想在调用方法时参数加%,可以使用拼接参数的方式设置Sql:

   <select id="findByNameLike2" parameterType="string" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user
        where username like '%${username}%';
    </select>

测试方法写法如下:

    @Test
    public void testFindByNameLike2() {
        List<User> users = userMapper.findByNameLike2("学");
        users.forEach(System.out::println);
    }

#和$的区别:

  1. #表示sql模板的占位符,$表示将字符串拼接到sql模板中。
  2. #可以防止sql注入,一般能用#就不用$。

使用定义参数

如果使用 # 还不想在调用方法的参数中添加 % ,可以使用 <bind> , <bind> 允许我们在 Sql语句以外创建一个变量,并可以将其绑定到当前的Sql语句中。用法如下:

	<select id="findByNameLike3" parameterType="string" resultType="com.itbaizhan.pojo.User">
        <bind name="likeName" value="'%'+username+'%'"/>
        select id, username, sex, address
        from user
        where username like #{likeName};
    </select>

测试方法写法如下:

    @Test
    public void testFindByNameLike3() {
        List<User> users = userMapper.findByNameLike3("学");
        users.forEach(System.out::println);
    }

6 MyBatis分页查询

在这里插入图片描述

分页查询时,Sql语句使用limit关键字,需要传入开始索引和每页条数两个参数。MyBatis的多参数处理有以下方式:

顺序传参

Sql中的参数使用arg0,arg1…或param1,param2…表示参数的顺序。此方法可读性较低,在开发中不建议使用。

  • 持久层接口方法

    /**
     * 分页查询
     *
     * @param startIndex 开始索引     (跳过的条数)
     * @param pageSize   每页条数     (每页展示的条数)
     * @return
     */
    //顺序传参
    List<User> findPage1(int startIndex, int pageSize);
  • 映射文件
    <select id="findPage1" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user limit #{arg0},#{arg1};
    </select>
  • 测试类
    @Test
    public void testFindPage1() {
        List<User> page1 = userMapper.findPage1(0, 2);
        List<User> page2 = userMapper.findPage1(2, 2);
        System.out.println(page1);
        System.out.println(page2);
    }

@Param传参

在接口方法的参数列表中通过@Param定义参数名称,在Sql语句中通过注解中所定义的参数名称指定参数位置。此方式参数比较直观的,推荐使用。

  • 持久层接口方法
 	//@Param传参
    List<User> findPage2(@Param("startIndex") int startIndex, @Param("pageSize") int pageSize);
  • 映射文件
    <select id="findPage2" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user limit #{startIndex},#{pageSize};
    </select>
  • 测试类
    @Test
    public void testFindPage2() {
        List<User> page1 = userMapper.findPage2(0, 2);
        List<User> page2 = userMapper.findPage2(2, 2);
        System.out.println(page1);
        System.out.println(page2);
    }

POJO传参

自定义POJO类,该类的属性就是要传递的参数,在SQL语句中绑定参数时使用POJO的属性名作为参数名即可。此方式推荐使用。

  • 自定义POJO
public class PageQuery {
    private int startIndex;
    private int pageSize;
 // 省略getter/setter/构造方法
}
  • 持久层接口方法
    //POJO传参
    List<User> findPage3(PageQuery pageQuery);
  • 映射文件
    <select id="findPage3" parameterType="com.itbaizhan.pojo.PageQuery" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user limit #{startIndex},#{pageSize};
    </select>
  • 测试类
    @Test
    public void testFindPage3() {
        PageQuery pageQuery1 = new PageQuery(0, 2);
        PageQuery pageQuery2 = new PageQuery(2, 2);
        List<User> page1 = userMapper.findPage3(pageQuery1);
        List<User> page2 = userMapper.findPage3(pageQuery2);
        System.out.println(page1);
        System.out.println(page2);
    }

Map传参

如果不想自定义POJO,可以使用Map作为传递参数的载体,在SQL语句中绑定参数时使用Map的Key作为参数名即可。此方法推荐使用。

  • 持久层接口方法
    //Map传参
    List<User> findPage4(Map<String, Object> params);
  • 映射文件
    <select id="findPage4" parameterType="map" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user limit #{startIndex},#{pageSize};
    </select>
  • 测试类
    @Test
    public void testFindPage4() {
        HashMap<String, Object> params = new HashMap<>();
        params.put("startIndex", 0);
        params.put("pageSize", 4);
        List<User> users = userMapper.findPage4(params);
        System.out.println(users);
    }

7 MyBatis聚合查询

查询用户总数

  • 持久层接口方法
    /**
     * 查询用户总数
     *
     * @return
     */
    int findCount();
  • 映射文件
    <select id="findCount" resultType="int">
        select count(id)
        from user;
    </select>
  • 测试类
    @Test
    public void testFindCount() {
        System.out.println(userMapper.findCount());
    }

8 MyBatis主键回填

有时我们需要获取新插入数据的主键值。如果数据库中主键是自增的,这时我们就需要使用MyBatis的主键回填功能。

  • 持久层接口方法
 	//主键回填
    void add2(User user);
  • 映射文件
    <insert id="add2" parameterType="com.itbaizhan.pojo.User">
        <!-- keyProperty:主键属性名,keyColumn:主键列名,resultType:主键类型,order:执行时机 -->
        <selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
            select LAST_INSERT_ID();
        </selectKey>
        insert into user(username, sex, address)
        values (#{username}, #{sex}, #{address});
    </insert>

SELECT LAST_INSERT_ID():查询刚刚插入的记录的主键值,只适用于自增主键,且必须和insert语句一起执行。

  • 测试类
    @Test
    public void testAdd2() {
        User user = new User("三好", "男", "昆明");
        userMapper.add2(user);
        sqlSession.commit();
        System.out.println(user);
    }

四 MyBatis配置文件

1 properties

MyBatis配置文件结构:

-configuration
	-properties(属性)
		-property
	-settings(全局配置参数)
		-setting
	-plugins(插件)
		-plugin
	-typeAliases(别名)
		-typeAliase
		-package
	-environments(环境)
		-environment
			-transactionManager(事务管理)
			-dataSource(数据源)
	-mappers(映射器)
		-mapper
		-package

properties

属性值定义。properties标签中可以定义属性值,也可以引入外部配置文件。无论是内部定义还是外部引入,都可以使用${name}获取值。

例如:我们可以将数据源配置写到外部的db.properties中,再使用properties标签引入外部配置文件,这样可以做到动态配置数据源。

  • 编写db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8
jdbc.username=root
jdbc.password=123456
  • 在配置文件中引入db.properties
    <properties resource="db.properties"></properties>
    <!--  配置环境  -->
    <environments default="mysql">
        <environment id="mysql">
            <!-- 事务类型   -->
            <transactionManager type="JDBC"></transactionManager>
            <!--  数据源    -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

当然我们也可以将数据源数据通过<properties> 配置到MyBatis配置文件内,但这样做没什么意义。

    <properties>
        <property name="jdbc.driver" value="com.mysql.jdbc.Driver"/>
        <property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"/>
        <property name="jdbc.username" value="root"/>
        <property name="jdbc.password" value="123456"/>
    </properties>
    <!--  配置环境  -->
    <environments default="mysql">
        <environment id="mysql">
            <!-- 事务类型   -->
            <transactionManager type="JDBC"></transactionManager>
            <!--  数据源    -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

2 settings

<settings> 是配置MyBatis运行时的一些行为的,例如缓存、延迟加载、命名规则等一系列控制性参数。后期我们会使用该标签配置缓存和延迟加载等。

3 plugins

<plugins> 是配置MyBatis插件的。插件可以增强MyBatis功能,比如进行sql增强,打印日志,异常处理等。后期我们会使用该标签配置分页插件。

4 typeAliases

MyBatis对常用类有默认别名支持,比如java.lang.Stirng的别名为string。除此之外,我们也可以使用 <typeAliases> 设置自定义别名。

为一个类配置别名

<typeAliases>
 	<typeAlias type="全类名" alias="别名">
	</typeAlias>
</typeAliases>

此时我们即可在映射文件中使用自定义别名,如:

  • 配置文件:
    <typeAliases>
        <typeAlias type="com.itbaizhan.pojo.User" alias="User"></typeAlias>
    </typeAliases>
  • 映射文件:
    <select id="findAll" resultType="User">
        select id, username, sex, address
        from user;
    </select>

为一个所有包下的所有类配置别名

<typeAliases>
    <package name="包名"></package>
</typeAliases>

此时该包下的所有类都有了别名,别名省略包名,和类名相同。

如:

  • 配置文件:
    <typeAliases>
       <package name="com.itbaizhan.pojo"/>
    </typeAliases>
  • 映射文件:
    <select id="findAll" resultType="User">
        select id, username, sex, address
        from user;
    </select>

5 environments

<environments> 可以为MyBatis配置数据环境。

事务管理

    <!--  配置环境  -->
    <environments default="mysql">
        <environment id="mysql">
             <!-- 
				  JDBC:使用JDBC的提交和回滚 	
				  MANAGED:不做事务处理
			-->
            <transactionManager type="JDBC"></transactionManager>
        </environment>
    </environments>

连接池

   <!--  配置环境  -->
    <environments default="mysql">
        <environment id="mysql">
            <!-- 事务类型   -->
            <transactionManager type="JDBC"></transactionManager>
            <!--  数据源    -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

dataSource的type属性:

  1. POOLED:使用连接池管理连接,使用MyBatis自带的连接池。

  2. UNPOOLED:不使用连接池,直接由JDBC连接。

  3. JNDI:由JAVAEE服务器管理连接,如果使用Tomcat作为服务器则使用Tomcat自带的连接池管理。

6 mappers

<mappers> 用于注册映射文件或持久层接口,只有注册的映射文件才能使用,共有四种方式都可以完成注册:

  • 使用相对路径注册映射文件
    <!-- 注册映射文件 -->
    <mappers>
        <mapper resource="com/itbaizhan/mapper/UserMapper.xml"></mapper>
    </mappers>
  • 使用绝对路径注册映射文件
    <mappers>
       <mapper url="file:///F:\001-after-end\code\mybatisdemo\src\main\resources\com\itbaizhan\mapper\UserMapper.xml"></mapper>
    </mappers>
  • 注册持久层接口
    <mappers>
        <mapper class="com.itbaizhan.mapper.UserMapper"></mapper>
    </mappers>
  • 注册一个包下的所有持久层接口
    <mappers>
        <package name="com.itbaizhan.mapper"/>
    </mappers>

五 MyBatis映射文件

1 resultMap

MyBatis映射文件中除了 <insert> 、 <delete> 、 <update> 、 <select> 外,还有一些标签可以使用:

resultMap

标签的作用的自定义映射关系。

MyBatis可以将数据库结果集封装到对象中,是因为结果集的列名和对象属性名相同:

在这里插入图片描述

当POJO属性名和数据库列名不一致时,MyBatis无法自动完成映射关系。如:

在这里插入图片描述

此时有两种解决方案:

  • Sql语句的查询字段起与POJO属性相同的别名。
    <select id="findAll" resultType="com.itbaizhan.pojo.Teacher">
        select tid as id, tname as teacherName
        from teacher;
    </select>
  • 自定义映射关系

在映射文件中,使用<resultMap> 自定义映射关系:

    <!-- id:自定义映射名 type:自定义映射的对象类型 -->
    <resultMap id="teacherMap" type="com.itbaizhan.pojo.Teacher">
        <!-- id定义主键列    property:POJO属性名    column:数据库列名 -->
        <id property="id" column="tid"></id>
        <!-- result定义普通列    property:POJO属性名    column:数据库列名 -->
        <result property="teacherName" column="tname"></result>
    </resultMap>

在 <select> 标签中,使用 resultMap 属性代替 resultType 属性,使用自定义映射关系。

    <select id="findAll" resultMap="teacherMap">
        select tid, tname
        from teacher;
    </select>

2 sql、include

<sql> 用来定义可重用的Sql片段,通过 <include> 引入该片段。如:Sql语句的查询字段起与POJO属性相同的别名,该Sql片段就可以重用。

   <sql id="selectAllField">
        select tid as id, tname as teacherName
    </sql>

    <select id="findAll2" resultType="com.itbaizhan.pojo.Teacher">
        <include refid="selectAllField"></include>
        from teacher;
    </select>

    <select id="findById" resultType="com.itbaizhan.pojo.Teacher">
        <include refid="selectAllField"></include>
        from teacher where tid=#{id};
    </select>

3 特殊字符处理

在Mybatis映射文件中尽量不要使用一些特殊字符,如: < , > 等。我们可以使用符号的实体来表示:

在这里插入图片描述

如:

<select id="findById2"
resultType="com.itbaizhan.pojo.Teacher">
    <include refid="selectAllField">
</include>
   from teacher where tid &gt; #{id}
</select>

六 MyBatis动态Sql

1 if

在这里插入图片描述

一个查询的方法的Sql语句不一定是固定的。比如电商网站的查询商品,用户使用不同条件查询,Sql语句就会添加不同的查询条件。此时就需要在方法中使用动态Sql语句。

<if>

<if> 标签内的Sql片段在满足条件后才会添加,用法为: <if test=“条件”> 。例如:根据不同条件查询用户:

  • 持久层接口添加方法
    // 用户通用查询
    List<User> findByCondition(User user);
  • 映射文件添加标签
    <select id="findByCondition" parameterType="com.itbaizhan.pojo.User" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user
        where 1=1
        <if test="username != null and username.length() != 0">
            and username like #{username}
        </if>
        <if test="sex != null and sex.length() != 0">
            and sex = #{sex}
        </if>
        <if test="address != null and address.length() != 0">
            and address = #{address}
        </if>
    </select>
  • 编写测试方法
    @Test
    public void testFindByCondition(){
        User user = new User();
        user.setSex("男");
        user.setAddress("昆明");
        List<User> users = userMapper.findByCondition(user);
        System.out.println(users);
    }

注意

  1. if中的条件不能使用&&/||,而应该使用and/or

  2. if中的条件可以直接通过属性名获取参数POJO的属性值,并且该值可以调用方法。

  3. where后为什么要加1=1?

​ 任意条件都可能拼接到Sql中。如果有多个条件,从第二个条件开始前都需要加And关键字。加上1=1这个永久成立的条件,就不需要考虑后面的条件哪个是第一个条件,后面的条件前都加And关键字即可。

2 where

<where> 可以代替sql中的where 1=1 和第一个and,更符合程序员的开发习惯,使用 <where> 后的映射文件如下:

    <select id="findByCondition" parameterType="com.itbaizhan.pojo.User" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user
        <where>
            <if test="username != null and username.length() != 0">
                username like #{username}
            </if>
            <if test="sex != null and sex.length() != 0">
                and sex = #{sex}
            </if>
            <if test="address != null and address.length() != 0">
                and address = #{address}
            </if>
        </where>
    </select>

3 set

<set> 标签用在update语句中。借助 <if> ,可以只对有具体值的字段进行更新。 <set> 会自动添加set关键字,并去掉最后一个if语句中多余的逗号。

    <update id="update2" parameterType="com.itbaizhan.pojo.User">
        update user
        <set>
            <if test="username != null and username.length() != 0">
                username=#{username},
            </if>
            <if test="sex != null and sex.length() != 0">
                sex=#{sex},
            </if>
            <if test="address != null and address.length() != 0">
                address=#{address},
            </if>
        </set>
        where id=#{id}
    </update>

4 when、choose、otherwise

这些标签表示多条件分支,类似JAVA中的 switch…case 。 <choose> 类似switch , <when> 类似 case , <otherwise> 类似 default ,用法如下:

    <select id="findByCondition2" parameterType="com.itbaizhan.pojo.User" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user
        <where>
            <choose>
                <when test="username.length() &lt; 5">
                    username like #{username}
                </when>
                <when test="username.length() &lt; 10">
                    username = #{username}
                </when>
                <otherwise>
                    id =1
                </otherwise>
            </choose>
        </where>
    </select>

这段代码的含义为:用户名<5时使用模糊查询,用户名>=5并且<10时使用精确查询,否则查询id为1的用户

5 foreach

<foreach> 类似JAVA中的for循环,可以遍历集合或数组。 <foreach> 有如下属性:

  • collection:遍历的对象类型

  • open:开始的sql语句

  • close:结束的sql语句

  • separator:遍历每项间的分隔符

  • item:表示本次遍历获取的元素,遍历List、Set、数组时表示每项元素,遍历map时表示键值对的值。

  • index:遍历List、数组时表示遍历的索引,遍历map时表示键值对的键。

遍历数组

我们使用 <foreach> 遍历数组进行批量删除。

  • 持久层接口添加方法
    void deleteBatch(int[] ids);
  • 映射文件添加标签
    <delete id="deleteBatch" parameterType="int">
        delete from user
        <where>
            <foreach open="id in(" close=")" separator="," collection="array" item="id">
                #{id}
            </foreach>
        </where>
    </delete>
  • 编写测试方法
    @Test
    public void testDeleteBatch() {
        int[] ids = {8, 9};
        userMapper.deleteBatch(ids);
        sqlSession.commit();
    }

遍历Collection

<foreach> 遍历List和Set的方法是一样的,我们使用 <foreach> 遍历List进行批量添加。

  • 持久层接口添加方法
    void insertBatch(List<User> users);
  • 映射文件添加标签
    <insert id="insertBatch" parameterType="com.itbaizhan.pojo.User">
        insert into user(id, username, sex, address) values
        <foreach collection="list" item="user" separator=",">
            (null,#{user.username},#{user.sex},#{user.address})
        </foreach>
    </insert>
  • 编写测试方法
    @Test
    public void testInsertBatch() {
        User user1 = new User("张三", "女", "啥事");
        User user2 = new User("李四", "女", "辅导");
        ArrayList<User> users = new ArrayList<>();
        users.add(user1);
        users.add(user2);
        userMapper.insertBatch(users);
        sqlSession.commit();
    }

遍历Map

我们使用 <foreach> 遍历Map进行多条件查询。

  • 持久层接口添加方法
    /**
     * 多条件查询
     *
     * @param map 查询的条件键值对
     *            键:属性名
     *            值:属性值
     * @return
     */
    List<User> findUser(@Param("queryMap") Map<String, Object> map);
  • 映射文件添加标签
    <select id="findUser" parameterType="map" resultType="com.itbaizhan.pojo.User">
        select id, username, sex, address
        from user
        <where>
            <foreach collection="queryMap" separator="and" index="key" item="value">
                ${key} = #{value}
            </foreach>
        </where>
    </select>
  • 编写测试方法
    @Test
    public void testFindUser() {
        Map<String, Object> queryMap = new HashMap();
        queryMap.put("sex", "男");
        queryMap.put("address", "北京");
        List<User> users = userMapper.findUser(queryMap);
        users.forEach(System.out::println);
    }

七 MyBatis缓存

1 缓存介绍

在这里插入图片描述

缓存是内存当中一块存储数据的区域,目的是提高查询效率。

MyBatis会将查询结果存储在缓存当中,当下次执行相同的SQL时不访问数据库,而是直接从缓存中获取结果,从而减少服务器的压力。

  • 什么是缓存?

​ 存在于内存中的一块数据。

  • 缓存有什么作用?

​ 减少程序和数据库的交互,提高查询效率,降低服务器和数据库的压力。

  • 什么样的数据使用缓存?

​ 经常查询但不常改变的,改变后对结果影响不大的数据。

  • MyBatis缓存分为哪几类?

​ 一级缓存和二级缓存

  • 如何判断两次Sql是相同的?

​ 查询的Sql语句相同

​ 传递的参数值相同

​ 对结果集的要求相同

​ 预编译的模板Id相同

2 MyBatis一级缓存

在这里插入图片描述

  • MyBatis一级缓存也叫本地缓存。SqlSession对象中包含一个Executor对象,Executor对象中包含一个PerpetualCache对象,在该对象存放一级缓存数据。

  • 由于一级缓存是在SqlSession对象中,所以只有使用同一个SqlSession对象操作数据库时才能共享一级缓存。

  • MyBatis的一级缓存是默认开启的,不需要任何的配置。

测试一级缓存


    @Test
    public void testCache1() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 使用同一个SqlSession查询
        UserMapper userMapper1 = sqlSession.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession.getMapper(UserMapper.class);

        User user1 = userMapper1.findById(1);
        System.out.println(user1.hashCode());
        System.out.println("---------------------------");
        User user2 = userMapper2.findById(1);
        System.out.println(user2.hashCode());
    }

    @Test
    public void testCache2() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        // 使用不同SqlSession查询
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

        User user1 = userMapper1.findById(1);
        System.out.println(user1.hashCode());
        System.out.println("---------------------------");
        User user2 = userMapper2.findById(1);
        System.out.println(user2.hashCode());
    }


3 清空一级缓存

进行以下操作可以清空MyBatis一级缓存:

  • SqlSession 调用 close() :操作后SqlSession对象不可用,该对象的缓存数据也不可用。

  • SqlSession 调用 clearCache() / commit() :操作会清空一级缓存数据。

  • SqlSession 调用增删改方法:操作会清空一级缓存数据,因为增删改后数据库发生改变,缓存数据将不确。

    @Test
    public void testCache3() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 使用同一个SqlSession查询
        UserMapper userMapper1 = sqlSession.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession.getMapper(UserMapper.class);

        User user1 = userMapper1.findById(1);
        System.out.println(user1.hashCode());

//        sqlSession.close();
//        sqlSession.clearCache();
//        sqlSession.commit();
        userMapper1.delete(5);

        System.out.println("---------------------------");
        User user2 = userMapper2.findById(1);
        System.out.println(user2.hashCode());
    }

4 MyBatis二级缓存

在这里插入图片描述

  • MyBatis二级缓存也叫全局缓存。数据存放在SqlSessionFactory中,只要是同一个工厂对象创建的SqlSession,在进行查询时都能共享数据。一般在项目中只有一个SqlSessionFactory对象,所以二级缓存的数据是全项目共享的。

  • MyBatis一级缓存存放的是对象,二级缓存存放的是对象的数据。所以要求二级缓存存放的POJO必须是可序列化的,也就是要实现Serializable接口。

  • MyBatis二级缓存默认不开启,手动开启后数据先存放在一级缓存中,只有一级缓存数据清空后,数据才会存到二级缓存中。

    注:SqlSession 调用 clearCache() 无法将数据存到二级缓存中。

开启二级缓存

  • POJO类实现Serializable接口。
public class User implements Serializable {

    private int id;
    private String username;
    private String sex;
    private String address;
    //构造、getter等略
}
  • 在MyBatis核心配置文件添加如下设置:
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

由于cacheEnabled默认值是true,所以该设置可以省略。

  • 在映射文件添加 <cache /> 标签,该映射文件下的所有方法都支持二级缓存。

如果查询到的集合中对象过多,二级缓存只能缓存1024个对象引用。可以通过 <cache /> 标签的size属性修改该数量。

    <cache size="2048"></cache>
  • 测试二级缓存
    @Test
    public void testCache4() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();


        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

        User user1 = userMapper1.findById(1);
        System.out.println(user1);
        System.out.println(user1.hashCode());

//        让一级缓存失效
        sqlSession1.commit();
        System.out.println("---------------------------");

        User user2 = userMapper2.findById(1);
        System.out.println(user2);
        System.out.println(user2.hashCode());
    }

八 MyBatis关联查询

1 一对一关联查询

MyBatis的关联查询分为一对一关联查询和一对多关联查询。

  • 查询对象时,将关联的另一个对象查询出来,就是一对一关联查询。

  • 查询对象时,将关联的另一个对象的集合查询出来,就是一对多关联查询。

例如有学生类和班级类:

一个学生对应一个班级,也就是学生类中有一个班级属性,这就是一对一关系。

一个班级对应多个学生,也就是班级类中有一个学生集合属性,这就是一对多关系。

实体类设计如下:
注意:实体类必须有无参构造

public class Student {
    private int sid;
    private String name;
    private int age;
    private String sex;
    private Classes classes;
   // 省略getter/setter/toString
}

public class Classes {
    private int cid;
    private String className;
    private List<Student> studentList;
    // 省略getter/setter/toString
}

数据库设计如下:

在这里插入图片描述

查询学生时,将关联的一个班级对象查询出来,就是一对一关联查询。

创建持久层接口

public interface StudentMapper {
    List<Student> findAll();
}

创建映射文件

    <resultMap id="studentMap" type="com.itbaizhan.pojo.Student">
        <!--  主键列    -->
        <id property="sid" column="sid"></id>
        <!--  普通列    -->
        <result property="name" column="name"></result>
        <result property="age" column="age"></result>
        <result property="sex" column="sex"></result>
        <!-- 一对一对象列 property:属性名  column:关联列名  javaType:对象类型-->
        <association property="classes" column="classId" javaType="com.itbaizhan.pojo.Classes">
            <!-- 关联对象主键列 -->
            <id property="cid" column="cid"></id>
            <!-- 关联对象普通列 -->
            <result property="className" column="className"></result>
        </association>

    </resultMap>

    <!-- 多表查询,级联查询学生和其班级 -->
    <select id="findAll" resultMap="studentMap">
        select sid, name, age, sex, className
        from student
                 left join classes
                           on student.classId = classes.cid;
    </select>

测试一对一关联查询

    @Test
    public void testFindAll(){
        List<Student> students = studentMapper.findAll();
        System.out.println(students);
    }

2 一对多关联查询

查询班级时,将关联的学生集合查询出来,就是一对多关联查询。

创建持久层接口

public interface ClassesMapper {
    List<Classes> findAll();
}

创建映射文件

    <resultMap id="classesMap" type="com.itbaizhan.pojo.Classes">
        <id property="cid" column="cid"></id>
        <result property="className" column="className"></result>
        <!-- 集合列 property:属性名    column:关联列名    ofType:集合的泛型 -->
        <collection property="studentList" column="classId" ofType="com.itbaizhan.pojo.Student">
            <id property="sid" column="sid"></id>
            <result property="name" column="name"></result>
            <result property="age" column="age"></result>
            <result property="sex" column="sex"></result>
        </collection>
    </resultMap>
	
    <!-- 多表查询,级联查询班级和它的学生 -->
    <select id="findAll" resultMap="classesMap">
        select cid, className, sid, name, age, sex
        from classes
                 left join student
                           on classes.cid = student.classId;
    </select>

测试一对多关联查询

    @Test
    public void testFindAll() {
        List<Classes> classes = classesMapper.findAll();
        System.out.println(classes);
    }

3 多对多关联查询

MyBatis多对多关联查询本质就是两个一对多关联查询。

例如有老师类和班级类:

一个老师对应多个班级,也就是老师类中有一个班级集合属性。

一个班级对应多个老师,也就是班级类中有一个老师集合属性。

实体类设计如下:

public class Teacher {
    private int tid;
    private String tname;
    private List<Classes> classesList;
}

public class Classes {
    private int cid;
    private String className;
   private List<Teacher> teacherList;
}
// 省略getter/setter/toString

在数据库设计中,需要建立中间表,双方与中间表均为一对多关系。

在这里插入图片描述

接下来测试查询老师时,将关联的班级集合查询出来。

创建持久层接口

public interface TeacherMapper {
    List<Teacher> findAll();
}

创建映射文件

    <resultMap id="teacherMap" type="com.itbaizhan.pojo.Teacher">
        <id property="tid" column="tid"></id>
        <result property="tname" column="tname"></result>
        <collection property="classesList" column="tid" ofType="com.itbaizhan.pojo.Classes">
            <id property="cid" column="cid"></id>
            <result property="className" column="className"></result>
        </collection>
    </resultMap>

    <select id="findAll" resultMap="teacherMap">
        select teacher.tid, teacher.tname, classes.cid, classes.className
        from teacher,
             classes,
             classes_teacher
        where teacher.tid = classes_teacher.tid
          and classes.cid = classes_teacher.cid;
    </select>

测试多对多关联查询

    @Test
    public void testFindAll() throws Exception {
        List<Teacher> teachers = teacherMapper.findAll();
        System.out.println(teachers);
    }

如果想查询班级时,将关联的老师集合查询出来,只需要修改班级映射文件的Sql语句和<resultMap> 即可:

    <resultMap id="classesMap" type="com.itbaizhan.pojo.Classes">
        <id property="cid" column="cid"></id>
        <result property="className" column="className"></result>
        <collection property="teacherList" column="cid" ofType="com.itbaizhan.pojo.Teacher">
            <id property="tid" column="tid"></id>
            <result property="tname" column="tname"></result>
        </collection>
    </resultMap>

    <select id="findAll" resultMap="classesMap">
        select classes.cid, classes.className, teacher.tid, teacher.tname
        from teacher,
             classes,
             classes_teacher
        where teacher.tid = classes_teacher.tid
          and classes.cid = classes_teacher.cid;
    </select>

4 一对多分解式查询

在这里插入图片描述

在MyBatis多表查询中,使用连接查询时一个Sql语句就可以查询出所有的数据。如:

# 查询班级时关联查询出学生
select *
    from classes
    left join student
    on student.classId = classes.cid

也可以使用分解式查询,即将一个连接Sql语句分解为多条Sql语句,如:

# 查询班级时关联查询出学生
select * from classes;
select * from student where classId = 1;
select * from student where classId = 2;

这种写法也叫N+1查询。

连接查询:

  • 优点:降低查询次数,从而提高查询效率。
  • 缺点:如果查询返回的结果集较多会消耗内存空间。

N+1查询:

  • 优点:结果集分步获取,节省内存空间。

  • 缺点:由于需要执行多次查询,相比连接查询效率低。

我们以查询班级时关联查询出学生为例,使用N+1查询:

创建每个查询语句的持久层方法

public interface ClassesMapper {
    //查询所有班级
    List<Classes> findAll();

}

public interface StudentMapper {
    //根据班级id查询学生
    List<Student> findByClassId(int classId);
}

在映射文件中进行配置

    <select id="findAll" resultType="com.itbaizhan.pojo.Classes">
        select cid, className
        from classes;
    </select>


	<select id="findByClassId" parameterType="int" resultType="com.itbaizhan.pojo.Student">
        select sid, name, age, sex
        from student
        where classId = ${classId};
    </select>

修改主表映射文件中的查询方法(这里就是classesMapper)

    <!--  自定义映射关系  -->
    <resultMap id="myClassMap" type="com.itbaizhan.pojo.Classes">
        <id property="cid" column="cid"></id>
        <result property="className" column="className"></result>
        <!-- select:从表查询调用的方法   column:调用方法时传入的参数字段 -->
        <collection property="studentList" ofType="com.itbaizhan.pojo.Student"
                    select="com.itbaizhan.mapper.StudentMapper.findByClassId" column="cid">
        </collection>
    </resultMap>

    <select id="findAll" resultMap="myClassMap">
        select cid, className
        from classes;
    </select>

测试查询方法

    @Test
    public void testFindAll() {
        List<Classes> classes = classesMapper.findAll();
        System.out.println(classes);
    }

我们可以看到在控制台打印出了多条Sql语句

5 一对一分解式查询

查询学生时关联查询出班级也可以使用分解式查询,首先将查询语句分开:

select * from student;
select * from classes where cid = ?;

创建每个查询语句的持久层方法

public interface StudentMapper {
     // 查询所有学生
    List<Student> findAll();
}
public interface ClassesMapper {
    // 根据ID查询班级
    Classes findByCid(int cid);
}

在映射文件中进行配置

    <select id="findAll" resultType="com.itbaizhan.pojo.Student">
        select sid, name, age, sex, classId
        from student;
    </select>

    <select id="findByCid" parameterType="int" resultType="com.itbaizhan.pojo.Classes">
        select cid, className
        from classes
        where cid = ${cid};
    </select>

修改主表映射文件中的查询方法(这里就是studentMapper)

    <resultMap id="myStudentMap" type="com.itbaizhan.pojo.Student">
        <id property="sid" column="sid"></id>
        <result property="name" column="name"></result>
        <result property="age" column="age"></result>
        <result property="sex" column="sex"></result>
        <association property="classes" javaType="com.itbaizhan.pojo.Classes"
                     select="com.itbaizhan.mapper.ClassesMapper.findByCid" column="classId">
        </association>
    </resultMap>

    <select id="findAll" resultMap="myStudentMap">
        select sid, name, age, sex, classId
        from student;
    </select>

6 延迟加载

分解式查询又分为两种加载方式:

  • 立即加载:在查询主表时就执行所有的Sql语句。
  • 延迟加载:又叫懒加载,首先执行主表的查询语句,使用从表数据时才触发从表的查询语句。

延迟加载在获取关联数据时速度较慢,但可以节约资源,即用即取。

开启延迟加载

  • 设置所有的N+1查询都为延迟加载:
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>
  • 设置某个方法为延迟加载:

在 <association> 、 <collection> 中添加fetchType属性设置加载方式。

lazy:延迟加载;eager:立即加载。

测试延迟加载

    @Test
    public void testFindAll() {
        List<Classes> classes = classesMapper.findAll();
        System.out.println(classes);
        System.out.println("----------------------");
        System.out.println(classes.get(0).getStudentList());
    }

由于打印对象时会调用对象的 toString 方法, toString 方法默认会触发延迟加载的查询,所以我们无法测试出延迟加载的效果。

我们在配置文件设置lazyLoadTriggerMethods属性,该属性指定对象的什么方法触发延迟加载,设置为空字符串即可

<settings>
	<setting name="lazyLoadTriggerMethods" value=""/>
</settings>

一般情况下,一对多查询使用延迟加载,一对一查询使用立即加载。

九 MyBatis注解开发

1 环境搭建

MyBatis可以使用注解替代映射文件。映射文件的作用就是定义Sql语句,可以在持久层接口上使用

@Select/@Delete/@Insert/@Update定义Sql语句,这样就不需要使用映射文件了。

  • 创建maven工程,引入依赖

  • 创建mybatis核心配置文件SqlMapConfig.xml

  • 将log4j.properties文件放入resources中,让控制台打印SQL语句。

  • 创建实体类

  • 创建持久层接口,并在接口方法上定义Sql语句

public interface UserMapper {
    @Select("select id,username,sex,address from user")
    List<User> findAll();
}

由于注解在方法上方,而方法中就有参数类型和返回值类型,所以使用注解开发不需要定义参数类型和返回值类型

  • 在核心配置文件注册持久层接口,由于没有映射文件,所以只能采用注册接口或注册包的方法。
    <!-- 注册映射文件 -->
    <mappers>
        <package name="com.lxx.mapper"/>
    </mappers>
  • 测试方法
public class UserMapperTest {
    InputStream inputStream = null;
    SqlSession sqlSession = null;
    UserMapper userMapper = null;

    @Before
    public void before() throws IOException {
        // (1) 读取核心配置文件
        inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // (2) 创建SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // (3) SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        // (4) SqlSessionFactory对象获取SqlSession对象
        sqlSession = sqlSessionFactory.openSession();
        // (5) SqlSession对象获取代理对象
        userMapper = sqlSession.getMapper(UserMapper.class);
    }

    @After
    public void after() throws IOException {
        // 释放资源
        sqlSession.close();
        inputStream.close();
    }

    @Test
    public void findAllTest() {
        List<User> users = userMapper.findAll();
        System.out.println(users);
    }

}

2 增删改查

接下来写一套基于MyBatis注解的增删改查方法:

    @SelectKey(keyColumn = "id", keyProperty = "id", resultType = int.class, before = false, statement = "select LAST_INSERT_ID();")
    @Insert("insert into user(username,sex,address) values(#{username},#{sex},#{address})")
    void add(User user);

    @Update("update user set username=#{username},sex=#{sex},address=#{address} where id=#{id}")
    void update(User user);

    @Delete("delete from user where id=#{id}")
    void delete(int id);

    @Select("select id,username,sex,address from user where username like #{username}")
    List<User> findByUsernameLike(String username);

3 动态sql

在这里插入图片描述

MyBatis注解开发中有两种方式构建动态Sql:

使用脚本标签

将Sql嵌套在 <script> 内即可使用动态Sql标签:

    // 根据任意条件查询
    @Select("<script>" +
            "        select id, username, sex, address\n" +
            "        from user\n" +
            "        <where>\n" +
            "            <if test=\"username != null and username.length() != 0\">\n" +
            "                username like #{username}\n" +
            "            </if>\n" +
            "            <if test=\"sex != null and sex.length() != 0\">\n" +
            "                and sex = #{sex}\n" +
            "            </if>\n" +
            "            <if test=\"address != null and address.length() != 0\">\n" +
            "                and address = #{address}\n" +
            "            </if>\n" +
            "        </where>" +
            "</script>")
    List<User> findByCondition(User user);

在方法中构建动态Sql

在MyBatis中有 @SelectProvider 、 @UpdateProvider 、 @DeleteProvider 、 @InsertProvider 注解。当使用这些注解时将不在注解中直接编写SQL,而是调用某个类的方法来生成SQL。

package com.lxx.provider;

import com.lxx.pojo.User;

public class UserProvider {
    // 生成根据任意条件查询的Sql语句
    public String findByConditionSql(User user) {
        StringBuffer sb = new StringBuffer("select * from user where 1=1");
        if (user.getUsername() != null &&
                user.getUsername().length() != 0) {
            sb.append(" and username like #{username} ");
        }
        if (user.getSex() != null &&
                user.getSex().length() != 0) {
            sb.append(" and sex = #{sex} ");
        }
        if (user.getAddress() != null &&
                user.getAddress().length() != 0) {
            sb.append(" and address = #{address} ");
        }
        return sb.toString();
    }
}

    @SelectProvider(type = UserProvider.class,method = "findByConditionSql")
    List<User> findByCondition(User user);

4 自定义映射关系

当POJO属性名与数据库列名不一致时,需要自定义实体类和结果集的映射关系,在MyBatis注解开发中,使用 @Results 定义并使用自定义映射,使用 @ResultMap 使用自定义映射,用法如下:

    // 查询所有用户
	@Results(id = "userDIYMapper", value = {
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "username1", column = "username"),
            @Result(property = "sex1", column = "sex"),
            @Result(property = "address1", column = "address"),
    })
    @Select("select id,username,sex,address from user")
    List<User> findAll();

    // 根据id查询
    @ResultMap("userDIYMapper")
    @Select("select id,username,sex,address from user where id = #{id}")
    User findById(int id);

5 二级缓存

在这里插入图片描述

MyBatis默认开启一级缓存,接下来我们学习如何在注解开发时使用二级缓存:

  • POJO类实现Serializable接口。

  • 在MyBatis配置文件添加如下设置

    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
  • 在持久层接口上方加注解@CacheNamespace(blocking=true),该接口的所有方法都支持二级缓存。

  • 测试二级缓存

public class CatchTest {
    @Test
    public void testCache() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        User user1 = sqlSession1.getMapper(UserMapper.class).findById(1);
        System.out.println(user1);
        System.out.println(user1.hashCode());
        sqlSession1.commit();// 清空一次缓存,将数据存到二级缓存

        User user2 = sqlSession2.getMapper(UserMapper.class).findById(1);
        System.out.println(user2);
        System.out.println(user2.hashCode());
    }
}

6 一对一关联查询

在这里插入图片描述

在MyBatis的注解开发中对于多表查询只支持分解查询,不支持连接查询。

  • 创建实体类
public class Student {
    private int sid;
    private String name;
    private int age;
    private String sex;
    private Classes classes;
}

public class Classes {
    private int cid;
    private String className;
    private List<Student> students;
}
//getter/setter/构造等略
  • 创建分解后的查询方法
public interface StudentMapper {
    //查询所有学生
    @Select("select sid,name,age,sex,classId from student")
    List<Student> findAll();
}


public interface ClassesMapper {
    //根据id查询班级
    @Select("select cid,className from classes where cid = #{cid}")
    Classes findByCid(int cid);
}
  • 主表的查询配置自定义映射关系
    //查询所有学生
    @Select("select sid,name,age,sex,classId from student")
    // 自定义映射关系
    @Results(id = "studentMapper", value = {
            @Result(id = true, property = "sid", column = "sid"),
            @Result(property = "name", column = "name"),
            @Result(property = "age", column = "age"),
            @Result(property = "sex", column = "sex"),
            /**
             *   property:属性名
             *   column:调用从表方法时传入的参数列
             *   one:表示该属性是一个对象
             *   select:调用的从表方法
             *   fetchType:加载方式
             */
            @Result(property = "classes", 
                    column = "classId", 
                    one = @One(select = "com.lxx.mapper.ClassesMapper.findByCid", fetchType = FetchType.EAGER))
    })
    List<Student> findAll();
  • 测试
    @Test
    public void testFindAll() {
        List<Student> list = studentMapper.findAll();
        list.forEach(System.out::println);
    }

7 一对多关联查询

  • 创建分解后的查询方法
public interface ClassesMapper {

    //查询所有班级信息
    @Select("select cid,className from classes")
    List<Classes> findAll();
}

public interface StudentMapper {
    //根据班级id查询学生
    @Select("select sid,name,age,sex from student where classId=#{classId}")
    List<Student> findByClassId(int classId);
}
  • 主表的查询配置自定义映射关系
    //查询所有班级信息
    @Select("select cid,className from classes")
    @Results(id = "classesMapper", value = {
            @Result(id = true, property = "cid", column = "cid"),
            @Result(property = "className", column = "className"),
            @Result(property = "students",
                    column = "cid",
                    // many:表示该属性是一个集合
                    many = @Many(select = "com.lxx.mapper.StudentMapper.findByClassId",
                            fetchType = FetchType.LAZY))
    })
    List<Classes> findAll();
  • 测试
    @Test
    public void testFindAll() {
        List<Classes> list = classesMapper.findAll();
        list.forEach(System.out::println);
    }

8 注解开发与映射文件开发的对比

在这里插入图片描述

MyBatis中更推荐使用映射文件开发,Spring、SpringBoot更推荐注解方式。具体使用要视项目情况而定。它们的优点对比如下:

映射文件

  • 代码与Sql语句是解耦的,修改时只需修改配置文件,无需修改源码。

  • Sql语句集中,利于快速了解和维护项目。

  • 级联查询支持连接查询和分解查询两种方式,注解开发只支持分解查询。

注解

  • 配置简单,开发效率高。

  • 类型安全,在编译期即可进行校验,不用等到运行时才发现错误。

十 PageHelper分页插件

在这里插入图片描述

开发过程中如果要进行分页查询,需要传入页数和每页条数。返回页面数据,总条数,总页数,当前页面,每页条数等数据。此时使用PageHelper插件可以快速帮助我们获取这些数据。

在这里插入图片描述

PageHelper是一款非常好用的开源免费的Mybatis第三方分页插件。使用该插件时,只要传入分页参数,即可自动生成页面对象。我们使用该插件分页查询所有用户:

  • 引入依赖
        <!-- PageHelper -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.3.0</version>
        </dependency>
  • Mybatis配置文件中配置PageHelper插件
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!--     设置数据库类型       -->
            <property name="helperDialect" value="mysql"/>
        </plugin>
    </plugins>
  • 使用PageHelper插件
   @Test
    public void testFindPage() {
        // (1)查询前设置分页参数,参数一:页数,从1开始。 参数二:每页条数
        PageHelper.startPage(1, 3);
        // (2)正常查询
        List<User> users = userMapper.findAll();
        // (3)创建页面对象,创建时将查询结果传入构造方法
        PageInfo<User> pageInfo = new PageInfo<>(users);
        // (4)打印页面对象的属性
        System.out.println("结果集: " + pageInfo.getList());
        System.out.println("总条数: " + pageInfo.getTotal());
        System.out.println("总页数: " + pageInfo.getPages());
        System.out.println("当前页: " + pageInfo.getPageNum());
        System.out.println("每页条数: " + pageInfo.getSize());
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值