手写Mybatis:第12章-完善ORM框架,增删改查操作

文章目录

  • 一、目标:完善增删改查
  • 二、设计:完善增删改查
  • 三、实现:完善增删改查
    • [3.1 工程结构](#3.1 工程结构)
    • [3.2 完善增删改查类图](#3.2 完善增删改查类图)
    • [3.3 扩展解析元素](#3.3 扩展解析元素)
    • [3.4 新增执行方法](#3.4 新增执行方法)
      • [3.4.1 执行器接口添加update](#3.4.1 执行器接口添加update)
      • [3.4.2 执行器抽象基类](#3.4.2 执行器抽象基类)
      • [3.4.3 简单执行器](#3.4.3 简单执行器)
    • [3.5 语句处理器实现](#3.5 语句处理器实现)
      • [3.5.1 语句处理器接口](#3.5.1 语句处理器接口)
      • [3.5.2 修改映射器语句类](#3.5.2 修改映射器语句类)
      • [3.5.3 抽象语句处理器基类](#3.5.3 抽象语句处理器基类)
      • [3.5.4 预处理语句处理器](#3.5.4 预处理语句处理器)
      • [3.5.5 简单语句处理器](#3.5.5 简单语句处理器)
    • [3.6 SqlSession 定义和实现CRUD接口](#3.6 SqlSession 定义和实现CRUD接口)
      • [3.6.1 SqlSession接口](#3.6.1 SqlSession接口)
      • [3.6.2 默认SqlSession实现类](#3.6.2 默认SqlSession实现类)
    • [3.7 映射器命令执行调度](#3.7 映射器命令执行调度)
  • 四、测试:完善增删改查
    • [4.1 测试环境配置](#4.1 测试环境配置)
      • [4.1.1 配置数据源配置](#4.1.1 配置数据源配置)
      • [4.1.2 修改User实体类](#4.1.2 修改User实体类)
      • [4.1.3 修改IUserDao用户持久层](#4.1.3 修改IUserDao用户持久层)
      • [4.1.4 修改User_Mapper配置文件](#4.1.4 修改User_Mapper配置文件)
    • [4.2 单元测试](#4.2 单元测试)
      • [4.2.1 插入测试](#4.2.1 插入测试)
      • [4.2.2 查询测试(多条数据)](#4.2.2 查询测试(多条数据))
      • [4.2.3 修改测试](#4.2.3 修改测试)
      • [4.2.4 删除测试](#4.2.4 删除测试)
  • 五、总结:完善增删改查

一、目标:完善增删改查

💡 目前框架中所提供的 SQL 处理仅有一个 select 查询操作.

还没有其他我们日常常用的 insert、update、delete,以及 select 查询返回的集合类型数据?

  • 这部分新增处理 SQL 的内容,也就是在 SqlSession 需要定义新的接口,通知让这些接口被映射器类方法 MappedMethod 进行调用处理。
  • 结合目前框架的开发结构,对于扩展 insert/update/delete 这部分功能来说,并不会太复杂。
    • 因为从 XML 对方法的解析、参数的处理、结果的封装,都是已经成型的结构。
    • 而我们只需要把这部分新增逻辑从前到后串联到 ORM 框架中就可是实现对数据库的新增、修改和删除。

二、设计:完善增删改查

💡 在现有的框架中完成对 insert/update/delete 方法的扩展?

思考:哪里是流程的开始?

  • 首先解决对 XML 的解析,之前的 ORM 框架的开发中,仅是处理了 selectSQL 信息,现在需要把 insert/update/delete 的语句也按照解析 select 的方式进行处理。
  • 在添加解析新类型 SQL 操作前提下,后续 DefaultSqlSession 中新增的执行 SQL 方法 insert/update/delete 就可以通过 Configuration 配置项拿到对应的映射器语句,并执行后续的处理流程。
  • 在执行 sqlSession.getMapper(IUserDao.class) 获取 Mapper 以后。
  • 后续的流程会依次串联到映射器工厂、映射器,以及获取对应的映射器方法开始,调用的就是 DefaultSqlSession
  • 注意 :定义的 insert/update/delete,都是调用内部的 update 方法,这是 Mybatis ORM 框架对此类语句处理的一个包装。
    • 因为除了 select 方法,insert、update、delete,都是共性处理逻辑,所以可以被包装成一个逻辑来处理。

三、实现:完善增删改查

3.1 工程结构

java 复制代码
mybatis-step-11
|-src
	|-main
	|	|-java
	|		|-com.lino.mybatis
    |			|-binding
    |			|	|-MapperMethod.java
	|			|	|-MapperProxy.java
	|			|	|-MapperProxyFactory.java
    |			|	|-MapperRegistry.java
    |			|-builder
    |			|	|-xml
    |			|	|	|-XMLConfigBuilder.java
    |			|	|	|-XMLMapperBuilder.java
    |			|	|	|-XMLStatementBuilder.java
    |			|	|-BaseBuilder.java
    |			|	|-MapperBuilderAssistant.java
    |			|	|-ParameterExpression.java
    |			|	|-SqlSourceBuilder.java
    |			|	|-StaticSqlSource.java
	|			|-datasource
	|			|	|-druid
	|			|	|	|-DruidDataSourceFacroty.java
	|			|	|-pooled
	|			|	|	|-PooledConnection.java
	|			|	|	|-PooledDataSource.java
	|			|	|	|-PooledDataSourceFacroty.java
	|			|	|	|-PoolState.java
	|			|	|-unpooled
	|			|	|	|-UnpooledDataSource.java
	|			|	|	|-UnpooledDataSourceFacroty.java
	|			|	|-DataSourceFactory.java
	|			|-executor
	|			|	|-parameter
	|			|	|	|-ParameterHandler.java
	|			|	|-result
	|			|	|	|-DefaultResultContext.java
	|			|	|	|-DefaultResultHandler.java
	|			|	|-resultset
	|			|	|	|-DefaultResultSetHandler.java
	|			|	|	|-ResultSetHandler.java
	|			|	|	|-ResultSetWrapper.java
	|			|	|-statement
	|			|	|	|-BaseStatementHandler.java
	|			|	|	|-PreparedStatementHandler.java
	|			|	|	|-SimpleStatementHandler.java
	|			|	|	|-StatementHandler.java
	|			|	|-BaseExecutor.java
	|			|	|-Executor.java
	|			|	|-SimpleExecutor.java
    |			|-io
    |			|	|-Resources.java
    |			|-mapping
    |			|	|-BoundSql.java
    |			|	|-Environment.java
    |			|	|-MappedStatement.java
    |			|	|-ParameterMapping.java
    |			|	|-ResultMap.java
    |			|	|-ResultMapping.java
    |			|	|-SqlCommandType.java
    |			|	|-SqlSource.java
    |			|-parsing
    |			|	|-GenericTokenParser.java
    |			|	|-TokenHandler.java
	|			|-reflection
	|			|	|-factory
	|			|	|	|-DefaultObjectFactory.java
	|			|	|	|-ObjectFactory.java
	|			|	|-invoker
	|			|	|	|-GetFieldInvoker.java
	|			|	|	|-Invoker.java
	|			|	|	|-MethodInvoker.java
	|			|	|	|-SetFieldInvoker.java
	|			|	|-property
	|			|	|	|-PropertyNamer.java
	|			|	|	|-PropertyTokenizer.java
	|			|	|-wrapper
	|			|	|	|-BaseWrapper.java
	|			|	|	|-BeanWrapper.java
	|			|	|	|-CollectionWrapper.java
	|			|	|	|-DefaultObjectWrapperFactory.java
	|			|	|	|-MapWrapper.java
	|			|	|	|-ObjectWrapper.java
	|			|	|	|-ObjectWrapperFactory.java
	|			|	|-MetaClass.java
	|			|	|-MetaObject.java
	|			|	|-Reflector.java
	|			|	|-SystemMetaObject.java
	|			|-scripting
	|			|	|-defaults
	|			|	|	|-DefaultParameterHandler.java
	|			|	|	|-RawSqlSource.java
	|			|	|-xmltags
	|			|	|	|-DynamicContext.java
	|			|	|	|-MixedSqlNode.java
	|			|	|	|-SqlNode.java
	|			|	|	|-StaticTextSqlNode.java
	|			|	|	|-XMLLanguageDriver.java
	|			|	|	|-XMLScriptBuilder.java
	|			|	|-LanguageDriver.java
	|			|	|-LanguageDriverRegistry.java
    |			|-session
    |			|	|-defaults
    |			|	|	|-DefaultSqlSession.java
    |			|	|	|-DefaultSqlSessionFactory.java
    |			|	|-Configuration.java
    |			|	|-ResultContext.java
    |			|	|-ResultHandler.java
    |			|	|-RowBounds.java
    |			|	|-SqlSession.java
    |			|	|-SqlSessionFactory.java
    |			|	|-SqlSessionFactoryBuilder.java
    |			|	|-TransactionIsolationLevel.java
    |			|-transaction
    |			|	|-jdbc
    |			|	|	|-JdbcTransaction.java
    |			|	|	|-JdbcTransactionFactory.java
    |			|	|-Transaction.java
    |			|	|-TransactionFactory.java
    |			|-type
    |			|	|-BaseTypeHandler.java
    |			|	|-IntegerTypeHandler.java
    |			|	|-JdbcType.java
    |			|	|-LongTypeHandler.java
    |			|	|-StringTypeHandler.java
    |			|	|-TypeAliasRegistry.java
    |			|	|-TypeHandler.java
    |			|	|-TypeHandlerRegistry.java
	|-test
		|-java
		|	|-com.lino.mybatis.test
		|	|-dao
		|	|	|-IUserDao.java
		|	|-po
		|	|	|-User.java
		|	|-ApiTest.java
        |-resources
        	|-mapper
        	|	|-User_Mapper.xml
        	|-mybatis-config-datasource.xml

3.2 完善增删改查类图

  • 首先在 XML 映射器构建中,扩展 XMLMapperBuilder#configurationElement 方法,添加对 insert/update/delete 的解析操作。
    • 添加解析类型。
    • 同时解析信息都会存放到 Configuration 配置项的映射语句 Map 集合 mappedStatement 中,供后续 DefaultSqlSession 执行 SQL 获取配置信息时使用。
  • 接下来是对 MappedMethod 映射器方法的改造,在这里扩展 INSERT、UPDATE、DELETE,同时还需要对 SELECT 进行扩展查询出多个结果集的方法。
  • 所需要扩展的信息,都是由 DefaultSqlSession 调用执行器 Executor 进行处理的。
    • 这里你会看到 Executor 中只有 update 这个新增方法,并没有 insert、delete,因为这两个方法也是调用的 update 进行处理的。
  • 以上这些内容实现完成后,所有新增方法的调用,都会按照前面章节的实现:语句执行、结果封装等步骤,把流程执行完毕,并返回最终的结果。

3.3 扩展解析元素

  • 新增 SQL 类型的 XML 语句,把 insert、update、delete,几种类型的 SQL 解析完成后,存放到 Configuration 配置项的映射器语句中。

XMLMapperBuilder.java

java 复制代码
package com.lino.mybatis.builder.xml;

import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.builder.MapperBuilderAssistant;
import com.lino.mybatis.io.Resources;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;

/**
 * @description: XML映射构建器
 */
public class XMLMapperBuilder extends BaseBuilder {

    ...

    /**
     * 配置mapper元素
     * <mapper namespace="org.mybatis.example.BlogMapper">
     * <select id="selectBlog" parameterType="int" resultType="Blog">
     * select * from Blog where id = #{id}
     * </select>
     * </mapper>
     *
     * @param element 元素
     */
    private void configurationElement(Element element) {
        // 1.配置namespace
        String namespace = element.attributeValue("namespace");
        if ("".equals(namespace)) {
            throw new RuntimeException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);

        // 2.配置select|insert|update|delete
        buildStatementFromContext(element.elements("select"), element.elements("insert"), element.elements("update"), element.elements("delete"));
    }

    /**
     * 配置select|insert|update|delete
     *
     * @param lists 元素列表
     */
    @SafeVarargs
    private final void buildStatementFromContext(List<Element>... lists) {
        for (List<Element> list : lists) {
            for (Element element : list) {
                final XMLStatementBuilder statementBuilder = new XMLStatementBuilder(configuration, builderAssistant, element);
                statementBuilder.parseStatementNode();
            }
        }
    }
}
  • 这里改造 buildStatementFromContext 方法的入参类型为 list 的集合,也就是处理所传递到方法中的所有语句的集合。
  • 之后在 XMLMapperBuilder#configurationElement 调用层,传递
    • element.elements("select")
    • element.elements("insert")
    • element.elements("update")
    • element.elements("delete")
  • 四个类型的方法,就可以配置到 Mapper XML 中的不同 SQL 解析存放起来了。

3.4 新增执行方法

  • MybatisORM 框架中,DefaultSqlSession 中最终的 SQL 执行都会调用到 Executor 执行器的。

3.4.1 执行器接口添加update

Executor.java

java 复制代码
package com.lino.mybatis.executor;

import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.transaction.Transaction;
import java.sql.SQLException;
import java.util.List;

/**
 * @description: 执行器
 */
public interface Executor {

    ...

    /**
     * 更新
     *
     * @param ms        映射器语句
     * @param parameter 参数
     * @return 返回的是受影响的行数
     * @throws SQLException SQL异常
     */
    int update(MappedStatement ms, Object parameter) throws SQLException;

    /**
     * 查询
     *
     * @param ms            映射器语句
     * @param parameter     参数
     * @param rowBounds     分页记录限制
     * @param resultHandler 结果处理器
     * @param boundSql      SQL对象
     * @param <E>           返回的类型
     * @return List<E>
     * @throws SQLException SQL异常
     */
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

    ...
}
  • updateExecutor 执行接口新增的方法。因为其他两个方法 insert、delete 的调用,也都是调用 update 就够了。

3.4.2 执行器抽象基类

BaseExecutor.java

java 复制代码
package com.lino.mybatis.executor;

import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.transaction.Transaction;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

/**
 * @description: 执行器抽象基类
 */
public abstract class BaseExecutor implements Executor {

    private org.slf4j.Logger logger = LoggerFactory.getLogger(BaseExecutor.class);

    protected Configuration configuration;
    protected Transaction transaction;
    protected Executor wrapper;

    private boolean closed;

    public BaseExecutor(Configuration configuration, Transaction transaction) {
        this.configuration = configuration;
        this.transaction = transaction;
        this.wrapper = this;
    }

    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        return doUpdate(ms, parameter);
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }
        return doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    }

    /**
     * 更新方法
     *
     * @param ms        映射器语句
     * @param parameter 参数
     * @return 返回的是受影响的行数
     * @throws SQLException SQL异常
     */
    protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

    /**
     * 查询方法
     *
     * @param ms            映射器语句
     * @param parameter     参数
     * @param rowBounds     分页记录限制
     * @param resultHandler 结果处理器
     * @param boundSql      SQL对象
     * @param <E>           返回的类型
     * @return List<E>
     * @throws SQLException SQL异常
     */
    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

    @Override
    public Transaction getTransaction() {
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }
        return transaction;
    }

    @Override
    public void commit(boolean required) throws SQLException {
        if (closed) {
            throw new RuntimeException("Cannot commit, transaction is already closed.");
        }
        if (required) {
            transaction.commit();
        }
    }

    @Override
    public void rollback(boolean required) throws SQLException {
        if (!closed) {
            if (required) {
                transaction.rollback();
            }
        }
    }

    @Override
    public void close(boolean forceRollback) {
        try {
            try {
                rollback(forceRollback);
            } finally {
                transaction.close();
            }
        } catch (SQLException e) {
            logger.warn("Unexpected exception on closing transaction. Cause: " + e);
        } finally {
            transaction = null;
            closed = true;
        }
    }

    /**
     * 关闭语句
     *
     * @param statement 语句
     */
    protected void closeStatement(Statement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException ignore) {

            }
        }
    }
}

3.4.3 简单执行器

SimpleExecutor.java

java 复制代码
package com.lino.mybatis.executor;

import com.lino.mybatis.executor.statement.StatementHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.transaction.Transaction;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

import static ch.qos.logback.core.db.DBHelper.closeStatement;

/**
 * @description: 简单执行器
 * @author: lingjian
 * @createDate: 2022/11/8 13:42
 */
public class SimpleExecutor extends BaseExecutor {

    public SimpleExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }

    @Override
    protected int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            // 新建一个 StatementHandler
            StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
            // 准备语句
            stmt = prepareStatement(handler);
            // StatementHandler.update
            return handler.update(stmt);
        } finally {
            closeStatement(stmt);
        }
    }

    @Override
    protected <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            // 新建一个 StatementHandler
            StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
            // 准备语句
            stmt = prepareStatement(handler);
            // 返回结果
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }

    private Statement prepareStatement(StatementHandler handler) throws SQLException {
        Statement stmt;
        Connection connection = transaction.getConnection();
        // 准备语句
        stmt = handler.prepare(connection);
        handler.parameterize(stmt);
        return stmt;
    }
}
  • SimpleExecutor#doUpdate 方法是 BeanExecutor 抽象类实现 Executor#update 接口后,定义的抽象方法。
  • 这个抽象方法中,和 doQuery 方法几乎类似,都是创建一个新的 StatementHandler 语句处理器,之后准备语句,执行处理。
  • 注意doUpdate 创建 StatementHandler 语句处理器的时候,是没有 resultHandler、boundSql 两个参数的。
    • 所以在创建的过程中,是需要对有必要使用的 boundSql 进行判断处理的。
    • 这部分内容主要体现在 BaseStatementHandler 的构造函数中,关于 boundSql 的判断和实例化处理

3.5 语句处理器实现

3.5.1 语句处理器接口

StatementHandler.java

java 复制代码
package com.lino.mybatis.executor.statement;

import com.alibaba.druid.support.spring.stat.annotation.Stat;
import com.lino.mybatis.session.ResultHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

/**
 * @description: 语句处理器
 */
public interface StatementHandler {

    /**
     * 准备语句
     *
     * @param connection 链接
     * @return Statement语句
     */
    Statement prepare(Connection connection);

    /**
     * 参数化
     *
     * @param statement 语句
     * @throws SQLException SQL异常
     */
    void parameterize(Statement statement) throws SQLException;

    /**
     * 执行更新
     *
     * @param statement 语句
     * @return 返回受影响的行数
     * @throws SQLException SQL异常
     */
    int update(Statement statement) throws SQLException;

    /**
     * 执行查询
     *
     * @param statement     语句
     * @param resultHandler 结果处理器
     * @param <E>           泛型类型
     * @return 泛型集合
     * @throws SQLException SQL异常
     */
    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
}

3.5.2 修改映射器语句类

MappedStatement.java

java 复制代码
package com.lino.mybatis.mapping;

import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import java.util.List;

/**
 * @description: 映射器语句类
 */
public class MappedStatement {

    private Configuration configuration;
    private String id;
    private SqlCommandType sqlCommandType;
    private SqlSource sqlSource;
    Class<?> resultType;
    private LanguageDriver lang;
    private List<ResultMap> resultMaps;

    public MappedStatement() {
    }

    /**
     * 获取SQL对象
     *
     * @param parameterObject 参数
     * @return SQL对象
     */
    public BoundSql getBoundSql(Object parameterObject) {
        // 调用 SqlSource#getBoundSql
        return sqlSource.getBoundSql(parameterObject);
    }

    ...
}

3.5.3 抽象语句处理器基类

  • 主要变化在 BaseStatementHandler 的构造函数中添加了 boundSql 的初始化。

BaseStatementHandler.java

java 复制代码
package com.lino.mybatis.executor.statement;

import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.executor.resultset.ResultSetHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

/**
 * @description: 语句处理器抽象基类
 */
public abstract class BaseStatementHandler implements StatementHandler {

    protected final Configuration configuration;
    protected final Executor executor;
    protected final MappedStatement mappedStatement;

    protected final Object parameterObject;
    protected final ResultSetHandler resultSetHandler;
    protected final ParameterHandler parameterHandler;

    protected final RowBounds rowBounds;
    protected BoundSql boundSql;

    public BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds,
                                ResultHandler resultHandler, BoundSql boundSql) {
        this.configuration = mappedStatement.getConfiguration();
        this.executor = executor;
        this.mappedStatement = mappedStatement;
        this.parameterObject = parameterObject;
        this.rowBounds = rowBounds;

        // 新增判断,因为update不会传入boundSql参数,这里做初始化处理
        if (boundSql == null) {
            boundSql = mappedStatement.getBoundSql(parameterObject);
        }

        this.boundSql = boundSql;
        this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
        this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, resultHandler, boundSql);
    }

    @Override
    public Statement prepare(Connection connection) {
        Statement statement = null;
        try {
            // 实例化 Statement
            statement = instantiateStatement(connection);
            // 参数设置,可以被抽取,提供配置
            statement.setQueryTimeout(350);
            statement.setFetchSize(10000);
            return statement;
        } catch (Exception e) {
            throw new RuntimeException("Error prepare statement. Cause: " + e, e);
        }
    }

    /**
     * 初始化语句
     *
     * @param connection 连接
     * @return 语句
     * @throws SQLException SQL异常
     */
    protected abstract Statement instantiateStatement(Connection connection) throws SQLException;

}
  • 因为只有获取了 boundSql 的参数,才能方便的执行后续对 SQL 处理的操作。
  • 所以在执行 update 方法,没有传入 boundSql 的时候,则需要这里进行判断以及自己获取的处理操作。

3.5.4 预处理语句处理器

PreparedStatementHandler.java

java 复制代码
package com.lino.mybatis.executor.statement;

import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

/**
 * @description: 预处理语句处理器(PREPARED)
 */
public class PreparedStatementHandler extends BaseStatementHandler {

    public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject,
                                    RowBounds rowBounds, ResultHandler resultSetHandler, BoundSql boundSql) {
        super(executor, mappedStatement, parameterObject, rowBounds, resultSetHandler, boundSql);
    }

    @Override
    protected Statement instantiateStatement(Connection connection) throws SQLException {
        String sql = boundSql.getSql();
        return connection.prepareStatement(sql);
    }

    @Override
    public void parameterize(Statement statement) throws SQLException {
        parameterHandler.setParameters((PreparedStatement) statement);
    }

    @Override
    public int update(Statement statement) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return ps.getUpdateCount();
    }

    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.handleResultSets(ps);
    }
}
  • PreparedStatementHandler 预处理语句处理器中,实现类 update 方法。
  • 相对于 query 方法的实现,其实只是相当于 JDBC 操作数据库返回结果集的变化,update 处理要返回 SQL 的操作影响了多少条数据的数量。

3.5.5 简单语句处理器

SimpleStatementHandler.java

java 复制代码
package com.lino.mybatis.executor.statement;

import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

/**
 * @description: 简单语句处理器(STATEMENT)
 */
public class SimpleStatementHandler extends BaseStatementHandler {

    public SimpleStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject,
                                  RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        super(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    }

    @Override
    protected Statement instantiateStatement(Connection connection) throws SQLException {
        return connection.createStatement();
    }

    @Override
    public void parameterize(Statement statement) {

    }

    @Override
    public int update(Statement statement) throws SQLException {
        String sql = boundSql.getSql();
        statement.execute(sql);
        return statement.getUpdateCount();
    }

    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        String sql = boundSql.getSql();
        statement.execute(sql);
        return resultSetHandler.handleResultSets(statement);
    }
}

3.6 SqlSession 定义和实现CRUD接口

3.6.1 SqlSession接口

  • SqlSession 中需要新增出处理数据库的接口,包括 selectList、insert、update、delete

SqlSession.java

java 复制代码
package com.lino.mybatis.session;

import java.util.List;

/**
 * @description: SqlSession 用来执行SQL,获取映射器,管理事务
 */
public interface SqlSession {

    /**
     * 根据指定的sqlID获取一条记录的封装对象
     *
     * @param statement sqlID
     * @param <T>       封装之后的对象类型
     * @return 封装之后的对象
     */
    <T> T selectOne(String statement);

    /**
     * 根据指定的sqlID获取一条记录的封装对象,只不过这个方法容许我们给sql传递一些参数
     *
     * @param statement sqlID
     * @param parameter 传递参数
     * @param <T>       封装之后的对象类型
     * @return 封装之后的对象
     */
    <T> T selectOne(String statement, Object parameter);

    /**
     * 获取多条基类,这个方法容许我们可以传递一些参数
     *
     * @param statement    sqlID
     * @param parameter    传递参数
     * @param <E>封装之后的对象列表
     * @return 封装之后的对象列表
     */
    <E> List<E> selectList(String statement, Object parameter);

    /**
     * 插入记录,容许传递参数
     *
     * @param statement sqlID
     * @param parameter 传递参数
     * @return 返回的是受影响的行数
     */
    int insert(String statement, Object parameter);

    /**
     * 插入记录
     *
     * @param statement sqlID
     * @param parameter 传递参数
     * @return 返回的是受影响的行数
     */
    int update(String statement, Object parameter);

    /**
     * 删除记录
     *
     * @param statement sqlID
     * @param parameter 传递参数
     * @return 返回的是受影响的行数
     */
    Object delete(String statement, Object parameter);

    /**
     * 以下是事务控制方法 commit,rollback
     */
    void commit();

    /**
     * 得到映射器,使用泛型,使得类型安全
     *
     * @param type 对象类型
     * @param <T>  封装之后的对象类型
     * @return 封装之后的对象
     */
    <T> T getMapper(Class<T> type);

    /**
     * 得到配置
     *
     * @return Configuration
     */
    Configuration getConfiguration();
}

3.6.2 默认SqlSession实现类

DefaultSqlSession.java

java 复制代码
package com.lino.mybatis.session.defaults;

import com.alibaba.fastjson.JSON;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.session.SqlSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.util.List;

/**
 * @description: 默认sqlSession实现类
 */
public class DefaultSqlSession implements SqlSession {

    private Logger logger = LoggerFactory.getLogger(DefaultSqlSession.class);

    private Configuration configuration;
    private Executor executor;

    public DefaultSqlSession(Configuration configuration, Executor executor) {
        this.configuration = configuration;
        this.executor = executor;
    }

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

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        } else if (list.size() > 1) {
            throw new RuntimeException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
            return null;
        }
    }

    @Override
    public <E> List<E> selectList(String statement, Object parameter) {
        logger.info("执行查询 statement:{} parameter:{}", statement, JSON.toJSONString(parameter));
        MappedStatement ms = configuration.getMappedStatement(statement);
        try {
            return executor.query(ms, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, ms.getSqlSource().getBoundSql(parameter));
        } catch (SQLException e) {
            throw new RuntimeException("Error querying database. Cause: " + e);
        }
    }

    @Override
    public int insert(String statement, Object parameter) {
        // 在 Mybatis 中 insert 调用的是 update
        return update(statement, parameter);
    }

    @Override
    public int update(String statement, Object parameter) {
        MappedStatement ms = configuration.getMappedStatement(statement);
        try {
            return executor.update(ms, parameter);
        } catch (SQLException e) {
            throw new RuntimeException("Errot updating database. Cause: " + e);
        }
    }

    @Override
    public Object delete(String statement, Object parameter) {
        return update(statement, parameter);
    }

    @Override
    public void commit() {
        try {
            executor.commit(true);
        } catch (SQLException e) {
            throw new RuntimeException("Error committing transaction. Cause: " + e);
        }
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        return configuration.getMapper(type, this);
    }

    @Override
    public Configuration getConfiguration() {
        return configuration;
    }
}
  • DefaultSqlSession 的具体实现中可以看到,update 方法调用了具体的执行器封装成方法后,insert、delete 都是调用的这个 update 方法进行操作的。
    • 接口定义的是单一执行,接口实现是做了适配封装
  • 另外这里单独提供了 selectList 方法,所以把之前在 selectOne 关于 executor.query 的执行处理,都迁移到 selectList 方法中。
  • 之后在 selectOne 中调用 selectList 方法,并给出相应的判断处理。

3.7 映射器命令执行调度

  • 以上这些所实现的语句执行器、SqlSession 包装,最终都会交给 MapperMethod 映射器方法根据不同的 SQL 命令调用不同的 SqlSession 方法进行执行。

MapperMethod.java

java 复制代码
package com.lino.mybatis.binding;

import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.SqlCommandType;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.SqlSession;
import java.lang.reflect.Method;
import java.util.*;

/**
 * @description: 映射器方法
 */
public class MapperMethod {

    private final SqlCommand command;
    private final MethodSignature method;

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration configuration) {
        this.command = new SqlCommand(configuration, mapperInterface, method);
        this.method = new MethodSignature(configuration, method);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result = null;
        switch (command.getType()) {
            case INSERT: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.insert(command.getName(), param);
                break;
            }
            case DELETE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.delete(command.getName(), param);
                break;
            }
            case UPDATE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.update(command.getName(), param);
                break;
            }
            case SELECT: {
                Object param = method.convertArgsToSqlCommandParam(args);
                if (method.returnsMany) {
                    result = sqlSession.selectList(command.getName(), param);
                } else {
                    result = sqlSession.selectOne(command.getName(), param);
                }
                break;
            }
            default:
                throw new RuntimeException("Unknown execution method for: " + command.getName());
        }
        return result;
    }

    /**
     * SQL 指令
     */
    public static class SqlCommand {

        private final String name;
        private final SqlCommandType type;

        public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
            String statementName = mapperInterface.getName() + "." + method.getName();
            MappedStatement ms = configuration.getMappedStatement(statementName);
            this.name = ms.getId();
            this.type = ms.getSqlCommandType();
        }

        public String getName() {
            return name;
        }

        public SqlCommandType getType() {
            return type;
        }
    }

    /**
     * 方法签名
     */
    public static class MethodSignature {

        private final boolean returnsMany;
        private final Class<?> returnType;
        private final SortedMap<Integer, String> params;

        public MethodSignature(Configuration configuration, Method method) {
            this.returnType = method.getReturnType();
            this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
            this.params = Collections.unmodifiableSortedMap(getParams(method));
        }

        public Object convertArgsToSqlCommandParam(Object[] args) {
            final int paramCount = params.size();
            if (args == null || paramCount == 0) {
                // 如果没参数
                return null;
            } else if (paramCount == 1) {
                return args[params.keySet().iterator().next().intValue()];
            } else {
                // 否则,返回一个ParamMap, 修改参数名,参数名就是其位置
                final Map<String, Object> param = new ParamMap<>();
                int i = 0;
                for (Map.Entry<Integer, String> entry : params.entrySet()) {
                    // 1.先加一个#{0},#{1},#{2}...参数
                    param.put(entry.getValue(), args[entry.getKey().intValue()]);
                    // issue #71, add param names as param1, param2...but ensure backward compatibility
                    final String genericParamName = "param" + (i + 1);
                    if (!param.containsKey(genericParamName)) {
                        /*
                            2.再加一个#{param1},#{param2}...参数
                            你可以传递多个参数给一个映射器方法。
                            默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等
                            如果你想改变参数的名称(只在多参数情况下),那么你可以在参数上使用@Param("paramName")注解
                         */
                        param.put(genericParamName, args[entry.getKey()]);
                    }
                    i++;
                }
                return param;
            }
        }

        private SortedMap<Integer, String> getParams(Method method) {
            // 用一个TreeMap,这样就保证还是按参数的先后顺序
            final SortedMap<Integer, String> params = new TreeMap<>();
            final Class<?>[] argTypes = method.getParameterTypes();
            for (int i = 0; i < argTypes.length; i++) {
                String paramName = String.valueOf(params.size());
                // 不做 Param 的实现,这部分不处理。
                params.put(i, paramName);
            }
            return params;
        }

        public boolean returnsMany() {
            return returnsMany;
        }
    }

    /**
     * 参数map,静态内部类,更严格的get方法,如果没有相应的key,报错
     */
    public static class ParamMap<V> extends HashMap<String, V> {

        private static final long serialVersionUID = -2212268410512043556L;

        @Override
        public V get(Object key) {
            if (!super.containsKey(key)) {
                throw new RuntimeException("Parameter '" + key + "' not found. Available parameters are " + keySet());
            }
            return super.get(key);
        }
    }
}
  • 映射器方法 MapperMethod#execute 会根据不同的 SqlCommand 指令调用到不同的方法上去,INSERT、UPDATE、DELETE 分别按照对应的方法调用即可。
    • 这里 SELECT 进行了扩展,因为需要按照不同的方法出参类型,调用不同的方法,主要是 selectList、selectOne 的区别。
  • 另外这里 method.returnsMany 来自于 MapperMethod.MethodSignature 方法签名中进行通过返回类型进行获取。

四、测试:完善增删改查

4.1 测试环境配置

4.1.1 配置数据源配置

mybatis-config-datasource.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
    </mappers>
</configuration>
  • 修改 url 地址信息,添加 characterEncoding=utf8 中文处理

4.1.2 修改User实体类

User.java

java 复制代码
package com.lino.mybatis.test.po;

import java.util.Date;

/**
 * @description: 用户实例类
 */
public class User {

    private Long id;
    /**
     * 用户ID
     */
    private String userId;
    /**
     * 头像
     */
    private String userHead;
    /**
     * 用户名称
     */
    private String userName;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新时间
     */
    private Date updateTime;

    public User() {
    }

    public User(Long id, String userId) {
        this.id = id;
        this.userId = userId;
    }

    public User(Long id, String userId, String userName) {
        this.id = id;
        this.userId = userId;
        this.userName = userName;
    }

    ...getter/setter
}

4.1.3 修改IUserDao用户持久层

IUserDao.java

java 复制代码
package com.lino.mybatis.test.dao;

import com.lino.mybatis.test.po.User;
import java.util.List;

/**
 * @Description: 用户持久层
 */
public interface IUserDao {

    /**
     * 根据ID查询用户信息
     *
     * @param uId ID
     * @return User 用户
     */
    User queryUserInfoById(Long uId);

    /**
     * 根据用户对象查询用户信息
     *
     * @param user 用户
     * @return User 用户
     */
    User queryUserInfo(User user);

    /**
     * 查询用户对象列表
     *
     * @return List<User> 用户列表
     */
    List<User> queryUserInfoList();

    /**
     * 更新用户信息
     *
     * @param user 用户对象
     * @return 受影响的行数
     */
    int updateUserInfo(User user);

    /**
     * 新增用户信息
     *
     * @param user 用户对象
     * @return 受影响的行数
     */
    int insertUserInfo(User user);

    /**
     * 根据ID删除用户信息
     *
     * @param uId ID
     * @return 受影响的行数
     */
    int deleteUserInfoByUserId(String uId);
}

4.1.4 修改User_Mapper配置文件

User_Mapper.xml

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.lino.mybatis.test.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="com.lino.mybatis.test.po.User">
        SELECT id, userId, userName, userHead
        FROM user
        WHERE id = #{id}
    </select>

    <select id="queryUserInfo" parameterType="com.lino.mybatis.test.po.User" resultType="com.lino.mybatis.test.po.User">
        SELECT id, userId, userName, userHead
        FROM user
        where id = #{id}
          and userId = #{userId}
    </select>

    <select id="queryUserInfoList" resultType="com.lino.mybatis.test.po.User">
        SELECT id, userId, userName, userHead
        FROM user
    </select>

    <update id="updateUserInfo" parameterType="com.lino.mybatis.test.po.User">
        UPDATE user
        SET userName = #{userName}
        WHERE id = #{id}
    </update>

    <insert id="insertUserInfo" parameterType="com.lino.mybatis.test.po.User">
        INSERT INTO user
            (userId, userName, userHead, createTime, updateTime)
        VALUES (#{userId}, #{userName}, #{userHead}, now(), now())
    </insert>

    <delete id="deleteUserInfoByUserId" parameterType="java.lang.String">
        DELETE
        FROM user
        WHERE userId = #{userId}
    </delete>
</mapper>

4.2 单元测试

4.2.1 插入测试

ApiTest.java

java 复制代码
@Test
public void test_insertUserInfo() {
    // 1.获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 2.测试验证
    User user = new User();
    user.setUserId("10002");
    user.setUserName("张三");
    user.setUserHead("1_05");
    userDao.insertUserInfo(user);
    logger.info("测试结果:{}", "Insert OK");

    // 3.提交事务
    sqlSession.commit();
}

测试结果

java 复制代码
16:32:32.510 [main] INFO  c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userId propertyType:class java.lang.String
16:32:32.510 [main] INFO  c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userName propertyType:class java.lang.String
16:32:32.510 [main] INFO  c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userHead propertyType:class java.lang.String
16:32:33.171 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:32:33.213 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
16:32:33.213 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"张三"
16:32:33.213 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"1_05"
16:32:33.213 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:Insert OK
  • 从测试日志信息和数据库的截图,可以看到数据已经插入到数据库,验证通过
  • 注意 :执行完 SQL 以后,还执行一次 sqlSession.commit()
    • 这是因为在 DefaultSqlSessionFactory#openSession 开启 Session 创建事务工厂的时候,传入给事务工厂构造函数的事务是否自动提交为 false
    • 所以这里就需要我们去手动提交事务,否则是不会插入到数据库中的。

4.2.2 查询测试(多条数据)

ApiTest.java

java 复制代码
@Test
public void test_queryUserInfoList() {
    // 1.获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 2.测试验证: 对象参数
    List<User> users = userDao.queryUserInfoList();
    logger.info("测试结果:{}", JSON.toJSONString(users));
}

测试结果

java 复制代码
16:40:47.699 [main] INFO  c.l.m.s.defaults.DefaultSqlSession - 执行查询 statement:com.lino.mybatis.test.dao.IUserDao.queryUserInfoList parameter:null
16:40:48.361 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 1192171522.
16:40:48.395 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:[{"id":1,"userHead":"1_04","userId":"10001","userName":"小零哥"},{"id":4,"userHead":"1_05","userId":"10002","userName":"张三"}]
  • 现在我们再查询结果的时候,就可以查询到2条记录的集合了,这说明我们添加的 MapperMethod#execute 调用 sqlSession.selectList(command.getName(), param) 是测试通过的。

4.2.3 修改测试

ApiTest.java

java 复制代码
@Test
public void test_updateUserInfo() {
    // 1.获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 2.测试验证
    int count = userDao.updateUserInfo(new User(1L, "10001", "小灵哥"));
    logger.info("测试结果:{}", count);

    // 3.提交事务
    sqlSession.commit();
}

测试结果

java 复制代码
16:44:11.979 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:44:12.027 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"小灵哥"
16:44:12.028 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1
16:44:12.037 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:1
  • 这里测试验证把 ID=1 的用户,userName 修改为 小灵哥,通过测试日志和数据库截图,测试通过。

4.2.4 删除测试

ApiTest.java

java 复制代码
@Test
public void test_deleteUserInfoByUserId() {
    // 1.获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 2.测试验证
    int count = userDao.deleteUserInfoByUserId("10002");
    logger.info("测试结果:{}", count == 1);

    // 3.提交事务
    sqlSession.commit();
}

测试结果

java 复制代码
16:47:54.591 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:47:54.633 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
16:47:54.643 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:true
  • 这里把数据表中 userId = '10002' 的用户删除掉,通过测试日志和数据库截图,测试通过。

五、总结:完善增删改查

  • 现在手写的 Mybatis 的全部主干流程串联实现完成了,可以执行对数据库的增删改查操作。
  • 本章在原有的内容下进行扩展的时候是非常方便的,甚至不需要多大的代码改动。这主要也得益于框架在设计实现过程中,合理运用设计原则和设计模式的好处。
相关推荐
眰恦ゞLYF10 分钟前
嵌入式单片机---串口通信及相关通信技术
数据库
CHANG_THE_WORLD11 分钟前
C++并发编程指南 std::promise 介绍与使用
java·开发语言·c++·promise
Bonnie_121511 分钟前
01-线上问题处理-树形结构拼接
java
重生成为编程大王18 分钟前
FreeMarker快速入门指南
java·后端
元闰子30 分钟前
怎么用CXL加速数据库?· SIGMOD'25
数据库·后端·面试
时序数据说33 分钟前
时序数据库IoTDB的核心优势
大数据·数据库·物联网·开源·时序数据库·iotdb
TDengine (老段)34 分钟前
中国时序数据库行业市场概览、投资热点及发展趋势预测报告
数据库·物联网·时序数据库·iot·tdengine
BillKu34 分钟前
Spring Boot中MyBatis的定义与使用
spring boot·mybatis
Lyinj1 小时前
springboot源码学习。(SPI和自动装配)
java·spring boot·学习
阿昭L1 小时前
Java内部类
java·开发语言