Entity Framework Core 5中实现批量更新、删除

本文介绍了一个在EntityFramework Core 5中不需要预先加载数据而使用一句SQL语句批量更新、删除数据的开发包,并且分析了其实现原理,并且与其他实现方案做了比较。

一、背景

随着微软全面拥抱开源,.Net开源社区百花开放,涌现了非常多优秀的开源,ORM项目就有Dapper、SqlSugar、PetaPoco、FreeSQL等。作为微软官方提供的ORM框架,Entity Framework Core(以下简称EF Core)显然是被关注最多的。EF Core非常优秀而且功能丰富,但是EF Core有一个一直被人诟病的地方就是它并不能很好支持数据的批量更新和批量删除。在EF Core中批量更新和删除数据都要先把数据加载到内存中,然后再对数据操作,最后再SaveChanges,比如下面的代码用于把所有Id大于2或者AuthorName中含有”zack”的价格增加3:

var books2 = ctx.Books.Where(b => b.Id >2||b.AuthorName.Contains("zack"));

foreach(var b in books2)

{

    b.Price =b.Price + 3;

}

ctx.SaveChanges();

 

让我们查看上面的程序幕后执行的SQL语句:

 

可以看到,EF Core先把数据用Select查询出来,然后在内存中逐个修改,最后再把被修改对象每个都执行一次Update语句去更新。

 

再比如,如下的代码用于删除Price大于5元的记录:

var books1 = ctx.Books.Where(b => b.Price > 5);

ctx.RemoveRange(books1);

ctx.SaveChanges();

 

让我们查看上面的程序运行幕后执行的SQL语句:

 

 

可以看到,EF Core先把数据用Select查询出来,然后再对每条记录都执行Delete语句去删除。

很显然,如果批量更新或者删除的数据量比较大,这样的操作性能是非常低的。

因此,我们需要一种在EF Core中使用一条SQL语句就高性能地删除或者更新数据的方法。

 

二、为什么微软不提供这样的方法

尽管用户的要求强烈,但是微软一直没有提供高效的批量删除和更新的方式。在EF Core Github的issue中 [1],微软给出的理由是:这样做会导致EF Core的对象状态跟踪混乱,比如对于同一个DbContext,如果用批量删除的方法删除了数据,那么在被删除之前查询出来的数据状态就混乱了,因此需要重构EF Core的代码,工作量比较大。

作为一个成熟的框架,考虑这些逻辑问题以避免潜在的风险是有必要的,是可以理解的。但是作为实际的开发者,我们是有办法规避这些问题的。比如一般的Web应用中,删除操作都是在一个单独的Http请求进行中的,因此不涉及到微软担心的问题。即使在有的场景下,涉及到在通过同一个DbContext在数据删除之前就把数据查询出来的场景,那么也完全可以通过在删除之后再查一次的方式来规避这个问题。

根据github上那个issue的回复,微软有考虑在EF Core 6.0中加入高效地批量删除和更新数据的方式,但是仅仅是“考虑”,并不确定。我们作为普通开发者可等不及了,因此要自己去解决。

三、已有解决方法

有如下三种已有的解决方法:

  1. 执行原生SQL语句。在EF Core中提供了ctx.Database.ExecuteSqlRaw()等方法可以用来执行原生SQL语句,因此我们可以直接编写Delete、Update语句来删除或者更新数据。这种方式比较直接,缺点就是这样代码中直接操作数据表的方式不太符合模型驱动、分层隔离等思想,程序员直接面对数据库表,无法利用EF Core强类型的特性,如果模型发生改变,必须手动变更SQL语句;而且如果调用了一些DBMS特有的语法、函数,一旦程序迁移到其他DBMS,就可能要重新编写SQL语句,而无法利用EF Core强大的SQL翻译机制来屏蔽不同底层数据库的差异。

  2. 使用其他ORM。FreeSQL等ORM中提供了批量Delete、Update语句的方法,使用也非常简单。这种方式的缺点是项目中必须引入第三方的ORM,无法复用EF Core的代码。

  3. 使用已有的EF Core扩展。EF Plus、EFCore.BulkExtensions等开源库中都提供了在EF Core框架下进行批量操作的方法。实现这个的核心就是要获得EF Core生成的SQL语句以及SelectExpression。由于EF Core 5.0之前的版本中没有提供公开的API用于获取一个LINQ操作对应的SQL语句,所以这些开源库都是通过访问EF Core框架中一些类的私有成员来完成的获取LINQ对应的SQL语句以及SelectExpression的方法 [2]。由于用的是访问私有成员这样不符合面向对象原则的方式,所以一旦EF Core框架代码发生改变,代码就可能会失败,之前就发生过EF Core新版本发布造成这些开源库无法工作的情况。而且,在撰写这篇文章的时候,这些开源库还没有适配.Net 5。

 

四、我的实现Zack.EFCore.Batch

我开发了一个EntityFramework Core的扩展库,让开发者在Entity Framework Core中可以用一句SQL进行数据的删除或者更新。由于开发中用到了Entity Framework Core5的API,所以这个库要求Entity FrameworkCore 5及以上版本,也就是.Net 5及以上版本。

 

下面介绍一下使用方法:

第一步,通过Nuget安装Install-Package Zack.EFCore.Batch

第二步,把如下代码添加到你的DbContext类的OnConfiguring方法中:

optionsBuilder.UseBatchEF();

第三步: 使用DbContext的扩展方法DeleteRangeAsync()来删除一批数据. DeleteRangeAsync()的参数就是过滤条件的lambda表达式。

批量删除的例子代码如下:

 

await ctx.DeleteRangeAsync<Book>(b =>b.Price > n || b.AuthorName == "zack yang");

 

上面的代码将会在数据库中执行如下SQL语句:

Delete FROM [T_Books] WHERE ([Price] > @__p_0) OR([AuthorName] = @__s_1)

 

DeleteRange()方法是DeleteRangeAsync()的同步方法版本。

使用DbContext的扩展方法BatchUpdate()来创建一个BatchUpdateBuilder对象。BatchUpdateBuilder类有如下四个方法:

  • Set()方法用于给一个属性赋值。方法的第一个参数是属性的lambda表达式,第二个参数是值的lambda表达式。

  • Where() 是过滤条件

  • ExecuteAsync()使用用于执行BatchUpdateBuilder的异步方法

  • Execute()是ExecuteAsync()的同步方法版本。

 

例子代码:

await ctx.BatchUpdate<Book>()

   .Set(b =>b.Price, b => b.Price + 3)

   .Set(b =>b.Title, b => s)

  .Set(b=>b.AuthorName,b=>b.Title.Substring(3,2)+b.AuthorName.ToUpper())

   .Set(b =>b.PubTime, b => DateTime.Now)

   .Where(b=> b.Id > n || b.AuthorName.StartsWith("Zack"))

  .ExecuteAsync();

 

上面的代码将会在SQLServer数据库中执行如下SQL语句:

Update [T_Books] SET [Price] = [Price] + 3.0E0,[Title] = @__s_1, [AuthorName] = COALESCE(SUBSTRING([Title], 3 + 1, 2), N'') +COALESCE(UPPER([AuthorName]), N''), [PubTime] = GETDATE()

WHERE ([Id] > @__p_0) OR ([AuthorName] IS NOT NULLAND ([AuthorName] LIKE N'Zack%'))

 

这个开发包使用EFCore实现的lambda表达式到SQL语句的翻译,所以几乎所有EF Core支持的lambda表达式写法都被支持。

 

项目的GitHub地址:https://github.com/yangzhongke/Zack.EFCore.Batch

五、实现原理分析

其实要把lambda表达式转换为SQL语句并不难,只要对表达式树进行解析就可以生成SQL语句,但是最难的部分是对于.Net函数到SQL片段的翻译,因为相同的.Net函数在不同DBMS中等效的SQL片段是不同的,如果我自己实现这个是很麻烦的,因此我想到了直接借用EF Core的表达式树到SQL语句的翻译引擎来实现是最佳的方法。

不幸的是,在.NetCore 3.x及之前,是无法直接获取一个Linq查询翻译后的SQL语句的。.Net Core中可以通过日志等方式获取翻译后的SQL语句,但是这些都是Linq执行后才能获得的,而且是无法在拿到一个Lambda表达式或者IQueryable的时候立即获得SQL的。经过询问.Net Core开发团队得知,在.Net Core 3.X及之前,也是没有公开的API可以完成表达式树到SQL片段翻译的功能。

 

从.Net 5开始,Entity Framework Core 中提供了不用执行查询,就可以直接获取Linq查询对应的SQL语句的方法,那就是调用IQueryable的ToQueryString()方法 [3]。

 

因此我就想通过这个ToQueryString()方法拿到的SQL语句来入手来实现这个功能。可以把用到的Lambda表达式片段、过滤表达式拼接到一个查询表达式中,然后调用ToQueryString()方法获取翻译后的SQL语句,然后编写词法分析器和语法分析器对SQL语句进行分析,提取出Where子句以及Select列中的表达式片段,然后再把这些片段重新组合成Update、Delete的SQL语句即可。

不过,由于不同DBMS的语法不同,编写这样的词法及语法分析器是很麻烦的,我就想能否研究ToQueryString()的实现原理,然后直接拿到解析过程中的SQL片段,这样就避免了生成SQL后再去解析的工作。

虽然EF Core是开源的,不过由于关于EF Core的源代码并没有一个全面介绍的文档,而EF Core的代码又是非常复杂的,所以研究EF Core的源代码是非常耗时的。研究过程中,我几次都想要放弃,最后终于把功能实现了,通过开发这个库,我也对于EF Core的内部原理,特别是从Lambda表达式到SQL的翻译的整个过程了解的非常透彻。我这里不对研究的过程去回顾,而是直接为大家讲解一下EFCore的原理,然后再讲解一下我这个Zack.EFCore.Batch的实现原理。

1.  EF Core的SQL翻译原理

EF Core中有很多的服务,比如对于IQueryable进行预处理的QueryTranslationPreprocessor、从查询中提取查询参数的RelationalParameterBasedSqlProcessor、把表达式树翻译为SQL语句的QuerySqlGenerator等。这些服务一般都是通过IXXX Factory这样的工厂类的Create()方法创建的,比如QueryTranslationPreprocessor对应的IQueryTranslationPreprocessorFactory、QuerySqlGenerator对应的IQuerySqlGeneratorFactory。而这些工厂类的对象则是通过dbContext.GetService<XXX>()来从DbContext中获得的。当然,也有的服务是不需要通过工厂直接获得的,比如Lambda编译器服务IQueryCompiler就可以直接通过ctx.GetService<IQueryCompiler>()获取。

 

因此,如果你想使用EF Core中其他的服务,都可以尝试把对应的服务接口类型或者工厂类型放到GetService()中查询一下试试。

EF Core中还允许调用DbContextOptionsBuilder的ReplaceService()方法把EF Core中的默认服务替换为自定义实现类。

 

EF Core中把一个IQueryable对象翻译为SQL语句的代码分散在各个类中,我经过努力,把它们整合为一段可以运行的代码,如下:

 

Expression query = queryable.Expression;

var databaseDependencies =ctx.GetService<DatabaseDependencies>();

IQueryTranslationPreprocessorFactory_queryTranslationPreprocessorFactory = ctx.GetService<IQueryTranslationPreprocessorFactory>();

IQueryableMethodTranslatingExpressionVisitorFactory_queryableMethodTranslatingExpressionVisitorFactory =ctx.GetService<IQueryableMethodTranslatingExpressionVisitorFactory>();

IQueryTranslationPostprocessorFactory_queryTranslationPostprocessorFactory =ctx.GetService<IQueryTranslationPostprocessorFactory>();

QueryCompilationContext queryCompilationContext =databaseDependencies.QueryCompilationContextFactory.Create(true);

 

IDiagnosticsLogger<DbLoggerCategory.Query>logger = ctx.GetService<IDiagnosticsLogger<DbLoggerCategory.Query>>();

QueryContext queryContext =ctx.GetService<IQueryContextFactory>().Create();

QueryCompiler queryComipler =ctx.GetService<IQueryCompiler>() as QueryCompiler;

//parameterize determines if it will use "Declare"or not

MethodCallExpression methodCallExpr1 =queryComipler.ExtractParameters(query, queryContext, logger, parameterize:true) as MethodCallExpression;

QueryTranslationPreprocessorqueryTranslationPreprocessor = _queryTranslationPreprocessorFactory.Create(queryCompilationContext);

MethodCallExpression methodCallExpr2 =queryTranslationPreprocessor.Process(methodCallExpr1) as MethodCallExpression;

QueryableMethodTranslatingExpressionVisitorqueryableMethodTranslatingExpressionVisitor =

       _queryableMethodTranslatingExpressionVisitorFactory.Create(queryCompilationContext);

ShapedQueryExpression shapedQueryExpression1 =queryableMethodTranslatingExpressionVisitor.Visit(methodCallExpr2) asShapedQueryExpression;

QueryTranslationPostprocessor queryTranslationPostprocessor=_queryTranslationPostprocessorFactory.Create(queryCompilationContext);

ShapedQueryExpression shapedQueryExpression2 =queryTranslationPostprocessor.Process(shapedQueryExpression1) asShapedQueryExpression;

 

IRelationalParameterBasedSqlProcessorFactory_relationalParameterBasedSqlProcessorFactory =

       ctx.GetService<IRelationalParameterBasedSqlProcessorFactory>();

RelationalParameterBasedSqlProcessor_relationalParameterBasedSqlProcessor =_relationalParameterBasedSqlProcessorFactory.Create(true);

 

SelectExpression selectExpression =(SelectExpression)shapedQueryExpression2.QueryExpression;

selectExpression =_relationalParameterBasedSqlProcessor.Optimize(selectExpression,queryContext.ParameterValues, out bool canCache);

IQuerySqlGeneratorFactory querySqlGeneratorFactory =ctx.GetService<IQuerySqlGeneratorFactory>();

QuerySqlGenerator querySqlGenerator =querySqlGeneratorFactory.Create();

var cmd =querySqlGenerator.GetCommand(selectExpression);

string sql = cmd.CommandText;

 

大致解释一下上面的代码:

queryable是一个待转换的IQueryable对象,ctx是一个DbContext对象。QueryCompilationContext是Lambda到SQL翻译这个“编译”过程的上下文,很多工厂类的Create方法都要用它做参数。QueryContext是查询语句的上下文。SelectExpression是Linq查询的表达式树翻译为强类型的抽象语法树的树根。QuerySqlGenerator的GetCommand()方法用于遍历SelectExpression生成目标SQL语句。

QuerySqlGenerator的GetCommand方法最终会调用VisitSelect(SelectExpressionselectExpression)来拼接生成SQL语句,其中会调用VisitSqlBinary(SqlBinaryExpression sqlBinaryExpression)、VisitFromSql(FromSqlExpression fromSqlExpression)、VisitLike(LikeExpression likeExpression)等方法来把运算表达式、From、Like等翻译成对应的SQL片段。由于不同DBMS中一些函数等实现不同,而SelectExpression、LikeExpression等都是一个抽象节点,是独立于具体DBMS的抽象模型,因此各个DBMS的EF Provider只要负责编写代码把这些XXExpression翻译为各自的SQL片段即可,不同DBMS的EF Core中的代码大部分都是各种XXTranslatorProvider。

2.  Zack.EFCore.Batch的实现原理

这个库最核心的代码就是ZackQuerySqlGenerator,它是一个继承自QuerySqlGenerator的类。它通过override父类的VisitSelect方法,然后把父类的VisitSelect方法的代码全部拷过来。这样的目的就是在VisitSelect拼接SQL语句的过程中把各个SQL片段截获到。以下面的代码为例:

if (selectExpression.Predicate != null)

{

       Sql.AppendLine().Append("WHERE");

       varoldSQL = Sql.Build().CommandText;//zack's code

       Visit(selectExpression.Predicate);

       this.PredicateSQL= Diff(oldSQL, this.Sql.Build().CommandText); //zack's code

}

这里就是首先把拼接Where条件之前的SQL语句保存到oldSQL变量中,再把拼接Where条件之后的SQL语句和oldSQL求一个差运算,就得到了Where语句的SQL片段。

 

然后通过optBuilder.ReplaceService<IQuerySqlGeneratorFactory,ZackQuerySqlGeneratorFactory>();把ZackQuerySqlGenerator对应的ZackQuerySqlGeneratorFactory替换为IQuerySqlGeneratorFactory的默认实现。这样EF Core再完成从SelectExpression到SQL语句的翻译,就会使用ZackQuerySqlGenerator类,这样我们就可以截获翻译生成的SQL片段了。

 

再解释一下批量更新数据库的BatchUpdateBuilder类的主要代码。代码主要就是把Age=Age+1,Name=AuthorName.Trim()这样的赋值表达式重新生成Select(new{b.Age,b.Age+1,b.Name,b.AuthorName.Trime()})这样的表达式,这样就把N个赋值表达式重新拼接为2*N个查询表达式,再把查询条件拼接形成一个IQueryable对象,再调用ZackQuerySqlGenerator翻译IQueryable获取到Where的SQL片段以及各个列的SQL片段,最后重新拼接成一个Update的SQL语句。

 

六、局限性

Zack.EFCore.Batch有如下局限性:

  1. 由于Zack.EFCore.Batch用到了EF Core 5.0的新API,所以暂不支持EF Core 3.X及以下版本。

  2. 由于Zack.EFCore.Batch是直接操作数据库,所以更新、删除后,会存在微软担心的同一个DbContext中已经查询出来的对象跟踪状态和数据库不一致的情况。在同一个DbContext实例中,如果需要在批量删除或者更新之后操作同一个DbContex中之前查询出来的数据,建议再执行一遍查询操作。

  3. 代码中使用了一个内部API QueryCompiler,这是不推荐的做法。

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

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

相关文章

篮子里拿鸡蛋问题

一个一个拿&#xff0c;正好拿完。两个两个拿&#xff0c;还剩一个。三个三个拿&#xff0c;正好拿完。 四个四个拿&#xff0c;还剩一个。五个五个拿&#xff0c;还差一个。六个六个拿&#xff0c;还剩三个。 七个七个拿&#xff0c;正好拿完。八个八个拿&#xff0c;还剩一个…

一套标准的ASP.NET Core容器化应用日志收集分析方案

点击上方蓝字给一个关注吧讲故事关注我公众号的朋友&#xff0c;应该知道我写了一些云原生应用日志收集和分析相关的文章&#xff0c;其中内容大多聚焦某个具体的组件&#xff1a;超级有用的TraceId&#xff0c;快点用起来吧&#xff01;如何利用NLog输出结构化日志&#xff0c…

算法设计与分析——递归与分治策略——棋盘覆盖

问题描述 棋盘覆盖问题要求在2^k * 2^k 个方格组成的棋盘中&#xff0c;你给定任意一个特殊点&#xff0c;用一种方案实现对除该特殊点的棋盘实现全覆盖。 建立模型如图&#xff1a; 解决方案就是利用分治法&#xff0c;将方形棋盘分成4部分&#xff0c;如果该特殊点在某一部…

函数求值需要运行所有线程_JavaScript函数式编程(二)

纯函数就是&#xff0c;对于相同的输入&#xff0c;永远会得到相同的输出&#xff0c;而且没有任何可观察的副作用&#xff0c;也不依赖外部环境的状态但是实际的编程中&#xff0c;特别是前端的编程范畴里&#xff0c;“不依赖外部环境”这个条件是根本不可能的&#xff0c;我…

算法设计与分析——递归与分治——归并排序

归并排序采用的是一种分治的思想&#xff0c;如下图&#xff0c;先将要排序的元素分为两块&#xff0c;每个块又开始分裂&#xff0c;然后逐个按照特定顺序合并&#xff0c;合成最后我们需要的数组。 归并排序的复杂度&#xff1a; 时间复杂度&#xff1a;O(nlogn) 空间复杂度&…

git 回退上一个版本_Git小白使用教程:详细、显现、真正手把手教!

不少小伙伴私信问我GitHub怎么使用&#xff1f;今天更一下&#xff0c;希望能帮到你&#xff0c;有问题评论区拍砖交流吧。

在传统行业做数字化转型之业务篇

【数字化转型】| 作者 / Edison Zhou这是EdisonTalk的第307篇原创内容在过去的两年时间里&#xff0c;我加入了一家传统行业的企业参与其数字化转型的过程&#xff0c;现在我将我的经历分享出来&#xff0c;本文是第三部分—业务篇&#xff0c;主要会介绍一下传统企业通用的三大…

算法设计与分析——递归与分治策略——快速排序

快速排序——递归算法 处理i,j的先后顺序不能改变 快速排序的基本思想&#xff1a;通过一趟排序将待排记录分隔成独立的两部分&#xff0c;其中一部分记录的关键字均比另一部分的关键字小&#xff0c;则可分别对这两部分记录继续进行排序&#xff0c;以达到整个序列有序。 函数…

git pull 覆盖本地_SVN与Git比较的优缺点差异

一、 集中式vs分布式1. Subversion属于集中式的版本控制系统集中式的版本控制系统都有一个单一的集中管理的服务器&#xff0c;保存所有文件的修订版本&#xff0c;而协同工作的人们都通过客户端连到这台服务器&#xff0c;取出最新的文件或者提交更新。Subversion的特点概括起…

C#阻塞队列BlockingCollection

BlockingCollection是一个比较冷门的类&#xff0c;我们先看下官方对这个类的定义&#xff1a;简单来说&#xff0c;BlockingCollection就是一个线程安全的阻塞队列&#xff0c;利用阻塞这个特性&#xff0c;我们可以实现进程内的生产者-消费者模式&#xff0c;比如消息转发、日…

算法设计与分析——递归与分治策略——线性时间选择

顾名思义&#xff1a;这篇文章讲解的就是如果用线性时间算法来作出元素选择问题。 问题描述&#xff1a;给定线性序集中n个元素和一个整数k&#xff0c;1<k<n.要求找出这n个元素中第k小的元素&#xff0c;即如果将这个n个元素依其线性序排列时&#xff0c;排在第k个位置的…

git push被拒绝_规范git项目提交并自动生成项目commit log

commit message 是开发的日常操作, 好的 log 不仅有助于他人 review, 还可以有效的输出 CHANGELOG, 对项目的管理实际至关重要, 但是在平时工作时&#xff0c;只依赖大致的开发规范和自觉&#xff0c;很难形成一种普遍约束。而通过本文&#xff0c;对项目进行一些基础配置&…

算法设计与分析——递归与分治策略——全排列

算法设计与分析——递归与分治策略——全排列 全排列问题的解决是通过分治与递归思想来解决的 首先判断是否递归到了最后一位&#xff0c;如果递归到了最后一位&#xff0c;则输出他当前的全排列序列。 如果没有到达最后一位&#xff0c;则循环的交换该第K个元素与其后面的所有…

asp.net core 集成 prometheus

asp.net core 集成 prometheusIntroPrometheus 是一个开源的现代化&#xff0c;云原生的系统监控框架&#xff0c;并且可以轻松的集成 PushGateway, AlertManager等组件来丰富它的功能。对于 k8s 下部署的系统来说使用 Prometheus 来做系统监控会是一个比较不错的选择&#xff…

vba 不等于_EXCEL中VBA基础语句(1)

萌二笔记分类目录及书单一、If-Then语句 说明&#xff1a;条件判断&#xff0c;如果......那么......例1&#xff1a;A2单元格的成绩大于等于60&#xff0c;则弹出对话框提示“及格”。Sub 判断成绩()If Range("A2") > 60 Then MsgBox "及格"End Sub操作…

leetcode——242. 有效的字母异位词

问题描述&#xff1a; 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的字母异位词。 示例 1: 输入: s “anagram”, t “nagaram” 输出: true 示例 2: 输入: s “rat”, t “car” 输出: false 说明: 你可以假设字符串只包含小写字母。 进阶: 如…

.NET5发布了,腾讯招聘点名要求精通MySQL,而不是SQLServer!

.NET5正式发布&#xff0c;社区一片欢腾&#xff0c;.NET相关技术栈也会迎来大变革&#xff0c;而大厂的招聘要求可谓是技术风向标&#xff01;紧盯腾讯网易顺丰等大厂的招聘&#xff0c;会发现都在明确要求。MySQL&#xff0c;而不是SQL Server了。究其根本&#xff0c;还是跨…

C# WPF:把文件给我拖进来!!!

❝首发公众号&#xff1a;Dotnet9作者&#xff1a;沙漠之尽头的狼日期&#xff1a;202-11-27一、本文开始之前上传文件时&#xff0c;一般是提供一个上传按钮&#xff0c;点击上传&#xff0c;弹出文件&#xff08;或者目录选择对话框&#xff09;&#xff0c;选择文件&#xf…

github设置中文_【Github】100+ Chinese Word Vectors 上百种预训练中文词向量

(给机器学习算法与Python学习加星标&#xff0c;提升AI技能) 该项目提供了不同表征(密集和稀疏)上下文特征(单词&#xff0c;ngram&#xff0c;字符等)和语料库训练的中文单词向量。开发者可以轻松获得具有不同属性的预先训练的向量&#xff0c;并将它们用于下游任务。此外&…

服务器重新部署踩坑记

服务器重新部署踩坑记Intro之前的服务器是 Ubuntu 18.04 &#xff0c;上周周末想升级一下服务器系统&#xff0c;从 18.04 升级到 20.04&#xff0c;结果升级升挂了... 后来 SSH 始终连不上&#xff0c;索性删除重新部署了一个&#xff0c;新部署了一个 Centos 8 的系统&#x…