Spring整合Mybatis原理

1、@MapperScan注解发挥作用

在Spring整合Mybatis的时候,只需要一个@MapperScan注解就可以来进行操作,所以更加好奇的是@MapperScan底层是怎么来做到的。

下面先来研究一下@MapperScan:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
...
}

@Import注解导入了一个MapperScannerRegistrar。而在Spring启动的时候,会在org.Springframework.context.support.AbstractApplicationContext#refresh方法中会来执行@Import导入的类,看下如何来解析@Import注解的。直接来到org.Springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass会来解析@Import注解导入进来的类,那么下面就需要来看一下MapperScannerRegistrar做了什么事情。

1.1、导入MapperScannerRegistrar类

下面来看一下MapperScannerRegistrar的结构体系

1.1.2、执行ImportBeanDefinitionRegistrar接口中的registerBeanDefinitions方法

MapperScannerRegistrar类实现了ImportBeanDefinitionRegistrar接口,而实现了接口中的方法,那么在注册BeanDefinition之前,就会来执行ImportBeanDefinitionRegistrar接口中的方法

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

那么只需要看一下MapperScannerRegistrar中的registerBeanDefinitions方法是如何来进行实现的:

1.2、MapperScannerConfigurer

而在上面的代码中,主要是注册了一个MapperScannerConfigurer类型的BeanDefinition

那么来看一下这个BeanDefinition有什么特点:

MapperScannerRegistrar实现了BeanDefinitionRegistryPostProcessor接口。而这个接口中又会在ConfigurationClassPostProcessor之后发挥作用,而MapperScannerRegistrar对应的BeanDefinition是在此之前来进行解析的,所以将会在下面来解析MapperScannerRegistrar对应的BeanDefinition

那么就会来执行MapperScannerConfigurer中的postProcessBeanDefinitionRegistry方法

1.2、ClassPathMapperScanner

那么看一下Spring-Mybatis中自定义整合的ClassPathMapperScanner中的注册过滤器和扫描逻辑:

将扫描出来的类利用包含过滤器添加到当前逻辑中。

那么来看一下扫描逻辑:

然后可以看到只要接口的类

1.2.1、对筛选出来的BeanDefinition进行处理

然后对扫描出来的接口来进行筛选判断:

org.mybatis.Spring.mapper.ClassPathMapperScanner#processBeanDefinitions

1、首先获取得到接口的全限定类型。如:com.guang.dao.UserDao;

2、将当前的BeanClass设置成MapperFactoryBean,而这个类是FactoryBean类型的。在创建对象的时候,说明是要用getObject方法来创建对象的;

3、设置MapperFactoryBean构造函数中的值。在MapperFactoryBean构造函数中是Class类型,而这里设置的是String,在创建的时候Spring回来进行转换;

4、将MapperFactoryBean的注入模型设置为By-Type。也就是说,MapperFactoryBean中的setXxx中的属性会从容器中来进行查找

那么看一下MapperFactoryBean的构造方法:

  public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

这里会将mapperInterface在进行赋值的时候将java.lang.String转换成Class类型,表示对应的接口。

第二个:

说明了在MapperFactoryBean中的setXxxx方法对应的xxx属性,Spring会自动来进行赋值。而MapperFactoryBean继承了SqlSessionDaoSupport,在SqlSessionDaoSupport类中的set方法中存在setSqlSessionFactory方法和setSqlSessionTemplate方法,所以容器中如果存在着对应类型的对象的时候,那么Spring到时候来进行赋值的时候将会从容器中来进行获取得到对应的bean。

所以:SqlSessionTemplate我们可以自己配置,但是SqlSessionFactory就得由我们自己来进行配置了。

1.3、SqlSessionFactoryBean类结构体系

SqlSessionFactoryBean类就是来帮我们创建SqlSessionFactory,看下SqlSessionFactory的结构如下所示:

SqlSessionFactoryBean也是一个FactoryBean类型的,产生的对象的类型是:SqlSessionFactory。

看一下对应的getObjectType方法:

public Class<? extends SqlSessionFactory> getObjectType() {
return this.sqlSessionFactory == null ? SqlSessionFactory.class : this.sqlSessionFactory.getClass();
}

看一下对应的getObject方法:

public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
} return this.sqlSessionFactory;
}
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
// 调用对应的buildSqlSessionFactory方法产生一下sqlSessionFactory
this.sqlSessionFactory = buildSqlSessionFactory();
}

而根据buildSqlSessionFactory方法可以知道,我们可以通过对SqlSessionFactoryBean对象的属性来进行设置,从而实现对Configuration对象属性的设置。

其中有一行代码值得注意下:

targetConfiguration.setEnvironment(new Environment(this.environment,this.transactionFactory == null ?
new SpringManagedTransactionFactory() : this.transactionFactory,
this.dataSource));

对于Environment对象来说,看看构造函数:

public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {
if (id == null) {
throw new IllegalArgumentException("Parameter 'id' must not be null");
}
if (transactionFactory == null) {
throw new IllegalArgumentException("Parameter 'transactionFactory' must not be null");
}
this.id = id;
if (dataSource == null) {
throw new IllegalArgumentException("Parameter 'dataSource' must not be null");
}
this.transactionFactory = transactionFactory;
this.dataSource = dataSource;
}

需要的第二个参数是TransactionFactory(事务工厂),Spring-Mybatis整合包中,利用SpringManagedTransactionFactory类实现TransactionFactory,重写其中的方法,让当前事务交给Spring来进行处理。

那么后续Mybatis如果存在事务操作的时候,将会由Spring中的事务来进行处理。

1.3.1、SqlSessionFactoryBean使用

这里需要注意的是,下面的使用方式是通过配置类的方式来进行注入的。

因为@MapperScan注解导入的MapperScannerRegistrar的作用就是来注册一个MapperScannerConfigurer类型的BeanDefinition。

所以在下面可以直接利用@Bean注入一个对应类型的BeanDefinition,然后给BeanDefinition来设置一些属性而已。

其中在MapperScannerConfigurer实现InitializingBean接口中的afterPropertiesSet方法中判断了扫描包不能够为空,也就是说要求必须设置basePackage属性

第一种使用方式

第二种使用方式

结合SpringBoot项目使用

@ConfigurationProperties(
prefix = "mybatis"
)
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
private String typeAliasesPackage;
private String[] mapperLocations;
private Integer batchExecuteSize = 1000; public MybatisProperties() {
} public String getTypeAliasesPackage() {
return this.typeAliasesPackage;
} public void setTypeAliasesPackage(String typeAliasesPackage) {
this.typeAliasesPackage = typeAliasesPackage;
} public String[] getMapperLocations() {
return this.mapperLocations;
} public void setMapperLocations(String[] mapperLocations) {
this.mapperLocations = mapperLocations;
} public Integer getBatchExecuteSize() {
return this.batchExecuteSize;
} public void setBatchExecuteSize(Integer batchExecuteSize) {
this.batchExecuteSize = batchExecuteSize;
}
}
@Configuration
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceConfig.class})
@MapperScan({"com.guang.common.dao"})
public class MybatisConfig {
private static final String CONFIG_LOCATION = "classpath:mybatis/mybatis-config.xml";
private static final String COMMON_MAPPER_LOCATIONS = "classpath:com/guang/common/dao/mapper/*Mapper.xml";
private final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); public MybatisConfig() {
} @Bean(
name = {"sqlSessionFactory"}
)
@ConditionalOnClass({DataSource.class})
@ConditionalOnBean({DataSource.class})
public SqlSessionFactory sqlSessionFactory(MybatisProperties mybatisProperties, DataSource dataSource, ObjectProvider<Interceptor> interceptorObjectProvider) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setConfigLocation(this.getConfigLocation());
sqlSessionFactoryBean.setMapperLocations(this.getMapperLocations(mybatisProperties));
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage(mybatisProperties.getTypeAliasesPackage());
List<Interceptor> interceptorList = (List)interceptorObjectProvider.stream().collect(Collectors.toList());
sqlSessionFactoryBean.setPlugins((Interceptor[])interceptorList.toArray(new Interceptor[0]));
return sqlSessionFactoryBean.getObject();
} @Primary
@Bean(
name = {"simpleSqlSessionTemplate"}
)
@ConditionalOnClass({SqlSessionFactory.class})
@ConditionalOnBean({SqlSessionFactory.class})
public SqlSessionTemplate simpleSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.SIMPLE);
} @Bean(
name = {"batchSqlSessionTemplate"}
)
@ConditionalOnClass({SqlSessionFactory.class})
@ConditionalOnBean({SqlSessionFactory.class})
public SqlSessionTemplate batchSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
} @Bean(
name = {"mapperBatchExecutor"}
)
@ConditionalOnClass({SqlSessionTemplate.class})
@ConditionalOnBean(
value = {SqlSessionTemplate.class},
name = {"batchSqlSessionTemplate"}
)
public MapperBatchExecutor mapperBatchExecutor(@Qualifier("batchSqlSessionTemplate") SqlSessionTemplate batchSqlSessionTemplate, MybatisProperties mybatisProperties) {
return new MapperBatchExecutor(batchSqlSessionTemplate, mybatisProperties.getBatchExecuteSize());
} @Bean(
name = {"pagingInterceptor"}
)
public PagingInterceptor pagingInterceptor() {
return new PagingInterceptor();
} private Resource getConfigLocation() {
return this.resourceResolver.getResource("classpath:mybatis/mybatis-config.xml");
} private Resource[] getMapperLocations(MybatisProperties mybatisProperties) throws IOException {
Resource[] mapperLocations = this.resourceResolver.getResources("classpath:com/guang/common/dao/mapper/*Mapper.xml");
String[] var3 = mybatisProperties.getMapperLocations();
int var4 = var3.length; for(int var5 = 0; var5 < var4; ++var5) {
String mapperLocation = var3[var5];
Resource[] addedMapperLocations = this.resourceResolver.getResources(mapperLocation);
Resource[] originMapperLocations = mapperLocations;
mapperLocations = new Resource[mapperLocations.length + addedMapperLocations.length];
System.arraycopy(originMapperLocations, 0, mapperLocations, 0, originMapperLocations.length);
System.arraycopy(addedMapperLocations, 0, mapperLocations, originMapperLocations.length, addedMapperLocations.length);
} return mapperLocations;
}
}

1.4、SqlSessionTemplate

首先看一下体系结构图

实现了SqlSession接口,那么就有了SqlSession的功能。

说明,我们可以使用SqlsesseionTemplate对象来代替SqlSession对象来进行使用。

1.4.1、构造方法

看一下对应的构造方法,只有一个构造方法。也就是说,如果想要来设置SqlSessionTemplate成为bean,那么当前容器中必须要有一个SqlSessionFactory类型的Bean。

注意这里的参数信息:

  • 1、SqlSessionFactory是创建SqlSessionTemplate对象时候传入进来的,然后赋值给当前SqlSessionTemplate类中的属性sqlSessionFactory;
  • 2、ExecutorType执行器类型是从当前SqlSessionFactory获取得到的,然后赋值给当前SqlSessionTemplate类中的属性ExecutorType;
  • 3、sqlSessionProxy是sqlSession的代理对象,是利用JDK动态代理产生的对象;

1.4.2、增删改查方法

随便看一下都是类似如下所示,使用sqlSessionProxy来进行执行的

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

那么执行方法的时候,肯定会指定到InvocationHandler中的invoke方法中来。

那么来看一下SqlSessionInterceptor中的invoke方法。

1.4.3、SqlSessionInterceptor

从上面来看,可以知道是一个InvocationHandler,那么直接来看一下对应的invoke方法实现:

Spring整合Mybatis后为什么一级缓存失效

Spring-Mybatis提供的用来整合Spring和Mybatis的,所以又利用到了Spring中的事务中的内容,那么重点来研究一下这里的代码:

SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

首先要获取得到对应的SqlSession,SqlSession是mybatis中的门面。获取得到了SqlSession之后,才能够来执行对应的方法。

那么看一下如何获取得到SqlSession的。

首先看看是如何放入到当前线程上下文的。

这里有几个需要注意的点:

  • 1、mybatis中的数据源和@Transactional注解或者是TransactionTemplate使用的要是同一个数据源;
  • 2、一定要在开启事务的情况,获取的才是同一个SqlSession;没有开启事务,每次获取得到的SqlSession都是新创建的;

然后看下执行阶段,如下所示:

@Transactional
public void insert(){
userMapper.insert(new User());
orderMapper.insert(new Order());
}

看一下对应的执行

首先执行的时候判断

public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
notNull(session, NO_SQL_SESSION_SPECIFIED);
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
// 判断是否是同一个SqlSession
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
return (holder != null) && (holder.getSqlSession() == session);
}

如果是同一个SqlSession,那么就先不提交!如果不是同一个SqlSession,那么一个方法执行完成之后,就执行对应的SqlSession.commit()方法,各自占用一个数据库连接。最终如果正常的执行的话,始终占用的是同一个数据库连接,在同一个数据库连接中来执行对应的方法。

Mybatis中的一级缓存是基于SqlSession来实现的,所以在执行同一个sql时,如果使用的是同一个SqlSession对象,那么就能利用到一级缓存,提高sql的执行效率。 但是在Spring整合Mybatis后,如果没有执行某个方法时,该方法上没有加@Transactional注解,也就是没有开启Spring事务,那么后面在执行具体sql时,没执行一个sql时都会新生成一个SqlSession对象来执行该sql,这就是我们说的一级缓存失效(也就是没有使用同一个SqlSession对象),而如果开启了Spring事务,那么该Spring事务中的多个sql,在执行时会使用同一个SqlSession对象,从而一级缓存生效,具体的底层执行流程在上图。

个人理解:实际上Spring整合Mybatis后一级缓存失效并不是问题,是正常的实现,因为,一个方法如果没有开启Spring事务,那么在执行sql时候,那就是每个sql单独一个事务来执行,也就是单独一个SqlSession对象来执行该sql,如果开启了Spring事务,那就是多个sql属于同一个事务,那自然就应该用一个SqlSession来执行这多个sql。所以,在没有开启Spring事务的时候,SqlSession的一级缓存并不是失效了,而是存在的生命周期太短了(执行完一个sql后就被销毁了,下一个sql执行时又是一个新的SqlSession了)。

sqlsession的提交和回滚在哪里

对于SqlSession来说,最终的commit和rollback方法,都会调用到connection.commit()和connection.rollback()方法上去。

而在Spring接管了事务之后,在Spring控制的事务中,也会在方法提交或者回滚之后释放数据库连接。

流程

Spring整合Mybatis之后SQL执行流程: Spring整合Mybatis之后SQL执行流程 | ProcessOn免费在线作图,在线流程图,在线思维导图 |

三、总结

由很多框架都需要和Spring进行整合,而整合的核心思想就是把其他框架所产生的对象放到Spring容器中,让其成为Bean。

比如Mybatis,Mybatis框架可以单独使用,而单独使用Mybatis框架就需要用到Mybatis所提供的一些类构造出对应的对象,然后使用该对象,就能使用到Mybatis框架给我们提供的功能,和Mybatis整合Spring就是为了将这些对象放入Spring容器中成为Bean,只要成为了Bean,在我们的Spring项目中就能很方便的使用这些对象了,也就能很方便的使用Mybatis框架所提供的功能了。

Mybatis-Spring 新版本底层源码执行流程

1、通过@MapperScan导入了MapperScannerRegistrar类;

2、MapperScannerRegistrar类实现了ImportBeanDefinitionRegistrar接口,所以Spring在启动时会调用MapperScannerRegistrar类中的registerBeanDefinitions方法;

3、在registerBeanDefinitions方法中定义了一个ClassPathMapperScanner对象,用来扫描mapper,设置ClassPathMapperScanner对象可以扫描到接口,因为在Spring中是不会扫描接口的

4、同时因为ClassPathMapperScanner中重写了isCandidateComponent方法,导致isCandidateComponent只会认为接口是备选者Component

通过利用Spring的扫描后,会把接口扫描出来并且得到对应的BeanDefinition

5、接下来把扫描得到的BeanDefinition进行修改,把BeanClass修改为MapperFactoryBean,把AutowireMode修改为byType

6、扫描完成后,Spring就会基于BeanDefinition去创建Bean了,相当于每个Mapper对应一个FactoryBean

7、在MapperFactoryBean中的getObject方法中,调用了getSqlSession()去得到一个sqlSession对象,然后根据对应的Mapper接口生成一个Mapper接口代理对象,这个代理对象就成为Spring容器中的Bean

8、sqlSession对象是Mybatis中的,一个sqlSession对象需要SqlSessionFactory来产生

9、MapperFactoryBean的AutowireMode为byType,所以Spring会自动调用set方法,有两个set方法,一个setSqlSessionFactory,一个setSqlSessionTemplate,而这两个方法执行的前提是根据方法参数类型能找到对应的bean,所以Spring容器中要存在SqlSessionFactory类型的bean和SqlSessionTemplate类型的bean。(注意要注册两个的话,记得要在SIMPLESQLSESSIONTEMPLATE上加上@Primary注解)

10、如果你定义的是一个SqlSessionFactory类型的bean,那么最终也会被包装为一个SqlSessionTemplate对象,并且赋值给sqlSession属性

11、而在SqlSessionTemplate类中就存在一个getMapper方法,这个方法中就产生一个Mapper接口代理对象

到时候,当执行该代理对象的某个方法时,就会进入到Mybatis框架的底层执行流程,详细的请看下图

Spring整合Mybatis之后SQL执行流程: Spring整合Mybatis之后SQL执行流程 | ProcessOn免费在线作图,在线流程图,在线思维导图 |

有点遗漏的步骤,我在这里补充一下:

带来的好处是,可以不使用@MapperScan注解,而可以直接定义一个Bean,比如:

@Bean
public static MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.luban");
return mapperScannerConfigurer;
}

Spring整合Mybatis原理的更多相关文章

  1. 深入源码理解Spring整合MyBatis原理

    写在前面 聊一聊MyBatis的核心概念.Spring相关的核心内容,主要结合源码理解Spring是如何整合MyBatis的.(结合右侧目录了解吧) MyBatis相关核心概念粗略回顾 SqlSess ...

  2. Spring整合Mybatis原理简单分析

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" ...

  3. 【springboot spring mybatis】看我怎么将springboot与spring整合mybatis与druid数据源

    目录 概述 1.mybatis 2.druid 壹:spring整合 2.jdbc.properties 3.mybatis-config.xml 二:java代码 1.mapper 2.servic ...

  4. spring基础:什么是框架,框架优势,spring优势,耦合内聚,什么是Ioc,IOC配置,set注入,第三方资源配置,综合案例spring整合mybatis实现

    知识点梳理 课堂讲义 1)Spring简介 1.1)什么是框架 源自于建筑学,隶属土木工程,后发展到软件工程领域 软件工程中框架的特点: 经过验证 具有一定功能 半成品 1.2)框架的优势 提高开发效 ...

  5. Spring学习总结(六)——Spring整合MyBatis完整示例

    为了梳理前面学习的内容<Spring整合MyBatis(Maven+MySQL)一>与<Spring整合MyBatis(Maven+MySQL)二>,做一个完整的示例完成一个简 ...

  6. Spring学习总结(五)——Spring整合MyBatis(Maven+MySQL)二

    接着上一篇博客<Spring整合MyBatis(Maven+MySQL)一>继续. Spring的开放性和扩张性在J2EE应用领域得到了充分的证明,与其他优秀框架无缝的集成是Spring最 ...

  7. 分析下为什么spring 整合mybatis后为啥用不上session缓存

    因为一直用spring整合了mybatis,所以很少用到mybatis的session缓存. 习惯是本地缓存自己用map写或者引入第三方的本地缓存框架ehcache,Guava 所以提出来纠结下 实验 ...

  8. 2017年2月16日 分析下为什么spring 整合mybatis后为啥用不上session缓存

    因为一直用spring整合了mybatis,所以很少用到mybatis的session缓存. 习惯是本地缓存自己用map写或者引入第三方的本地缓存框架ehcache,Guava 所以提出来纠结下 实验 ...

  9. spring整合mybatis错误:class path resource [config/spring/springmvc.xml] cannot be opened because it does not exist

    spring 整合Mybatis 运行环境:jdk1.7.0_17+tomcat 7 + spring:3.2.0 +mybatis:3.2.7+ eclipse 错误:class path reso ...

  10. spring 整合Mybatis 《报错集合,总结更新》

    错误:java.lang.NoClassDefFoundError: org/aspectj/weaver/reflect/ReflectionWorld$ReflectionWorldExcepti ...

随机推荐

  1. 【LeetCode】剑指 Offer 30. 包含min函数的栈

    题目描述 定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min.push 及 pop 的时间复杂度都是 O(1). 思路 最初看到O(1)复杂度的时候,就想 ...

  2. vue移动端封装底部导航

    <template> <div class="myfooter flex-betweenCenter"> <router-link tag=" ...

  3. 【转载】【WinAPI】LockWindowUpdate的函数的用法

    DelPhi LockWindowUpdate的函数的用法 Application.ProcessMessages; LockWindowUpdate(Self.Handle); //锁住当前窗口 L ...

  4. 【kubernetes入门到精通】Kubernetes的健康监测机制以及常见ExitCode问题分析「探索篇」

    kubernetes进行Killed我们服务的问题背景 无论是在微服务体系还是云原生体系的开发迭代过程中,通常都会以Kubernetes进行容器化部署,但是这也往往带来了很多意外的场景和情况.例如,虽 ...

  5. elasticsearch实现简单的脚本排序(script sort)

    目录 1.背景 2.分析 3.构建数据 3.1 mapping 3.2 插入数据 4.实现 4.1 根据省升序排序 4.1.1 dsl 4.1.2 运行结果 4.2 湖北省排第一 4.2.1 dsl ...

  6. 抽奖动画 - 播放svga动画

    svga动画 本文介绍的动画不是css,js动画,是使用插件播放svga文件. 1.需求 UI同学在做一个春节活动,活动中需要有个开场动画,原本想的简单,不涉及接口调用逻辑,就直接用做一个gif图片由 ...

  7. angular11 报错 ERROR Error: If ngModel is used within a form tag, either the name attribute must be set or the form

    angular 报错 ERROR Error: If ngModel is used within a form tag, either the name attribute must be set ...

  8. MySQL 中一条 sql 的执行过程

    一条 SQL 的执行过程 前言 查询 查询缓存 分析器 优化器 执行器 数据更新 日志模块 redo log (重做日志) binlog (归档日志) undo log (回滚日志) 两阶段提交 为什 ...

  9. Avalonia 实现动态托盘

    先下载一个gif图片,这里提供一个gif图片示例 在线GIF图片帧拆分工具 - UU在线工具 (uutool.cn) 使用这个网站将gif切成单张图片 创建一个Avalonia MVVM的项目,将图片 ...

  10. 安装Windows Server 2022 - 初学者系列 - 学习者系列文章

    这天要写一个关于系统部署的系列文章,涉及到Windows Server 2022操作系统的安装,所以就写了此文.Windows系列的操作系统安装,以前的博文中都有介绍,这里再次做一个安装描述吧.需要的 ...