欢迎访问shiker.tech

请允许在我们的网站上展示广告

您似乎使用了广告拦截器,请关闭广告拦截器。我们的网站依靠广告获取资金。

Mybatis如何简化CRUD过程?
(last modified Jun 7, 2023, 10:08 AM )
by
侧边栏壁纸
  • 累计撰写 176 篇文章
  • 累计创建 61 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

Mybatis如何简化CRUD过程?

橙序员
2023-04-02 / 3 评论 / 1 点赞 / 561 阅读 / 3,937 字 / 正在检测百度是否收录... 正在检测必应是否收录...
文章摘要(AI生成)

在上文 java如何连接数据库?中,我们已经了解了java是如何通过驱动管理器加载驱动创建数据库连接了,在上述流程中我们的输入输出如下:【输入】:数据库连接信息:数据库主机数据库端口号数据库库名用户名密码要执行的sql静态查询sql预占用sql,入参可根据需要设置:sql入参【输出】:sql执行结果

在上文 java如何连接数据库?中,我们已经了解了java是如何通过驱动管理器加载驱动创建数据库连接了,在上述流程中我们的输入输出如下:

【输入】:

  1. 数据库连接信息:
    1. 数据库主机
    2. 数据库端口号
    3. 数据库库名
    4. 用户名密码
  2. 要执行的sql
    1. 静态查询sql
    2. 预占用sql,入参可根据需要设置:
      1. sql入参

输出】:

  1. sql执行结果集

对于我们【执行sql】和【sql结果集】来说,在实际使用jdbc进行CRUD操作时,我们又有很多重复的操作,我们会发现针对一张表的CRUD时会出现:

  1. 查询获取的结果为数据库表对应实体类
  2. 保存数据到数据库时,需要插入对应数据库表的实体类

假如有个需求~~~

如果我们的需求要对数据库中的user表进行CRUD操作,user表结构如下:

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `create_time` datetime(6) DEFAULT NULL,
  `update_time` datetime(6) DEFAULT NULL,
  `avatar` varchar(1023) DEFAULT NULL,
  `description` varchar(1023) DEFAULT NULL,
  `email` varchar(127) DEFAULT NULL,
  `expire_time` datetime(6) DEFAULT NULL,
  `mfa_key` varchar(64) DEFAULT NULL,
  `mfa_type` int(11) NOT NULL DEFAULT '0',
  `nickname` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `username` varchar(50) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

我们对其进行CRUD时,会发现

  1. 查询结果返回后,我们需要将如下查询语句的返回结果封装到实体中,方便取用

image-20230401173055266

  1. 插入和更新时,我们也是相当于把我们的实体持久化:
INSERT INTO `users` ( `id`, `create_time`, `update_time`, `avatar`, `description`, `email`, `expire_time`, `mfa_key`, `mfa_type`, `nickname`, `password`, `username` )
VALUES
( 1, '2022-07-17 19:50:45.200000', '2022-07-17 20:00:13.648000', '/upload/34307/logo.jpg', '', '12345678@qq.com', '2022-07-17 19:50:45.200000', NULL, 0, '🍊序员', '123456', 'shiker' );

UPDATE `halodb-dev`.`users` 
SET `create_time` = '2022-07-17 19:50:45.200000',
`update_time` = '2022-07-17 20:00:13.648000',
`avatar` = '/upload/34307/logo.jpg',
`description` = '',
`email` = '12345678@qq.com',
`expire_time` = '2022-07-17 19:50:45.200000',
`mfa_key` = NULL,
`mfa_type` = 0,
`nickname` = '🍊序员',
`password` = '123456',
`username` = 'shiker' 
WHERE
`id` = 1;

作为研发,你需要考虑的是:

  1. 如何用最小的改动或者不改动,应对多重多样的查询和更新诉求
  2. 避免代码重复,结果集解析应该做到一次解析,到处可用
  3. 为了安全风险,禁止在代码中进行硬编码

总结-面临的问题

通过上述的需求描述,我们需要解决的问题可以如下一张图介绍:

image-20230401174454998

我们可以发现需要解决4大问题:

  1. 表对象关系映射(ORM)
  2. 类的持久化保存
  3. 结果集转义
  4. 数据库信息配置

如何进行需求实现

面对以上诉求,我们可以通过如下步骤实现:

  1. 从配置文件读取数据库信息,加载数据库驱动,获取对应数据库连接
  2. 从配置文件中读取定义的sql语句
  3. 获取sql执行器,为sql语句设置参数,如果为插入或更新语句:
    1. 从配置文件中读取ORM映射关系,将CRUD方法的参数赋值到sql语句中
  4. 执行sql语句,如果为查询则获取sql结果集:
    1. 从配置文件中读取ORM映射关系,将查询的结果集封装为对应的java bean
    2. 返回java bean作为CRUD方法的查询结果

我们对实现步骤进行拆解:

  1. 配置文件解析器】从【配置文件】读取数据库信息,加载数据库驱动到【全局配置】中并创建数据库连接
  2. 配置文件解析器】从配置文件中读取定义的sql语句到【全局配置】中
  3. sql运行器】通过获取sql执行器:
    1. 参数处理器】将CRUD方法的参数赋值到sql语句中
  4. sql运行器】调用执行sql语句,如果为查询则获取sql结果集:
    1. sql运行器】从【全局配置】中读取ORM映射关系
    2. 结果集处理器】将查询的结果集封装为对应的java bean,将返回java bean作为查询结果

基于上述过程中的对象分析,我们可以定义出各个对象的职责:

  1. 配置文件解析器】:解析配置文件
  2. 全局配置】:负责存储解析后的事务、数据源、映射关系等
  3. sql运行器】:封装JDBC执行器的操作:
    1. 获取数据库连接创建创建【sql执行器
    2. 调用参数处理器将CRUD方法入参转换为sql参数
    3. 调用【sql执行器】执行sql
    4. 调用【结果处理器】封装返回结果
  4. 参数处理器】:将用户传递的参数转换为JDBC执行器所需参数
  5. 结果处理器】:将JDBC返回的结果集转为对应的java bean集合

看mybatis如何封装

mybatis将上述步骤中针对不同对象的操作也进行抽离:

  1. 针对数据库连接:【数据库会话工厂构建器】调用【配置文件解析器】读取数据库信息,构建【数据库会话工厂】,通过【数据库会话工厂】创建数据库连接
  2. 针对配置解析:【配置文件解析器】调用【映射文件解析器】【执行器解析器】从配置文件中读取定义的sql语句到【全局配置】中
  3. 针对sql执行:【sql运行器】通过【sql执行处理器】获取数据库驱动中的sql执行器执行sql

所以我们可以归纳出:

  1. 数据库会话工厂】:负责解析配置文件创建数据库会话
    1. 数据库会话】:用户操作数据库会话完成对数据库的CRUD操作
  2. 配置文件解析器】:
    1. 全局文件解析器】:解析全局配置文件
      1. 读取数据库连接信息
      2. 读取映射文件信息
    2. 映射文件解析器】:解析ORM映射文件(数据表维度)
      1. 完成接口-映射文件映射解析与入参和返回映射解析
    3. 执行器解析器】:解析sql语句(CRUD方法维度)
  3. sql执行处理器】:由【sql运行器】执行sql语句,转义结果集
    1. 创建参数处理器和结果处理器
    2. 调用【sql执行器】执行sql语句
    3. 调用结果处理器进行转义

所以对上述过程进行总结,我们可以得到如下过程:
image-1680615778218

结合源码对照:

名称 源码类名 类型
数据库会话工厂构建器 org.apache.ibatis.session.SqlSessionFactoryBuilder
数据库会话工厂 org.apache.ibatis.session.SqlSessionFactory 接口
数据库会话 org.apache.ibatis.session.SqlSession 接口
全局配置 org.apache.ibatis.session.Configuration
全局文件解析器 org.apache.ibatis.builder.xml.XMLConfigBuilder
映射文件解析器 org.apache.ibatis.builder.xml.XMLMapperBuilder
执行器解析器 org.apache.ibatis.builder.xml.XMLStatementBuilder
事务工厂 org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory
事务 org.apache.ibatis.transaction.jdbc.JdbcTransaction
sql运行器 org.apache.ibatis.executor.Executor 接口
sql执行处理器 org.apache.ibatis.executor.statement.StatementHandler 接口
参数处理器 org.apache.ibatis.executor.parameter.ParameterHandler 接口
结果处理器 org.apache.ibatis.executor.resultset.ResultSetHandler 接口

加亿点点拓展~

在上述组件中,所有接口类型的组件都可以通过配置获取自定义实现接口的方式进行拓展,我们主要看下sql运行器和sql执行处理器

sql运行器

sql运行器mybatis默认为我们作了三种实现,在设置选项中有如下介绍:

SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理sql执行器(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。

以更新方法为例,在SimpleExecutor中实现如下:

  @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      //获取sql执行器
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      //关闭sql执行器
      closeStatement(stmt);
    }
  }

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    //创建sql执行器
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

在ReuseExecutor中实现为:

  @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
    //获取sql执行器
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.update(stmt);
  }

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    //如果对应的sql有对应的sql执行器,直接返回
    if (hasStatementFor(sql)) {
      stmt = getStatement(sql);
      //更新事务查询超时时间
      applyTransactionTimeout(stmt);
    } else {
      //不存在sql执行器,则创建
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      putStatement(sql, stmt);
    }
    handler.parameterize(stmt);
    return stmt;
  }
  
  //判断sql是否有对应sql执行器
  private boolean hasStatementFor(String sql) {
    try {
      //从sql-sql执行器集合中获取sql执行器
      Statement statement = statementMap.get(sql);
      //如果sql执行器不为空且sql执行器中的数据库连接未关闭,则返回true
      return statement != null && !statement.getConnection().isClosed();
    } catch (SQLException e) {
      //其他情况为false
      return false;
    }
  }

在BatchExecutor中实现为:

  @Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      //输入sql和获取的执行器为当前sql和当前sql执行器
      //则获取sql执行器列表最后一个
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      //更新事务查询超时时间
      applyTransactionTimeout(stmt);
      handler.parameterize(stmt);// fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      //输入sql和获取的执行器不为当前sql和当前sql执行器
      //创建sql执行器
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt); 
      //当前sql为输入sql,当前sql执行器为创建的sql执行器
      currentSql = sql;
      currentStatement = ms;
      //将创建的sql执行器添加到sql执行器列表
      statementList.add(stmt);
      //将sql、入参和映射语句添加到批量结果集中
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }

sql执行处理器

在xml映射器配置中,有如下属性:

statementType-可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。

同样以更新方法为例,在simpleStatemenHandler中实现如下:

  @Override
  public int update(Statement statement) throws SQLException {
    String sql = boundSql.getSql();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    int rows;
    //有设置主键生成器时,使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键
    if (keyGenerator instanceof Jdbc3KeyGenerator) {
      statement.execute(sql, Statement.RETURN_GENERATED_KEYS);
      rows = statement.getUpdateCount();
      keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else if (keyGenerator instanceof SelectKeyGenerator) {
      statement.execute(sql);
      rows = statement.getUpdateCount();
      keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else {
      // 没有主键生成器时,直接执行sql
      statement.execute(sql);
      rows = statement.getUpdateCount();
    }
    return rows;
  }

在PreparedStatementHandler实现为:

  @Override
  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    //有设置主键生成器时,使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }

在CallableStatementeHandler实现为:

  @Override
  public int update(Statement statement) throws SQLException {
    CallableStatement cs = (CallableStatement) statement;
    cs.execute();
    int rows = cs.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, cs, parameterObject);
    resultSetHandler.handleOutputParameters(cs);
    return rows;
  }

加亿点点技术~

全局配置

全局配置中存储了很多,包括数据源创建和sql解析,查看源码如下:

  //包括数据库驱动和事务
  protected Environment environment;
  //config.xml中settings元素存储
  protected boolean safeRowBoundsEnabled;
  protected boolean safeResultHandlerEnabled = true;
  protected boolean mapUnderscoreToCamelCase;
  protected boolean aggressiveLazyLoading;
  protected boolean multipleResultSetsEnabled = true;
  protected boolean useGeneratedKeys;
  protected boolean useColumnLabel = true;
  protected boolean cacheEnabled = true;
  protected boolean callSettersOnNulls;
  protected boolean useActualParamName = true;
  protected boolean returnInstanceForEmptyRow;
  protected boolean shrinkWhitespacesInSql;
  protected String logPrefix;
  protected Class<? extends Log> logImpl;
  protected Class<? extends VFS> vfsImpl;
  protected Class<?> defaultSqlProviderType;
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
  protected Integer defaultStatementTimeout;
  protected Integer defaultFetchSize;
  protected ResultSetType defaultResultSetType;
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
  protected boolean lazyLoadingEnabled = false;
  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
  //config.xml中属性(properties)元素存储
  protected Properties variables = new Properties();
  //如果mapper是注解形式,会使用此存储住粗
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  //插件存储
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  //config.xml中类型处理器注册
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
  //config.xml中类型别名和默认类型别名注册
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
  //config.xml中mappers元素存储
  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
  protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
  //mapper.xml中resultMap元素存储
  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
  //mapper.xml中parameterMap 元素存储
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
  //mapper.xml中useGeneratedKeys对应的keyGenerator存储
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");

其中mappedStatement中存储了我们的sql语句块:

  //sql处理执行器类型
  private StatementType statementType;
  //结果集类型
  private ResultSetType resultSetType;
  //sql语句块
  private SqlSource sqlSource;
  private Cache cache;
  //入参映射
  private ParameterMap parameterMap;
  //结果映射
  private List<ResultMap> resultMaps;

解析sql语句块

SqlSource的构建使用了策略模式,在XMLStatementBuilder.parseStatementNode方法中,其调用了XMLScriptBuilder.parseScriptNode进行解析。根据不同的标签,需要调用不同的标签处理器进行处理

  public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
    super(configuration);
    this.context = context;
    this.parameterType = parameterType;
    //初始化sql标签处理器
    initNodeHandlerMap();
  }

  //初始化sql标签处理器
  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }

  public SqlSource parseScriptNode() {
    //根据动态标签,将sql语句块拆分为一个一个sqlNode
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        //如果子节点类型为CDATA标签或者文本标签
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        //如果子节点类型为其他元素
        String nodeName = child.getNode().getNodeName();
        //根绝节点类型获取节点处理器
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        //使用节点处理器处理节点
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }
1

评论区