笔记:MyBatis(Plus)+Spring+SpringBoot+JDBC

笔记:MyBatis(Plus)+Spring+SpringBoot+JDBC

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

这是一篇小笔记,主要用来串起之前学习过的关于ORM框架(mybatis)的知识。可以当作一篇以前学习路径的整理复习笔记。具体的使用细节还是参考官网为主。


一、JDBC

1.1 JDBC简介

JDBC(Java Database Connectivity)是 Java 编程语言用于操作关系型数据库的 API。它允许 Java 应用程序通过统一的接口访问各种关系型数据库,如MySQL、Oracle、SQL Server等,而无需关注特定数据库的实现细节。
所以使用JDBC的好处是不需要针对不同关系型数据库分别开发,可在替换数据库时,让Java的代码基本不变。

1.2 JDBC开发流程

程序开发流程基本如下:
1、导入jdbc依赖包:mysql-connector-java
2、注册驱动
3、获取连接
4、定义sql语句
5、获取执行sql对象(可以是预处理对象再设置参数)
6、执行sql
7、处理返回结果
8、释放资源
参考代码如下:

//导包
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.23</version>
</dependency>

/*参考主程序*/
//static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
static final String DB_URL = "jdbc:mysql://localhost:3306/data_1?useServerPrepStmts=true";
// 数据库用户和密码
static final String USER = "root";
static final String PASS = "123456";
public static void main(String[] args) throws SQLException {
    // DQL
    Connection conn = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    List<User> userList = new ArrayList<>();
    try {
        // 注册数据库驱动类,mysql5后可忽略
        //Class.forName(JDBC_DRIVER);
        // 获取数据库连接
        conn = DriverManager.getConnection(DB_URL, USER, PASS);
        // 设置手动提交事务,默认是自动提交
        //conn.setAutoCommit(false);
        // 定义sql语句
        String sql = "select * from tb_user where name = ?;";
        // 获取预处理
        preparedStatement = conn.prepareStatement(sql);
        // 设置参数
        preparedStatement.setString(1,"zhangsan");
        resultSet = preparedStatement.executeQuery();
        // 处理结果集(获取字段并封装对象)
        while (resultSet.next()) {
            long id = resultSet.getInt("id");
            String name = resultSet.getString("name");
            int age = resultSet.getInt("age");
            String userLevel = resultSet.getString("user_level");
            User user = new User();
            user.setId(id);
            user.setName(name);
            user.setAge(age);
            user.setUserLevel(userLevel);
            userList.add(user);
        }
        System.out.println(userList);
        //conn.commit();
    } catch (Exception e) {
        e.printStackTrace();
        //conn.rollback();
    } finally {
        // 关闭资源
        try {
            if(resultSet != null){
                resultSet.close();
            }
            if (preparedStatement != null) {
                preparedStatement.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (SQLException se) {
            se.printStackTrace();
        }
    }

/*
    // DML
    Connection conn = null;
    Statement statement = null;
    PreparedStatement preparedStatement = null;
    Integer resultSet = null;
    Integer age = 27;
    String name = "wangwu";
    String userLevel = "A2";
    try {
        // 注册数据库驱动类,mysql5后可忽略
        //Class.forName(JDBC_DRIVER);
        // 获取数据库连接
        conn = DriverManager.getConnection(DB_URL, USER, PASS);
        // 定义sql语句
        String sql = "insert into `tb_user`(age,name,user_level) VALUES(?,?,?);";
        // 执行sql
//        statement = conn.createStatement();
//        resultSet = statement.executeUpdate(sql);
        // 预编译后执行
        preparedStatement = conn.prepareStatement(sql);
        preparedStatement.setInt(1,age);
        preparedStatement.setString(2,name);
        preparedStatement.setString(3,userLevel);
        resultSet = preparedStatement.executeUpdate();
        // 处理结果集
        if(resultSet > 0){
            System.out.println(resultSet);
        }else {
            System.out.println("fail!");
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 关闭资源
        try {
//            if (statement != null) {
//                statement.close();
//            }
            if (preparedStatement != null) {
                preparedStatement.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (SQLException se) {
            se.printStackTrace();
        }
    }
*/

}

注意点:
1、Connection对象负责获取连接和设置事务。默认自动提交。
2、Statement负责执行sql语句,执行DDL、DML使用executeUpdate,执行DQL使用executeQuery。
3、PreparedStatement也负责执行sql语句,但它是预编译的,可以防止SQL注入(也就是脚本拼接修改数据)。其原理是在预编译时将预编译sql发送给数据库,数据库会执行检查语法和编译两步操作,等到执行时再发送数据库执行;这样做可以一个预编译模板只执行一次语法检查和编译,多次执行,提高效率;尤其在进行批量处理时推荐一定要使用预编译。另外,记得要开启预编译功能设置useServerPrepStmts=true(数据源连接路径上)。

1.3 数据库连接池

使用数据库连接池可以减少数据库的连接和释放连接次数,提高整体性能,同时方便管理。JDBC常用的连接池是Druid,一款由阿里巴巴开发维护的开源数据库连接池项目。
代码的改动比较简单:导入依赖,将原来从DriverManager对象获取连接改为从Druid连接池获取连接;并增加连接池的配置。参考代码如下:

/*改动点*/
// 配置 Druid 数据源
dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(dataSourceProperties());
// 从连接池获取连接
conn = dataSource.getConnection();
conn = DriverManager.getConnection(DB_URL, USER, PASS);

/*配置连接池*/
// 定义连接池配置,也可以改成从配置文件中获取
private static Properties dataSourceProperties() {
    Properties properties = new Properties();
    properties.setProperty("url", DB_URL);
    properties.setProperty("username", USER);
    properties.setProperty("password", PASS);
    // 初始化连接数量
    properties.setProperty("initialSize", "10");
    // 最大连接数
    properties.setProperty("maxActive", "20");
    // 最大等待时间
    properties.setProperty("maxWait", "3000");
    // 其他配置项,参考 Druid 文档或源码
    // properties.setProperty("validationQuery", "SELECT 1");
    // properties.setProperty("testWhileIdle", "true");
    // properties.setProperty("testOnBorrow", "false");
    // properties.setProperty("testOnReturn", "false");
    // properties.setProperty("poolPreparedStatements", "true");
    // properties.setProperty("maxOpenPreparedStatements", "20");
    // else
    return properties;
}

/*导包*/
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.6</version>
</dependency>

二、MyBatis

2.1 简介

MyBatis是在封装JDBC基础上发展的一款持久层框架(也有说是ORM框架)。几乎免除了JDBC代码设置参数和获取结果集的工作,可以通过XML或注解来配置和映射对象。

2.2 使用

2.2.1 快速入门

推荐直接查看官方文档(https://mybatis.org/mybatis-3/zh_CN/getting-started.html)跟着上手,快速启动项目。也可以参考如下:
1、导入依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.23</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.11</version>
</dependency>

2、创建mybatis配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/data_1?useServerPrepStmts=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

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

</configuration>

3、创建映射文件

<mapper namespace="mapper.UserMapper">
    <select id="selectUser" resultType="entity.User">
        select * from tb_user where id = #{id}
    </select>
</mapper>
public interface UserMapper {
    User selectUser(@Param("id") Long id);
}

4、创建实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private Integer age;
    private String name;
    private String userLevel;
}

5、主程序(步骤其实比较简单易理解)

public class MyBatisTest {

    public static void main(String[] args) throws IOException {
        // 通过配置文件获取SqlSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 获取SqlSession对象
        //   --也可以不写Mapper接口直接通过namespace获取结果User user = (User) session.selectOne("mapper.UserMapper.selectUser", 7L);但是官方建议写mapper接口
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 获取mapper(映射)对象
            UserMapper mapper = session.getMapper(UserMapper.class);
            // 执行方法(sql)并获取映射结果
            User user = mapper.selectUser(7L);
            System.out.println(user);
        }
    }
}

通过与上面JDBC代码的简单对比可以发现:sql语句的拼写统一放在了xml文件或注解上,而调用程序需要调用映射类实例像调用方法一样传参和直接获得返回对象,极大的简化了JDBC的代码。
从mybatis的代码可以看到,新建SqlSessionFactoryBuilder实例,由该示例根据数据源信息配置去创建SqlSessionFactory实例。一旦创建完成便可以丢弃SqlSessionFactoryBuilder实例,而SqlSessionFactory保存有数据源信息应该随着应用存活着。由SqlSessionFactory创建的SqlSession的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域,每个线程应该有自己的SqlSession实例。可以从SqlSession中获取映射器实例,虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。

2.2.2 xml映射文件和动态SQL

关于xml映射文件有几个顶级的标签需要记住,详情可以直接官网查找,不再赘述。动态SQL其实就是通过标签增加了一些逻辑约定,用以适应业务变化,具体语法还是直接从官网查找。下面列举一些比较复杂的例子:

/** 复杂动态SQL查询 */
@Test
public void test2(){
    Map<String,Object> map = new HashMap<>(8);
    map.put("name","lisi");
    List<Long> ids = new ArrayList<>();
    ids.add(7L);
    ids.add(9L);
    map.put("ids",ids);
    List<User> userList = userMapper.select1(map,26);
    System.out.println(userList);
}

public interface UserMapper {
    //多入参复杂动态sql查询
    List<User> select1(@Param("map") Map<String,Object> map, @Param("ageMax") Integer ageMax);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.demo.mapper.UserMapper">
    <!-- 某字段如需特殊处理可以设置结果集,一般推荐注解方式 -->
    <resultMap id="userResultMap" type="User">
        <id property="id" column="id" />
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="userLevel" column="user_level"/>
    </resultMap>

    <!-- 可以适用sql标签写一些需要重复使用的sql语句 -->
    <sql id="user">
        id,name,age,user_level
    </sql>

    <!-- 复杂动态sql查询 -->
    <select id="select1" resultType="com.demo.entity.User">
        select
        <include refid="user" />  <!--引用sql标签内容-->
        from tb_user
        <where>
            <if test="map.name != null and map.name != ''">
                and name = #{map.name}  <!--#{}会替换预编译里的?,而${}是拼接sql语句或字符串替换的占位符;需要根据实际情况选用-->
            </if>
            <if test="map.ids != null and map.ids.size() > 0">
                and id in
                <foreach collection="map.ids" item="id" open="(" separator="," close=")">
                    #{id}
                </foreach>
            </if>
            <if test="ageMax != null">
                and age <![CDATA[ <= ]]> #{ageMax}  <!--特殊字符需要使用<![CDATA[xxx]]>包起来(IDEA快捷键CD-->
            </if>
        </where>
    </select>

</mapper>
2.2.3 xml配置属性

mybatis映射文件提供了很多配置属性,具体的只能是使用到去官网查找。说一些比较常用的,比如:useGeneratedKeys强制使用数据库自动生成主键;defaultExecutorType执行器,SIMPLE 就是普通的执行器,REUSE 执行器会重用预处理语句(PreparedStatement),BATCH 执行器不仅重用语句还会执行批量更新;defaultStatementTimeout设置超时时间;mapUnderscoreToCamelCase是否开启驼峰命名自动映射;localCacheScope本地缓存,默认值为SESSION,会缓存一个会话中执行的所有查询。STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存;

2.2.4 批量操作

mybatis支持批量操作数据库,建议在有使用mybatis的情况下不要再直接使用JDBC,很容易出错的。如果要使用JDBC批量操作一定要注意开启rewriteBatchedStatement=true,同时代码要使用预处理语句的方式。好了,下面列举使用mybatis批量插入的两种方法。

//mapper接口
public interface UserMapper {
    Long insertOne(User user);

    Long insertBatch(@Param("users") List<User> users);
}

//mapper文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.demo.mapper.UserMapper">
    <insert id="insertOne" parameterType="com.demo.entity.User" useGeneratedKeys="true" keyProperty="id">
        insert into `tb_user`(`name`,`age`,`user_level`)
        values(#{name}, #{age}, #{userLevel})
    </insert>

    <insert id="insertBatch" parameterType="list" useGeneratedKeys="true" keyProperty="id">
        insert into tb_user(name,age,user_level)
        values
        <foreach collection="users" item="user" separator=",">
            (#{user.name}, #{user.age}, #{user.userLevel})
        </foreach>
    </insert>
</mapper>

/** 批量插入测试 */
@Test
public void test4(){
    List<User> users = new ArrayList<>();
    User user1 = new User();
    user1.setAge(24);
    user1.setName("aa2");
    user1.setUserLevel("A2");
    User user2 = new User();
    user2.setAge(24);
    user2.setName("aa3");
    user2.setUserLevel("A2");
    users.add(user1);
    users.add(user2);
    // 方法一:使用批量sql语句执行
    Long id = userMapper.insertBatch(users);
    // 方法二(推荐):使用sqlSession批量预处理后手动提交
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    for (User user : users) {
        mapper.insertOne(user);
    }
    sqlSession.commit();  //sqlSession默认是不自动提交的,需要手动提交
}                                        

批量操作写法虽然不复杂,但实际在使用中要考虑的因素是很多的。比如:内存占用过大、事务提交过慢、主从延迟、回滚处理等。这里不展开。

2.2.5 多数据源

其实就是创建多个SqlSessionFactory,可以通过多个不同配置来创建多个SqlSessionFactory实现多数据源。参考代码如下:

InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
2.2.5 缓存

mybatis默认开启一级缓存。在每一次会话结束后会在本地开启缓存,将结果存储下来。而会话的发起者是sqlSession,所以一级缓存作用域只在单个sqlSession内有用。一旦同个sqlSession内有改删增操作便会清空缓存。这样做的好处是两次相同查询sqlSession不会进行第二次查询直接将缓存返回。
二级缓存是全局的,作用域是同一命名空间下的mapper文件。需要在SQL映射文件中添加一行开启(如果配合springboot,cache-enabled设置为true)。这样就可以跨sqlSession共享缓存,相同查询会返回缓存内容,淘汰策略是LRU,有改删增操作也会清空缓存。

三、MyBatis与Spring整合

现在的主流还是SSM框架,所以我们还是先根据官网指导(https://mybatis.org/spring/zh_CN/getting-started.html)快速上手,后面再对整合的原理进行分析。

3.1 快速上手

1、导入依赖,注意上官网查看版本兼容。

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.20</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.20</version>
    <scope>provided</scope>
</dependency>

2、spring的配置。这里需要注意的是:可以选择将后续要创建的映射器接口实例注册在这里,将实例交由spring实例化(实际上在相同路径下是MapperFactoryBean 对象自动解析,如果路径不同需要修改userMapper里的sqlSessionFactory属性里的mapperLocations属性);也可以选择导入mybatis-spring:scan标签或标签里的basePackage属性。这里先按官网例子来,如果mapper接口较多推荐使用第二种方法。

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

    <!-- 扫描 Mapper 接口所在的包 -->
    <!-- <context:component-scan base-package="mapper"/> -->
    <!-- 扫描 MyBatis Mapper 接口 -->
    <!-- <mybatis-spring:scan base-package="mapper"/> -->

    <!-- <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="mapper" />
    </bean> -->
  
    <!-- mapper接口 -->
    <bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
        <property name="mapperInterface" value="mapper.UserMapper" />
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>

    <!-- 定义数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/data_1?useServerPrepStmts=true"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>

    <!-- sqlSessionFactory配置 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mapperLocations" value="classpath*:mapper/*.xml"/>
    </bean>

</beans>

3、通过SqlSessionFactoryBean来创建 SqlSessionFactory,并注册bean容器。如果在上一步的xml配置里已经注入了sqlSessionFactory,请忽略此步骤。

@Configuration
public class MyBatisConfig {
    @Autowired
    DruidDataSource dataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();
    }
}

4、mapper映射接口和映射配置文件照旧,与正常mybatis的没有区别,不再重复写。

public interface UserMapper {
    User selectUser(@Param("id") Long id);
}

<mapper namespace="mapper.UserMapper">
    <select id="selectUser" resultType="entity.User">
        select * from tb_user where id = #{id}
    </select>
</mapper>

5、运行主类。

public class Application {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application-dev.xml");
        UserMapper userMapper = (UserMapper) applicationContext.getBean(UserMapper.class);
        User user = userMapper.selectUser(7L);
        System.out.println(user);
    }
}

3.2 浅析源码

1、首先我们先分析下目的和思路。为什么要整合spring和mybatis?目的肯定是为了简化开发,也就是在保持mybatis功能完整的情况下,将mybatis实例交由spring容器管理。对比两者代码也可以发现省略了SqlSessionFactory的创建过程,也省略了创建SqlSession的过程,直接获取映射器实例就能执行sql方法。理解了要做什么,那么思路就比较明确了。一个是想方设法把需要重复创建实例的代码交给spring管理,获取实例从容器中获取。
2、注意看注入bean的配置文件,sqlSessionFactory类变成是SqlSessionFactoryBean类,userMapper类变成是MapperFactoryBean类;这两者都实现FactoryBean接口,泛型类型才是mybatis类。下面我以SqlSessionFactoryBean源码举例:

//重写FactoryBean接口的getObject()方法
public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
        this.afterPropertiesSet();
    }
    return this.sqlSessionFactory;
}
//执行getObject()方法里的afterPropertiesSet()方法
public void afterPropertiesSet() throws Exception {
    Assert.notNull(this.dataSource, "Property 'dataSource' is required");
    Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    Assert.state(this.configuration == null && this.configLocation == null || this.configuration == null || this.configLocation == null, "Property 'configuration' and 'configLocation' can not specified with together");
    this.sqlSessionFactory = this.buildSqlSessionFactory();
}
//执行afterPropertiesSet()方法里的buildSqlSessionFactory()方法
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    XMLConfigBuilder xmlConfigBuilder = null;
    Configuration targetConfiguration;
    //...  省略代码主要是配置的获取(Configuration类的设置)
    //这里执行了mybatis包下的Environment有参构造方法,入参之一便是application-dev.xml文件配置的dataSource
    targetConfiguration.setEnvironment(new Environment(this.environment, (TransactionFactory)(this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory), this.dataSource));
    //...  省略的代码主要是mapper配置xml的读取
    //返回的SqlSessionFactory依旧是mybatis包下的sqlSessionFactoryBuilder.build()方法
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}

3、接着再简单说下MapperFactoryBean类,它不但实现FactoryBean接口,还继承了一个叫SqlSessionDaoSupport的类。官方给出的介绍是:SqlSessionDaoSupport是一个抽象的支持类,用来为你提供SqlSession。调用getSqlSession()方法你会得到一个 SqlSessionTemplate。SqlSessionTemplate是线程安全的,可以被多个DAO或映射器所共享使用。SqlSessionTemplate将会保证使用的SqlSession与当前Spring的事务相关;管理 session的生命周期,包含必要的关闭、提交或回滚操作;也负责将 MyBatis的异常翻译成 Spring中的DataAccessExceptions。

//重写FactoryBean接口的getObject()方法;
//这里直接调用mybatis包下的getMapper()方法,所以重点看getSqlSession()是如何工作的
public T getObject() throws Exception {
    return this.getSqlSession().getMapper(this.mapperInterface);
}
//跟进去发现是在构造时调用createSqlSessionTemplate()方法
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
        this.sqlSessionTemplate = this.createSqlSessionTemplate(sqlSessionFactory);
    }
}
//通过sqlSessionFactory类加载器加载sqlSession,同时包装有executorType和exceptionTranslator两个类的功能
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
    Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    Assert.notNull(executorType, "Property 'executorType' is required");
    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
}

4、浅说下MapperScannerConfigurer的作用。MapperScannerConfigurer实现BeanDefinitionRegistryPostProcessor接口,会执行重写的postProcessBeanDefinitionRegistry()方法,大致流程如下:

//postProcessBeanDefinitionRegistry()方法里执行扫描
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
        this.processPropertyPlaceHolders();
    }
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    //...
    scanner.registerFilters();
    //执行scanner.scan()扫描方法
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n"));
}

//进去以后继续执行doScan()方法
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    // 存放扫描结果的beanDefinition对象集合
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet();
    String[] var3 = basePackages;
    int var4 = basePackages.length;
    for(int var5 = 0; var5 < var4; ++var5) {
        String basePackage = var3[var5];
        // 执行读取配置文件注入的方法入口
        Set<BeanDefinition> candidates = this.findCandidateComponents(basePackage);
        Iterator var8 = candidates.iterator();
        while(var8.hasNext()) {
            BeanDefinition candidate = (BeanDefinition)var8.next();
            //...  一些判断逻辑
            if (this.checkCandidate(beanName, candidate)) {
                BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                // 添加注册到BeanDefinitionMap容器,这样执行后就会有mapper文件的注册信息
                beanDefinitions.add(definitionHolder);
                this.registerBeanDefinition(definitionHolder, this.registry);
            }
        }
    }
    return beanDefinitions;
}

//继续跟进可以到scanCandidateComponents()方法可以看到一些查找逻辑
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    LinkedHashSet candidates = new LinkedHashSet();
    try {
        //路径拼接:main方法下的相对路径+basePackage设置的路径
        String packageSearchPath = "classpath*:" + this.resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        Resource[] resources = this.getResourcePatternResolver().getResources(packageSearchPath);
        //...
        Resource[] var7 = resources;
        int var8 = resources.length;
        for(int var9 = 0; var9 < var8; ++var9) {
            Resource resource = var7[var9];
            //...
            try {
                //资源读取
                MetadataReader metadataReader = this.getMetadataReaderFactory().getMetadataReader(resource);
                if (this.isCandidateComponent(metadataReader)) {
                    ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                    sbd.setSource(resource);
                    //...
                        //逻辑判断后添加资源
                        candidates.add(sbd);
                    } 
                //...
            } 
        }
        return candidates;
    //...
}

四、Mybatis与SpringBoot整合

4.1快速上手

有了前面的Mybatis与Spring整合的开发,Mybatis与SpringBoot的整合开发就轻松很多了。还是按照官网的指导在上面与spring整合的基础上修改。比较大的变化一个是配置修改,另一个是映射器扫描修改。
1、导入依赖。主要导入springboot-starter和mybatis-spring-boot-starter,这里还导入junit依赖方便后续测试。

<dependencies>
  <!-- springboot starter -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.5</version>
  </dependency>
  <!-- MyBatis Starter -->
  <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
  </dependency>
  <!-- 测试 -->
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>compile</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.9</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test</artifactId>
    <version>2.7.5</version>
  </dependency>
</dependencies>

2、配置文件修改。这里使用yml文件只是因为个人习惯偏好而已。

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/data_1?useServerPrepStmts=true
    username: root
    password: 123456

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml #classpath:com/vehicle/adaptation/mapper/*Mapper.xml  #注意:一定要对应mapper映射xml文件的所在路径
  type-aliases-package: com.demo.entity  # 注意:对应实体类的路径
  configuration:
    map-underscore-to-camel-case: true  #自动匹配驼峰命名

3、映射接口类和映射配置文件,以及实体类保持不变。
4、编写主程序和测试方法。如果在启动类上增加了@MapperScan注解,则映射接口类不需要增加@Mapper注解,两者选其一即可。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MybatisSpringbootApplication.class)
@MapperScan("com.demo.mapper")
@SpringBootApplication
public class MybatisSpringbootApplication {

    @Resource
    UserMapper userMapper;

    public static void main(String[] args) {
        SpringApplication.run(MybatisSpringbootApplication.class, args);
    }

    @Test
    public void test1(){
        User user = userMapper.selectUser(7L);
        System.out.println(user);
    }
}

4.2 多数据源

这里其实和mybatis一样,都是创建多个SqlSessionFactory,每个SqlSessionFactory有自己配置的数据源。只是将创建的对象SqlSessionFactory和映射接口交由spring的bean容器管理。参考代码如下:
1、配置文件

spring:
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/data_1?useServerPrepStmts=true
      username: root
      password: 123456
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/data_2?useServerPrepStmts=true
      username: root
      password: 123456

2、增加配置,注册bean对象

@Configuration
public class DataSourceConfig {
    
    // 映射文件路径
    static final String MASTER_MAPPER_XML = "classpath:mapper/UserMapper.xml";
    static final String SLAVE_MAPPER_XML = "classpath:mapper/UserMapper.xml";
    // 实体映射路径
    static final String MASTER_ENTITY_PACKAGE = "com.demo.entity";
    static final String SLAVE_ENTITY_PACKAGE = "com.demo.entity";

    @Bean(name = "masterDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "masterSqlSessionFactory")
    @Primary
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MASTER_MAPPER_XML));
        sessionFactory.setTypeAliasesPackage(MASTER_ENTITY_PACKAGE);
        return sessionFactory.getObject();
    }

    @Bean(name = "slaveSqlSessionFactory")
    public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(SLAVE_MAPPER_XML));
        sessionFactory.setTypeAliasesPackage(SLAVE_ENTITY_PACKAGE);
        return sessionFactory.getObject();
    }

    @Bean(name = "masterSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = "slaveSqlSessionTemplate")
    public SqlSessionTemplate slaveSqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = "masterTransactionManager")
    @Primary
    public DataSourceTransactionManager masterTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "slaveTransactionManager")
    public DataSourceTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

3、测试。使用SqlSessionFactory或包装类masterSqlSessionTemplate进行测试。

@Resource(name = "masterSqlSessionFactory")
SqlSessionFactory masterSqlSessionFactory;
@Resource(name = "slaveSqlSessionFactory")
SqlSessionFactory slaveSqlSessionFactory;

@Resource(name = "masterSqlSessionTemplate")
SqlSessionTemplate masterSqlSessionTemplate;
@Resource(name = "slaveSqlSessionTemplate")
SqlSessionTemplate slaveSqlSessionTemplate;

/** 多数据源测试 :这里图方便没有在mapper 接口层和配置文件层区分数据源;如果有区分应该可以直接注入mapper接口 */
@Test
public void test5(){
    SqlSession masterSqlSession = masterSqlSessionFactory.openSession();
    UserMapper masterMapper = masterSqlSession.getMapper(UserMapper.class);
    User user1 = masterMapper.selectUser(1L);
    System.out.println(user1);
    System.out.println("=====");
    SqlSession slaveSqlSession = slaveSqlSessionFactory.openSession();
    UserMapper slaveMapper = slaveSqlSession.getMapper(UserMapper.class);
    User user2 = slaveMapper.selectUser(1L);
    System.out.println(user2);
}

/** 多数据源测试 :这里图方便没有在mapper 接口层和配置文件层区分数据源;如果有区分应该可以直接注入mapper接口 */
@Test
public void test6(){
    UserMapper masterMapper = masterSqlSessionTemplate.getMapper(UserMapper.class);
    User user1 = masterMapper.selectUser(1L);
    System.out.println(user1);
    System.out.println("=====");
    UserMapper slaveMapper = slaveSqlSessionTemplate.getMapper(UserMapper.class);
    User user2 = slaveMapper.selectUser(1L);
    System.out.println(user2);
}

4.3 事务

这里的事务其实指的是springboot的事务,mybatis借助spring的DataSourceTransactionManager来进行事务管理。还记得前面提到的SqlSessionTemplate吗?它将会保证使用的 SqlSession 与当前 Spring 的事务相关。 此外,它管理 session 的生命周期,包含必要的关闭、提交或回滚操作。
所以这里主要介绍的还是springboot的事务管理。springboot提供了声名式的方法(注解)方便开发,其本质是在方法前后执行AOP代码,开启事务或提交、回滚事务。所以被注解的方法必须是public方法且不能是发生自身调用,当然方法所在类对象是要交由spring容器管理的。
说一说@Transactional注解。主要有两个注解参数建议显式写上,第一个注解参数是事务的传播行为Propagation。有七种,如下:

1REQUIRED(默认)如果当前存在事务,则加入该事务(内部方法或外部方法哪个出错都会回滚,其实就是都在同一个事务);如果当前没有事务,则创建一个新的事务。
2REQUIRES_NEW总是创建一个新的事务。如果当前存在事务,则将其挂起。(这里需要与REQUIRED对比理解的是,内部方法不出错但外部方法出错只会回滚外部方法;只要内部方法出错都会回滚)
3SUPPORTS如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
4NOT_SUPPORTED总是以非事务方式执行,如果当前存在事务,则将其挂起。(适用能在不需要事务或甚至要避免事务的方法)
5MANDATORY必须在一个现有事务中执行,如果当前没有事务,则抛出异常。(确保在上下文事务中运行)
6NEVER必须在非事务环境中执行,如果当前存在事务,则抛出异常。(确保没有上下文事务)
7NESTED如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新的事务。(嵌套区别于加入,内部方法事务回滚不影响外部方法事务)

总结就是:外部方法有事务,内部方法事务可以选择加入(一起回滚或提交)/新建/挂起/抛异常/嵌套。外部方法没有事务,内部方法事务可以选择新建/非事务/抛异常。常用的就前三个。第二个注解参事是rollbackFor需要回滚的错误。建议显式写上。可以自己写上运行时异常,避开受检查异常的处理;Error异常程序一般是无法处理的。
参考代码如下:

@Service
public class TransactionService {

    @Autowired
    private IUserService iUserService;

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {RuntimeException.class,Error.class}, isolation = Isolation.DEFAULT, timeout = -1)
    public void saveOne(){
        User user = new User();
        user.setAge(27);
        user.setName("cc1");
        user.setUserLevel("A1");
        iUserService.save(user);
        TransactionService transactionService = (TransactionService) ApplicationContextUtil.getBean("transactionService");
        transactionService.saveOne2();
        //int temp = 1/0;
    }

    @Transactional(propagation = Propagation.SUPPORTS, rollbackFor = {RuntimeException.class,Error.class})
    public void saveOne2(){
        User user = new User();
        user.setAge(27);
        user.setName("cc2");
        user.setUserLevel("A1");
        iUserService.save(user);
        int temp = 1/0;
    }
}

使用中尤其要注意常见失效问题:
1、数据库引擎不支持。Mysql的MyISAM引擎不支持事务,InnoDB支持。
2、不是被spring容器管理的对象。
3、方法不是public。
4、发生自身调用。(通常同一类内方法调用)
5、异常被捕获没有抛出。

五、MyBatis-Plus

mybatisPlus是mybatis的增强。
有了前面一系列mybatis的认识和使用,mybatisPlus就简单多了。而且官网是中文的。

5.1 快速上手

1、下载安装插件MyBatisPlus(二次元头像的);使用插件自动生产类、接口等代码。
2、使用自动生成类、接口等代码的代码。参考链接:https://baomidou.com/pages/779a6e/
3、手动写简化版的mybatis类、接口等代码。参考链接:https://baomidou.com/pages/226c21/
推荐使用插件自动生成,可以帮我们简化mapper层的接口类和配置文件代码,简化实体类代码,还可以选择增加服务层或控制层代码。

5.2 便捷化使用

5.2.1 CRUD

针对普通的CRUD功能,IService接口类和BaseMapper 接口类封装好了不少基础接口可以实现后直接使用。入参条件可以使用Wrapper接口实现筛选。当然如果实在是比较复杂还是建议写在xml配置文件里。参考:https://baomidou.com/pages/49cc81/。其核心是CRUD接口和条件构造器。

5.2.2 使用插件工具

mybatisPlus提供了几个好用的拦截器插件,需要重新注册MybatisPlusInterceptor对象到bean容器里,在注册前增加自己需要的拦截器对象即可。下面介绍下分页插件的使用:
1、添加分页插件

@Configuration
@MapperScan("com.demo.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加
        return interceptor;
    }
}

2、使用IPage接口分页

@PostMapping("/t3")
public String t3() {
    Page<User> page = new Page<>(1,3);
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.lambda().gt(User::getAge,23)
            .lt(User::getAge,30)
            .orderBy(true,false,User::getName);
    IPage<User> pageResult = iUserService.page(page, queryWrapper);
    return JSONObject.toJSONString(pageResult);
}
5.2.3 多数据源

mybatisPlus提供了一个十分好用的多数据源依赖包dynamic-datasource-spring-boot-starter。可以在配置文件配置多数据源后使用注解便可以实现动态数据源切换。代码示例如下:
1、引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

2、配置文件增加动态数据源配置

spring:
  datasource:
    dynamic:
      primary: master_1 #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master_1:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/data_1?useServerPrepStmts=true&rewriteBatchedStatement=true
          username: root
          password: 123456
        slave_1:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/data_2?useServerPrepStmts=true&rewriteBatchedStatement=true
          username: root
          password: 123456

3、使用注解切换

@DS("master_1")
@RestController
@RequestMapping("/dynamic")
public class DateSourceController {

    @Autowired
    private IUserService iUserService;

    @PostMapping("/t5")
    public boolean t5() {
        User user = new User();
        user.setAge(25);
        user.setName("bb2");
        user.setUserLevel("A1");
        return iUserService.save(user);
    }

    @PostMapping("/t6")
    @DS("slave_1")
    public boolean t6() {
        User user = new User();
        user.setAge(25);
        user.setName("bb2");
        user.setUserLevel("A1");
        return iUserService.save(user);
    }
}

public interface IUserService extends IService<User> {
}

关于多数据源,除了以上方法外还有其它可以借助的工具或方法。比如使用mybatis-mate-sharding插件;轻量级分库分表工具sharding-jdbc;重量级分库分表中间件mycat;其它等等;各有优缺点,根据实际需要选用。读写分离、分库其实都依赖多数据源的实现,并不难。

5.2.4 批量操作

可以像之前mybatis提到的一样自定义在xml写批量语句。记得要开启预提交使能。也可以使用mybatisPlus的API简化代码开发。参考如下:

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {RuntimeException.class,Error.class})
public void t6(){
    List<User> userList = new ArrayList<>();
    for (int i = 1; i < 10; i++) {
        User user = new User();
        user.setAge(28);
        user.setName("dd" + i);
        user.setUserLevel("A3");
        userList.add(user);
    }
    System.out.println(userList.size());
    iUserService.saveBatch(userList,1000);
}

不过这里需要注意的是,虽然入参可以设置批量的大小,但是该方法本身是等到所有批次预提交完成后才提交事务,如果大小很大需要考虑事务延迟问题,建议控制集合大小多次调用。这里的批次大小更多是为了控制应用程序内存而不是自动多批次事务提交。如果对API性能和事务不是很确定,建议测试时打开sql日志。

5.2.5 流式操作

mybatisPlus3.5.4以上的版本支持流式查询。参考如下:

@PostMapping("/t5")
public void t5(){
    userMapper.selectList(Wrappers.emptyWrapper(), new ResultHandler<User>() {
        int count = 0;
        @Override
        public void handleResult(ResultContext<? extends User> resultContext) {
            User user = resultContext.getResultObject();
            System.out.println("当前处理第" + (++count) + "条记录: " + user);
            // 在这里进行你的业务处理,比如分发任务
        }
    });
}
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值