Loading

1 2

MyBatis 3学习笔记

MyBatis 3

一、MyBatis简介

优秀的持久层框架,支持支持自定义 SQL、存储过程以及高级映射,专注于SQL的编写。

2021-11-03_100928

​ 为什么不使用工具类进行数据库操作:

​ 功能简单,sql语句编写在Java代码里面,是一种硬编码高耦合的方式,不推荐。

Hibernate:

​ 另一种持久层框架,全自动全映射ORM(Object Relation Mapping)框架,旨在消除SQL

2021-11-03_101306

​ 主要特点:一个JavaBean对象与数据库中一条记录建立映射,中间过程对于程序员来讲是黑箱操作,无须关注SQL语句的编写。

​ 缺点:框架自动生成SQL语句,程序员无法进行SQL优化;JavaBean中每一个字段都在数据库中有相应映射,加重了数据库的负担。

需求:SQL语句交给我们开发人员编写,希望SQL不失去灵活性。于是,诞生了MyBatis:

2021-11-03_101741

MyBatis官方文档

二、MyBatis接口式编程 Hello World

1. 搭建环境

​ lombok介绍:

​ Lombok 是一种 Java™ 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注解实现这一目的。

​ 通过@Data注解,让被修饰的类自动生成getter和setter方法、构造器、equals方法、或者hash方法等。

​ 如果觉得@Data这个注解有点简单粗暴的话,Lombok提供一些更精细的注解,比如@Getter,@Setter,(这两个是field注解),@ToString,@AllArgsConstructor(这两个是类注解)。

​ log4j介绍:

​ Apache的开源项目log4j是一个功能强大的日志组件,提供方便的日志记录。可以查看在debug时候的sql语句。

​ 一般在resources目录下创建log4j.properties文件,进行配置。

1.1 Maven

导入依赖:

  <dependencies>
    <!--单元测试框架 Junit-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <!--lombok-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <!--引入Mybatis-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.6</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <!-- 日志 -->
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
     <!-- 注意与mysql版本的对应 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.26</version>
    </dependency>
  </dependencies>

1.2 Bean

@Data
public class Employee {
    private int id;
    private String name;
    private String age;
}

1.3 配置文件

放在resources目录下:

<?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: 核心配置文件 -->
<configuration>
    
    <!-- 导入外部数据库配置文件, 放在最前面 -->
    <properties resource="jdbc.properties"/>
    
    <!--
    environments配置项目的运行环境, 可以配置多个
    default: 启用的环境
    -->
    <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>

    <!-- 每一个Mapper.xml映射文件,都需要在MyBatis全局配置文件中注册!!! -->
    <mappers>
        <mapper resource="EmployeeMapper.xml"/>
    </mappers>
</configuration>

数据库配置文件properties,同样也存放在resources目录下

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatisdb
username=root
password=zouwenhao

日志的配置文件properties,resources目录下:

log4j.rootLogger=DEBUG,A1

log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=[%t] [%c]-[%p] %m%n

mapper配置文件,在这里面编写sql:

<!--
    namespace:名称空间,对应接口的全类名
    id:唯一标识
    resultType:返回值类型,使用类的全路径
    #{id}:从传递过来的参数中取出id值,用于作为数据查询条件的值
-->
<mapper namespace="com.atguigu.mybatis.mapper">
    <select id="selectEmp" resultType="com.atguigu.mybatis.bean.Employee">
        select * from info where id = #{id}
    </select>
</mapper>

1.4 通过SqlSession建立与数据库的会话

老版本的编程方法:

    /* MyBatis操作数据库的整体思路:
     * 1、根据xml配置文件(全局配置文件)创建一个SqlSessionFactory对象,有数据源一些运行环境信息 (config)
     * 2、sql映射配置文件;配置了每一个sql,以及sql的封装规则等。 (mapper)
     * 3、将sql映射文件注册在全局配置文件中 (config配置文件中的mapper标签进行注册)
     * 4、写代码:
     * 		1)、根据全局配置文件得到SqlSessionFactory;
     * 		2)、使用sqlSession工厂,获取到sqlSession对象使用他来执行增删改查(一个与数据库操作的代理对象)
     * 			一个sqlSession就是代表和数据库的一次会话,用完关闭,使用close方法
     * 		3)、使用sql的唯一标志来告诉MyBatis执行哪个sql。sql都是保存在sql映射文件中的。
     */
    // 返回SqlSessionFactory对象
    public SqlSessionFactory getSqlSessionFactory() throws IOException {
        // MyBatis全局配置文件路径
        String resource = "mybatis_config.xml";
        // 获取MyBatis全局配置文件的输入流
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // 获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        return  sqlSessionFactory;
    }

    // 查询测试,老的方法
    @Test
    public void testHello() throws IOException {
        // 1.获取SqlSessionFactory实例
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();

        // 2.获取SqlSession实例,能直接执行已经映射的sql语句
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            // 为了避免mapper映射出现重名的情况,一般使用 命名空间+查询id
            /*
            *   第一个参数:sql的唯一标识,statement Unique identifier matching the statement to use.
            *   第二个参数:执行sql要用的参数,parameter A parameter object to pass to the statement.
            * */
            Employee employee = sqlSession.selectOne("com.atguigu.mybatis.mapper.selectEmp", 1001);
            System.out.println(employee);
        } finally {
            // sqlSession表示与数据库的一次对话,用完之后必须手动关闭
            sqlSession.close();
        }
    }

1.5 接口式编程(推荐)

​ 使用接口与配置文件进行动态绑定,接口中的方法与select标签进行绑定

  1. 第一步:创建接口,getEmpById方法返回Employee对象

    public interface EmployeeMapper {
    
        public Employee getEmpById(Integer id);
    
    }
    
  2. 第二步:修改原EmployeeMapper.xml文件

    <!--
        namespace:名称空间,对应接口的全类名
        id:唯一标识
        resultType:返回值类型,使用类的全路径
        #{id}:从传递过来的参数中取出id值,用于作为数据查询条件的值
    
        public Employee getEmpById(Integer id);
    -->
    <mapper namespace="com.atguigu.mybatis.mapper.EmployeeMapper.java"> <!--绑定接口-->
        <select id="getEmpById" resultType="com.atguigu.mybatis.bean.Employee"><!--绑定id-->
            select * from info where id = #{id}
        </select>
    </mapper>
    
  3. 第三步:编写测试类

    思路是:拿到接口的实现类对象,然后调用接口的方法,进行数据库的查询操作

        @Test
        public void testHelloByInterface() throws IOException {
            // 1.获取SqlSessionFactory实例
            SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
            // 2.获取SqlSession实例,与数据库建立会话
            SqlSession sqlSession = sqlSessionFactory.openSession();
            try {
                // 3. 获取接口的实现类对象
                // 会为接口自动的创建一个代理对象,代理对象去执行增删改查方法
                EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);//获取代理对象
                Employee emp = mapper.getEmpById(1001);
                System.out.println(emp);
            } finally {
                sqlSession.close();
            }
        }
    

    ​ 使用接口式编程的好处:

    ​ 拥有更强的类型检查,以及返回值的限制,能够将DAO层与实现进行分离出来。

1.6 总结

  1. SqlSession和connection一样都不是线程安全的,因此不能共享,每次使用都应该去获取新的对象。

  2. SqlSession代表和数据库的一次会话,用完就必须关闭。

  3. mapper接口没有实现类,但是MyBatis会为这个接口生成一个代理对象:(将接口与xml进行绑定)

    ​ EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);

  4. 两个重要的配置文件:

    ​ mybatis的全局配置文件:包含数据库连接池的信息、事务管理器信息等

    ​ sql映射文件:保存了每一个sql语句的映射信息,包括这个sql语句的标识、返回的信息类型(select标签)

三、MyBatis-全局配置文件

​ 全局配置文件中的dtd约束,对应了语法规则,在编写时,会有相关提示信息

<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

1. properties_引入外部配置文件

<configuration>
	<!--
		1、mybatis可以使用properties来引入外部properties配置文件的内容;
		resource:引入类路径下的资源
		url:引入网络路径或者磁盘路径下的资源
	  -->
	<properties resource="jdbc.properties"></properties> <!--资源文件夹下,直接使用,否则应该添加全类名+名称-->
    
    ......
    
</configuration>    

如果属性在不只一个地方进行了配置,那么 MyBatis 将按照下面的顺序来加载:

​ – 在 properties 元素体内指定的属性首先被读取。

​ – 然后根据 properties 元素中的 resource 属性读取类路径下属性文件或根据 url 属性指定的路径读取属性文件,并覆盖已读取的同名属性。

​ – 最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。

2. settings_运行时行为设置

2021-11-04_100228

​ 以是否开启驼峰命名规则映射为例:

	<!-- 
		2、settings包含很多重要的设置项
		setting:用来设置每一个设置项
			name:设置项名
			value:设置项取值
	 -->
	<settings>
		<setting name="mapUnderscoreToCamelCase" value="true"/>
	</settings>

3. typeAliases_别名处理器(不推荐使用)

​ 将全类名与别名建立映射关系,这样在一些使用全类名的条件下,可以使用别名。

	<!-- 3、typeAliases:别名处理器:可以为我们的java类型起别名 
			别名不区分大小写(注意)
	-->
	<typeAliases>
		<!-- 1、typeAlias:为某个java类型起别名
				type:指定要起别名的类型全类名; 默认别名就是类名小写: employee
				alias:指定新的别名
		 -->
		<typeAlias type="com.atguigu.mybatis.bean.Employee" alias="emp"/>
		
		<!-- 2、package:为某个包下的所有类批量起别名 
				name:指定包名(为当前包以及下面所有的后代包的每一个类都起一个默认别名(类名小写))
		-->
		<package name="com.atguigu.mybatis.bean"/>
		
		<!-- 3、批量起别名的情况下,使用@Alias注解为某个类型指定新的别名(在类名前使用注解标签) -->
        
	</typeAliases>
2021-11-04_100309

4. typeHandlers_类型处理器(了解)

​ MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。部分如下所示:

2021-11-03_161459

5. plugins_插件(本章了解)

​ MyBatis 允许你在映射SQL语句执行过程中的某一过程进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)

    通常使用插件来修改MyBatis的一些核心行为,插件通过动态代理机制

6. environments_运行环境

​ MyBatis可以配置多种环境,比如开发、测试和生 产环境需要有不同的配置。

​ 每种环境使用一个environment标签进行配置并指 定唯一标识符。

​ 可以通过environments标签中的default属性指定一个环境的标识符来快速的切换环境。

​ 配置当前运行环境,比如数据库、事务管理器的信息,可以创建多个运行环境,如下所示:有MySQL和Oracle两种数据库环境。

	<!-- 
		4、environments:环境们,mybatis可以配置多种环境 ,default指定使用某种环境。可以达到快速切换环境。
			environment:配置一个具体的环境信息;必须有两个标签;id代表当前环境的唯一标识
				transactionManager:事务管理器;
					type:事务管理器的类型:JDBC(JdbcTransactionFactory.class)|MANAGED(ManagedTransactionFactory.class)
						自定义事务管理器:实现TransactionFactory接口,type指定为全类名
				
				dataSource:数据源;
					type:三种数据源类型:UNPOOLED(UnpooledDataSourceFactory)
								|POOLED(PooledDataSourceFactory) 连接池技术
								|JNDI(JndiDataSourceFactory)
					自定义数据源:实现DataSourceFactory接口,type是全类名
		 -->
		 
	<environments default="dev_mysql"> <!--指定当前运行的环境,根据下面environment标签中id值属性指定-->
        
		<environment id="dev_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>
	
		<environment id="dev_oracle">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="${orcl.driver}" />
				<property name="url" value="${orcl.url}" />
				<property name="username" value="${orcl.username}" />
				<property name="password" value="${orcl.password}" />
			</dataSource>
		</environment>
        
	</environments>

• type: JDBC | MANAGED | 自定义

– JDBC:使用了 JDBC 的提交和回滚设置,**依赖于从数据源得到的连接来管理事务范围**。 		

​ JdbcTransactionFactory

​ – MANAGED:不提交或回滚一个连接,让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下 文)。

​ ManagedTransactionFactory

​ – 自定义:实现TransactionFactory接口,type=全类名/ 别名

• type: UNPOOLED | POOLED | JNDI | 自定义

​ – UNPOOLED:不使用连接池, UnpooledDataSourceFactory

​ – POOLED:使用连接池, PooledDataSourceFactory

​ – JNDI: 在EJB或应用服务器这类容器中查找指定的数据源

​ – 自定义:实现DataSourceFactory接口,定义数据源的获取方式。

• 实际开发中我们使用Spring管理数据源,并进行事务控制的配置来覆盖上述配置。

7. databaseProvider_多数据库支持

2021-11-03_163009

下图databaseId指定了在什么数据库环境下,才会执行这条sql语句。至于当前是在什么环境下,则在environments标签下设置default属性值,用于设置当前数据库运行环境。

​ 例如:当前在MySQL环境下,因此一旦需要执行这条语句,那么就会执行这条语句,如果是Oracle环境下,就不会执行这条语句,而是会执行另一条databaseId为oracle的语句。

2021-11-03_163042
	<!-- 5、databaseIdProvider:支持多数据库厂商的;
		 type="DB_VENDOR":VendorDatabaseIdProvider
		 	作用就是得到数据库厂商的标识(驱动getDatabaseProductName()),mybatis就能根据数据库厂商标识来执行不同的sql;
		 	MySQL,Oracle,SQL Server,xxxx
	  -->
	<databaseIdProvider type="DB_VENDOR">
		<!-- 为不同的数据库厂商起别名 -->
		<property name="MySQL" value="mysql"/>
		<property name="Oracle" value="oracle"/>
		<property name="SQL Server" value="sqlserver"/>
	</databaseIdProvider>

8. mappers_sql映射注册

将sql映射注册到全局配置中来

	<!-- 将我们写好的sql映射文件(EmployeeMapper.xml)一定要注册到全局配置文件(mybatis-config.xml)中 -->
	<!-- 6、mappers:将sql映射注册到全局配置中 -->
	<mappers>
		<!-- 
			mapper:注册一个sql映射 
				注册配置文件
				resource:引用类路径下的sql映射文件
					mybatis/mapper/EmployeeMapper.xml
				url:引用网路路径或者磁盘路径下的sql映射文件
					file:///var/mappers/AuthorMapper.xml
					
				注册接口
				class:引用(注册)接口,
					1、有sql映射文件,映射文件名必须和接口同名,并且配置文件放在与接口同一目录下;
					2、没有sql映射文件,所有的sql都是利用注解写在接口上;
					推荐:
						比较重要的,复杂的Dao接口我们来写sql映射文件
						不重要,简单的Dao接口为了开发快速可以使用注解;
		-->
		<mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
        
		<mapper class="com.atguigu.mybatis.dao.EmployeeMapperAnnotation"/>
		
		<!-- 批量注册: -->
		<package name="com.atguigu.mybatis.dao"/>
	</mappers>

​ 注解方式的举例:将sql语句写在注解上,注意在全局配置文件中使用class属性进行接口注册

public interface EmployeeMapperAnnotation {
  @Select({"select * from tbl_employee where id=#{id}"})
  Employee getEmpById(Integer paramInteger);
}

​ 测试方法的编写:

    @Test
    public void testHelloByAnno() throws IOException {
        // 1.获取SqlSessionFactory实例
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();

        // 2.获取SqlSession实例,与数据库建立会话
        SqlSession sqlSession = sqlSessionFactory.openSession();

        try {
            // 3. 获取接口的实现类对象
            EmployeeMapperAnnotation mapper = sqlSession.getMapper(EmployeeMapperAnnotation.class);
            Employee emp = mapper.getEmpById(1001);
            System.out.println(emp);
        } finally {
            sqlSession.close();
        }
    }

四、MyBatis-映射文件(重要)

1. 增删改查

​ 前提,MyBatis配置环境已经像前文一样配置完毕!

1.1 接口方法

public interface EmployeeMapper {

    Employee getEmpById(Integer paramInteger);

    // mybatis允许增删改直接定义以下类型返回值
    //      Integer、Long、Boolean、void
    Long addEmp(Employee employee);

    Long updateEmp(Employee employee);

    Long deleteEmp(Integer id);

}

1.2 Mapper配置文件

​ 在mapper配置文件中,建立与接口方法的映射,同时记得在MyBatis全局配置文件中,进行注册:

    <mappers>
         <mapper resource="EmployeeMapper.xml"/>
    </mappers>

​ EmployeeMapper.xml:

	<!--如果要返回的是一个集合,那么就要写集合中元素的类型,用resultType进行设置-->
    <select id="getEmpById" resultType="com.atguigu.mybatis.bean.Employee">
        select * from info where id = #{id}
    </select>

    <!--    void addEmp(Employee employee);-->
    <!-- parameterType: 可以省略,设置形参参数类型
            因为id为自增,所以只需要传入其他字段值即可-->
    <insert id="addEmp" parameterType="com.atguigu.mybatis.bean.Employee">
        insert into info(name, age) values (#{name}, #{age})
    </insert>

    <!--    void updateEmp(Employee employee);-->
    <update id="updateEmp" >
        update info
        set name=#{name}, age=#{age}
        where id=#{id}
    </update>

    <!--    void deleteEmp(Integer id);-->
    <delete id="deleteEmp">
        delete from info where id=#{id}
    </delete>

1.3 测试

    @Test
    public void EmpTest() throws IOException {
        // 1. 获取sqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        // 2. 获取sqlSession对象,但并不会自动提交
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 自动提交:sqlSessionFactory.openSession(true);

        try {
            // 3. 获取接口的代理对象
            EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);

            // 4. 更新数据
//            Long result = mapper.updateEmp(new Employee(1001, "Paul", 23));

            // 4. 添加数据,id是自增的,所以可以传一个null
            Long result = mapper.addEmp(new Employee(null, "Dick", 21));

            // 4. 删除操作,指定id值
//            Long result = mapper.deleteEmp(1005);

            System.out.println(result > 0 ? "操作成功!" : "操作失败!");

            // 5. 手动提交事务
            sqlSession.commit();
        } finally {
            // 5. 关闭会话
            sqlSession.close();
        }
    }

注意:

  1. openSession()获取到的SqlSession实例是不会自动提交的,需要在sql语句执行完,手动commit提交,如果想自动提交,则使用openSession(true)方法获取实例。

  2. 关于MySQL数据库使用自增id,当我们需要使用自增id时,类对象只需要将自增id传入一个null值即可,但一定要保证数据库中该键是开启了自动增长。如果报以下错误:

    java.sql.SQLException: Field 'id' doesn't have a default value

    ​ 说明没有开启自动增长,开启后,需要重启以下数据库!

2. Insert

  1. MySQL数据库获取自增主键的值:
	<!-- public void addEmp(Employee employee); -->
	<!-- parameterType:参数类型,可以省略, 
		获取自增主键的值:
			mysql支持自增主键,自增主键值的获取,mybatis也是利用statement.getGenreatedKeys();
			useGeneratedKeys="true";使用自增主键获取主键值策略
			keyProperty;指定对应的主键属性,也就是mybatis获取到主键值以后,将这个值封装给javaBean的哪个属性
	-->
	<insert id="addEmp" parameterType="com.atguigu.mybatis.bean.Employee"
		useGeneratedKeys="true" keyProperty="id" databaseId="mysql">
		insert into tbl_employee(last_name,email,gender) 
		values(#{lastName},#{email},#{gender})
	</insert>
  1. Oracle数据库不支持自增:

    使用序列来模拟自增,就需要手动执行sql语句,才查询下一个应该自增的值。

    有两种解决思路:

    1. 先执行查询sql,获取到自增主键值后,再进行insert插入语句。

      ​ 用BEFORE修饰selectKey标签,并通过 .nextva 获取自增后的主键值,再进行插入操作

    2. 先执行insert插入语句,使用 .nextval 表示新记录自增主键值,然后执行AFTER修饰的selectKey标签,使用 .currval 获取当前新纪录自增后的主键值。

	<!-- 
	获取非自增主键的值:
		Oracle不支持自增;Oracle使用序列来模拟自增;
			每次插入的数据的主键是从序列中拿到的值(类似于执行一条语句,使得主键自增,得到序列的值);
			如何获取到这个值;
	 -->
	<insert id="addEmp" databaseId="oracle">
	
		<!-- 
		keyProperty:查出的主键值封装给javaBean的哪个属性
		order="BEFORE":当前sql在插入sql之前运行
			   AFTER:当前sql在插入sql之后运行
		resultType:查出的数据的返回值类型
		
		BEFORE运行顺序:
			1.先运行selectKey查询id的sql;查出id值封装给javaBean的id属性 nextval
			2.再运行插入的sql;就可以取出id属性对应的值
			
		AFTER运行顺序:
			1.先运行插入的sql(从序列中取出新值作为id);
			2.再运行selectKey查询id的sql;currval
		 -->
		<selectKey keyProperty="id" order="BEFORE" resultType="Integer">
			<!-- 编写查询主键的sql语句,必须得先执行,得到主键值 -->
			<!-- BEFORE-->
			select EMPLOYEES_SEQ.nextval from dual 
			<!-- AFTER:
			 select EMPLOYEES_SEQ.currval from dual -->
		</selectKey>
		
		<!-- 插入时的主键是从序列中拿到的,查出的id结果,会用integer进行封装 -->
		<!-- BEFORE:-->
		insert into employees(EMPLOYEE_ID,LAST_NAME,EMAIL) 
		values(#{id},#{lastName},#{email<!-- ,jdbcType=NULL -->}) 
		
		<!-- AFTER:id值先使用employees_seq.nextval,当插入sql结束之后,再获取id值(查询当前id,而不是nextval)
		insert into employees(EMPLOYEE_ID,LAST_NAME,EMAIL) 
		values(employees_seq.nextval,#{lastName},#{email}) -->
	</insert>

3. 参数处理

3.1 单个参数/多个参数/命名参数

​ 在mapper配置文件中,在将接口方法与CRUD标签进行映射的时候,需要使用接口方法中的参数作为sql语句查询的条件。MyBatis对于单个参数和多个参数有着不一样的处理:

单个参数:

​ MyBatis不会做特殊处理,

​ #{参数名/任意名}:取出参数值。

多个参数:

​ Mybatis会做特殊处理。

​ 多个参数会被封装成 一个map,规则如下:

​ key:param1...paramN,或者参数的索引也可以

​ value:传入的参数值

​ #{}就是从map中获取指定的key的值;

异常:
org.apache.ibatis.binding.BindingException: 
Parameter 'id' not found. 
Available parameters are [1, 0, param1, param2]

错误操作:
	方法:public Employee getEmpByIdAndLastName(Integer id,String lastName);
	取值:#{id}, #{lastName} (正确取值操作:#{param1}, #{param2})

命名参数:

​ 明确指定封装参数时map的key;在接口方法中使用@Param注解;@Param("id")

​ 多个参数会被封装成 一个map,

​ key:使用@Param注解指定的值

​ value:参数值

​ #{指定的key}取出对应的参数值,如 #{id}, #{lastName}

Long deleteEmp(@Param("id") Integer id, @Param("name") String name)

3.2 POJO/Map/TO

POJO:

​ 如果多个参数正好是我们业务逻辑的数据模型,我们就可以直接传入pojo;

​ #{属性名}:取出传入的pojo的属性值

​ 就像上文中测试,因为数据参数传入的是一个实例对象,就可以直接使用属性值来获取值。

Map:

​ 如果多个参数不是业务模型中的数据,没有对应的pojo,不经常使用,为了方便,我们也可以传入map

​ #{key}:取出map中对应的值

​ 示例:

2021-11-04_153453 2021-11-04_153513

TO:
​ 如果多个参数不是业务模型中的数据,但是经常要使用,推荐来编写一个TO(Transfer Object)数据传输对象:

​ Page{
​ int index;
​ int size;
​ }

3.3 MyBatis的参数封装(源码理解)

​ MyBatis如何处理参数的:

2021-11-04_211456

底层调用的sqlSession对象的一些增删改查的API

​ 总结:参数多的时候,MyBatis会封装map,但为了不混乱,我们可以使用@Param来指定封

装时使用的key;

​ #{key}就可以取出map中的值;

​ (@Param("id")Integer id,@Param("lastName")String lastName);

ParamNameResolver解析参数封装map的:

​ 该类有参构造器:

/**  names参数的确定流程:

        1.获取每一个被@Param注解修饰的参数的值,如:id,lastName的值,并赋值给name;
        
		2.每次解析一个参数给map中保存信息:(key:参数索引,value:name的值)
    		name的值要分两种情况:
    			第一种:标注了param注解,那么name的值就是注解指定的值。
    			第二种:没有标注(下面代码)
            		1.全局配置:useActualParamName(jdk1.8):name=参数名
					2.name=map.size();相当于当前元素的索引,所以最终保存形式如下:
    					{0=id, 1=lastName,2=2}
*/

    public ParamNameResolver(Configuration config, Method method) {
        ................
                if (name == null) {
                    if (config.isUseActualParamName()) { // 全局配置过没有?
                        name = this.getActualParamName(method, paramIndex);
                    }

                    if (name == null) {
                        name = String.valueOf(map.size());
                    }
                }

                map.put(paramIndex, name);
            }
        }

        this.names = Collections.unmodifiableSortedMap(map);
    }

getNamedParams()方法:对args参数,用map类型进行封装

​ 在遍历names集合的时候,进行了两次封装:

第一次封装:使得用户可以使用指定的key去获取参数,往往是用@Param进行修饰的,满足用户自定义param的需求。

第二次封装:是使用MyBatis提供的一种默认的方式,通过#{param1...paramN}这种方式进行参数值的获取。

// args传的参数:args[1,"Tom",'hello']
public Object getNamedParams(Object[] args) {
    final int paramCount = names.size(); 
    //1、参数为null直接返回
    if (args == null || paramCount == 0) {
      return null;
     
    //2、如果只有一个元素,并且没有Param注解;args[0]:单个参数直接返回
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
      
    //3、多个元素或者有Param标注
    } else {
      final Map<String, Object> param = new ParamMap<Object>();
      int i = 0;
      
      //4、遍历names集合;{0=id, 1=lastName, 2=2}
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
         
        // 封装一次:@Param指定key
      	// key:names集合的value
        // value:names集合的key又作为取值的参考args[0]:args【1,"Tom"】:
      	// 封装效果:Map集合param = {id=args[0]:1, lastName=args[1]:Tom, 2=args[2]:2}
        param.put(entry.getValue(), args[entry.getKey()]);//相当于将args参数集合用map进行封装了
        
        // 封装一次:默认方式,Param1...ParamN
        // add generic param names (param1, param2, ...)param
        // 额外的将每一个参数也保存到map中,使用新的key:param1...paramN
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      // 效果:有Param注解可以#{指定的key},或者#{param1}
      return param;
    }
  }
}

3.4 参数值的获取——#与$取值的区别

​ #{}:可以获取map中的值或者pojo对象属性的值;

​ ${}:可以获取map中的值或者pojo对象属性的值;

主要区别:$是先拼接后编译,#是先编译后拼接,所以#没有SQL注入的风险。

2021-11-04_203423
select * from tbl_employee where id=${id} and last_name=#{lastName}

Preparing: select * from tbl_employee where id=2 and last_name=?

对比:

#{}:是以预编译的形式,将参数设置到sql语句中;PreparedStatement;防止sql注入

​ ${}:取出的值直接拼装在sql语句中;会有安全问题;

​ 大多情况下,我们去取参数的值都应该去使用 #{} ;

​ 原生jdbc不支持占位符的地方,我们就可以使用 ${} 进行取值

​ 比如分表、排序。。。。;按照年份分表拆分

select * from ${year}_salary where xxx;

select * from tbl_employee order by ${f_name} ${order}

# 取值时指定参数相关规则

{}:更丰富的用法:

​ 规定参数的一些规则:

​ javaType、 jdbcType、 mode(存储过程)、 numericScale、

​ resultMap、 typeHandler、 jdbcTypeName、 expression(未来准备支持的功能);

​ jdbcType通常需要在某种特定的条件下被设置:

​ 在我们数据为null的时候,有些数据库可能不能识别mybatis对null的默认处理。比如Oracle(报错);

​ JdbcType OTHER:无效的类型;因为mybatis对所有的null都映射的是原生Jdbc的OTHER类型,oracle不能

正确处理;

​ 由于全局配置中:jdbcTypeForNull=OTHER;Oracle并不支持,解决的两种办法:

1、#{email, jdbcType=NULL}; 在sql语句中进行设置

2、jdbcTypeForNull=NULL:直接在全局配置文件中,通过setting标签配置

			<setting name="jdbcTypeForNull" value="NULL"/>

4. Select

2021-11-04_215455

4.1 返回记录封装在List

接口方法

public interface EmployeeMapper {

    Employee getEmpById(Integer paramInteger);

    Long addEmp(Employee employee);

    Long updateEmp(Employee employee);

    // List
    List<Employee> getEmpByAge(String paramName);

}

Mapper配置文件中的sql

​ 使用模糊查询

    <!--List<Employee> getEmpByAge(Integer paramInteger);-->
    <!--如果返回的是集合的话,只需要定义集合中的泛型类型即可-->
    <select id="getEmpByAge" resultType="com.atguigu.mybatis.bean.Employee">
        select * from info where name like #{paramName}
    </select>

测试方法

​ 在测试方法中,传入模糊查询的条件:查姓名是J开头的记录

    @Test
    public void ListTest() throws IOException {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        try {
            EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
            List<Employee> employeeList = mapper.getEmpByAge("J%");
            System.out.println(employeeList);
        } finally {
            sqlSession.close();
        }
    }

结果

[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByAge]-[DEBUG] ==>  Preparing: select * from info where name like ? 
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByAge]-[DEBUG] ==> Parameters: J%(String)
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByAge]-[DEBUG] <==      Total: 2
[Employee(id=1005, name=Jack, age=23), Employee(id=1006, name=James, age=26)]

4.2 返回记录封装在Map

接口方法

    // 封装到map对象中,单个对象
    Map<String, Object> getEmpByMap(Integer paramId);

    // 多条记录封装到map对象中,Map
    @MapKey("id") // 这个注解设置了mybatis在封装为map的过程中,将查询结果的id属性作为map的key
    Map<Integer, Employee> getEmpByNameReturnMap(String name);

Mapper配置xml文件

    <!--Map<String, Object> getEmpByMap(Integer paramId);-->
    <!--注意使用map作为返回值类型,因为只是封装了单个记录-->
    <select id="getEmpByMap" resultType="map">
        select * from info where id = #{paramId}
    </select>

    <!--Map<Integer, Employee> getEmpByNameReturnMap(String name);-->
    <!--由于返回多条记录,所以resultType设置返回记录的类型即可-->
    <select id="getEmpByNameReturnMap" resultType="com.atguigu.mybatis.bean.Employee">
        select * from info where name like #{name}
    </select>

测试方法

    @Test
    public void MapTest() throws IOException {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        try {
            EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);

            // Map<String, Object> employeeMap = mapper.getEmpByMap(1003); // 单条记录
            Map<Integer, Employee> employeeMap = mapper.getEmpByNameReturnMap("J%"); // 返回名字为J开头的记录
            System.out.println(employeeMap); // 打印map,调用的是实现子类的toString()方法
        } finally {
            sqlSession.close();
        }
    }

结果

// 单条记录封装在Map中
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByMap]-[DEBUG] ==>  Preparing: select * from info where id = ? 
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByMap]-[DEBUG] ==> Parameters: 1003(Integer)
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByMap]-[DEBUG] <==      Total: 1
{name=Mary, id=1003, age=21}
// 多条记录封装在Map中,在接口方法中,设置了map中id值作为key键
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByNameReturnMap]-[DEBUG] ==>  Preparing: select * from info where name like ? 
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByNameReturnMap]-[DEBUG] ==> Parameters: J%(String)
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpByNameReturnMap]-[DEBUG] <==      Total: 2
{1005=Employee(id=1005, name=Jack, age=23), 1006=Employee(id=1006, name=James, age=26)}

4.3 自定义结果集映射规则(重要)

2021-11-05_110602

resultType:是跟自动封装有关的,在mapper配置文件中指定封装为什么类型,MyBatis就将查询结果封装为对应类型的数据格式。

​ 但存在这样的情况,就是查询出来的结果集中,列名跟JavaBean的属性字段名不一样,这个时候是封装不成功的(打印出来,属性值为null),对此上文提到过有两种解决方案:

​ 第一种:使用@Param注解,起别名

​ 第二种:开启驼峰命名法,如果符合要求的话

​ 第三种:使用自定义结果集映射

自定义resultMap,实现高级结果集的映射。

    <!--自定义某个javaBean的封装规则
            type:需要自定义规则的JavaBean
            id:唯一id,方便引用sql语句标签引用这个规则
    -->
    <resultMap type="com.atguigu.mybatis.bean.Employee" id="MySimpleEmp">
        <!--指定主键列的封装规则
                id标签定义的主键,MyBatis底层会有优化;
                column:指定哪一列
                property:指定对应的JavaBean属性
          -->
        <id column="id" property="id"/> <!--建立新规则下,主键与对象中主键属性的映射关系-->
        <!-- 定义普通列封装规则,非主键属性使用result标签进行映射 -->
        <result column="last_name" property="lastName"/>
        <!-- 其他不指定的列会自动封装:我们只要写resultMap就把全部的映射规则都写上。 -->
        <result column="email" property="email"/>
        <result column="gender" property="gender"/>
    </resultMap>

    <!-- resultMap:自定义结果集映射规则;它的值,使用resultMap自定义标签中的id,进行引用-->
    <!-- public Employee getEmpById(Integer id); -->
    <select id="getEmpById"  resultMap="MySimpleEmp"> 
        select * from tbl_employee where id=#{id}
    </select>

4.4 关联查询

级联属性封装结果

Mapper配置文件:

	<!-- 
	场景一:
		查询Employee的同时查询员工对应的部门
		Employee===Department
		一个员工有与之对应的部门信息;
		id  last_name  gender    d_id     did  dept_name (private Department dept;)
	 -->
	 
	<!--
		联合查询:级联属性封装结果集
	  -->
	<resultMap type="com.atguigu.mybatis.bean.Employee" id="MyDifEmp">
		<id column="id" property="id"/>
		<result column="last_name" property="lastName"/>
		<result column="gender" property="gender"/>
		<result column="did" property="dept.id"/> <!--使用级联属性-->
		<result column="dept_name" property="dept.departmentName"/> <!--使用级联属性-->
	</resultMap>

	<!--  public Employee getEmpAndDept(Integer id);-->
	<select id="getEmpAndDept" resultMap="MyDifEmp">
		SELECT e.id id,e.last_name last_name,e.gender gender,e.d_id d_id,
		d.id did,d.dept_name dept_name FROM tbl_employee e,tbl_dept d
		WHERE e.d_id=d.id AND e.id=#{id}
	</select>

association定义关联对象封装规则

association可以指定联合的JavaBean对象:在resultType标签中

<!--association可以指定联合的javaBean对象
		property="dept":指定哪个属性是联合的对象
		javaType:指定这个属性对象的类型[不能省略]
-->
<association property="dept" javaType="com.atguigu.mybatis.bean.Department">
	<id column="did" property="id"/>
	<result column="dept_name" property="departmentName"/>
</association>

相当于使用嵌套的方式:在mapper配置文件中完整的代入如下所示:

	<!-- 
		使用association定义关联的单个对象的封装规则;
	 -->
	<resultMap type="com.atguigu.mybatis.bean.Employee" id="MyDifEmp2">
		<id column="id" property="id"/>
		<result column="last_name" property="lastName"/>
		<result column="gender" property="gender"/>
		
		<!--  association可以指定联合的javaBean对象
				property="dept":指定哪个属性是联合的对象
				javaType:指定这个属性对象的类型[不能省略]
		-->
		<association property="dept" javaType="com.atguigu.mybatis.bean.Department">
			<id column="did" property="id"/><!--column指定列名,也就是别名,property属性字段值,两者进行绑定-->
			<result column="dept_name" property="departmentName"/>
		</association>
	</resultMap>
	<!--  public Employee getEmpAndDept(Integer id);-->
	<select id="getEmpAndDept" resultMap="MyDifEmp">
		SELECT e.id id,e.last_name last_name,e.gender gender,e.d_id d_id,
		d.id did,d.dept_name dept_name FROM tbl_employee e,tbl_dept d
		WHERE e.d_id=d.id AND e.id=#{id}
	</select>
2021-11-05_141430

association分步查询

注意分步查询与级联封装中,association的用法区别。主要是根据sql语句来的。

JavaBean对象:

@Data
public class Department {
    private Integer id;
    private String departmentName;
    private List<Employee> emps;

    public Department(Integer id, String departmentName, List<Employee> emps) {
        this.id = id;
        this.departmentName = departmentName;
        this.emps = emps;
    }
}
@Data
public class Employee {
    private Integer id;
    private String name;
    private Integer age;
    private Department dept;

    public Employee(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
}

Department:

接口方法:

public interface DepartmentMapper {
    Department getDeptById(Integer id);
}

mapper配置文件:将查询department信息的sql语句与接口方法进行绑定,准备后面的分步查询

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//com.atguigu.mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.mybatis.dao.DepartmentMapper">

    <!--public Department getDeptById(Integer id);  -->
    <select id="getDeptById" resultType="com.atguigu.mybatis.bean.Department">
        select id,dept_name departmentName from tbl_dept where id=#{id}
    </select>

</mapper>

在EmployeeMapper的配置文件中:

<!-- 使用association进行分步查询步骤:
		1、先按照员工id查询员工信息
		2、根据查询员工信息中的d_id值去部门表查出部门信息(调用另一条sql语句)
		3、部门设置到员工中;
	 -->
	 
	 <!--  id  last_name  email   gender    d_id   -->
	 <resultMap type="com.atguigu.mybatis.bean.Employee" id="MyEmpByStep">
	 	<id column="id" property="id"/><!--设置主键-->
	 	<result column="last_name" property="lastName"/>
	 	<result column="email" property="email"/>
	 	<result column="gender" property="gender"/>
	 	<!-- association定义关联对象的封装规则
	 		select:表明当前属性是调用select指定的方法查出的结果(调用哪条sql,接口方法已经绑定了对应的sql语句)
	 		column:指定将哪一列的值传给这个方法
	 		
	 		流程:使用select指定的方法(传入column指定的这列参数的值)查出对象,并封装给property指定的属性
	 	 -->
 		<association property="dept" 
	 		select="com.atguigu.mybatis.dao.DepartmentMapper.getDeptById"
	 		column="d_id">
 		</association>
	 </resultMap>

	 <!--  public Employee getEmpByIdStep(Integer id);-->
	 <select id="getEmpByIdStep" resultMap="MyEmpByStep">
	 	select * from tbl_employee where id=#{id}
	 	<if test="_parameter!=null">
	 		and 1=1
	 	</if>
	 </select>

结果:

​ 查1号员工信息,在执行第一条查询sql语句的时候,也去执行了查部门信息的sql语句,然后获取到该员工的部门信息,最后打印输出。

2021-11-05_151046

2021-11-05_150928 2021-11-05_150859

延迟加载

​ 当前的问题:

​ 因为在Employee这个Bean对象中,有一个Department类属性Dept,而我们每次查询Employee对象的时候,都会将其Dept属性一起查询出来,如果我们并不立即使用这个属性的信息的话,这样会导致性能的下降。所以想在使用部门信息的时候,再去查询。用延迟加载解决:

延迟加载,就是按需加载,当需要使用某个关联属性的时候,如输出打印操作,才会进行执行。

​ 在分段查询的基础上,全局配置中设置两个参数:

2021-11-05_152112

​ 对aggressiveLazyLoading:开启时候,所有属性都会加载,关闭时,才会按需加载。

	<settings>	
		<!--显示的指定每个我们需要更改的配置的值,即使他是默认的。防止版本更新带来的问题  -->
		<setting name="lazyLoadingEnabled" value="true"/>
		<setting name="aggressiveLazyLoading" value="false"/>
	</settings>

注意如果报异常:

Error creating lazy proxy. Cause: java.lang.NullPointerException

需要把mybatis的包换成Mybatis-3.5.1.jar及其以上的版本

collection定义关联集合封装规则

association:定义关联对象封装规则。

collection:定义关联集合封装规则。

上文都是只关联一个对象,现在是这个类的某个属性,关联多个对象的情况:

​ 场景二:

查询部门的时候将部门对应的所有员工信息也查询出来

使用传统SQL语句:查询开发部的员工信息

2021-11-05_154143

mapper配置文件:

	<!-- 
	public class Department {
			private Integer id;
			private String departmentName;
			private List<Employee> emps;
			......	
	}
	  did  dept_name  || emps (eid  last_name  email   gender)  
			部门表			员工信息表(Emp对象,需要封装Emp对象集合)
	 -->
	 
	<!--嵌套结果集的方式,使用collection标签定义关联的集合类型的属性封装规则  -->
	<resultMap type="com.atguigu.mybatis.bean.Department" id="MyDept">
		<id column="did" property="id"/>
		<result column="dept_name" property="departmentName"/>
        
		<!-- 
			collection定义关联集合类型的属性的封装规则(association只能定义一个关联对象)
			ofType:指定集合里面元素的类型
		-->
		<collection property="emps" ofType="com.atguigu.mybatis.bean.Employee">
			<!-- 定义这个集合中元素的封装规则 -->
			<id column="eid" property="id"/>
			<result column="last_name" property="lastName"/>
			<result column="email" property="email"/>
			<result column="gender" property="gender"/>
		</collection>
        
	</resultMap>

	<!-- public Department getDeptByIdPlus(Integer id); -->
	<select id="getDeptByIdPlus" resultMap="MyDept">
		SELECT d.id did,d.dept_name dept_name,
				e.id eid,e.last_name last_name,e.email email,e.gender gender
		FROM tbl_dept d
		LEFT JOIN tbl_employee e
		ON d.id=e.d_id
		WHERE d.id=#{id}
	</select>

测试:

2021-11-05_155654

collection分步查询

​ 场景二:

查询部门的时候将部门对应的所有员工信息也查询出来,用分步查询的方法

MySQL数据库

​ info表:员工信息表

2021-11-05_171352

​ department表:部门信息表

2021-11-05_171400

大致思路
  1. 先执行查询特定部门的sql语句
  2. 然后嵌套查询在这个部门里面的员工信息
Bean代码
@Data
public class Department {
    private Integer id;
    private String departmentName;
    private List<Employee> emps;

    public Department() {
    }

    public Department(Integer id, String departmentName, List<Employee> emps) {
        this.id = id;
        this.departmentName = departmentName;
        this.emps = emps;
    }

}
@Data
public class Employee {
    private Integer id;
    private String name;
    private Integer age;
    private Department dept;

    public Employee() {
    }

    public Employee(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
Dao与其映射配置文件

DepartmentMapper接口方法:

public interface DepartmentMapper {
    Department getDeptById(Integer id);
    Department getDeptByIdStep(Integer id);
}

建立DepartmentMapper接口与其mapper配置文件的映射:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//com.atguigu.mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.atguigu.mybatis.dao.DepartmentMapper">

    <!-- collection:分段查询 -->
    <resultMap type="com.atguigu.mybatis.bean.Department" id="MyDeptStep">
        <id column="id" property="id"/>
        <result column="departmentName" property="departmentName"/>
        <collection property="emps"
                    select="com.atguigu.mybatis.dao.EmployeeMapperPlus.getEmpsByDeptId"
                    column="{depId=id}"> <!--key=depId, value=id-->
        </collection>
    </resultMap>
    
    <!-- public Department getDeptByIdStep(Integer id); -->
    <select id="getDeptByIdStep" resultMap="MyDeptStep">
        select id, departmentName from department where id=#{id}
    </select>
    
    <!-- 扩展:多列的值传递过去:
         将多列的值封装map传递;
        	column="{key1=column1,key2=column2}"
   		 fetchType="lazy":表示使用延迟加载;
            - lazy:延迟
            - eager:立即
    -->

</mapper>

嵌套:EmployeeMapperPlus接口方法:用于查询某部门的员工信息

public interface EmployeeMapperPlus {
    List<Employee> getEmpsByDeptId(Integer paramInteger);
}

​ 建立EmployeeMapperPlus接口与其mapper配置文件的映射:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//com.atguigu.mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.mybatis.dao.EmployeeMapperPlus">
    <!--
    场景二:
        查询部门的时候将部门对应的所有员工信息也查询出来
     -->
    <!-- public List<Employee> getEmpsByDeptId(Integer deptId); -->
    <select id="getEmpsByDeptId" resultType="com.atguigu.mybatis.bean.Employee">
        select * from info where depId = #{depId}
    </select>

</mapper>
测试
    @Test
    public void CollectionTest() throws IOException {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        try {
            DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
            Department deptByIdStep = mapper.getDeptByIdStep(1); // 查询部门ID为1的部门信息

            System.out.println(deptByIdStep); // 打印部门信息

        } finally {
            sqlSession.close();
        }
    }
结果
[main] [com.atguigu.mybatis.dao.DepartmentMapper.getDeptByIdStep]-[DEBUG] ==>  Preparing: select id, departmentName from department where id=? 
[main] [com.atguigu.mybatis.dao.DepartmentMapper.getDeptByIdStep]-[DEBUG] ==> Parameters: 1(Integer)
[main] [com.atguigu.mybatis.dao.DepartmentMapper.getDeptByIdStep]-[DEBUG] <==      Total: 1
[main] [com.atguigu.mybatis.dao.EmployeeMapperPlus.getEmpsByDeptId]-[DEBUG] ==>  Preparing: select * from info where depId = ? 
[main] [com.atguigu.mybatis.dao.EmployeeMapperPlus.getEmpsByDeptId]-[DEBUG] ==> Parameters: 1(Integer)
[main] [com.atguigu.mybatis.dao.EmployeeMapperPlus.getEmpsByDeptId]-[DEBUG] <==      Total: 3
Department(id=1, departmentName=业务部, emps=[Employee{id=1001, name='Smith', age=23}, Employee{id=1003, name='Mary', age=21}, Employee{id=1005, name='Jack', age=23}])

discriminator鉴别器(了解)

判断某列的值,然后进行重新封装,改变原来的封装行为。

	<!-- =======================鉴别器============================ -->
	<!-- <discriminator javaType=""></discriminator>
		鉴别器:mybatis可以使用discriminator判断某列的值,然后根据某列的值改变封装行为
		封装Employee:
			如果查出的是女生:就把部门信息查询出来,否则不查询;
			如果是男生,把last_name这一列的值赋值给email;
	 -->
	 <resultMap type="com.atguigu.mybatis.bean.Employee" id="MyEmpDis">
	 	<id column="id" property="id"/>
	 	<result column="last_name" property="lastName"/>
	 	<result column="email" property="email"/>
	 	<result column="gender" property="gender"/>
         
	 	<!--
	 		column:指定判定的列名
	 		javaType:列值对应的java类型  -->
	 	<discriminator javaType="string" column="gender">
	 		<!--如果是女生,进入一种封装。resultType:指定封装的结果类型;不能缺少。/resultMap-->
            <!--这里使用的是分步查询,注意association的用法-->
	 		<case value="0" resultType="com.atguigu.mybatis.bean.Employee">
	 			<association property="dept" select="com.atguigu.mybatis.dao.DepartmentMapper.getDeptById"
			 		column="d_id"><!--绑定关联对象dept,并将id传到getDeptById接口方法映射的sql语句中-->
		 		</association>
	 		</case>
	 		<!--如果是男生,把last_name这一列的值赋值给email; 重新封装 -->
	 		<case value="1" resultType="com.atguigu.mybatis.bean.Employee">
		 		<id column="id" property="id"/>
			 	<result column="last_name" property="lastName"/>
			 	<result column="last_name" property="email"/>
			 	<result column="gender" property="gender"/>
	 		</case>
	 	</discriminator>
         
	 </resultMap>

五、MyBatis-动态SQL

​ 动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

是基于OGNL表达式的

<!-- 
    • if:判断
    • choose (when, otherwise):分支选择;带了break的swtich-case
        如果带了id就用id查,如果带了lastName就用lastName查;只会进入其中一个
    • trim 字符串截取(where(封装查询条件), set(封装修改条件))
    • foreach 遍历集合
-->

1. if

	 <!-- 查询员工,要求,携带了哪个字段查询条件就带上这个字段的值,也就是拼接sql语句 -->
	 <!-- public List<Employee> getEmpsByConditionIf(Employee employee); -->
	 <select id="getEmpsByConditionIf" resultType="com.atguigu.mybatis.bean.Employee">
	 	select * from tbl_employee where
		 	<!-- test:判断表达式(OGNL)
		 	OGNL参照PPT或者官方文档。
		 	  	 c:if  test
		 	从参数中取值进行判断
		 	
		 	遇见特殊符号应该去写转义字符:
		 	&&:&amp;&amp;
		 	-->
		 	<if test="id!=null">
		 		id=#{id}
		 	</if>
		 	<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
		 		and last_name like #{lastName}
		 	</if>
		 	<if test="email!=null and email.trim()!=&quot;&quot;">
		 		and email=#{email}
		 	</if> 
		 	<!-- ognl会进行字符串与数字的转换判断  "0"==0 -->
		 	<if test="gender==0 or gender==1">
		 	 	and gender=#{gender}
		 	</if>
	 </select>

2. where

引出问题:

​ 在上一节中,如果在测试方法中,没有传入id 的值但传入了lastName的值,那么就会出现类似于:

... where and last_name like #{lastName} 

​ 这样的sql语句,肯定是错误的。两个解决方法:

​ 第一种:第一条语句where后面加上 1 = 1,而下面的每条if标签,最前面都写上and,如下

	 <select id="getEmpsByConditionIf" resultType="com.atguigu.mybatis.bean.Employee">
	 	select * from tbl_employee where 1 = 1
		 	<if test="id!=null">
		 		and id=#{id}
		 	</if>
		 	<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
		 		and last_name like #{lastName}
		 	</if>
		 	<if test="email!=null and email.trim()!=&quot;&quot;">
		 		and email=#{email}
		 	</if> 
		 	<!-- ognl会进行字符串与数字的转换判断  "0"==0 -->
		 	<if test="gender==0 or gender==1">
		 	 	and gender=#{gender}
		 	</if>
	 </select>

第二种:MyBatis推荐!使用where标签

用where标签会将所有的查询条件包括在内,MyBatis就会对where标签中拼接的sql语句进行处理,将sql语句多出来的and或or去掉。(仅限于sql语句的开头,即where标签中只会去掉第一个多出来的and或者or)

	 <select id="getEmpsByConditionIf" resultType="com.atguigu.mybatis.bean.Employee">
	 	select * from tbl_employee
	 	<!-- where -->
	 	<where>
		 	<if test="id!=null">
		 		id=#{id}
		 	</if>
		 	<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
		 		and last_name like #{lastName}
		 	</if>
		 	<if test="email!=null and email.trim()!=&quot;&quot;">
		 		and email=#{email}
		 	</if> 
		 	<!-- ognl会进行字符串与数字的转换判断  "0"==0 -->
		 	<if test="gender==0 or gender==1">
		 	 	and gender=#{gender}
		 	</if>
	 	</where>
	 </select>

3. trim_自定义字符串截取

​ 在上一节的情况下,如果用户在if标签的内的sql语句的末尾加上and,即使在where标签内,mybatis并不会处理sql语句尾巴的and,此时会出现sql语句的错误:

	 <select id="getEmpsByConditionIf" resultType="com.atguigu.mybatis.bean.Employee">
	 	select * from tbl_employee
	 	<!-- where -->
	 	<where>
		 	<if test="id!=null">
		 		id=#{id}
		 	</if>
		 	<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
		 		and last_name like #{lastName}
		 	</if>
		 	<if test="email!=null and email.trim()!=&quot;&quot;">
		 		and email=#{email} and
		 	</if> 
		 	<!-- ognl会进行字符串与数字的转换判断  "0"==0 -->
		 	<if test="gender==0 or gender==1">
		 	 	and gender=#{gender}
		 	</if>
	 	</where>
	 </select>

​ 若id、lastName和gender均为空,而email有值,则sql:

... where last_name like #{lastName} and

​ sql出错!

trim标签的介绍:

	 <!--public List<Employee> getEmpsByConditionTrim(Employee employee);  -->
	 <select id="getEmpsByConditionTrim" resultType="com.atguigu.mybatis.bean.Employee">
	 	select * from tbl_employee
	 	<!-- 后面多出的and或者or where标签不能解决 
		trim标签的四个属性:
			prefix="":前缀:trim标签体中是整个字符串拼串后的结果。
					加前缀,prefix给拼串后的整个字符串加一个前缀 
			prefixOverrides="":
					去前缀,去掉整个字符串前面多余的字符
			suffix="":后缀
					加后缀,suffix给拼串后的整个字符串的结果加一个后缀 
			suffixOverrides=""
					去后缀,去掉整个字符串后面多余的字符
	 	-->
	 	<!-- 自定义字符串的截取规则 -->
	 	<trim prefix="where" suffixOverrides="and">
	 		<if test="id!=null">
		 		id=#{id} and
		 	</if>
		 	<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
		 		last_name like #{lastName} and
		 	</if>
		 	<if test="email!=null and email.trim()!=&quot;&quot;">
		 		email=#{email} and
		 	</if> 
		 	<!-- ognl会进行字符串与数字的转换判断  "0"==0 -->
		 	<if test="gender==0 or gender==1">
		 	 	gender=#{gender}
		 	</if>
		 </trim>
	 </select>

4. choose_分支选择

​ 相当于Java中的 switch-case

	 <!-- public List<Employee> getEmpsByConditionChoose(Employee employee); -->
	 <select id="getEmpsByConditionChoose" resultType="com.atguigu.mybatis.bean.Employee">
	 	select * from tbl_employee 
	 	<where>
	 		<!-- 如果带了id就用id查,如果带了lastName就用lastName查;只会进入其中一个 -->
	 		<choose>
	 			<when test="id!=null">
	 				id=#{id}
	 			</when>
	 			<when test="lastName!=null">
	 				last_name like #{lastName}
	 			</when>
	 			<when test="email!=null">
	 				email = #{email}
	 			</when>
	 			<otherwise>
	 				gender = 0
	 			</otherwise>
	 		</choose>
	 	</where>
	 </select>

​ 如果在测试方法中,传入了多个参数,此时就类似于switch-case一样,哪个参数在前面先判断,只会进入一个分支,会选择该参数条件成立下的sql语句,并拼接执行,结束。

5. set与if结合的动态更新

原来的更新,是写死了,必须提供sql语句set后的字段的值,下面举个例子:

    <!--    void updateEmp(Employee employee);-->
    <update id="updateEmp" >
        update info
        set name=#{name}, age=#{age}
        where id=#{id}
    </update>

现在要实现动态更新,即测试方法中,只需要提供需要更新的字段属性名称,实现效果:

第一种方法:使用set标签

	 <!--public void updateEmp(Employee employee);-->
	 <update id="updateEmp">
	 	<!-- Set标签的使用 -->
	 	update info 
		<set>
			<if test="lastName!=null">
				name=#{name},
			</if>
			<if test="email!=null">
				age=#{age}
			</if>
		</set>
		where id=#{id} 
	 </update>

set标签会自动去掉sql语句末尾多的逗号,如果有逗号的话。

第二种方法:使用trim标签

​ 添加前缀set,后缀覆盖,去掉所有的逗号

	 <!--public void updateEmp(Employee employee);-->
	 <update id="updateEmp">
		update info 
		<trim prefix="set" suffixOverrides=","><!--增加前缀set,去掉后缀逗号-->
			<if test="lastName!=null">
				last_name=#{lastName},
			</if>
			<if test="email!=null">
				email=#{email},
			</if>
			<if test="gender!=null">
				gender=#{gender}
			</if>
		</trim>
		where id=#{id}
	 </update>

6. foreach_遍历集合

	 <!--public List<Employee> getEmpsByConditionForeach(List<Integer> ids);  -->
	 <select id="getEmpsByConditionForeach" resultType="com.atguigu.mybatis.bean.Employee">
	 	select * from tbl_employee
	 	<!--
	 		collection:指定要遍历的集合:
	 			list类型的参数会特殊处理封装在map中,map的key就叫list

	 		item:将当前遍历出的元素赋值给指定的变量
	 		separator:每个元素之间的分隔符
	 		open:遍历出所有结果拼接一个开始的字符(以什么开始)
	 		close:遍历出所有结果拼接一个结束的字符(以什么结束)

	 		index:索引。遍历list的时候是index就是索引,item就是当前值
	 				      遍历map的时候index表示的就是map的key,item就是map的值
	 		
	 		#{变量名}就能取出变量的值也就是当前遍历出的元素
	 	  -->
	 	<foreach collection="ids" item="item_id" separator=","
	 		open="where id in(" close=")">
	 		#{item_id}
	 	</foreach>
         
	 </select

想实现的动态sql效果:

select * from tbl_employee where id in(?, ..., ?)

用户传入参数:两个或多个
select * from tbl_employee where id in(1, 2)
select * from tbl_employee where id in(2, 3, 4)

7. MySQL下foreach批量插入的两种方式

实现一条sql语句插入多个value值,根据用户输入的value值数目,进行动态插入。

2021-11-05_223421

第一种方式:有多少条想插入的记录,就执行多少次insert插入语句,或者执行一次,一次插入多条记录

注意:

第一种方法:执行多条插入语句的方法中,foreeach间隔符是分号 ; ,因为多条插入语句sql用分号进行隔开。

第二种方法:而执行一条插入语句但values有多条记录的方法中,foreeach的间隔符是逗号, ,因为是分隔多条value记录。

第一种方式:需要这种方式需要数据库连接属性allowMultiQueries=true

2021-11-05_224559

	 <!-- 批量保存 -->
	 <!--public void addEmps(@Param("emps")List<Employee> emps);  -->
<insert id="addEmps">
	<foreach collection="emps" item="emp" separator=";"><!--注意这里的间隔符是分号-->
		insert into tbl_employee(last_name,email,gender,d_id)
		values(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
	</foreach>
</insert>
2021-11-05_223952

第二种方式:推荐这种方法,效率更高!

<insert id="addEmps">
	insert into tbl_employee() 
    values
	<foreach collection="emps" item="emp" separator=","><!--注意这里的间隔符是逗号-->
		(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
	</foreach>
</insert

8. Oracle下foreach批量插入的两种方式(了解)

	 <!-- Oracle数据库批量保存: 
	 	Oracle不支持values(),(),()  即,不支持一条sql语句插入多个value记录
	 	Oracle支持的批量方式
	 	1、多个insert放在begin - end里面
	 		begin
			    insert into employees(employee_id,last_name,email) 
			    values(employees_seq.nextval,'test_001','test_001@atguigu.com');
			    insert into employees(employee_id,last_name,email) 
			    values(employees_seq.nextval,'test_002','test_002@atguigu.com');
			end;
		2、利用中间表:
			insert into employees(employee_id,last_name,email)
		       select employees_seq.nextval,lastName,email from(
		              select 'test_a_01' lastName,'test_a_e01' email from dual
		              union
		              select 'test_a_02' lastName,'test_a_e02' email from dual
		              union
		              select 'test_a_03' lastName,'test_a_e03' email from dual
		       )	
	 -->

	<!-- oracle第一种批量方式 -->
	 <insert id="addEmps" databaseId="oracle">
         <!-- 其实就是把多条插入sql语句一起执行 -->
	 	<foreach collection="emps" item="emp" open="begin" close="end;">
	 		insert into employees(employee_id,last_name,email) 
			    values(employees_seq.nextval,#{emp.lastName},#{emp.email});
	 	</foreach>
     </insert>
	 	
	<!-- oracle第二种批量方式  -->
	<insert id="addEmps" databaseId="oracle">
	 	insert into employees(employee_id,last_name,email)
        	select employees_seq.nextval,lastName,email from(
	 		<foreach collection="emps" item="emp" separator="union">
	 			select #{emp.lastName} lastName,#{emp.email} email from dual
	 		</foreach>
        )
	 </insert>
	<--或者-->
     <insert id="addEmps" databaseId="oracle"> 
         insert into employees(employee_id,last_name,email)
	 		<foreach collection="emps" item="emp" separator="union"
	 			open="select employees_seq.nextval,lastName,email from("
	 			close=")">
	 			select #{emp.lastName} lastName,#{emp.email} email from dual
	 		</foreach>
	 </insert> 

9. _parameter & _databaseId & bind

	 <!-- 两个内置参数:
	 	不只是方法传递过来的参数或者取值,可以被用来判断
	 	mybatis默认还有两个内置参数,可以被使用或者取值:
            _parameter:代表整个参数
                单个参数:_parameter就是这个参数
                多个参数:参数会被封装为一个map,那么_parameter就是代表这个map

            _databaseId:如果配置了databaseIdProvider标签。
                _databaseId就是代表当前数据库的别名oracle
	  -->
	  
	  <!--public List<Employee> getEmpsTestInnerParameter(Employee employee);-->
	  <select id="getEmpsTestInnerParameter" resultType="com.atguigu.mybatis.bean.Employee">
	  		<!-- bind:可以将OGNL表达式的值绑定到一个变量中,方便后来引用这个变量的值 
				但是模糊查询的话,建议在测试方法中,直接传一个模糊字符串
			-->
	  		<bind name="_lastName" value="'%'+lastName+'%'"/>
          	<!--可以判断当前数据库类型,然后执行对应的sql语句,不用分开单独写多个方法-->
	  		<if test="_databaseId=='mysql'">
	  			select * from tbl_employee
	  			<if test="_parameter!=null"><!--这里可以使用 '%${lastName}%' ,但使用$存在SQL注入的风险,所以要使用#{},就需要使用bind标签-->
	  				where last_name like #{_lastName}
	  			</if>
	  		</if>
	  		<if test="_databaseId=='oracle'">
	  			select * from employees
	  			<if test="_parameter!=null">
	  				where last_name like #{_parameter.lastName}
	  			</if>
	  		</if>
	  </select>

10. sql_抽取可重用的sql片段

定义sql抽取:

	  <!-- 
	  	抽取可重用的sql片段。方便后面引用 
	  	1、sql抽取:经常将要查询的列名,或者插入用的列名抽取出来方便引用
	  	2、include来引用已经抽取的sql:
	  	3、include还可以自定义一些property,sql标签内部就能使用自定义的属性
	  			include-property:取值的正确方式${prop},
	  			#{不能使用这种方式}
	  -->
	  <sql id="insertColumn">
	  		<if test="_databaseId=='oracle'">
	  			employee_id,last_name,email
	  		</if>
	  		<if test="_databaseId=='mysql'">
	  			last_name,email,gender,d_id
	  		</if>
	  </sql>

使用示例:

	<insert id="addEmps">
	 	insert into tbl_employee(
	 		<include refid="insertColumn"></include>
	 	) 
		values
		<foreach collection="emps" item="emp" separator=",">
			(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
		</foreach>
	 </insert>

六、MyBatis-缓存机制

​ MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。缓存可以极大的提升查询效率。

MyBatis系统中默认定义了两级缓存。

一级缓存和二级缓存。 

​ 1、默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本地缓存)开启。

​ 2、二级缓存需要手动开启和配置,他是基于namespace级别的缓存。

	3、为了提高扩展性。MyBatis定义了缓存接口Cache。我们可以通过**实现Cache接口来自定**

义二级缓存。

1. 体验封装的存在

2021-11-06_111703

都是来查1号员工,最后判断输出 true,说明emp02是直接使用之前查询的结果,使用了缓存。

2. 两级缓存

两级缓存:

一级缓存:(本地缓存)sqlSession级别的缓存,一级缓存是一直开启的;类似于一个Map,查询之前去看看这个要查的数据,有没有在map中。

​ 与数据库同一次会话期间查询到的数据,将会放在本地缓存中

​ 以后如果需要获取相同数据的话,直接就会去缓存中拿去,就没必要再去访问数据库

​ 如果开启新的sqlSession,那么两个sqlSession会有属于自己的缓存

​ 一级缓存失效的四种情况:(没有使用到当前一级缓存的情况,还需要再向数据库发出查询)

​ 1. sqlSession不同,数据存放在各自的缓存中

​ 2. sqlSession相同,查询条件不同(当前缓存中没有这个数据)

​ 3. sqlSession相同,但两次查询之间执行了增删改操作(因为这次增删改操作可能会对当前数据有影响,所以不能选择还继续用缓存中的数据)

​ 4. sqlSession相同,但手动清除了一级缓存。( openSession.clearCache() )

二级缓存:(全局缓存)基于namespace级别的缓存,一个namespace对应一个二级缓存。

​ 工作机制:

​ 1. 一个会话,查询一条数据,这个数据就会被放在当前会话的一级缓存中

​ 2. 如果会话关闭,那么一级缓存中的数据会被保存到二级缓存中,新的会话查询信息,就可以先去参照二级缓存

​ 3. sqlSession == EmployeeMapper ==》Employee数据

​ DepartmentMapper==》Department数据

​ 不同namespace查出来的数据,会放在自己对应的缓存中,如上面的两个mapper配置文件,由于定义了各自的命名空间,所以也就是两个二级缓存(底层实现其实是map)

3. 二级缓存使用&细节

​ 默认是开启二级缓存的,但为了防止被版本更新改为默认关闭,所以还是最好在MyBatis全局配置中手动开启。

​ 需要注意的,只有会话关闭之后,一级缓存的数据才会写入到二级缓存中去。

使用步骤:

​ 第一步:在MyBatis全局配置文件中,显示开启全局二级缓存配置

    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

​ 第二步:在mapper配置xml文件中,配置使用二级缓存

<mapper namespace="com.atguigu.mybatis.dao.DepartmentMapper">
    <cache></cache>
</mapper>

​ cache标签属性:

	<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
	<!-- <cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache> -->
	<!--  
	eviction:缓存的回收策略:
		• LRU – 最近最少使用的:移除最长时间不被使用的对象。
		• FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
		• SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
		• WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
		• 默认的是 LRU。
	flushInterval:缓存刷新间隔
		缓存多长时间清空一次,默认不清空,设置一个毫秒值
	readOnly:是否只读:
		true:只读;mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。
				 mybatis为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,速度快
		false:非只读:mybatis觉得获取的数据可能会被修改。
				mybatis会利用序列化&反序列的技术克隆一份新的数据给你。安全,速度慢
	size:缓存存放多少元素;
	type="":指定自定义缓存的全类名;
			实现Cache接口即可;
	-->

和缓存有关的设置/属性:

​ 1)默认cacheEnabled=true;

​ 如果false的话:关闭缓存(二级缓存关闭,一级缓存一直可用)

​ 2)每个select标签都默认useCache="true";

​ 如果false的话:不使用缓存(二级缓存关闭,一级缓存依然可以使用)

​ 3)每个增删改标签默认flushCache="true";查询标签是默认为false,查询完不清空缓存

​ 增删改执行完成后就会清除缓存

​ 如果flushCache="false"的话, 一级缓存、二级缓存都会被清空,虽然去查了,也就是命中了key,但没有对应value值

​ 4)openSession.clearCache();只会清除当前sqlSession的一级缓存

​ 5)localCacheScope:全局设置setting中的属性,本地缓存作用域(相当于一级缓存)

​ SESSION:当前会话的所有数据都会被保存到一级缓存中

​ STATEMENT:禁用一级缓存

第三步:我们的POJO需要实现序列化接口

测试一级缓存的测试方法:

  @Test
  public void testFirstLevelCache() throws IOException {
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    SqlSession openSession = sqlSessionFactory.openSession();
    try {
      EmployeeMapper mapper = (EmployeeMapper)openSession.getMapper(EmployeeMapper.class);
        
      Employee emp01 = mapper.getEmpById(Integer.valueOf(1));
      System.out.println(emp01);
        
      Employee emp02 = mapper.getEmpById(Integer.valueOf(1));
      System.out.println(emp02);
        
      System.out.println((emp01 == emp02));
    } finally {
      openSession.close();
    } 
  }

如果一级缓存开启的话,最终结果会打印 true 。

测试二级缓存的测试方法:

  @Test
  public void testSecondLevelCache() throws IOException {
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    SqlSession openSession = sqlSessionFactory.openSession();
    SqlSession openSession2 = sqlSessionFactory.openSession();
    EmployeeMapper mapper = (EmployeeMapper)openSession.getMapper(EmployeeMapper.class);
    EmployeeMapper mapper2 = (EmployeeMapper)openSession2.getMapper(EmployeeMapper.class);
      
    Employee emp01 = mapper.getEmpById(Integer.valueOf(1));
    System.out.println(emp01);
    openSession.close();
      
    Employee emp02 = mapper2.getEmpById(Integer.valueOf(1));
    System.out.println(emp02);
    openSession2.close();
  }

如果二级缓存开启的话,那么在debug打印的信息中,可以发现只执行了一条sql语句并显示有命中信息,也就是第二条语句去查询的时候,命中了二级缓存中的数据。

需要注意的是,由于sqlSession会话只有关闭的时候,才会把一级缓存的数据写入到二级缓存中,因为如果将上面测试方法中两个session会话给放到最后查询执行完才关闭的话,那么debug显示出来的信息是执行了两条sql语句。

4. 缓存原理示意图

2021-11-06_171428

5. 第三方缓存框架EhCache整合

MyBatis缓存的底层就是map,由于在MyBatis中cache是接口类型,往往需要使用第三方强大的缓存框架进行整合。

EhCache:

​ EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认

CacheProvider。Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。

  Spring 提供了对缓存功能的抽象:即允许绑定不同的缓存解决方案(如Ehcache),但本身

不直接提供缓存功能的实现。它支持注解方式使用缓存,非常方便。

​ pom.xml引入依赖:

<!-- https://mvnrepository.com/artifact/net.sf.ehcache/ehcache -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.6</version>
</dependency>

EhCache入门

七、MyBatis-Spring整合(SSM整合)

后面会更新SSM整合的学习文档,翻翻我博客看看,如果没有就说明还没有更新

八、MyBatis-逆向工程(代码生成器)

MyBatis Generator:

​ 简称MBG,是一个专门为MyBatis框架使用者定制的代码生成器,可以快速的根据表生成对

应的映射文件、接口、以及bean类。支持基本的增删改查,以及QBC风格的条件查询。但是表连

接、 存储过程等这些复杂sql的定义需要我们手工编写。

maven导入依赖:

<!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core -->
<dependency>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-core</artifactId>
    <version>1.3.7</version>
</dependency>

简单来说,就是能够根据数据库的表,然后生成对应的JavaBean、mapper接口方法以及sql与接口映射配置文件。

1. MBG配置文件

注意:

1. 根据项目需求生成不同级别的CRUD版本
2. 注意生成文件的输出地址
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <properties resource="jdbc.properties"></properties>
    <!--
        targetRuntime="MyBatis3Simple":生成简单版的CRUD
        MyBatis3:豪华版 用于复杂的数据库CRUD操作
     -->
    <context id="DB2Tables" targetRuntime="MyBatis3Simple">
        <!-- jdbcConnection:指定如何连接到目标数据库 -->
        <jdbcConnection driverClass="${jdbc.driver}"
                        connectionURL="${jdbc.url}"
                        userId="${jdbc.username}"
                        password="${jdbc.password}">
        </jdbcConnection>

        <!--类型解析器-->
        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <!-- javaModelGenerator:指定javaBean的生成策略
            targetPackage="test.model":目标包名
            targetProject="\MBGTestProject\src":目标工程
        -->
        <javaModelGenerator targetPackage="com.atguigu.mybatis.bean"
                            targetProject=".\src\main\java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <!-- sqlMapGenerator:sql语句与接口方法映射生成策略 xml配置文件 -->
        <sqlMapGenerator targetPackage="resources"
                         targetProject=".\src\main\">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <!-- javaClientGenerator:指定mapper接口方法所在的位置 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.atguigu.mybatis.dao"
                             targetProject=".\src\main\java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>

        <!-- 指定要逆向分析数据库中的哪些表:根据表要创建javaBean -->
        <table tableName="department" domainObjectName="Department"></table>
        <table tableName="info" domainObjectName="Employee"></table>
    </context>
</generatorConfiguration>

2. 执行逆向生成

    @Test
    public void testMBG() throws XMLParserException, IOException, InvalidConfigurationException, SQLException, InterruptedException {
        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        File configFile = new File("src/main/resources/mbg.xml");
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        myBatisGenerator.generate(null);
    }

3. 测试

targetRuntime="MyBatis3Simple"

效果展示:

​ 主要看接口类,生成了哪些支持的CRUD操作:

2021-11-06_221422

​ sql映射配置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.atguigu.mybatis.dao.DepartmentMapper">
  <resultMap id="BaseResultMap" type="com.atguigu.mybatis.bean.Department">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat Nov 06 21:39:59 CST 2021.
    -->
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="departmentName" jdbcType="VARCHAR" property="departmentname" />
  </resultMap>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat Nov 06 21:39:59 CST 2021.
    -->
    delete from department
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.atguigu.mybatis.bean.Department">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat Nov 06 21:39:59 CST 2021.
    -->
    insert into department (id, departmentName)
    values (#{id,jdbcType=INTEGER}, #{departmentname,jdbcType=VARCHAR})
  </insert>
  <update id="updateByPrimaryKey" parameterType="com.atguigu.mybatis.bean.Department">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat Nov 06 21:39:59 CST 2021.
    -->
    update department
    set departmentName = #{departmentname,jdbcType=VARCHAR}
    where id = #{id,jdbcType=INTEGER}
  </update>
  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat Nov 06 21:39:59 CST 2021.
    -->
    select id, departmentName
    from department
    where id = #{id,jdbcType=INTEGER}
  </select>
  <select id="selectAll" resultMap="BaseResultMap">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat Nov 06 21:39:59 CST 2021.
    -->
    select id, departmentName
    from department
  </select>
</mapper>

测试查询所有记录:

    @Test
    public void test() throws IOException {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
            List<Employee> list = mapper.selectAll();
            System.out.println(list);
        } finally {
            sqlSession.close();
        }
    }

结果:

[main] [com.atguigu.mybatis.dao.EmployeeMapper.selectAll]-[DEBUG] ==>  Preparing: select id, name, age, depId from info
[main] [com.atguigu.mybatis.dao.EmployeeMapper.selectAll]-[DEBUG] ==> Parameters: 
[main] [com.atguigu.mybatis.dao.EmployeeMapper.selectAll]-[DEBUG] <==      Total: 6
[Employee{id=1001, name='Smith', age=23, depid=1}, Employee{id=1002, name='Tom', age=23, depid=2}, Employee{id=1003, name='Mary', age=21, depid=1}, Employee{id=1004, name='Dick', age=21, depid=2}, Employee{id=1005, name='Jack', age=23, depid=1}, Employee{id=1006, name='James', age=26, depid=2}]

targetRuntime="MyBatis3"

支持复杂的CRUD操作

2021-11-06_221854

与simple的最明显差别,就是多了个两个Example的bean对象。

支持的操作:

2021-11-06_222053

2021-11-06_222117

测试:

  @Test
  public void testMyBatis3() throws IOException {
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    SqlSession openSession = sqlSessionFactory.openSession();
    try {
      EmployeeMapper mapper = (EmployeeMapper)openSession.getMapper(EmployeeMapper.class);
      // xxxExample这个类,是用来封装查询条件的
      EmployeeExample example = new EmployeeExample();
        
      // criteria是用来拼接查询条件的
      EmployeeExample.Criteria criteria = example.createCriteria();
      // 拼接Name的模糊查询,%a%
      criteria.andNameLike("%a%"); // 查询员工名字中有字母a的
      // 拼接部门id为1
      criteria.andDepidEqualTo("1"); // 查询部门id为1的
      
      // 如果查询条件由or连接两个条件组成,则还需要创建criteria用来拼接另一个条件
      EmployeeExample.Criteria criteria2 = example.createCriteria();
      criteria2.andAgeBetween(22, 23); // age在22到23岁
      
      // 用 or 连接
      example.or(criteria2);
      
      List<Employee> list = mapper.selectByExample(example);
	  System.out.println(list);
    } finally {
      openSession.close();
    } 
  }

最后的sql语句如下:

2021-11-06_223738

通过上面的例子,就可以举一反三,使用框架进行复杂的CRUD操作。

九、MyBatis-运行原理

1. 框架分层架构

2021-11-07_103343

2. SQLSessionFactory的初始化

		// 获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
2021-11-07_111302

测试过程:

2021-11-07_111516

1)进入SqlSessionFactoryBuilder()

2021-11-07_111625

2)调用SqlSessionFactoryBuilder().build(),创建解析器parser,进入XMLConfigBuilder(),对配置文件进行解析

2021-11-07_111816

2021-11-07_113056

2021-11-07_112007

3)XMLConfigBuilder()中,调用parseConfiguration()方法

2021-11-07_112054

解析全局配置文件,其中this.mapperElement将会解析mapper映射配置文件

2021-11-07_112807

2021-11-07_115459

创建mapperParse解析器,然后调用parse()方法解析映射xml配置文件:

2021-11-07_115752

configuration对象保存了所有配置文件的详细信息:

2021-11-07_113425

3. openSession获取SqlSession对象

        // 2.获取SqlSession实例,与数据库建立会话
        SqlSession sqlSession = sqlSessionFactory.openSession();
2021-11-08_162623

4. getMapper获取到接口的代理对象

            // 3. 获取接口的实现类对象
            EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);

2021-11-08_163922

5. 查询实现

            Employee emp = mapper.getEmpById(1001);

底层其实是JDBC操作,MyBatis对其进行封装

图片1

查询流程总结:重要

2021-11-09_100710

首先,创建一个代理对象进行增删改查操作,但实际上这个代理对象是用DefaultSqlSession进行增删改查,而这个DefaultSqlSession是通过Executor进行增删改查,而Executor会创建一个StatementHandler对象,用于处理sql语句、预编译和设置参数等相关工作。StatementHandler对象在进行处理工作的时候,创建了ParameterHandler和ResultSetHandler,前者是用来设置预编译参数用的,执行操作,此时,后者是用来处理执行返回的结果集。而设置参数和处理结果集执行,实际上是二者是通过TypeHandler进行设置参数并执行。而TypeHandler实际执行,是通过调用底层原生JDBC操作。

2021-11-09_101347

十、MyBatis-插件

2021-11-07_145502

2021-11-07_150939

拦截目标对象,用插件为目标对象创建一个动态代理,然后使用AOP的思想,通过动态代理对目标对象的目标方法进行拦截,同时对目标方法可以进行增强的操作,然后执行。从而实现控制sql语句执行的过程。

十一、MyBatis-扩展

1. PageHelper插件进行分页

MyBatis 分页插件 PageHelper

首先需要在全局配置文件中添加依赖:

    <!--
    plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
    properties?, settings?,
    typeAliases?, typeHandlers?,
    objectFactory?,objectWrapperFactory?,
    plugins?,
    environments?, databaseIdProvider?, mappers?
    -->
    <plugins>
        <!-- com.github.pagehelper为PageHelper类所在包名 -->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
            <property name="param1" value="value1"/>
        </plugin>
    </plugins>

测试方法:

    @Test
    public void testSelectAll() throws IOException {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession();

        try {
            EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);

            Page<Object> page = PageHelper.startPage(3, 2);

            List<Employee> list = mapper.getEmpAll();

            // 传入要连续显示多少页
            PageInfo<Employee> info = new PageInfo<>(list, 2);

            for (Employee employee : list) {
                System.out.println(employee);
            }
            System.out.println("当前页码:" + page.getPageNum());
            System.out.println("总记录数:" + page.getTotal());
            System.out.println("每页记录数:" + page.getPageSize());
            System.out.println("总页码:" + page.getPages());

            System.out.println();
            System.out.println("当前页码:" + info.getPageNum());
            System.out.println("总记录数:" + info.getTotal());
            System.out.println("每页记录数:" + info.getPageSize());
            System.out.println("总页码:" + info.getPages());
            System.out.println("是否是第一页:" + info.isIsFirstPage());
            System.out.println("连续显示的页码:");
            int[] nums = info.getNavigatepageNums();
            for (int i : nums) {
                System.out.println(i);
            }

        } finally {
            sqlSession.close();
        }
    }

查询结果展示:

[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll_COUNT]-[DEBUG] ==>  Preparing: SELECT count(0) FROM info
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll_COUNT]-[DEBUG] ==> Parameters: 
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll_COUNT]-[DEBUG] <==      Total: 1
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll]-[DEBUG] ==>  Preparing: select * from info LIMIT ?, ?
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll]-[DEBUG] ==> Parameters: 4(Long), 2(Integer)
[main] [com.atguigu.mybatis.dao.EmployeeMapper.getEmpAll]-[DEBUG] <==      Total: 2
Employee{id=1005, name='Jack', age=23}
Employee{id=1006, name='James', age=26}
当前页码:3
总记录数:6
每页记录数:2
总页码:3

当前页码:3
总记录数:6
每页记录数:2
总页码:3
是否是第一页:false
连续显示的页码:
2
3

2. 批量操作

openSession(ExecutorType.BATCH); 开启一个批处理的session

  @Test
  public void testBatch() throws IOException {
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    
      // 在创建sqlSession的时候,设置这是一个批处理的session
    SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
      
    long start = System.currentTimeMillis();
    try {
      EmployeeMapper mapper = (EmployeeMapper)openSession.getMapper(EmployeeMapper.class);
      for (int i = 0; i < 10000; i++)
        mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1")); 
      openSession.commit();
      long end = System.currentTimeMillis();
      System.out.println("+ (end - start));
    } finally {
      openSession.close();
    } 
  }

非批处理:执行流程是,每进行一次插入语句,就 发送sql语句—》预编译设置参数—》执行 ,每一次的插入都是一次完成的数据库访问操作,消耗大量的预编译时间。

批处理:发一条sql语句给数据库,数据库预编译好之后,批处理不断传参数,数据库只需要等待这条sql语句需要执行多少遍的参数,然后再一次性执行。

posted @ 2021-11-09 19:01  Komorebi_WH  阅读(144)  评论(0编辑  收藏  举报