Spring系列:MyBatis详解和与Spring整合

Spring系列:MyBatis详解和与Spring整合

MyBatis 简介

概述

MyBatis 是一个优秀的基于 Java 的持久层框架,它内部封装了 JDBC,使开发者只需关注 SQL 语句本身,而不用再花费精力去处理诸如注册驱动、创建 Connection、配置 Statement 等繁杂过程。

Mybatis 通过 xml 或注解的方式将要执行的各种 Statement(Statement、PreparedStatement 等)配置起来,并通过 Java 对象和 Statement 中 SQL 的动态参数进行映射生成最终执行的 SQL 语句,最后由 MyBatis 框架执行 SQL 并将结果映射成 Java 对象并返回。

在这里插入图片描述

MyBatis 与 Hibernate

Hibernate 框架是提供了全面的数据库封装机制的 “全自动” ORM,即实现了 POJO 和数据库表之间的映射,以及 SQL 的自动生成和执行。

相对于此,MyBatis 只能算作是 “半自动” ORM。其着力点,是在 POJO 类与 SQL 语句之间的映射关系。也就是说,MyBatis 并不会为程序员自动生成 SQL 语句。具体的 SQL 需要程序员自己编写,然后通过 SQL 语句映射文件,将 SQL 所需的参数,以及返回的结果字段映射到指定 POJO。因此,MyBatis 成为了“全自动”ORM 的一种有益补充。

MyBatis 的特点

  • 在 XML 文件中配置 SQL 语句,实现了 SQL 语句与代码的分离,给程序的维护带来了很大便利。
  • 因为需要程序员自己去编写 SQL 语句,程序员可以结合数据库自身的特点灵活控制 SQL 语句,因此能够实现比 Hibernate 等全自动 ORM 框架更高的查询效率,能够完成复杂查询。
  • 简单,易于学习,易于使用,上手快。

在这里插入图片描述

Druid 简介

概述

Druid 是阿里巴巴开源平台上的一个项目,整个项目由数据库连接池、插件框架和 SQL 解析器组成。该项目主要是为了扩展 JDBC 的一些限制,可以让程序员实现一些特殊的需求,比如向密钥服务请求凭证、统计 SQL 信息、SQL 性能收集、SQL 注入检查、SQL 翻译等,程序员可以通过定制来实现自己需要的功能。

各种连接池性能对比测试

测试执行申请归还连接 1,000,000(一百万)次总耗时性能对比。

测试环境

环境版本
OSOS X 10.8.2
CPUIntel i7 2GHz 4 Core
JVMJava Version 1.7.0_05

基准测试结果

Jdbc Connection Pool1 thread2 threads5 threads10 threads20 threads50 threads
Druid8981,1911,3241,3621,3251,459
tomcat-jdbc1,2691,3782,0292,1031,8792,025
DBCP2,3245,0555,4465,4715,5245,415
BoneCP3,7383,1503,1945,68111,01823,125
jboss-datasource4,3772,9883,6803,98032,70837,742
C3P010,84113,63710,68211,05514,49720,351
Proxool16,33716,18718,310(Exception)25,94533,706(Exception)39,501 (Exception)

结论

  • Druid 是性能最好的数据库连接池,tomcat-jdbc 和 druid 性能接近。

  • proxool 在激烈并发时会抛异常,完全不靠谱。

  • c3p0 和 proxool 都相当慢,慢到影响 sql 执行效率的地步。

  • bonecp 性能并不优越,采用 LinkedTransferQueue 并没有能够获得性能提升。

  • 除了 bonecp,其他的在 JDK 7 上跑得比 JDK 6 上快

  • jboss-datasource 虽然稳定,但是性能很糟糕

Spring 整合 Druid

POM

pom.xml 文件中新增 com.alibaba:druidmysql:mysql-connector-java 依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.6</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.46</version>
</dependency>

配置数据库连接

配置数据库连接 jdbc.properties ,配置代码如下:

# JDBC
# MySQL 8.x: com.mysql.cj.jdbc.Driver
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.connectionURL=jdbc:mysql://192.168.137.128:3306/mydb?useUnicode=true&characterEncoding=utf-8&useSSL=false
jdbc.username=root
jdbc.password=123456

# JDBC Pool
jdbc.pool.init=1
jdbc.pool.minIdle=3
jdbc.pool.maxActive=20

# JDBC Test
jdbc.testSql=SELECT 'x' FROM DUAL

Spring 集成 Druid

创建一个名为 spring-context-druid.xml 的 Spring 配置文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 加载配置属性文件 -->
    <context:property-placeholder ignore-unresolvable="true" location="classpath:jdbc.properties"/>

    <!-- 数据源配置, 使用 Druid 数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
        <property name="driverClassName" value="${jdbc.driverClass}"/>

        <!-- 基本属性 url、user、password -->
        <property name="url" value="${jdbc.connectionURL}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${jdbc.pool.init}"/>
        <property name="minIdle" value="${jdbc.pool.minIdle}"/>
        <property name="maxActive" value="${jdbc.pool.maxActive}"/>

        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="60000"/>

        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>

        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>

        <property name="validationQuery" value="${jdbc.testSql}"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>

        <!-- 配置监控统计拦截的filters -->
        <property name="filters" value="stat"/>
    </bean>
</beans>

配置 Druid 监控中心

Druid 提供了大量的监控数据,只需要在 web.xml 中配置一个 Servlet 就可以方便的查看这些信息。

修改 web.xml 配置文件,增加 Druid 提供的 Servlet

<servlet>
    <servlet-name>DruidStatView</servlet-name>
    <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>DruidStatView</servlet-name>
    <url-pattern>/druid/*</url-pattern>
</servlet-mapping>

打开浏览器,输入:http://localhost:8080/druid/index.html 浏览器显示效果如下:

在这里插入图片描述

Spring 整合 MyBatis

POM

pom.xml 文件中增加 MyBatis 相关依赖:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.2.8</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>4.3.17.RELEASE</version>
</dependency>

主要增加了 3 个依赖,分别为 org.mybatis:mybatisorg.mybatis:mybatis-springorg.springframework:spring-jdbc

创建 MyBatis 配置文件

创建一个名为 mybatis-config.xml 的配置文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 全局参数 -->
    <settings>
        <!-- 打印 SQL 语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    
        <!-- 使全局的映射器启用或禁用缓存。 -->
        <setting name="cacheEnabled" value="false"/>

        <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
        <setting name="lazyLoadingEnabled" value="true"/>

        <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
        <setting name="aggressiveLazyLoading" value="true"/>

        <!-- 是否允许单条 SQL 返回多个数据集 (取决于驱动的兼容性) default:true -->
        <setting name="multipleResultSetsEnabled" value="true"/>

        <!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
        <setting name="useColumnLabel" value="true"/>

        <!-- 允许 JDBC 生成主键。需要驱动器支持。如果设为了 true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 default:false  -->
        <setting name="useGeneratedKeys" value="false"/>

        <!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不映射 PARTIAL:部分 FULL:全部  -->
        <setting name="autoMappingBehavior" value="PARTIAL"/>

        <!-- 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新) -->
        <setting name="defaultExecutorType" value="SIMPLE"/>

        <!-- 使用驼峰命名法转换字段。 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>

        <!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
        <setting name="localCacheScope" value="SESSION"/>

        <!-- 设置 JDBC 类型为空时,某些驱动程序 要指定值, default:OTHER,插入空值时不需要指定类型 -->
        <setting name="jdbcTypeForNull" value="NULL"/>
    </settings>
</configuration>

Spring 集成 MyBatis

创建一个名为 spring-context-mybatis.xml 的 Spring 配置文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 配置 SqlSession -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 用于配置对应实体类所在的包,多个 package 之间可以用 ',' 号分割 -->
        <property name="typeAliasesPackage" value="com.luxiu.single.shop.domain"/>
        <!-- 用于配置对象关系映射配置文件所在目录 -->
        <property name="mapperLocations" value="classpath:/mapper/**/*.xml"/>
        <property name="configLocation" value="classpath:/mybatis-config.xml"></property>
    </bean>

    <!-- 扫描 Mapper -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.luxiu.single.shop.web.admin.dao" />
    </bean>
</beans>

Spring 中多数据源配置

代码参考:https://github.com/luguangdong/spring-origin.git

引言

项目中我们经常会遇到多数据源的问题,尤其是数据同步或定时任务等项目更是如此。多数据源让人最头痛的,不是配置多个数据源,而是如何能灵活动态的切换数据源。例如在一个spring和hibernate的框架的项目中,我们在spring配置中往往是配置一个dataSource来连接数据库,然后绑定给sessionFactory,在dao层代码中再指定sessionFactory来进行数据库操作。

在这里插入图片描述

正如上图所示,每一块都是指定绑死的,如果是多个数据源,也只能是下图中那种方式。

在这里插入图片描述

可看出在Dao层代码中写死了两个SessionFactory,这样日后如果再多一个数据源,还要改代码添加一个SessionFactory,显然这并不符合开闭原则。

那么正确的做法应该是

在这里插入图片描述

配置

jdbc.properties

#============================#
#==== Database settings ====#
#============================#

# JDBC
# MySQL 8.x: com.mysql.cj.jdbc.Driver
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.connectionURL=jdbc:mysql://192.168.137.128:3306/myshop?useUnicode=true&characterEncoding=utf-8&useSSL=false
jdbc.username=root
jdbc.password=123456


jdbc2.driverClass=oracle.jdbc.driver.OracleDriver
jdbc2.connectionURL=jdbc:oracle:thin:@192.168.137.129:1521/orcl.oracle.com
jdbc2.username=luxiu
jdbc2.password=123456

# JDBC Pool
jdbc.pool.init=1
jdbc.pool.minIdle=3
jdbc.pool.maxActive=20

# JDBC Test
jdbc.testSql=SELECT 'x' FROM DUAL

spring-context-druid.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 加载配置属性文件 -->
    <context:property-placeholder ignore-unresolvable="true" location="classpath:jdbc.properties"/>

    <!-- 数据源配置, 使用 Druid 数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
        <property name="driverClassName" value="${jdbc.driverClass}"/>

        <!-- 基本属性 url、user、password -->
        <property name="url" value="${jdbc.connectionURL}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${jdbc.pool.init}"/>
        <property name="minIdle" value="${jdbc.pool.minIdle}"/>
        <property name="maxActive" value="${jdbc.pool.maxActive}"/>

        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="60000"/>

        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>

        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>

        <property name="validationQuery" value="${jdbc.testSql}"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>

        <!-- 配置监控统计拦截的filters -->
        <property name="filters" value="stat"/>
    </bean>


    <bean id="dataSourceTwo" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
        <property name="driverClassName" value="${jdbc2.driverClass}"/>

        <!-- 基本属性 url、user、password -->
        <property name="url" value="${jdbc2.connectionURL}"/>
        <property name="username" value="${jdbc2.username}"/>
        <property name="password" value="${jdbc2.password}"/>

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${jdbc.pool.init}"/>
        <property name="minIdle" value="${jdbc.pool.minIdle}"/>
        <property name="maxActive" value="${jdbc.pool.maxActive}"/>

        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="60000"/>

        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>

        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>

        <property name="validationQuery" value="${jdbc.testSql}"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>

        <!-- 配置监控统计拦截的filters -->
        <property name="filters" value="stat"/>
    </bean>


    <bean id="dynamicDataSource" class="com.luxiu.spring.conf.datasource.DynamicDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <!-- write -->
                <entry value-ref="dataSource" key="dataSource"></entry>
                <!-- read -->
                <entry value-ref="dataSourceTwo" key="dataSourceTwo"></entry>
            </map>
        </property>
        <property name="defaultTargetDataSource" ref="dataSource"></property>
    </bean>

   <!--利用aop,达到动态更改数据源的目的。当需要增加数据源的时候,我们只需要在spring-context-druid.xml配置文件中添加aop配置,新建个DataSourceInterceptor即可。而不需要更改任何代码。-->
    <!--如果事务的拦截器配置在service层,那么切换数据源的拦截器DataSourceInterceptor就应该在service层之上,这样在执行事务之前就已经选择数据源了-->
    <!--何为service层之上,就是调用service层的方法,例如controller层,我们也可以自定义一层例如 manager 层 位于service层之上-->
    <bean id="dataSourceInterceptor" class="com.luxiu.spring.conf.datasource.DataSourceInterceptor"></bean>
    <aop:config>
        <aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor">
              <aop:pointcut id="daoOne" expression="execution(* com.luxiu.spring.manager.datasourceone.*.*(..))" />
            <aop:pointcut id="daoTwo" expression="execution(* com.luxiu.spring.manager.datasourcetwo.*.*(..))" />
            <aop:before pointcut-ref="daoOne" method="setDataSource" />
            <aop:before pointcut-ref="daoTwo" method="setDataSourceTwo" />
        </aop:aspect>
    </aop:config>
</beans>

spring-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.luxiu.spring">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dynamicDataSource"/>
    </bean>

    <!-- 配置事务通知 -->
    <tx:advice id="myAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="save*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    <!-- 配置顾问和切入点 -->
    <aop:config>
        <aop:pointcut id="myPointcut" expression="execution(* com.luxiu.spring.service.*.*(..))" />
        <aop:advisor advice-ref="myAdvice" pointcut-ref="myPointcut" />
    </aop:config>

    <!-- 开启事务注解驱动 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

spring-context-mybatis.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置 SqlSession -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dynamicDataSource"/>
        <!-- 用于配置对应实体类所在的包,多个 package 之间可以用 ',' 号分割 -->
        <property name="typeAliasesPackage" value="com.luxiu.spring.domain"/>
        <!-- 用于配置对象关系映射配置文件所在目录 -->
        <!--  <property name="mapperLocations" value="classpath:/mapper/**/*.xml"/>-->
        <property name="mapperLocations">
            <array>
                <value>classpath:/mapper/**/*.xml</value>
              <!--  <value>classpath:/readmapper/**/*.xml</value>-->
            </array>
        </property>
        <property name="configLocation" value="classpath:/mybatis-config.xml"></property>
    </bean>

    <!-- 扫描 Mapper ,多个包则用","隔开-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.luxiu.spring.mapper" />
        <!--<property name="basePackage" value="com.luxiu.spring.transaction.mapper,com.luxiu.spring.transaction.readmapper" />-->
    </bean>
</beans>

DynamicDataSource

package com.luxiu.spring.conf.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * <p>
 * Description: 根据 DatabaseContextHolder 设置的数据源来获取数据源
 * </p>
 *
 * @author luguangdong
 * @version 1.0.0
 * @ClassName DynamicDataSource
 * @date 2020/7/31 17:46
 * @company https://www.beyond.com/
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getCustomerType();
    }

}

DatabaseContextHolder

package com.luxiu.spring.conf.datasource;

/**
 * <p>
 * Description: 设置具体数据源数
 * </p>
 *
 * @author luguangdong
 * @version 1.0.0
 * @ClassName DatabaseContextHolder
 * @date 2020/7/31 17:46
 * @company https://www.beyond.com/
 */
public class DatabaseContextHolder {
    /**
     * 注意:数据源标识保存在线程变量中,避免多线程操作数据源时互相干扰
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    /**
     *
     * @param customerType
     * 设置对应的数据源
     * customerType 对应的值就是 spring-context-druid.xml 中  <entry value-ref="dataSource" key="dataSource"></entry> 中的 dataSource
     *
     */
    public static void setCustomerType(String customerType) {
        contextHolder.set(customerType);
    }

    public static String getCustomerType() {
        return contextHolder.get();
    }

    public static void clearCustomerType() {
        contextHolder.remove();
    }
}

DataSourceInterceptor

package com.luxiu.spring.conf.datasource;

import org.aspectj.lang.JoinPoint;
import org.springframework.stereotype.Component;

/**
 * <p>
 * Description: 使用AOP来动态切换数据源
 * </p>
 *
 * @author luguangdong
 * @version 1.0.0
 * @ClassName DataSourceInterceptor
 * @date 2020/7/31 17:51
 * @company https://www.beyond.com/
 */

@Component
public class DataSourceInterceptor {
    public void setDataSource(JoinPoint jp) {
        DatabaseContextHolder.setCustomerType("dataSource");
    }

    public void setDataSourceTwo(JoinPoint jp) {
        DatabaseContextHolder.setCustomerType("dataSourceTwo");
    }
}

多数据源在事物下的调用

事物添加到service层,在service层切换数据源

该场景切换数据源会失败

事物配置

<aop:config>
        <aop:pointcut id="myPointcut" expression="execution(* com.luxiu.spring.transaction.service.*.*(..))" />
        <aop:advisor advice-ref="myAdvice" pointcut-ref="myPointcut" />
    </aop:config>

动态数据源切换配置

<aop:config>
        <aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor">
            <aop:pointcut id="daoOne" expression="execution(* com.luxiu.spring.service.*.*(..))" />
            <aop:pointcut id="daoTwo" expression="execution(* com.luxiu.spring.readservice.*.*(..))" />
            <aop:before pointcut-ref="daoOne" method="setDataSource" />
            <aop:before pointcut-ref="daoTwo" method="setDataSourceTwo" />
        </aop:aspect>
    </aop:config>

测试单个插入

/**
     * 使用动态切换数据源,切割点配置在service层 <aop:pointcut id="daoOne" expression="execution(* com.luxiu.spring.service.*.*(..))" />
     * DataSourceInterceptor 拦截器切点拦截的方法是 service层
     * 测试单个插入失败
     */
    @Test
    public void testTransactionService() {
        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(89L);
        tbContent.setTitle("主题");
        tbContent.setContent("内容");
        tbContent.setCreated(new Date());
        tbContent.setUpdated(new Date());
        tbContentService.save(tbContent);

        Person person = new Person();
        person.setPid(123458L);
        person.setPname("哈哈");
        person.setGender("1");
        personService.save(person);

    }

问题分析:

事务如果拦截service层,在service方法内部切换数据源是不起作用的,因为进入service后数据源和事务是绑定的,此时是切换不了的。

首先要确认事务拦截的地方,事务如果拦截service层,在service方法内部切换数据源是不起作用的,因为进入service后数据源和事务是绑定的。 

在进入带有@transactional 注解方法,作为被事物管理的数据源的链接已经被获取了,真正执行sql的时间 之前拿到的connection 链接也是进入方法之前设置的; 所以同一个被@transactional 注解的方法中数据源是不会切换的。

这是首要的一条。首先你要明白,Spring的事务管理是与数据源绑定的,一旦程序执行到事务管理的那一层(如service)的话,由于在进入该层之前事务已经通过拦截器开启,因此在该层切换数据源是不行的,明白事务的原理是尤为重要的,案例中将切换数据源的拦截器(dataSourceInterceptor)配置在了事务拦截器(txadvice)的上一层,自定义的manager层。

其次,使用注解时,要注意spring的applicationContext.xml与spring mvc的注解context:component-scan标签配置,会有冲突导致事务无法生效,即service层要排除注解@Controller

解决方案:

1.
	在进入事物层之前,手动设置数据源。 
	这样的坏处是没法控制事物了。
	但是数据源是可以强制切换了。
	DatabaseContextHolder.setCustomerType("dataSource");
	
2.
	将切换数据源的拦截器配置位于事务拦截器service的上一层。
	可以是Controller层。
	也可以是自定义manager层,案例中使用自定义mananger层。
	这个方案也是没法控制事物了。(可以考虑分布式事物)

解决方案一 :在进入事物层之前,手动设置数据源。
/**
     * 使用动态切换数据源,切割点配置在service层 <aop:pointcut id="daoOne" expression="execution(* com.luxiu.spring.service.*.*(..))" />
     * DataSourceInterceptor 拦截器切点拦截的方法是 service层
     * 手动设置数据源则成功
     */
    @Test
    public void testTransactionService() {
        //手动设置数据源
        DatabaseContextHolder.setCustomerType("dataSource");
        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(89L);
        tbContent.setTitle("主题");
        tbContent.setContent("内容");
        tbContent.setCreated(new Date());
        tbContent.setUpdated(new Date());
        tbContentService.save(tbContent);

        //手动设置数据源
        DatabaseContextHolder.setCustomerType("dataSourceTwo");
        Person person = new Person();
        person.setPid(123458L);
        person.setPname("哈哈");
        person.setGender("1");
        personService.save(person);

    }

测试结果:

执行成功

测试异常状况下单个插入

package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.mapper.TbContentMapper;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentServiceImpl
 * @date 2020/5/23 22:06
 * @company https://www.singlewindow.cn/
 */
@Service
@Transactional
public class TbContentServiceImpl implements TbContentService {
    @Autowired
    private TbContentMapper tbContentMapper;
    public void save(TbContent tbContent) {
        tbContentMapper.insert(tbContent);
        int i = 1/0;
    }
}

/**
     * 使用动态切换数据源,切割点配置在service层 <aop:pointcut id="daoOne" expression="execution(* com.luxiu.spring.service.*.*(..))" />
     * DataSourceInterceptor 拦截器切点拦截的方法是 service层
     * 测试单个插入失败,手动设置数据源则成功
     */
    @Test
    public void testTransactionService() {
        //手动设置数据源
        DatabaseContextHolder.setCustomerType("dataSourceTwo");
        Person person = new Person();
        person.setPid(123458L);
        person.setPname("哈哈");
        person.setGender("1");
        personService.save(person);

        //手动设置数据源
        DatabaseContextHolder.setCustomerType("dataSource");
        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(89L);
        tbContent.setTitle("主题");
        tbContent.setContent("内容");
        tbContent.setCreated(new Date());
        tbContent.setUpdated(new Date());
        tbContentService.save(tbContent);

    }

测试结果:事物不生效,person表中数据插入。

解决方案二 :数据源切面位于事务切面的上一层。
何为数据源切面位于事务切面的上一层。
就是该层会调用service层的方法,例如controller层。(事物通常会配置在servie层)
我们也可以自定义一层例如manager层位于service层之上,manager层中的方法调用service层中的方法。

新建manager层

在这里插入图片描述

配置数据源拦截器配置

 <aop:config>
        <aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor">
           <aop:pointcut id="daoOne" expression="execution(* com.luxiu.spring.transaction.manager.datasourceone.*.*(..))" />
            <aop:pointcut id="daoTwo" expression="execution(* com.luxiu.spring.transaction.manager.datasourcetwo.*.*(..))" />
            <aop:before pointcut-ref="daoOne" method="setDataSource" />
            <aop:before pointcut-ref="daoTwo" method="setDataSourceTwo" />
        </aop:aspect>
    </aop:config>

测试

 /**
     * 使用动态切换数据源,切割点配置在manager层  <aop:pointcut id="daoOne" expression="execution(* com.luxiu.spring.transaction.manager.datasourceone.*.*(..))" />
     * DataSourceInterceptor 拦截器切点拦截的方法是 manager层
     * 测试单个插入成功
     */
    @Test
    public void testTransactionManager() {
        Person person = new Person();
        person.setPid(123458L);
        person.setPname("哈哈");
        person.setGender("1");
        personManager.save(person);

        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(89L);
        tbContent.setTitle("主题");
        tbContent.setContent("内容");
        tbContent.setCreated(new Date());
        tbContent.setUpdated(new Date());
        contentManager.save(tbContent);
    }

测试结果:

执行成功

测试异常状况下单个插入

测试结果:事物不生效,person表中数据插入。

原因分析:

切换数据源的拦截器配置位于事务拦截器service的上一层。调用manager层中两个数据源各种的save方法,会产生两个不同SqlSession连接,进入servie层后会产生两个不同的事物,所以不能保证事物的一致性。

解决方案:可以采用分布式事物。

多数据源使用分布式事物实现一致性

Atomikos 的使用场景

atomikos 一般在同一个JVM中跨库操作来解决分布式事务的问题

Atomikos配置

引入依赖
 <!--JTA START-->
        <dependency>
            <groupId>javax.transaction</groupId>
            <artifactId>jta</artifactId>
            <version>${jta.version}</version>
        </dependency>
        <dependency>
            <groupId>com.atomikos</groupId>
            <artifactId>transactions-jdbc</artifactId>
            <version>${atomikos.version}</version>
        </dependency>
        <!--JTA END-->
AtomikosDataSource配置多数据源

spring-context-druid.xml

 <!-- #===============================================#-->
                                                <!-- #=======AtomikosDataSource POOL settings========#-->
                                                <!-- #===============================================#-->

    <!--********************************************************* AtomikosDataSource多数据源配置 START *********************************************************-->
    <!--配置抽象XA数据源属性-->
    <bean id="abstractXADataSource" class="com.atomikos.jdbc.AtomikosDataSourceBean" init-method="init"
          destroy-method="close" abstract="true">
        <property name="poolSize" value="10" />
        <property name="minPoolSize" value="10"/>
        <property name="maxPoolSize" value="30"/>
        <property name="borrowConnectionTimeout" value="60"/>  <!--获取连接失败重新获等待最大时间,在这个时间内如果有可用连接,将返回-->
        <property name="reapTimeout" value="20"/> <!--最大获取数据时间,如果不设置这个值,Atomikos使用默认的5分钟,那么在处理大批量数据读取的时候,一旦超过5分钟,就会抛出类似 Resultset is close 的错误.-->
        <property name="maxIdleTime" value="60"/>    <!--最大闲置时间,超过最小连接池连接的连接将将关闭-->
        <property name="maintenanceInterval" value="60" />  <!--连接回收时间-->
        <property name="loginTimeout" value="60" />     <!--java数据库连接池,最大可等待获取datasouce的时间-->
        <property name="logWriter" value="60"/>
        <property name="testQuery">
            <value>SELECT 'x' FROM DUAL</value>
        </property>
    </bean>
    <!--配置数据源-->
    <bean id="dataSource" parent="abstractXADataSource">
        <property name="xaDataSourceClassName" value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/>
        <property name="uniqueResourceName" value="dataSource"/>
        <property name="xaProperties">
            <props>
                <prop key="user">root</prop>
                <prop key="password">123456</prop>
                <prop key="URL">${jdbc.connectionURL}</prop>
            </props>
        </property>
    </bean>
    <!--配置数据源-->
    <bean id="dataSourceTwo" parent="abstractXADataSource">
        <property name="xaDataSourceClassName" value="oracle.jdbc.xa.client.OracleXADataSource" />
        <property name="uniqueResourceName" value="dataSourceTwo"/>
        <property name="xaProperties">
            <props>
                <prop key="user">luxiu</prop>
                <prop key="password">123456</prop>
                <prop key="URL">${jdbc2.connectionURL}</prop>
            </props>
        </property>
    </bean>
    <!--********************************************************* AtomikosDataSource多数据源配置 END *********************************************************-->

配置事务管理器

spring-context.xml

 <!-- #===============================================#-->
                                            <!-- #==== DataSourceTransactionManager settings ====#-->
                                            <!-- #===============================================#-->
    <!--********************************************************* atomikos事物管理 START *********************************************************-->
    <!-- 配置事务管理器 -->
    <bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init"
          destroy-method="close">
        <property name="forceShutdown" value="true"/>
    </bean>
    <bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp">
        <property name="transactionTimeout" value="300"/>
    </bean>
    <bean id="springTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
        <property name="transactionManager" ref="atomikosTransactionManager"/>
        <property name="userTransaction" ref="atomikosUserTransaction"/>
    </bean>
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="springTransactionManager"/>
    </bean>
    <!--开始spring事务-->
    <tx:annotation-driven transaction-manager="springTransactionManager"/>
    <tx:advice id="txAdvice" transaction-manager="springTransactionManager">
        <tx:attributes>
            <tx:method name="save*" rollback-for="Exception" />
        </tx:attributes>
    </tx:advice>
    <!--配置AOP-->
    <aop:config>
        <aop:pointcut id="jtaServiceOperation" expression="execution(* com.luxiu.spring.service.*.*(..))"></aop:pointcut>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="jtaServiceOperation" ></aop:advisor>
    </aop:config>
    <!--********************************************************* atomikos事物管理 END   *********************************************************-->

Atomikos使用

不使用分布式事务
  • 测试异常状态下数据是否入库
@Test
	public void testTransactionManager() {
		Person person = new Person();
		person.setPid(123458L);
		person.setPname("哈哈");
		person.setGender("1");
		personManager.save(person);

		TbContent tbContent = new TbContent();
		tbContent.setCategoryId(89L);
		tbContent.setTitle("主题");
		tbContent.setContent("内容");
		tbContent.setCreated(new Date());
		tbContent.setUpdated(new Date());
		int i = 1/0;
		contentManager.save(tbContent);
	}
  • 数据库表 PERSON 有一条插入成功

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YYrgbixv-1597041856187)(D:\file\撸圣\博客原稿\图片\mybatis\mybatis-11.png)]

  • 结论

    事务没有控制住。

使用分布式事务时
  • 测试异常状态下数据是否入库
@Autowired
	JtaTransactionManager jtaTransactionManager;

	/**
	 * 使用动态切换数据源,切割点配置在manager层 <aop:pointcut id="daoOne" expression="execution(*
	 * com.luxiu.spring.manager.datasourceone.*.*(..))" /> DataSourceInterceptor
	 * 使用分布式事务中间件 atomikos 实现全局异常回滚
	 *
	 *
	 */
	@Test
	public void testTransactionManagerByAtomikos() {
		// 获取事务
		UserTransaction userTransaction = jtaTransactionManager.getUserTransaction();
		try {
			// 开启事务
			userTransaction.begin();

			Person person = new Person();
			person.setPid(123458L);
			person.setPname("哈哈");
			person.setGender("1");
			personManager.save(person);

			TbContent tbContent = new TbContent();
			tbContent.setCategoryId(89L);
			tbContent.setTitle("主题");
			tbContent.setContent("内容");
			tbContent.setCreated(new Date());
			tbContent.setUpdated(new Date());
			int i = 1/0;
			contentManager.save(tbContent);

			// 事务提交
			userTransaction.commit();

		}
		catch (Exception e) {
			// 事务回滚
			try {
				userTransaction.rollback();
			}
			catch (SystemException ex) {
				ex.printStackTrace();
			}
			e.printStackTrace();
		}

	}
  • 数据库表 PERSON 没有插入记录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ZJjoBpa-1597041856189)(D:\file\撸圣\博客原稿\图片\mybatis\mybatis-12.png)]

  • 结论

    分布式事务控制住了。

Atomikos自定义事务增强管理器

  • 配置AOP切面,避免重复写atomikos控制事物的代码

    package com.luxiu.spring.conf.aop.atomikos;
    
    import com.luxiu.spring.conf.response.ResponseCode;
    import com.luxiu.spring.conf.response.ResponseResult;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.transaction.jta.JtaTransactionManager;
    
    import javax.transaction.SystemException;
    import javax.transaction.UserTransaction;
    
    /**
     * <p>
     * Description: atomikos事务增强管理器
     * </p>
     *
     * @author luguangdong
     * @version 1.0.0
     * @ClassName AtomikosTransactionAdvice
     * @date 2020/8/9 20:45
     * @company https://www.beyond.com/
     */
    
    @Aspect
    @Component
    public class AtomikosTransactionAdvice {
    
    	@Autowired
    	JtaTransactionManager jtaTransactionManager;
    
    	// 设置好增强点
    	@Pointcut("execution(* com.luxiu.spring.controller.*.*save*(..))")
    	private void atomikosPointCut() {
    	}
    
    	// around方法
    	@Around("atomikosPointCut()")
    	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    		UserTransaction userTransaction = null;
    		try {
    			// 获取事务
    			userTransaction = jtaTransactionManager.getUserTransaction();
    			// 开启事务
    			userTransaction.begin();
    			// 这里是核心
    			joinPoint.proceed();
    			// 事务提交
    			userTransaction.commit();
    
    		}
    		catch (Exception e) {
    			// 事务回滚
    			try {
    				userTransaction.rollback();
    			}
    			catch (SystemException ex) {
    				ex.printStackTrace();
    			}
    			e.printStackTrace();
    			return ResponseResult.failure(ResponseCode.INTERFACE_INNER_INVOKE_ERROR, e);
    		}
    		return ResponseResult.success("执行成功");
    	}
    
    }
    
    
  • 在对应的xml中配置织入@Aspect切面

    spring-mvc.xml 配置 <aop:aspectj-autoproxy />

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <description>Spring MVC Configuration</description>
    
        <!-- 加载配置属性文件 -->
        <context:property-placeholder ignore-unresolvable="true" location="classpath:view.properties"/>
    
        <!-- 使用 Annotation 自动注册 Bean,只扫描 @Controller -->
        <context:component-scan base-package="com.luxiu.spring" use-default-filters="false">
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
            <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" />
        </context:component-scan>
    
        <!-- 默认的注解映射的支持 -->
        <mvc:annotation-driven />
    
        <!-- 定义视图文件解析 -->
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="${web.view.prefix}"/>
            <property name="suffix" value="${web.view.suffix}"/>
        </bean>
    
        <!-- 静态资源映射 -->
        <mvc:resources mapping="/static/**" location="/static/" cache-period="31536000"/>
    
        <!-- 拦截器配置 -->
    
    
        <!-- 上传文件拦截,设置最大上传文件大小 10M = 10*1024*1024(B) = 10485760 bytes -->
    
        <aop:aspectj-autoproxy />
    
    </beans>
    
  • 对应增强的方法一定不要try…catch操作,因为异常在atomikos事务增强管理器中处理。如果增强方法中try…catch,那么atomikos事务增强管理器就无法捕捉到异常,则事物无法回滚。

    package com.luxiu.spring.controller;
    
    import cn.hutool.json.JSONUtil;
    import com.luxiu.spring.conf.response.ResponseResult;
    import com.luxiu.spring.domain.Person;
    import com.luxiu.spring.domain.TbContent;
    import com.luxiu.spring.manager.datasourceone.ContentManager;
    import com.luxiu.spring.manager.datasourcetwo.PersonManager;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * <p>
     * Description:
     * </p>
     *
     * @author luguangdong
     * @version 1.0.0
     * @ClassName ContentController
     * @date 2020/8/1 16:07
     * @company https://www.beyond.com/
     */
    @RestController
    @RequestMapping("/content")
    public class ContentController {
    
    	public static final Logger logger = LoggerFactory.getLogger(ContentController.class);
    
    	@Autowired
    	private PersonManager personManager;
    
    	@Autowired
    	private ContentManager contentManager;
    
    	@RequestMapping(value = "/save", method = RequestMethod.POST)
    	public ResponseResult save(@RequestBody TbContent tbContent) {
    		logger.info("save方法入参tbContent={}", JSONUtil.parseObj(tbContent, false));
    		contentManager.save(tbContent);
    		Person person = new Person();
    		person.setPid(456L);
    		person.setPname("小明");
    		person.setGender("1");
    		int i = 1 / 0;
    		personManager.save(person);
    		logger.info("save方法保存成功");
    		return ResponseResult.success("创建成功");
    
    	}
    
    }
    
    

MyBatis 单表 CRUD 操作

INSERT

简单处理

继续以 tb_user 表为例,修改映射文件,增加如下配置:

<insert id="insert">
    INSERT INTO tb_user (
      id,
      username,
      password,
      phone,
      email,
      created,
      updated
    )
    VALUES
      (
        #{id},
        #{username},
        #{password},
        #{phone},
        #{email},
        #{created},
        #{update}
      )
</insert>

单元测试代码如下:

@Test
public void testInsert() {
    TbUser tbUser = new TbUser();
    tbUser.setEmail("admin@admin.com");
    tbUser.setPassword("admin");
    tbUser.setPhone("15888888888");
    tbUser.setUsername("Lusifer");
    tbUser.setCreated(new Date());
    tbUser.setUpdate(new Date());

    tbUserDao.insert(tbUser);
}

mybatis中Insert后实体中返回主键(MySql)

keyColumn: 设置数据表自动生成的主键名。对特定数据库(如PostgreSQL),若自动生成的主键不是第一个字段则必须设置(数据库中对应的主键名称)

keyProperty: 默认值unset,用于设置getGeneratedKeys方法或selectKey子元素返回值将赋值到领域模型的哪个属性中(实体中对应的主键名称)

parameterType: 入参的全限定类名或类型别名

useGeneratedKeys: 取值范围true|false(默认值),设置是否使用JDBC的getGenereatedKeys方法获取主键并赋值到keyProperty设置的领域模型属性中。
<insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.luxiu.spring.domain.TbContent" useGeneratedKeys="true">
    <!--@mbg.generated-->
    insert into tb_content
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="categoryId != null">
        category_id,
      </if>
      <if test="title != null">
        title,
      </if>
      <if test="subTitle != null">
        sub_title,
      </if>
      <if test="titleDesc != null">
        title_desc,
      </if>
      <if test="url != null">
        url,
      </if>
      <if test="pic != null">
        pic,
      </if>
      <if test="pic2 != null">
        pic2,
      </if>
      <if test="content != null">
        content,
      </if>
      <if test="created != null">
        created,
      </if>
      <if test="updated != null">
        updated,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="categoryId != null">
        #{categoryId,jdbcType=BIGINT},
      </if>
      <if test="title != null">
        #{title,jdbcType=VARCHAR},
      </if>
      <if test="subTitle != null">
        #{subTitle,jdbcType=VARCHAR},
      </if>
      <if test="titleDesc != null">
        #{titleDesc,jdbcType=VARCHAR},
      </if>
      <if test="url != null">
        #{url,jdbcType=VARCHAR},
      </if>
      <if test="pic != null">
        #{pic,jdbcType=VARCHAR},
      </if>
      <if test="pic2 != null">
        #{pic2,jdbcType=VARCHAR},
      </if>
      <if test="content != null">
        #{content,jdbcType=LONGVARCHAR},
      </if>
      <if test="created != null">
        #{created,jdbcType=TIMESTAMP},
      </if>
      <if test="updated != null">
        #{updated,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>

单元测试代码如下:

 /**
     * 当在mapper中设置主键自增返回时,实体中返回主键
     */
    @Test
    public void testTbContentInsertBackId() {
        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(89L);
        tbContent.setTitle("主题");
        tbContent.setContent("内容");
        tbContent.setCreated(new Date());
        tbContent.setUpdated(new Date());
        tbContentMapper.insertSelective(tbContent);

        System.out.println(tbContent.getId());

    }

mybatis中Insert后实体中返回主键(Oracle)

Oracle设置主键自增
  • 创建序列
/*创建自增序列*/
CREATE SEQUENCE LUXIU_PERSON_PID_SEQUENCE
MINVALUE 1       --最小值
NOMAXVALUE       --不设置最大值
START WITH 1     --从1开始计数
INCREMENT BY 1   --每次加1
NOCYCLE          --一直累加,不循环
NOCACHE;         --不建缓冲区
  • 查询序列(利用nextval查询序列下一次的值)
 select LUXIU_PERSON_PID_SEQUENCE.nextval from dual;
  • 利用序列+显式化调用 进行自增
 insert into person(pid,pname,gender) values (LUXIU_PERSON_PID_SEQUENCE.nextval,'张三','1');
  • 删除序列
DROP SEQUENCE LUXIU_PERSON_PID_SEQUENCE;
mapper中实现返回主键

在insert标签中添加selectKey标签,selectKey标签中的sql语句执行出的结果字段将会赋值给之后执行对应的insert标签中的字段

keyProperty:将查询到的主键值设置到parameterType指定对象的哪个属性。
order:selectKey 标签内的sql语句相对于insert语句的执行顺序,AFBEFORETER表示select LUXIU_PERSON_PID_SEQUENCE.NEXTVAL as pid from DUAL 这个语句将在insert语句之前执行。

<insert id="insertSelective" parameterType="com.luxiu.spring.domain.Person">
    <selectKey keyProperty="pid" order="BEFORE" resultType="java.lang.Long">
      select LUXIU_PERSON_PID_SEQUENCE.NEXTVAL as pid from DUAL
    </selectKey>
    <!--@mbg.generated-->
    insert into PERSON
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="pid != null">
        PID,
      </if>
      <if test="pname != null">
        PNAME,
      </if>
      <if test="gender != null">
        GENDER,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="pid != null">
        #{pid,jdbcType=DECIMAL},
      </if>
      <if test="pname != null">
        #{pname,jdbcType=VARCHAR},
      </if>
      <if test="gender != null">
        #{gender,jdbcType=VARCHAR},
      </if>
    </trim>
  </insert>
  • 一些对于主键有要求的场景
<selectKey keyProperty="pid" order="BEFORE" resultType="java.lang.Long">
      <!--SELECT 'PES'||TO_CHAR(SYSDATE,'YYYYMMDD')||LPAD(LUXIU_PERSON_PID_SEQUENCE.Nextval, 10, '0') FROM DUAL-->
      SELECT TO_CHAR(SYSDATE,'YYYYMMDD')||LPAD(LUXIU_PERSON_PID_SEQUENCE.Nextval, 10, '0') FROM DUAL
    </selectKey>
  • 单元测试代码如下:
/**
     *  oracle在mapper中设置主键自增返回时,实体中返回主键
     */
    @Test
    public void testOracleInsertBackId() {
        Person person = new Person();
        person.setPname("哈哈");
        person.setGender("1");
        personMapper.insertSelective(person);
        System.out.println(person.getPid());

    }

DELETE

继续以 tb_user 表为例,修改映射文件,增加如下配置:

<delete id="delete">
    DELETE FROM tb_user WHERE id = #{id}
</delete>

单元测试代码如下:

@Test
public void testDelete() {
    TbUser tbUser = new TbUser();
    tbUser.setId(37L);

    tbUserDao.delete(tbUser);
}

SELECT

简单处理

继续以 tb_user 表为例,修改映射文件,增加如下配置:

<select id="getById" resultType="TbUser">
    SELECT
      a.id,
      a.username,
      a.password,
      a.phone,
      a.email,
      a.created,
      a.updated AS "update"
    FROM
      tb_user AS a
    WHERE
      a.id = #{id}
</select>

单元测试代码如下:

@Test
public void testGetById() {
    TbUser tbUser = tbUserDao.getById(36L);
    System.out.println(tbUser.getUsername());
}

MySql自定义分页带条件动态查询

  • mapper接口
Long findTotalCountOnCondition(TbContent record);

List<TbContent> findByPageOnCondition(TbContent record);
  • 查询带条件总条数mapper
<select id="findTotalCountOnCondition" parameterType="com.luxiu.spring.domain.TbContent" resultType="java.lang.Long">
    select count(*) from tb_content
    <where>
      <if test="categoryId != null">
        category_id = #{categoryId,jdbcType=BIGINT}
      </if>
      <if test="title != null">
        AND title LIKE concat('%',#{title,jdbcType=VARCHAR},'%')
      </if>
      <if test="subTitle != null">
        AND sub_title = #{subTitle,jdbcType=VARCHAR}
      </if>
      <if test="titleDesc != null">
        AND title_desc = #{titleDesc,jdbcType=VARCHAR}
      </if>
      <if test="url != null">
        AND url = #{url,jdbcType=VARCHAR}
      </if>
      <if test="pic != null">
        AND pic = #{pic,jdbcType=VARCHAR}
      </if>
      <if test="pic2 != null">
        AND pic2 = #{pic2,jdbcType=VARCHAR}
      </if>
      <if test="content != null">
        AND content = #{content,jdbcType=LONGVARCHAR}
      </if>
      <if test="created != null">
        AND created = #{created,jdbcType=TIMESTAMP}
      </if>
      <if test="updated != null">
        AND updated = #{updated,jdbcType=TIMESTAMP}
      </if>
    </where>
  </select>
  • 分页查询mapper
<select id="findByPageOnCondition" parameterType="com.luxiu.spring.domain.TbContent" resultMap="BaseResultMap">
      select
      <include refid="Base_Column_List" />
      from tb_content
      <where>
        <if test="categoryId != null">
          category_id = #{categoryId,jdbcType=BIGINT}
        </if>
        <if test="title != null">
          AND title LIKE concat('%',#{title,jdbcType=VARCHAR},'%')
        </if>
        <if test="subTitle != null">
          AND sub_title = #{subTitle,jdbcType=VARCHAR}
        </if>
        <if test="titleDesc != null">
          AND title_desc = #{titleDesc,jdbcType=VARCHAR}
        </if>
        <if test="url != null">
          AND url = #{url,jdbcType=VARCHAR}
        </if>
        <if test="pic != null">
          AND pic = #{pic,jdbcType=VARCHAR}
        </if>
        <if test="pic2 != null">
          AND pic2 = #{pic2,jdbcType=VARCHAR}
        </if>
        <if test="content != null">
          AND content = #{content,jdbcType=LONGVARCHAR}
        </if>
        <if test="created != null">
          AND created = #{created,jdbcType=TIMESTAMP}
        </if>
        <if test="updated != null">
          AND updated = #{updated,jdbcType=TIMESTAMP}
        </if>
      </where>
      limit #{pageNumber,jdbcType=DECIMAL},#{pageSize,jdbcType=DECIMAL}
    </select>
  • 对应模型简单化
<select id="findByPageOnCondition" parameterType="com.luxiu.spring.domain.TbContent" resultMap="BaseResultMap">
      select
      <include refid="Base_Column_List" />
      from tb_content
      	[多表关联] on [多表关联条件]
      <where>
        [条件]
      </where>
      [分组]/[升序/降序] 
      limit #{pageNumber,jdbcType=DECIMAL},#{pageSize,jdbcType=DECIMAL}
    </select>
  • 测试
 /**
     * 自定义mysql分页带条件动态查询
     * mysql中分页是从0开始的 SELECT * from tb_content limit 0,2;
     * 所以便于理解当前页 pageNumber = (pageNumber-1)* pageSize
     */
    @Test
    public void testMySqlSelectByPageOnCondition() {
        TbContent tbContent = new TbContent();
        tbContent.setTitle("a");
        // 每页条数
        Integer pageSize=3;
        //当前页码从1开始
        Integer pageNumber =1;
        tbContent.setPageSize(pageSize);
        tbContent.setPageNumber((pageNumber-1)*pageSize);
        Long countOnCondition = tbContentMapper.findTotalCountOnCondition(tbContent);
        System.out.println(countOnCondition);
        List<TbContent> pages = tbContentMapper.findByPageOnCondition(tbContent);
        System.out.println(pages);
    }

Oracle自定义分页带条件动态查询

  • mapper接口
Long findTotalCountOnCondition(Person record);

List<Person> findByPageOnCondition(Person record);
  • 查询带条件总条数mapper
<select id="findTotalCountOnCondition" parameterType="com.luxiu.spring.domain.Person" resultType="long">
    select count(*) from PERSON
    <where>
      <if test="pid != null">
        pid = #{pid,jdbcType=DECIMAL}
      </if>
      <if test="pname != null">
        and pname = #{pname,jdbcType=VARCHAR}
      </if>
      <if test="gender != null">
        and gender = #{gender,jdbcType=VARCHAR}
      </if>
    </where>
  </select>
  • 分页查询mapper
<select id="findByPageOnCondition" parameterType="com.luxiu.spring.domain.Person" resultMap="BaseResultMap">
    select <include refid="Base_Column_List" /> from(
    select rownum rn,<include refid="Base_Column_List" /> from(
    select <include refid="Base_Column_List" /> from person
    <where>
      <if test="pid != null">
        pid = #{pid,jdbcType=DECIMAL}
      </if>
      <if test="pname != null">
        and pname = #{pname,jdbcType=VARCHAR}
      </if>
      <if test="gender != null">
        and gender = #{gender,jdbcType=VARCHAR}
      </if>
    </where>
    ) t1
    where <![CDATA[ rownum <= #{pageSize,jdbcType=DECIMAL}]]>) t2
    where <![CDATA[t2.rn >= #{pageNumber,jdbcType=DECIMAL}]]>
  </select>
  • 对应模型简单化
<select id="findByPageOnCondition" parameterType="com.luxiu.spring.domain.Person" resultMap="BaseResultMap">
    select <include refid="Base_Column_List" /> from(
    select rownum rn,<include refid="Base_Column_List" /> from(
    select <include refid="Base_Column_List" /> from person
    [多表关联] on [多表关联条件]
    <where>
      [条件]
    </where> 
     [分组]/[升序/降序] 
    ) t1
    where <![CDATA[ rownum <= #{pageSize,jdbcType=DECIMAL}]]>) t2
    where <![CDATA[t2.rn >= #{pageNumber,jdbcType=DECIMAL}]]>
  </select>
  • 测试
/**
     * 自定义oracle分页查询带条件动态查询
     * oracle分页参数rownum是从1开始计数的
     * begin= (pageNumber - 1) * pageSize + 1;
     */
    @Test
    public void testOracleSelectByPageOnCondition() {
        Person person = new Person();
        person.setPname("哈哈1");
        person.setGender("1");
        // 每页条数
        Integer pageSize=3;
        //当前页码从1开始
        Integer pageNumber =1;

        Integer begin= (pageNumber - 1) * pageSize + 1;
        Integer end= (pageNumber * pageSize);

        person.setPageSize(end);
        person.setPageNumber(begin);
        Long countOnCondition = personMapper.findTotalCountOnCondition(person);
        System.out.println(countOnCondition);
        List<Person> page = personMapper.findByPageOnCondition(person);
        System.out.println(page);
    }

MySql和Oracle自定义分页查询总结

limit [m],n;
m—— [m]为可选,如果填写表示skip步长,即跳过m条。
n—— 显示条数。指从第m+1条记录开始,取n条记录。
初始记录行的偏移量是 0(而不是 1),所以便于理解传入的pagerNumber必须大于等于1。
pageSize
pageNumber = (pageNumber-1) * pageSize
limit pageNumber,pageSize


rownum
oracle分页参数rownum是从1开始计数的
begin= (pageNumber - 1) * pageSize + 1;
end= (pageNumber * pageSize);

封装自定义分页对象Pagination

  • 分页对象
package com.luxiu.spring.conf.page;

import java.io.Serializable;
import java.util.List;

/**
 * <p>
 * Description: 自定义分页类
 * </p>
 *
 * @author luguangdong
 * @version 1.0.0
 * @ClassName Pagination
 * @date 2020/8/1 23:04
 * @company https://www.beyond.com/
 */
public class Pagination<T> implements Serializable {
    private static final long serialVersionUID = -5626846339655628738L;
    // 总条数
    private Integer totalCount;
    // 总页数
    private Integer totalPages;
    // 当前页码从1开始
    private Integer pageNumber;
    // 每页条数
    private Integer pageSize;
    // oracle分页起始行
    private Integer begin;
    // oracle分页结束行
    private Integer end;
    // 前一页
    private Integer prePage;
    // 是否首页
    private Boolean firstPage;
    // 是否末页
    private Boolean lastPage;
    // 下一页
    private Integer nextPage;
    // 偏移量
    private Integer offset;
    // 是否有下页
    private Boolean hasNextPage;
    // 是否有上页
    private Boolean hasPrePage;
    // 当前页从第几条开始查
    private Integer startRow;
    // 返回数据
    private List<T> rows;

    public Pagination() {
    }

    public Integer getTotalCount() {
        return totalCount;
    }

    public void setTotalCount(Integer totalCount) {
        this.totalCount = totalCount;
    }

    public Integer getTotalPages() {
        return totalPages;
    }

    public void setTotalPages(Integer totalPages) {
        this.totalPages = totalPages;
    }

    public Integer getPageNumber() {
        return pageNumber;
    }

    public void setPageNumber(Integer pageNumber) {
        this.pageNumber = pageNumber;
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    public Integer getPrePage() {
        return prePage;
    }

    public void setPrePage(Integer prePage) {
        this.prePage = prePage;
    }

    public Boolean getFirstPage() {
        return firstPage;
    }

    public void setFirstPage(Boolean firstPage) {
        this.firstPage = firstPage;
    }

    public Boolean getLastPage() {
        return lastPage;
    }

    public void setLastPage(Boolean lastPage) {
        this.lastPage = lastPage;
    }

    public Integer getNextPage() {
        return nextPage;
    }

    public void setNextPage(Integer nextPage) {
        this.nextPage = nextPage;
    }

    public Integer getOffset() {
        return offset;
    }

    public void setOffset(Integer offset) {
        this.offset = offset;
    }

    public Boolean getHasNextPage() {
        return hasNextPage;
    }

    public void setHasNextPage(Boolean hasNextPage) {
        this.hasNextPage = hasNextPage;
    }

    public Boolean getHasPrePage() {
        return hasPrePage;
    }

    public void setHasPrePage(Boolean hasPrePage) {
        this.hasPrePage = hasPrePage;
    }

    public Integer getStartRow() {
        return startRow;
    }

    public void setStartRow(Integer startRow) {
        this.startRow = startRow;
    }

    public Integer getBegin() {
        return begin;
    }

    public void setBegin(Integer begin) {
        this.begin = begin;
    }

    public Integer getEnd() {
        return end;
    }

    public void setEnd(Integer end) {
        this.end = end;
    }

    public List<T> getRows() {
        return rows;
    }

    public void setRows(List<T> rows) {
        this.rows = rows;
    }
}

  • 分页工具类
package com.luxiu.spring.conf.page;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0.0
 * @ClassName PageUtil
 * @date 2020/8/2 15:11
 * @company https://www.beyond.com/
 */
public class PageUtil {
    private PageUtil() {
    }

    /**
     *
     * @param pageNumber
     * @param pageSize
     * @return
     *
     *  limit [m],n;
     *  m—— [m]为可选,如果填写表示skip步长,即跳过m条。
     *  n—— 显示条数。指从第m+1条记录开始,取n条记录。
     *  初始记录行的偏移量是 0(而不是 1),所以便于理解传入的pagerNumber必须大于等于1。
     *  pageSize
     *  pageNumber = (pageNumber-1) * pageSize
     *  limit pageNumber,pageSize
     */
    public static Pagination getMysqlPagination(int pageNumber, int pageSize) {
        Pagination pagination = new Pagination();
        if(pageNumber < 1){
            pageNumber =1;
        }
        if(pageSize < 0){
            pageSize =0;
        }
        pagination.setPageNumber((pageNumber-1) * pageSize);
        pagination.setPageSize(pageSize);
        return pagination;
    }

    /**
     *
     * @param pageNumber
     * @param pageSize
     * @return
     *
     *  rownum
     *  oracle分页参数rownum是从1开始计数的
     *  begin= (pageNumber - 1) * pageSize + 1;
     *  end= (pageNumber * pageSize);
     */
    public static Pagination getOraclePagination(int pageNumber, int pageSize) {
        Pagination pagination = new Pagination();
        if(pageNumber < 1){
            pageNumber =1;
        }
        if(pageSize < 0){
            pageSize =0;
        }
        pagination.setBegin((pageNumber - 1) * pageSize + 1);
        pagination.setEnd(pageNumber * pageSize);
        pagination.setPageNumber(pageNumber);
        pagination.setPageSize(pageSize);
        return pagination;
    }


}

使用PageHelper分页查询

  • 导入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version> 
</dependency>
  • spring-context-mybatis.xml配置分页插件
 <!--配置mybatis 插件 -->
        <property name="plugins">
            <set>
                <!--配置pageHelper 分页插件-->
                <bean class="com.github.pagehelper.PageInterceptor">
                    <property name="properties">
                        <props>
                            <!--方言:-->
                            <prop key="helperDialect">mysql</prop>
                        </props>
                    </property>
                </bean>
            </set>
        </property>
  • PageHelper方法API说明
 /**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize) {
        return startPage(pageNum, pageSize, DEFAULT_COUNT);
    }


/**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     * @param count    是否进行count查询
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
        return startPage(pageNum, pageSize, count, null, null);
    }

 /**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     * @param orderBy  排序
     * orderBy="字段名 排序规律"
     * 例如:String orderBy="id desc";
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize, String orderBy) {
        Page<E> page = startPage(pageNum, pageSize);
        page.setOrderBy(orderBy);
        return page;
    }
  • 测试

    /**
     *  MySql中使用PageHelper分页带条件动态查询
     */
    @Test
    public void testMySqlSelectByPageOnPageHelper() {
        TbContent tbContent = new TbContent();
        //tbContent.setTitle("a");
        Integer pageSize=2;
        Integer pageNumber =2;
        //执行count(*)操作
        PageHelper.startPage(pageNumber,pageSize,true);
        List<TbContent> pages = tbContentMapper.findAll(tbContent);
        PageInfo pageInfo = new PageInfo(pages);
        System.out.println("总条数:"+pageInfo.getTotal());
        System.out.println("分页结果:"+pageInfo.getList());
    }

 /**
     *  MySql中使用PageHelper分页带条件动态查询
     */
    @Test
    public void testOracleSelectByPageOnPageHelper() {
        Person person = new Person();
       person.setPname("哈哈1");
        person.setGender("1");
        Integer pageSize=2;
        Integer pageNumber =2;
        //执行count(*)操作
        PageHelper.startPage(pageNumber,pageSize,true);
        List<Person> page = personMapper.findAll(person);
        PageInfo pageInfo = new PageInfo(page);
        System.out.println("总条数:"+pageInfo.getTotal());
        System.out.println("分页结果:"+pageInfo.getList());
    }

pagehelper配置多数据源自动切换数据库方言

<!--配置mybatis 插件 -->
        <property name="plugins">
            <set>
                <!--配置pageHelper 分页插件-->
                <bean class="com.github.pagehelper.PageInterceptor">
                    <property name="properties">
                        <props>
                            <!--方言:-->
                           <!-- <prop key="helperDialect">mysql</prop>-->
                            <!--根据具体的查询语句动态切换方言-->
                            <prop key="autoRuntimeDialect">true</prop>
                        </props>
                    </property>
                </bean>
            </set>
        </property>

UPDATE

继续以 tb_user 表为例,修改映射文件,增加如下配置:

<update id="update">
    UPDATE
      tb_user
    SET
      username = #{username},
      password = #{password},
      phone = #{phone},
      email = #{email},
      created = #{created},
      updated = #{update}
    WHERE id = #{id}
</update>

单元测试代码如下:

@Test
public void testUpdate() {
    TbUser tbUser = tbUserDao.getById(36L);
    tbUser.setUsername("Lusifer");

    tbUserDao.update(tbUser);
}

使用模糊查询

继续以 tb_user 表为例,修改映射文件,增加如下配置:

<select id="selectByName" resultType="TbUser">
    SELECT
      a.id,
      a.username,
      a.password,
      a.phone,
      a.email,
      a.created,
      a.updated AS "update"
    FROM
      tb_user AS a
    WHERE
      a.username LIKE CONCAT ('%', #{username}, '%')
</select>

在进行模糊查询时,需要进行字符串的拼接。SQL 中的字符串的拼接使用的是函数 concat(arg1, arg2, …) 。注意不能使用 Java 中的字符串连接符 +

单元测试代码如下:

@Test
public void testSelectByName() {
    List<TbUser> tbUsers = tbUserDao.selectByName("uni");
    for (TbUser tbUser : tbUsers) {
        System.out.println(tbUser.getUsername());
    }
}

MyBatis 动态 SQL

动态 SQL,主要用于解决查询条件不确定的情况:在程序运行期间,根据用户提交的查询条件进行查询。提交的查询条件不同,执行的 SQL 语句不同。若将每种可能的情况均逐一列出,对所有条件进行排列组合,将会出现大量的 SQL 语句。此时,可使用动态 SQL 来解决这样的问题。

在这里插入图片描述

动态 SQL,即通过 MyBatis 提供的各种标签对条件作出判断以实现动态拼接 SQL 语句。

这里的条件判断使用的表达式为 OGNL 表达式。常用的动态 SQL 标签有 <if><where><choose><foreach> 等。

注意事项

在 mapper 的动态 SQL 中若出现大于号(>)、小于号(<)、大于等于号(>=),小于等于号(<=)等符号,最好将其转换为实体符号。否则,XML 可能会出现解析出错问题。

**特别是对于小于号(<),在 XML 中是绝对不能出现的。**否则,一定出错。

在这里插入图片描述

if 标签

对于该标签的执行,当 test 的值为 true 时,会将其包含的 SQL 片断拼接到其所在的 SQL 语句中。

本例实现的功能是:查询出满足用户提交查询条件的所有学生。用户提交的查询条件可以包含一个姓名的模糊查询,同时还可以包含一个年龄的下限。当然,用户在提交表单时可能两个条件均做出了设定,也可能两个条件均不做设定,也可以只做其中一项设定。

这引发的问题是,查询条件不确定,查询条件依赖于用户提交的内容。此时,就可使用动态 SQL 语句,根据用户提交内容对将要执行的 SQL 进行拼接。

定义映射文件

为了解决两个条件均未做设定的情况,在 where 后添加了一个“1=1”的条件。这样就不至于两个条件均未设定而出现只剩下一个 where,而没有任何可拼接的条件的不完整 SQL 语句。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luxiu.mybatis.dao.DynamicStudentDao">
    <!-- if -->
    <select id="selectByIf" resultType="com.luxiu.mybatis.entity.Student">
        SELECT
            id,
            name,
            age,
            score
        FROM
            student
        WHERE 1 = 1
        <if test="name != null and name != ''">
            AND name LIKE concat('%', #{name}, '%')
        </if>
        <if test="age != null and age > 0">
            AND age > #{age}
        </if>
    </select>
</mapper>

where 标签

<if/> 标签的中存在一个比较麻烦的地方:需要在 where 后手工添加 1=1 的子句。因为,若 where 后的所有 <if/> 条件均为 false,而 where 后若又没有 1=1 子句,则 SQL 中就会只剩下一个空的 where,SQL 出错。所以,在 where 后,需要添加永为真子句 1=1,以防止这种情况的发生。但当数据量很大时,会严重影响查询效率。

定义映射文件

<!-- where-->
<select id="selectByWhere" resultType="com.luxiu.mybatis.entity.Student">
    SELECT
        id,
        name,
        age,
        score
    FROM
      student
    <where>
        <if test="name != null and name != ''">
            AND name LIKE concat('%', #{name}, '%')
        </if>
        <if test="age != null and age > 0">
            AND age > #{age}
        </if>
    </where>
</select>

choose 标签

该标签中只可以包含 <when/> <otherwise/>,可以包含多个 <when/> 与一个 <otherwise/>。它们联合使用,完成 Java 中的开关语句 switch…case 功能。

**choose标签是按顺序判断其内部when标签中的test条件出否成立,如果有一个成立,则 choose 结束。当 choose 中所有 when 的条件都不满则时,则执行 otherwise 中的sql。**类似于Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。

本例要完成的需求是,若姓名不空,则按照姓名查询;若姓名为空,则按照年龄查询;若没有查询条件,则没有查询结果。

定义映射文件

<!-- choose -->
<select id="selectByChoose" resultType="com.lusifer.mybatis.entity.Student">
    SELECT
        id,
        name,
        age,
        score
    FROM
      student
    <where>
        <choose>
            <when test="name != null and name != ''">
                AND name LIKE concat('%', #{name}, '%')
            </when>
            <when test="age != null and age > 0">
                AND age > #{age}
            </when>
            <otherwise>
                AND 1 != 1
            </otherwise>
        </choose>
    </where>
</select>

foreach 标签-遍历数组

<foreach/> 标签用于实现对于数组与集合的遍历。对其使用,需要注意:

  • collection 表示要遍历的集合类型,这里是数组,即 array。
  • opencloseseparator 为对遍历内容的 SQL 拼接。

本例实现的需求是,查询出 id 为 2 与 4 的学生信息。

定义 DAO 接口
/**
 * 使用 foreach 标签以 Integer数组的形式查询
 * @param ids
 * @return
 */
public List<TbUser> selectByForeach(Integer[] ids);
定义映射文件

动态 SQL 的判断中使用的都是 OGNL 表达式。OGNL 表达式中的数组使用 array 表示,数组长度使用 array.length 表示。

在这里插入图片描述

<!-- foreach -->
<select id="selectByForeach" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from tb_user
    <if test="array != null and array.length > 0">
      WHERE id IN
      <foreach collection="array" open="(" close=")" item="id" separator=",">
        #{id}
      </foreach>
    </if>
  </select>
测试
@Test
public void testSelectByForeach(){
        Integer arr[] = new Integer[4];
        arr[1] = 40;
        arr[2] = 42;
        arr[3] = 45;
        List<TbUser> tbUsers = tbUserMapper.selectByForeach(arr);
        System.out.println(tbUsers);
    }

foreach 标签-遍历集合

遍历集合的方式与遍历数组的方式相同,只不过是将 array 替换成了 list

遍历泛型为基本类型的 List

定义 DAO 接口
/**
 * 使用 foreach 标签以 list 基本类型的形式查询
 * @param ids
 * @return
 */
public List<TbUser> selectByForeachWithListBase(List<Long> ids);
定义映射文件
<!-- foreach -->
<select id="selectByForeachWithListBase" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from tb_user
    <if test="list != null and list.size() > 0">
      WHERE id IN
      <foreach collection="list" open="(" close=")" item="id" separator=",">
        #{id}
      </foreach>
    </if>
  </select>
测试
@Test
public void testSelectByForeachWithListBase(){
        ArrayList<Long> list = new ArrayList<Long>();
        list.add(40L);
        list.add(42L);
        list.add(45L);
        List<TbUser> tbUsers = tbUserMapper.selectByForeachWithListBase(list);
        System.out.println(tbUsers);


    }

遍历泛型为自定义类型的 List

定义 DAO 接口
 /**
     * 使用 foreach 标签以 list 自定义类型的形式查询
     * @param tbUser
     * @return
     */
    public List<TbUser> selectByForeachWithListCustom(List<TbUser> tbUsers);
定义映射文件
<!-- foreach -->
 <select id="selectByForeachWithListCustom" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from tb_user
    <if test="list != null and list.size() > 0">
      WHERE id IN
      <foreach collection="list" open="(" close=")" item="tbUser" separator=",">
        #{tbUser.id}
      </foreach>
    </if>
  </select>
测试
 @Test
 public void testSelectByForeachWithListCustom(){
        ArrayList<TbUser> tbUsers = new ArrayList<TbUser>();
        TbUser tbUser1 = new TbUser();
        tbUser1.setId(40L);
        TbUser tbUser2 = new TbUser();
        tbUser2.setId(42L);
        TbUser tbUser3 = new TbUser();
        tbUser3.setId(45L);
        tbUsers.add(tbUser1);
        tbUsers.add(tbUser2);
        tbUsers.add(tbUser3);
        List<TbUser> tbUserList = tbUserMapper.selectByForeachWithListCustom(tbUsers);
        System.out.println(tbUserList);
}

遍历泛型为Map

定义 DAO 接口
/**
     * 使用 foreach 标签以 Map 类型的形式查询
     * @param tbUsers
     * @return
     */
    public List<TbUser> selectByForeachWithMap(Map tbUsers);
定义映射文件

在这里值得注意的是,实现类中put到map中集合的是key要与collection的值相等

<select id="selectByForeachWithMap" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from tb_user
    <if test="ids != null and ids.length > 0">
      WHERE id IN
      <foreach collection="ids" open="(" close=")" item="id" separator=",">
        #{id}
      </foreach>
    </if>
  </select>
测试
@Test
public void testSelectByForeachWithMap(){
    HashMap<String, Long[]> tbUsers = new HashMap<String, Long[]>();
    Long[] arr = new Long[3];
    arr[0] = 40L;
    arr[1] = 42L;
    arr[2] = 45L;
    tbUsers.put("ids",arr);
    List<TbUser> tbUserList = tbUserMapper.selectByForeachWithMap(tbUsers);
    System.out.println(tbUserList.toString());

}

sql 标签

<sql/> 标签用于定义 SQL 片断,以便其它 SQL 标签复用。而其它标签使用该 SQL 片断, 需要使用 <include/> 子标签。该 <sql/> 标签可以定义 SQL 语句中的任何部分,所以 <include/> 子标签可以放在动态 SQL 的任何位置。

修改映射文件

<sql id="select">
    SELECT
        id,
        name,
        age,
        score
    FROM
      student
</sql>
<!-- foreach -->
<select id="selectByForeachWithListCustom" resultType="com.luxiu.mybatis.entity.Student">
    <!-- select * from student where id in (2, 4) -->
    <include refid="select" />

    <if test="list != null and list.size > 0">
        WHERE id IN
        <foreach collection="list" open="(" close=")" item="student" separator=",">
            #{student.id}
        </foreach>
    </if>
</select>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值