一条 SQL 是如何在 MyBatis 中执行的

news2024/11/13 9:36:01
前言

MyBatis 执行 SQL 的核心接口为 SqlSession 接口,该接口提供了一些 CURD 及控制事务的方法,另外还可以通过 SqlSession 先获取 Mapper 接口的实例,然后通过 Mapper 接口执行 SQL,Mapper 接口方法的执行最终还是委托到 SqlSession 中的方法。因此可以由 SqlSession 入手分析 SQL 执行流程。由于本篇文章内容较多,感兴趣的小伙伴可以先收藏,待空闲时间耐心阅读,或直接翻到最后查看总结。

SQL 执行流程分析

MyBatis 中的 SQL 都是由 SqlSession 进行执行,由于日常工作中使用的 SQL 类型多为查询,并且 MyBatis 中的查询也最为复杂,因此本篇以 SqlSession#selectList(String, Object, RowBounds) 作为入口进行分析,中间穿插 SqlSession 的其他 API 介绍,重要的组件将在后面的章节中单独列出。

public interface SqlSession extends Closeable {
    <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);  
}

#selectList 的方法定义如上所示,该方法查询数据库然后将结果转换为用户需要的类型。各参数的具体含义如下。

statement:表示 SQL 语句的标识,Mapper 接口中的方法调用时会使用 接口全限定名. 方法名,对应 Mapper xml 配置中 mapper 节点 namespace.select 节点 id。

parameter:MyBatis SQL 语句中的参数,可以是原生类型或原生类型的 包装类,可以是 Map ,也可以是其他的 Object,如果 Mapper 接口方法中包含多个参数将转换为 Map,MyBatis 取 Map 或 Object 中的字段值替换 Mapper xml 文件中的 ${paramName} 或将 #{paramName} 指定的 SQL 参数设置为对应的字段值。

rowBounds:分页信息,包含 offset 和 limit ,MyBatis 在内存中对返回的结果进行分页。

了解方法的功能后,我们再看方法的实现,MyBatis 中 SqlSession 默认的实现为 DefaultSqlSession,跟踪源码。

public class DefaultSqlSession implements SqlSession {
    // Mybatis 配置
    private final Configuration configuration;
    // 执行器
    private final Executor executor;

    @Override
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
}

SqlSession 进行数据库查询时先从配置中获取表示 SQL 语句的 MappedStatement,然后使用执行器 Executor 进行执行。调用的 Executor#query 方法定义如下。

public interface Executor {
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
}

Executor 执行查询时多了一个新的参数 ResultHandler,它用于处理每一行数据库记录对应的 Java 对象,例如可以将结果保存到 List 或者 Map 中。Executor 作为接口具有多个实现,CachingExecutor 和其他 Executor 相比仅多了 Statement 级别缓存的支持,因此我们跟踪 BaseExecutor#query 方法的实现。

public abstract class BaseExecutor implements Executor {
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
}

BaseExecutor 先使用 MappedStatement 获取到 BoundSql 的实例,然后创建了表示当前查询的 CacheKey,最后调用了另一个 #query 方法。

BoundSql:MappedStatement 包含 MyBatis 执行 SQL 需要的完整元数据,如结果映射、参数映射、动态 SQL 等,MappedStatement 将动态 SQL 解析后生成 BoundSql,BoundSql 仅包含最终执行的 SQL 及参数信息。

CacheKey:MyBatis 可以将每个 SQL 的查询结果缓存下来,CacheKey 就是用来表示缓存的 key 值,它由 SQL、参数、分页等构成,当下次使用相同的条件查询数据库时可以优先从缓存获取到查询结果。

了解完这些参数后再看调用的 #query 方法。

public abstract class BaseExecutor implements Executor {
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ... 省略校验及缓存处理代码
        List<E> list;
        try {
            queryStack++;
            // 优先从缓存获取
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
                // 处理存储过程 OUT 参数
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                // 缓存中没有数据,从数据库中查询
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        ... 省略缓存处理相关代码
        return list;
    }
}

为了将重点放在主要流程,上面的代码省略了部分缓存处理的方法,关于缓存处理将在后面单独分析。#query 方法优先从缓存中获取查询结果,如果没有获取到则会从数据库进行查询,再看数据库查询的代码。

public abstract class BaseExecutor implements Executor {

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

#queryFromDatabase 调用 #doQuery 方法进行数据库查询,然后将查询结果缓存下来。#doQuery 是一个抽象方法,我们看其在默认使用的 SimpleExecutor 中的实现。

public class SimpleExecutor extends BaseExecutor {
    @Override
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            // 创建 Statement,设置 SQL 参数
            stmt = prepareStatement(handler, ms.getStatementLog());
            // 使用 StatementHandler 执行查询
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }
    // 准备好 Statement
    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        Connection connection = getConnection(statementLog);
        // 创建 Statement
        stmt = handler.prepare(connection, transaction.getTimeout());
        // 设置 SQL 参数
        handler.parameterize(stmt);
        return stmt;
    }
}

到了这里,终于看到了熟悉的 JDBC API。#doQuery 方法先使用配置创建了一个 StatementHandler,使用 StatementHandler 创建 Statement 并设置了 SQL 的参数后就开始调用 #StatementHandler#query 执行数据库查询。StatementHandler 用于创建 StatementHandler、设置参数、执行 SQL,如果没有指定则使用

PreparedStatementHandler。

先看 StatementHandler 创建 Statement 的方法 #prepared。

public abstract class BaseStatementHandler implements StatementHandler {
    @Override
    public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
        ErrorContext.instance().sql(boundSql.getSql());
        Statement statement = null;
        try {
            // 实例化 Statement
            statement = instantiateStatement(connection);
            // 设置 Statement 的数据库参数
            setStatementTimeout(statement, transactionTimeout);
            setFetchSize(statement);
            return statement;
        } catch (SQLException e) {
            closeStatement(statement);
            throw e;
        } catch (Exception e) {
            closeStatement(statement);
            throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
        }
    }
    // 实例化 Statement
    protected abstract Statement instantiateStatement(Connection connection) throws SQLException;
}

StatementHandler#prepared 方法由基类 BaseStatementHandler 实现,先调用了模板方法 #instantiateStatement 实例化 Statement,然后设置 Statement,模板方法由具体的子类实现,如 PreparedStatementHandler 会实例化出 PreparedStatement。

再跟踪 StatementHandler 的实现 PreparedStatementHandler 设置参数的 #parameterize 方法。

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

由于 PreparedStatement 需要设置参数,因此这里 PreparedStatementHandler 将设置参数的动作委托给 ParameterHandler 进行处理。

再看 StatementHandler 查询数据的方法 StatementHandler#query 的实现。

public class PreparedStatementHandler extends BaseStatementHandler {
    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.handleResultSets(ps);
    }
}

PreparedStatementHandler 先调用 PreparedStatement#execute 方法执行 SQL,然后使用 ResultSetHandler 将查询结果转换为需要的类型。ResultSetHandler 会根据 resultMap 将数据库记录转换为 Mapper 接口方法返回的对象,由于其内部实现比较复杂,这里暂不进行分析。

MappedStatement

概念理解:根据 CURD 四种 SQL 类型,Mapper xml 文件中操作数据库的节点分为 insert、update、select、delete 四种,对应到 Mapper 接口方法上可以使用的注解则为 @Insert、@Update、@Select、@Delete,MappedStatement 表示这四种语句的元数据,MyBatis 将其保存在 Configuration 中。

解析存储:MyBatis 解析 Mapper xml 文件时会使用 xml 节点中的元数据构建 MappedStatement 实例然后添加到 Configuration,也可以直接将 Mapper 接口直接添加到 Configuration,此时会使用 Mapper 接口中的注解信息构建 MappedStatement 然后添加到 Configuration。

源码位置:参见 XMLMapperBuilder#buildStatementFromContext(List)、MapperAnnotationBuilder#parseStatement

Executor

概念理解:Executor 接口是 SQL 的执行器,它根据 SQL 语句的抽象 MappedStatement 及参数操作数据库,返回操作结果,如果进行数据库查询还可以将结果转换为用户期望的类型。

配置 Executor:通常情况下在 MyBatis 中不需要显式指定具体 Executor,如果需要指定则有以下两种方式。

xml 配置文件中 /configuration/settings 节点下指定 defaultExecutorType 的值来配置默认的 Executor。

通过 Configuration#newExecutor(Transaction, ExecutorType) 方法创建 Executor 的实例。

Executor 接口定义:Executor 接口定义如下。

public interface Executor {
    // 数据库新增、修改、或删除
    int update(MappedStatement ms, Object parameter) throws SQLException;
    // 数据库查询
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
    <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
    // 支持批处理的 Executor 批量执行 SQL
    List<BatchResult> flushStatements() throws SQLException;
    // 事务管理
    void commit(boolean required) throws SQLException;
    void rollback(boolean required) throws SQLException;
    Transaction getTransaction();    
    // 从缓存中加载对象的属性值,或记录要从缓存中获取的属性
    void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
    // 连接管理
    void close(boolean forceRollback);
    boolean isClosed();
    // 设置当前 Executor 的包装器
    void setExecutorWrapper(Executor executor);   
}

Executor 实现:Executor 作为一个接口在 MyBatis 中有多种实现,类图如下。

BaseExecutor:Executor 的基类。

SimpleExecutor:MyBatis 中默认的 Executor。

ReuseExecutor:复用 Statement 的 Executor,对于相同的 SQL,使用的是同一个 Statement。

BatchExecutor:支持批处理的 Executor,每次执行 #update 方法时会将 SQL 添加到 Statement 中,直到调用 #flushStatements 方法开始提交到数据库执行。

CachingExecutor:作为其他 Executor 的包装器,支持 Statement 级别的缓存,其他 Executor 仅支持 Session 级别的缓存。

ResultHandler

ResultHandler 用于处理 MyBatis 将某一条数据库记录转换成的 Java 对象,通常会将转换结果保存到其内部,待使用时再获取。其接口定义如下。

public interface ResultHandler<T> {
     // 处理每一行对应的值
    void handleResult(ResultContext<? extends T> resultContext);
}

接口中只有一个方法,根据结果 (即数据库单行记录对应的 Java 对象) 的上下文处理结果。ResultHandler 在 MyBatis 中的实现有两个,具体如下。

DefaultResultHandler:将结果存储至内部的 List,Mapper 接口方法返回类型为 List 时使用。

DefaultMapResultHandler:将结果存储至内部的 Map 中,Mapper 接口方法返回类型为 Map 时使用,此时需要在 Mapper 方法上使用 @MapKey 注解指定 key 使用结果的哪个属性。

StatementHandler

StatementHandler 表示 JDBC 中 Statement 的处理器,用于创建 Statement、设置 Statement 中的 SQL 参数、执行 SQL,接口定义如下。

public interface StatementHandler {
    // 创建 Statement,并设置数据库相关参数
    Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
    // 设置 Statement 中的 SQL 参数
    void parameterize(Statement statement) throws SQLException;  
    // 将 SQL 添加到批量执行列表中
    void batch(Statement statement) throws SQLException; 
    // 执行 添加、更新、删除 SQL
    int update(Statement statement) throws SQLException;  
    // 执行查询
    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;         
    <E> Cursor<E> queryCursor(Statement statement) throws SQLException;  
    // 获取 SQL 信息
    BoundSql getBoundSql();
    // 获取参数处理器
    ParameterHandler getParameterHandler();            
}

根据不同的 Statement 类型,StatementHandler 具有不同的实现,其设计和 Executor 很类似,具体如下。

BaseStatementHandler:抽象的 StatementHandler 基类,提供子类的通用实现。

SimpleStatementHandler:简单 StatementHandler,处理普通的 Statement。

PreparedStatementHandler:支持预处理的 StatementHandler,处理 PreparedStatement。

CalableStatementHandler:支持存储过程的 StatementHandler,处理 CallableStatement。

RoutingStatementHandler:其他 StatementHandler 的装饰器,根据 Statement 的类型委托给其他 StatementHandler 做具体的处理。

ParameterHandler

ParameterHandler 是 SQL 参数的处理器,用于设置 SQL 中的参数。其定义比较简单,具体如下,它只有一个默认的实现 DefaultParameterHandler。

public interface ParameterHandler {
    // 获取参数对象,该对象包含 SQL 中可用的参数
    Object getParameterObject();
    
    // 设置 SQL 参数
    void setParameters(PreparedStatement ps) throws SQLException;
}
ResultSetHandler

ResultSetHandler 是 ResultSet 的处理器,当 Statement 执行 SQL 之后,就会使用 ResultSetHandler 处理产生的 ResultSet,ResultHandler 会根据 Mapper xml 文件中定义的 resultMap 或 resultType 将数据库记录转换为 Mapper 接口方法的返回值类型。对于每一行转换为的 Java 对象,使用 ResultHandler 进行处理。该接口默认的实现是 DefaultResultSetHandler ,该接口定义如下。

public interface ResultSetHandler {
    // 处理 ResultSet 为 List
    <E> List<E> handleResultSets(Statement stmt) throws SQLException;
    // 处理 ResultSet 为 Cursor
    <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
    // 处理存储过程 OUT 参数
    void handleOutputParameters(CallableStatement cs) throws SQLException;    
}
总结

前面以 SqlSession 的方法为入口,分析了 SQL 在 MyBatis 内部执行的代码,并对重要的 API 进行了介绍,这里使用文字的方式进行总结整个过程。

首先我们会使用 SqlSessionFactoryBuilder 构建 SqlSessionFactory。期间 MyBatis 会解析 Mapper xml 文件或注解,生成 SQL 语句元数据 MappedStatement,并保存到 Configuration 中。

使用 SqlSessionFactory 获取 SqlSession,默认的 SqlSession 是 DefaultSqlSession。

使用 SqlSession 获取 Mapper 接口实现,或直接执行 SqlSession 其他方法操作数据库。

SqlSession 根据语句的标识从 Configuration 中获取 MappedStatement,然后委托 Executor 操作数据库。

Executor 优先从缓存中获取数据,如果缓存中没有则执行数据库查询,对于 update 操作会先刷新缓存。

Executor 使用 Configuration 创建出 Statement 的处理器 StatementHandler,委托 StatementHandler 执行数据库操作。

StatementHandler 先实例化出 Statement,然后使用 ParameterHandler 设置 SQL 的参数,最后执行 Statement。

StatementHandler 委托 ResultSetHandler 处理结果集。

ResultSetHandler 处理结果集,根据 resultMap 或 resultType 将每一行数据库的记录转换为 Java 对象,然后将 Java 对象交由 ResultHandler 处理,最后转换为 Mapper 接口方法的返回类型。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1153703.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

循环神经网络 - RNN

循环神经网络&#xff08;Rerrent Neural Network,RNN&#xff09;是神经网络的一种&#xff0c;类似的还有深度神经网络&#xff08;DNN&#xff09;、卷积神经网路(CNN)、生成对抗网络&#xff08;GAN)等。**RNN对具有时序特性的数据非常有成效&#xff0c;他能挖掘数据中的时…

初识webpack和打包过程

文章目录 一、path模块1.内置模块path2.path常见的API 二、webpack1.认识webpack2.脚手架依赖于webpack3.Webpack到底是什么呢&#xff1f;4.Vue项目加载的文件有哪些呢&#xff1f;5.Webpack的使用前提6.Webpack的安装7.Webpack的默认打包8.创建局部的webpack 三、webpack配置…

第19章_写在最后

第19章_写在最后 分享1 分享2 如何看待生活上、工作上的冗余、反冗余&#xff1f; 又如何看待社会的脆弱性&#xff1f;反脆弱性&#xff1f; 个人如何应对&#xff1f;

基于深度学习的人脸性别年龄识别 - 图像识别 opencv 计算机竞赛

文章目录 0 前言1 课题描述2 实现效果3 算法实现原理3.1 数据集3.2 深度学习识别算法3.3 特征提取主干网络3.4 总体实现流程 4 具体实现4.1 预训练数据格式4.2 部分实现代码 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 毕业设计…

血的教训-踩了定时线程池的坑

ScheduledExecutorService 一、背景 大家好呀&#xff0c;上周我们公司由于定时线程池使用不当出了一个故障&#xff0c;几千万的单子可能没了 给兄弟们分享分享这个坑&#xff0c;希望兄弟们以后别踩&#xff01; 业务中大量的使用定时线程池&#xff08;ScheduledExecuto…

CSS3盒模型

CSS3盒模型规定了网页元素的显示方式&#xff0c;包括大小、边框、边界和补白等概念。2015年4月&#xff0c;W3C的CSS工作组发布了CSS3基本用户接口模块&#xff0c;该模块负责控制与用户接口界面相关效果的呈现方式。 1、盒模型基础 在网页设计中&#xff0c;经常会听到内容…

软件测试面试,一定要提前准备好的面试题

收集了2023年所有粉丝的面试题后&#xff0c;负责整理出了7个高频出现的面试题&#xff0c;一起来看看。 问题1&#xff1a;请自我介绍下&#xff1f; 核心要素&#xff1a;个人技能优势工作背景经验亮点 参考回答&#xff1a; 第一种&#xff1a;基本信息离职理由 面试官您好&…

吐血整理,Jmeter接口测试-项目案例场景,直接上高速...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 场景一&#xff1…

人工智能-softmax回归

回归可以用于预测多少的问题。 比如预测房屋被售出价格&#xff0c;或者棒球队可能获得的胜场数&#xff0c;又或者患者住院的天数。 事实上&#xff0c;我们也对分类问题感兴趣&#xff1a;不是问“多少”&#xff0c;而是问“哪一个”&#xff1a; 某个电子邮件是否属于垃圾…

groovy下载与安装

Groovy是一种基于JVM&#xff08;Java虚拟机&#xff09;的敏捷开发语言&#xff0c;它结合了Python、Ruby和Smalltalk的许多强大的特性&#xff0c;Groovy 代码能够与 Java 代码很好地结合&#xff0c;也能用于扩展现有代码。由于其运行在 JVM 上的特性&#xff0c;Groovy也可…

ElasticSearch深度解析入门篇:高效搜索解决方案的介绍与实战案例讲解,带你避坑

ElasticSearch深度解析入门篇&#xff1a;高效搜索解决方案的介绍与实战案例讲解&#xff0c;带你避坑 1.Elasticsearch 产生背景 大规模数据如何检索 如&#xff1a;当系统数据量上了 10 亿、100 亿条的时候&#xff0c;我们在做系统架构的时候通常会从以下角度去考虑问题&a…

【大数据基础平台】星环TDH社区开发版单机部署

&#x1f341; 博主 "开着拖拉机回家"带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——&#x1f390;开着拖拉机回家_大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#…

产品解读 | GreatADM如何高效实现数据库资源池化部署?

前段时间&#xff0c;介绍了万里数据库的GreatADM数据库管理平台如何图形化部署MGR&#xff08;详见文章&#xff1a;探索GreatADM&#xff1a;如何快速定义监控&#xff09;&#xff0c;有朋友想了解GreatADM是否支持资源池化管理、租户隔离、监控等功能。 今天&#xff0c;我…

MaxQuant的安装和使用(linux OR windows)

目录 1. 安装2. 用法2.1 命令行方式&#xff08;linux&#xff09;2.2 GUI方式&#xff08;windows&#xff09;1) completeAndromeda配置/Configuration&#xff08;2&#xff09;数据上传/Raw files (14)组特异参数/Group-specific parameters&#xff08;17&#xff09;全局…

如何选择安全又可靠的文件数据同步软件?

数据实时同步价值体现在它能够确保数据在多个设备或系统之间实时更新和保持一致。这种技术可以应用于许多领域&#xff0c;如电子商务、社交媒体、金融服务等。在这些领域中&#xff0c;数据实时同步可以带来很多好处&#xff0c;如提高工作效率、减少数据不一致、提高用户体验…

后台管理(一)

1、管理员登录 1.1、创建Md5加密工具类&#xff1a; public static String md5(String source) {//判断source是否生效if (source null || source.length() 0) {//不是有效的数据throw new RuntimeException(CrowdConstant.MESSAGE_STRING_INVALIDATE);}String algorithm &…

带你学习毫秒级的故障检测技术BFD

【微|信|公|众|号&#xff1a;厦门微思网络】 【微思网络http://www.xmws.cn&#xff0c;成立于2002年&#xff0c;专业培训21年&#xff0c;思科、华为、红帽、ORACLE、VMware等厂商认证及考试&#xff0c;以及其他认证PMP、CISP、ITIL等】 什么是BFD&#xff1f; BFD&#x…

【uniapp+vue3】scroll-view实现纵向自动滚动及swiper实现纵向自动滚动

scroll-view本身不支持自动滚动&#xff0c;通过scroll-top属性控制滚动&#xff0c;但是不可以循环滚动 <scroll-view class"notice-bar" scroll-y"true" ref"scrollViewRef" :scroll-top"data.scrollViewTop"scroll-with-animati…

Power BI 傻瓜入门 18. 让您的数据熠熠生辉

本章内容包括&#xff1a; 配置Power BI以使数据增量刷新发现使用Power BI Desktop and Services保护数据集的方法在不影响性能和完整性的情况下管理海量数据集 如果有更新的、更相关的数据可用&#xff0c;旧数据对组织没有好处。而且&#xff0c;老实说&#xff0c;如果数据…

一文搞懂“支付·清结算·账务”全局

《上帝视角看支付&#xff0c;总架构解析》 对支付的宏观层面做了分析&#xff0c;详解了整个支付体系每一层的架构和业务模型&#xff0c;而每一层的企业内部支付体系建设是什么样的&#xff1f;会涉及到哪些环节和系统&#xff1f;每个系统会涉及到哪些单据和逻辑&#xff0c…