手写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 的全部主干流程串联实现完成了,可以执行对数据库的增删改查操作。
  • 本章在原有的内容下进行扩展的时候是非常方便的,甚至不需要多大的代码改动。这主要也得益于框架在设计实现过程中,合理运用设计原则和设计模式的好处。
相关推荐
PGCCC13 分钟前
【PGCCC】postgresql 缓存池并发设计
数据库·缓存·postgresql
哎呦没15 分钟前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
小爬虫程序猿20 分钟前
如何利用Python解析API返回的数据结构?
数据结构·数据库·python
wowocpp1 小时前
查看 磁盘文件系统格式 linux ubuntu blkid ext4
linux·数据库·ubuntu
m0_571957582 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟6 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
Ai 编码助手7 小时前
MySQL中distinct与group by之间的性能进行比较
数据库·mysql