zoukankan      html  css  js  c++  java
  • 12.SpringBoot学习(十二)——JDBC之 Spring Boot Mybatis

    1.简介

    1.1 概述

    The MyBatis-Spring-Boot-Starter help you build quickly MyBatis applications on top of the Spring Boot.

    By using this module you will achieve:

    • Build standalone applications
    • Reduce the boilerplate to almost zero
    • Less XML configuration

    MyBatis-Spring-Boot-Starter可帮助您在Spring Boot之上快速构建MyBatis应用程序。

    通过使用此模块,您将实现:

    • 构建独立的应用程序
    • 将样板减少到几乎为零
    • 减少XML配置

    1.2 特点

    As you may already know, to use MyBatis with Spring you need at least an SqlSessionFactory and at least one mapper interface.

    MyBatis-Spring-Boot-Starter will:

    • Autodetect an existing DataSource
    • Will create and register an instance of a SqlSessionFactory passing that DataSource as an input using the SqlSessionFactoryBean
    • Will create and register an instance of a SqlSessionTemplate got out of the SqlSessionFactory
    • Auto-scan your mappers, link them to the SqlSessionTemplate and register them to Spring context so they can be injected into your beans

    MyBatis-Spring-Boot-Starter 将会:

    • 自动检测已有的 DataSource
    • 将创建并注册一个 SqlSessionFactory 实例,并使用 SqlSessionFactoryBean 将该 DataSource 作为输入
    • 将创建并注册一个从 SqlSessionFactory 中获取的 SqlSessionTemplate 实例
    • 自动扫描您的映射器,将它们链接到 SqlSessionTemplate 并将它们注册到 Spring 上下文中,以便可以将它们注入到您的 bean 中

    2.演示环境

    1. JDK 1.8.0_201
    2. Spring Boot 2.2.0.RELEASE
    3. 构建工具(apache maven 3.6.3)
    4. 开发工具(IntelliJ IDEA )

    3.演示代码

    3.1 代码说明

    spring boot 集成 mybatis,使用了 xml 和注解 两种配置方式。实现了单表的增删改查。

    3.2 代码结构

    image-20200723214856482

    3.3 maven 依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
    

    3.4 配置文件

    application.properties

    mybatis.config-location=classpath:mybatis/mybatis-config.xml
    mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
    mybatis.type-aliases-package=com.soulballad.usage.springboot
    
    spring.datasource.url=jdbc:mysql://172.16.11.125:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    

    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>
        <typeAliases>
            <typeAlias alias="Integer" type="java.lang.Integer"/>
            <typeAlias alias="Long" type="java.lang.Long"/>
            <typeAlias alias="HashMap" type="java.util.HashMap"/>
            <typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap"/>
            <typeAlias alias="ArrayList" type="java.util.ArrayList"/>
            <typeAlias alias="LinkedList" type="java.util.LinkedList"/>
        </typeAliases>
    </configuration>
    

    UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    
    <mapper namespace="com.soulballad.usage.springboot.mapper.UserMapper">
    
        <resultMap id="baseResultMap" type="com.soulballad.usage.springboot.model.UserModel">
            <id column="id" property="id" jdbcType="BIGINT"/>
            <result column="name" property="name" jdbcType="VARCHAR"/>
            <result column="age" property="age" javaType="INTEGER"/>
            <result column="birthday" property="birthday" jdbcType="VARCHAR"/>
            <result column="address" property="address" jdbcType="VARCHAR"/>
            <result column="phone" property="phone" jdbcType="VARCHAR"/>
        </resultMap>
    
        <sql id="Base_Column_List">
            id, `name`, age, birthday, address, phone
        </sql>
    
        <select id="findUserByName" parameterType="java.lang.String" resultMap="baseResultMap">
            SELECT
            <include refid="Base_Column_List"/>
            FROM t_user
            WHERE `name` like concat('%', #{name}, '%')
        </select>
    
        <select id="findUserByPhone" parameterType="java.lang.String" resultMap="baseResultMap">
            SELECT
            <include refid="Base_Column_List"/>
            FROM t_user
            WHERE phone = ${phone}
        </select>
    
    
        <update id="updateByName" parameterType="com.soulballad.usage.springboot.model.UserModel">
            UPDATE t_user SET phone = #{phone} WHERE `name` = #{name}
        </update>
    
        <delete id="deleteByName" parameterType="java.lang.String">
           DELETE FROM t_user WHERE `name` = #{name}
        </delete>
    
    </mapper>
    

    3.5 java代码

    UserModel.java

    public class UserModel implements Serializable {
        private Long id;
        private String name;
        private Integer age;
        private String birthday;
        private String address;
        private String phone;
    
        public UserModel() {}
    
        public UserModel(String name, Integer age, String birthday, String address, String phone) {
            this.name = name;
            this.age = age;
            this.birthday = birthday;
            this.address = address;
            this.phone = phone;
        }
    
       	// get&set&toString
    }
    

    UserMapper.java

    @Repository
    public interface UserMapper {
    
        @Select("SELECT id, `name`, age, birthday, address, phone FROM t_user")
        @Results({
                @Result(property = "id", column = "id"),
                @Result(property = "name", column = "name"),
                @Result(property = "age", column = "age"),
                @Result(property = "birthday", column = "birthday"),
                @Result(property = "address", column = "address"),
                @Result(property = "phone", column = "phone")
        })
        List<UserModel> findAll();
    
        UserModel findUserByName(String name);
    
    //    Page<UserModel> findByPage(SpringDataWebProperties.Pageable pageable);
    
        List<UserModel> findUserByPhone(@Param("phone") String phone);
    
        int updateByName(UserModel userModel);
    
        int deleteByName(@Param("name") String name);
    
        @Insert("INSERT INTO t_user(`name`, age, birthday, address, phone) VALUES(#{name}, #{age}, #{birthday}, #{address}, #{phone})")
        int insertUser(UserModel userModel);
    }
    

    UserService.java

    public interface UserService {
    
        /**
         * 查询所有数据
         * @return user
         */
        List<UserModel> selectList();
    
        /**
         * 根据名称查询
         * @param name name
         * @return user
         */
        UserModel findUserByName(String name);
    
        /**
         * 根据电话查询
         * @param phone 电话
         * @return user
         */
        List<UserModel> findUserByPhone(String phone);
    
        /**
         * 根据名称更新电话
         * @param phone 电话
         * @param name 名称
         * @return 影响行数
         */
        UserModel updateByName(String phone, String name);
    
        /**
         * 根据名称删除
         * @param name 名称
         * @return 影响行数
         */
        UserModel deleteByName(String name);
    
        /**
         * 新增
         * @param user user
         * @return user
         */
        UserModel add(UserModel user);
    }
    

    UserServiceImpl.java

    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public List<UserModel> selectList() {
            return userMapper.findAll();
        }
    
        @Override
        public UserModel findUserByName(String name) {
            return userMapper.findUserByName(name);
        }
    
        @Override
        public List<UserModel> findUserByPhone(String phone) {
            return userMapper.findUserByPhone(phone);
        }
    
        @Override
        public UserModel updateByName(String phone, String name) {
            UserModel userModel = new UserModel();
            userModel.setPhone(phone);
            userModel.setName(name);
            userMapper.updateByName(userModel);
            return findUserByName(name);
        }
    
        @Override
        public UserModel deleteByName(String name) {
            UserModel user = findUserByName(name);
            userMapper.deleteByName(name);
            return user;
        }
    
        @Override
        public UserModel add(UserModel user) {
            userMapper.insertUser(user);
            return findUserByName(user.getName());
        }
    }
    

    UserController.java

    @RestController
    @RequestMapping(value = "/user")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @GetMapping(value = "/list")
        public List<UserModel> list() {
            return userService.selectList();
        }
    
        @GetMapping(value = "/findByName/{name}")
        public UserModel findByName(@PathVariable String name) {
            return userService.findUserByName(name);
        }
    
        @GetMapping(value = "/findByPhone/{phone}")
        public List<UserModel> findByPhone(@PathVariable String phone) {
            return userService.findUserByPhone(phone);
        }
    
        @PostMapping(value = "/add")
        public UserModel add(@RequestBody UserModel user) {
            return userService.add(user);
        }
    
        @PutMapping(value = "/updateByName")
        public UserModel updateByName(@RequestBody UserModel user) {
            return userService.updateByName(user.getPhone(), user.getName());
        }
    
        @DeleteMapping(value = "/deleteByName/{name}")
        public UserModel deleteByName(@PathVariable String name) {
            return userService.deleteByName(name);
        }
    }
    

    3.6 git 地址

    spring-boot/spring-boot-06-jdbc/spring-boot-mybatis

    4.效果展示

    启动 SpringBootMybatisApplication.main 方法,在 spring-boot-mybatis.http 访问下列地址,观察输出信息是否符合预期。

    查询用户列表(所有)

    ### GET /user/list
    GET http://localhost:8080/user/list
    Accept: application/json
    

    image-20200721210408036

    根据用户名查询

    ### GET /user/findByName/{name}
    GET http://localhost:8080/user/findByName/lisi
    Accept: application/json
    

    image-20200721210526269

    根据手机号查询

    ### GET /user/findByPhone/{phone}
    GET http://localhost:8080/user/findByPhone/13666666666
    Accept: application/json
    

    image-20200721210624524

    添加用户

    ### POST user/add
    POST http://localhost:8080/user/add
    Content-Type: application/json
    
    {
      "name": "abcd12",
      "age": 33,
      "birthday": "1987-07-20",
      "address": "washington",
      "phone": "15666666666"
    }
    

    image-20200726164659485

    更新用户信息

    ### PUT /user/updateByName
    PUT http://localhost:8080/user/updateByName
    Content-Type: application/json
    
    {
      "name": "zhangsan",
      "phone": "13456789012"
    }
    

    image-20200721210849107

    删除用户

    ### DELETE /user/deleteByName/{name}
    DELETE http://localhost:8080/user/deleteByName/zhangsan
    Content-Type: application/json
    

    image-20200721210950916

    5.源码分析

    以 UserServiceImpl#findUserByName 为例,分析一下相关源码。

    5.1 mybatis 的加载

    image-20200726184744481

    SqlSessionFactoryBean 实现了 InitializingBean 接口,重写了 afterPropertiesSet 方法,在这个方法中对 sqlSessionFactory 进行了初始化

    public void afterPropertiesSet() throws Exception {
      notNull(dataSource, "Property 'dataSource' is required");
      notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
      state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
          "Property 'configuration' and 'configLocation' can not specified with together");
    
      this.sqlSessionFactory = buildSqlSessionFactory();
    }
    

    在 buildSqlSessionFactory 中解析 config 和 mapper 配置文件、扫描注解,将所有配置信息保存到 configuration 中。

    // 解析 mybatis-config.xml
    private void parseConfiguration(XNode root) {
        try {
            // issue #117 read properties first
    		// 解析 properties 节点
            propertiesElement(root.evalNode("properties"));
            // 解析 settings 节点
            Properties settings = settingsAsProperties(root.evalNode("settings"));
            loadCustomVfs(settings);
            loadCustomLogImpl(settings);
            // 解析 typeAliases 节点
            typeAliasesElement(root.evalNode("typeAliases"));
            // 解析 plugins 节点
            pluginElement(root.evalNode("plugins"));
            // 解析 objectFactory 节点
            objectFactoryElement(root.evalNode("objectFactory"));
            // 解析 objectWrapperFactory 节点
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            // 解析 reflectorFactory 节点
            reflectorFactoryElement(root.evalNode("reflectorFactory"));
            settingsElement(settings);
            // read it after objectFactory and objectWrapperFactory issue #631
            environmentsElement(root.evalNode("environments"));
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            typeHandlerElement(root.evalNode("typeHandlers"));
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    
    // 解析 UserMapper.xml
    public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
    		// 解析 mapper 节点
            configurationElement(parser.evalNode("/mapper"));
            configuration.addLoadedResource(resource);
    		// 生成 MappedStatement 对象,保存到 configuration 中
            bindMapperForNamespace();
        }
    
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
    }
    

    5.2 UserMapper 的动态代理

    image-20200726190240145

    MapperFactoryBean 是 UserMapper 实例化是对应的 beanClass,它继承自 DaoSupport 间接实现了 InitializingBean 接口,同时自己实现了 FactoryBean 接口,在 UserMapper 实例化时,会调用它的 getObject 方法,按照自定义逻辑创建对象。

    在 getObject 中,它要返回一个 Mapper 对象

    @Override
    public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
    }
    

    这里的 getSqlSession 实际上调用父类 SqlSessionDaoSupport 的方法,返回一个 SqlSessionTemplate。它是 SqlSession 的一个实现类,重写了 SqlSession 中一些操作数据库的常用方法,相当于一个工具类。

    @Override
    public <T> T getMapper(Class<T> type) {
        return getConfiguration().getMapper(type, this);
    }
    
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }
    

    getMapper 是从解析配置文件后保存的 mapperRegistry 中获取一个 mapper 对象

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // MapperProxyFactory 工厂类
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
    

    它通过 MapperProxyFactory 工厂类来创建具体的对象,创建时,最终生成为 Mapper 的代理对象

    protected T newInstance(MapperProxy<T> mapperProxy) {
        // jdk 动态代理
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }
    
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
    

    5.3 mybatis查询执行流程

    image-20200726193730269

    由于 UserMapper 实际上是一个 MapperProxy 的代理对象,所以 UserServiceImpl 在调用 UserMapper 中方法时,实际调用到 MapperProxy 中的 invoke 方法。

    image-20200726213127704

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else {
                // 返回一个 PlainMethodInvoker 对象,并调用 invoke 方法
                return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }
    

    PlainMethodInvoker 是 MapperMethodInvoker 的一个实现,它的 invoke 方法如下

    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
        return mapperMethod.execute(sqlSession, args);
    }
    

    最终调用 sqlSession.selectOne 方法,这里的 sqlSession 是 SqlSessionTemplate

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        return this.sqlSessionProxy.selectOne(statement, parameter);
    }
    

    而 sqlSessionProxy 是 sqlSession 的一个代理对象,它在 SqlSessionTemplate 构造函数中创建

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                              PersistenceExceptionTranslator exceptionTranslator) {
    
        notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
        notNull(executorType, "Property 'executorType' is required");
    
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        // jdk 代理,代理对象是 SqlSessionInterceptor,被代理对象时 SqlSessionFactory 的实例    
        this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor());
    }
    

    所以执行 SqlSessionInterceptor 中的 invoke 方法

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 获取 sqlSession,返回一个 DefaultSqlSession
        SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
                                              SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
        try {
            // 调用 selectOne 方法
            Object result = method.invoke(sqlSession, args);
            if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                // force commit even on non-dirty sessions because some databases require
                // a commit/rollback before calling close()
                sqlSession.commit(true);
            }
            return result;
        } catch (Throwable t) {
            Throwable unwrapped = unwrapThrowable(t);
            if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
                closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                sqlSession = null;
                Throwable translated = SqlSessionTemplate.this.exceptionTranslator
                    .translateExceptionIfPossible((PersistenceException) unwrapped);
                if (translated != null) {
                    unwrapped = translated;
                }
            }
            throw unwrapped;
        } finally {
            if (sqlSession != null) {
                closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
            }
        }
    }
    

    通过反射调用 selectOne 方法,目标对象是 DefaultSqlSession,

    @Override
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            // MappedStatement 中封装了每个 sql 操作中各项参数
            MappedStatement ms = configuration.getMappedStatement(statement);
            // 使用 executor 进行查询
            return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
    

    这里的 executor 获取 DefaultSqlSession 时创建

    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        // 执行器类型,默认是 Simple
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            // 创建一个 SimpleExecutor
            executor = new SimpleExecutor(this, transaction);
        }
        // cacheEnabled 默认为 true
        if (cacheEnabled) {
            // 使用 CachingExecutor 对 SimpleExecutor 进行装饰
            executor = new CachingExecutor(executor);
        }
        // 这里使用插件对 executor 进行代理,生成拦截器链。当前没配置插件
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
    

    获取到的 executor 类型为 CachingExecutor,它装饰了 SimpleExecutor,SimpleExecutor 继承于 BaseExecutor。

    所以执行查询时先调用 CachingExecutor#query,调用 delegate.query 时,又调用 BaseExecutor#query,这个 query 方法为模板 方法,最终调用到 SimpleExecutor#queryFromDatabase。

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
            // 执行查询
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            localCache.removeObject(key);
        }
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }
    

    在 doQuery 方法中,调用 handler.query

    @Override
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            // 创建 handler
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            // 构建 statement,包括获取 connection、设置参数
            stmt = prepareStatement(handler, ms.getStatementLog());
            // 使用 handler 查询
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }
    

    这里的 handler 是被 RoutingStatementHandler 装饰的 PreparedStatementHandler

    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        // 执行sql
        ps.execute();
        // 处理结果集
        return resultSetHandler.handleResultSets(ps);
    }
    

    最终调用 PreparedStatement 的 execute 方法完成查询。

    6.参考

    1. Mybatis官网
  • 相关阅读:
    iOS-runtime-根据协议名调某一个类有与协议里面放的相同的方法
    Mac下显示隐藏文件
    OC开发_整理笔记——多线程之GCD
    兵器簿之cocoaPods的安装和使用
    手写代码UI,xib和StoryBoard间的的优劣比较
    OC开发_Storyboard——MapKit
    smartFloat
    固定浮动侧边栏(SmartFloat)
    一个模拟时钟的时间选择器 ClockPicker
    分布式事务TransactionScope
  • 原文地址:https://www.cnblogs.com/col-smile/p/13382630.html
Copyright © 2011-2022 走看看