FreeSql 导入数据的各种场景总结 [C#.NET ORM]
前言
导入数据这种脏活、累活,相信大家多多少少都有经历,常见的场景有:
- 同服务器从A表导数据到B表
- 批量导入新数据
- 批量新增或更新数据
- 跨服务器从A表导数据到B表
每种场景有自己的特点,我们一般会根据特点定制做导入数据优化,减少总体导入的耗时,或者避免数据库IO/CPU占用过高,而影响到其他正常业务。
FreeSql 有好几个实用功能,流式读取数据、查询并插入、批量对比更新、插入或修改(支持实体类或字典),用好这些功能可以很方便的实现各种导入数据场景。其实 FreeSql 对应的文档一直都有,只是内容介绍比较零散,这篇文章将针对数据导入详细介绍它们的使用方法,既然 FreeSql bug 少那就多优化一下文档吧!
本文讲解以上四种常见的数据导入实现,让使用者高效解决工作问题。如果你在使用其他更好的导入方案,欢迎加入讨论!
C#.NET ORM概念
.NET ORM Object Relational Mapping 是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。FreeSql .NET ORM 支持 .NetFramework4.0+、.NetCore、Xamarin、MAUI、Blazor、以及还有说不出来的运行平台,因为代码绿色无依赖,支持新平台非常简单。目前单元测试数量:8500+,Nuget下载数量:1M+。使用最宽松的开源协议 MIT https://github.com/dotnetcore/FreeSql ,可以商用,文档齐全,甚至拿去卖钱也可以。
FreeSql 主要优势在于易用性上,基本是开箱即用,在不同数据库之间切换兼容性比较好,整体的功能特性如下:
- 支持 CodeFirst 对比结构变化迁移、DbFirst 从数据库生成实体类;
- 支持 丰富的表达式函数,独特的自定义解析;
- 支持 批量添加、批量更新、BulkCopy、导航属性,贪婪加载、延时加载、级联保存、级联删除;
- 支持 读写分离、分表分库,租户设计,分布式事务;
- 支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/Firebird/达梦/神通/人大金仓/翰高/Clickhouse/MsAccess Ado.net 实现包,以及 Odbc 的专门实现包;
8000+个单元测试作为基调,支持10多数数据库,我们提供了通用Odbc理论上支持所有数据库,目前已知有群友使用 FreeSql 操作华为高斯、mycat、tidb 等数据库。安装时只需要选择对应的数据库实现包:
dotnet add packages FreeSql.Provider.Sqlite
static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.Sqlite, @"Data Source=db1.db")
.UseAutoSyncStructure(true) //自动同步实体结构到数据库
.UseNoneCommandParameter(true) //SQL不使用参数化,以便调试
.UseMonitorCommand(cmd => Console.WriteLine(cmd.CommandText)) //打印 SQL
.Build();
FreeSql 提供多种 CRUD 使用习惯,请根据实际情况选择团队合适的一种:
- 要么 FreeSql,原始用法;
- 要么 FreeSql.Repository,仓储+工作单元习惯;
- 要么 FreeSql.DbContext,很像 EFCore 的使用习惯;
- 要么 FreeSql.BaseEntity,充血模式;
- 要么 直接像 dapper 那样使用 DbConnection 扩展方法;
场景一:同服务器从A表导数据到B表
导入数据,正常需要根据源数据量的大小来评估,评估过程需要在实践中慢慢积累,以便选择对应的导入方案。同服务器导入数据的方案有:
1、insert into A(field1, field2) select name, code from B where ...
fsql.Select<B>()
.Where(b => b.Time > DateTime.Parse("2022-08-01"))
.InsertInto("A", b => new { b.Name, b.Code });
如果数据源是多个表组成,也可以:
fsql.Select<B, C>()
.InnerJoin((b, c) => b.Id == c.Id)
.Where((b, c) => b.Time > DateTime.Parse("2022-08-01"))
.InsertInto("A", (b, c) => new { b.Name, c.Code });
2、分段插入
FreeSql 提供流式分段返回数据,防止读取的数据源量过多时占用内存,假设数据表有100W万记录,我们可以设置每次只返回 1000 条。提示:ToChunk 只执行了一次 SQL 查询。
fsql.Select<B>()
.Where(b => b.Time > DateTime.Parse("2022-08-01"))
.ToChunk(b => new A { field1 = b.Name, field2 = b.Code }, 1000, cb => {
fsql.Insert(cb.Object).NoneParameter().ExecuteAffrows();
//如果数据库支持 BulkCopy 可以调用对应的 API 方法,如 SqlServer:
//fsql.Insert(cb.Object).ExecuteSqlBulkCopy();
});
3、分页插入
利用分页多次读取,分页可能造成新旧数据问题,尽量设置好分页排序并记录好偏移量,确保重复问题。(不推荐)
var pageNumber = 1;
while (true)
{
var list = fsql.Select<B>()
.Where(b => b.Time > DateTime.Parse("2022-08-01"))
.Page(pageNumber++, 1000)
.OrderBy(b => b.Time)
.ToList(b => new A { field1 = b.Name, field2 = b.Code }, 1000);
if (list.Count == 0) break;
fsql.Insert(cb.Object).NoneParameter().ExecuteAffrows();
//如果数据库支持 BulkCopy 可以调用对应的 API 方法,如 SqlServer:
//fsql.Insert(cb.Object).ExecuteSqlBulkCopy();
//停顿5秒后再插入,这个值可以根据需要自己调
Thread.CurrentThread.Join(TimeSpan.FromSeconds(5));
}
场景二:批量导入新数据
从 Excel/CVS 文件批量导入新数据,第一步需要将文件内容转换为 DataTable/List<T> c# 对象,这一步网上有很多工具类,在此就不讲解了。
此场景适合导入的数据是全新的、不存在于目标数据库表,假设我们都已经将 Excel/CVS 内容转换成为了 List<T>
1、利用无参数化插入
批量插入利用无参数化,会比参数化效率更高,注意参数化与SQL注入没有必然联系。
fsql.Insert(list).NoneParameter().ExecuteAffrows();
2、利用 BulkCopy 插入
BulkCopy 是大批量数据插入的最优方案,只可惜不是每种数据库都支持,FreeSql 支持了 SqlServer/Oracle/MySql/PostgreSQL/达梦 等数据库的 BulkCopy API,如果是其他数据库建议使用无参数化插入。
fsql.Insert(list).ExecuteSqlBulkCopy(); //SqlServer
fsql.Insert(list).ExecuteOracleBulkCopy(); //Oracle
fsql.Insert(list).ExecuteMySqlBulkCopy(); //MySql
fsql.Insert(list).ExecutePgCopy(); //PostgreSQL
fsql.Insert(list).ExecuteDmBulkCopy(); //达梦
为什么不统一 API?
目前每种数据库驱动的 BulkCopy API 参数不一致,这些参数可以针对性的进行调优。
场景三:批量新增或更新数据
相比场景二,场景三会更麻烦,因为不是简单的追加数据,还要处理历史数据的更新,甚至对历史数据存在则忽略。正因为复杂才衍生出了更多的方案,每种方案都有优缺点,需要使用者根据自身实际情况选择最适合的一种方法。
同上,我们假设已经将 Excel/CVS 内容转换成为了 List<T>
1、内存循环 list 查询判断(不推荐)
foreach (var item in list)
{
if (fsql.Select<T>(item).Any() == false)
fsql.Insert(item).ExecuteAffrows();
else
fsql.Update<T>().SetSource(item).ExecuteAffrows();
}
这种方式实在不推荐作为批量操作,性能非常低。其实 FreeSql.Repository 提供了上述的封装:
var repo = fsql.GetRepository<T>();
foreach (var item in list)
repo.InsertOrUpdate(item);
2、利用数据库 MERGE INTO 特性
IFreeSql 定义了 InsertOrUpdate 方法实现添加或修改的功能,利用数据库特性:
| Database | Features | Database | Features | |
|---|---|---|---|---|
| MySql | on duplicate key update | 达梦 | merge into | |
| PostgreSQL | on conflict do update | 人大金仓 | on conflict do update | |
| SqlServer | merge into | 神通 | merge into | |
| Oracle | merge into | 南大通用 | merge into | |
| Sqlite | replace into | MsAccess | 不支持 | |
| Firebird | merge into |
fsql.InsertOrUpdate<T>()
.SetSource(list) //需要操作的数据
//.IfExistsDoNothing() //如果数据存在,啥事也不干(相当于只有不存在数据时才插入)
.ExecuteAffrows();
//或者..
var sql = fsql.Select<T2, T3>()
.ToSql((a, b) => new
{
id = a.id + 1,
name = "xxx"
}, FieldAliasOptions.AsProperty);
fsql.InsertOrUpdate<T>()
.SetSource(sql)
.ExecuteAffrows();
fsql.InsertOrUpdateDict 方法可针对非实体类操作(字典类型)
题外话,是否见过这种 SQL:
insert into T
select name, code
from dual
where not exists(
select 1 from T where code = dual.code
)
3、利用 BeginEdit 批量编辑
MERGE INTO 数据库特性,其实性能是很低的。800万行记录导入7000行大约7秒,各数据库性能差不多。
BeginEdit 是 FreeSql 特色功能之一,非常实用,可它却是少有被发掘到的功能。创意源自于医疗软件,比如操作员编辑清单,会新增,会删除,会修改,等操作完后再点保存。
其实我过往开发的项目也有过类似需求,每步操作都进行数据库保存,没什么问题吧?让我们看下最后统一保存应该怎么做。
//将 list 返回给 UI 端渲染
var list = fsql.Select<T>().Where(a => a.OrderId == 100).ToList();
//统一保存
//旧数据可通过查询,或者由 UI 端提供
List<T> oldlist = fsql.Select<T>().Where(a => a.OrderId == 100).ToList();
//新数据由 UI 端提供
List<T> newlist = ...;
var repo = fsql.GetRepository<T>();
repo.BeginEdit(oldlist); //开始进行编辑
repo.EndEdit(newlist); //对比新旧List
BeginEdit/EndEdit 只针对 oldlist 对比,而不是针对全表。在内存中对比计算 Insert/Update/Delete 比 MERGE INTO 性能快得多,利用该功能可以很方便的实现批量导入或更新,例如重复导入一天的数据。
应当注意当导入的数据量过大时,应该分批进行操作,而不是一次性对比全部,假设我们每批执行 1000条:
//查询旧数据
var oldlist = fsql.Select<T>().WhereDynamic(list1000).ToList();
//使用 IN 查询性能可能较慢,可以按时间范围查询,如下:
//var minTime = list1000.Select(a => a.Time).Min();
//var maxTime = list1000.Select(a => a.Time).Max();
//var oldlist = fsql.Select<T>().Where(a=> a.Time.Between(minTime, maxTime)).ToList();
//在内存二次过滤,还可以进一步优化将 list1000.Any 改成字典
//oldlist = oldlist.Where(a=> !list1000.Any(b => b.Id == a.Id)).ToList();
var repo = fsql.GetRepository<T>();
//被编辑的数据
repo.BeginEdit(oldlist);
//将 list1000 与 oldlist 进行对比,计算出 delete/insert/update 语句执行
repo.EndEdit(list1000);
EndEdit 最多执行 3条 SQL,从而大大提升了命令往返时间消耗。特别适合导入大批量数据,且大部分数据已经存在,或者数据未发生变更的场景。
4、MySql insert ignore into
如果只插入不存的的数据,并且使用 MySql 数据库,可以使用以下方式:
fsql.Insert<T>().MySqlIgnoreInto().AppendData(list).NoneParameter().ExecuteAffrows();
///INSERT IGNORE INTO `T`(...)
//VALUES(...),(...),(...)
场景四:跨服务器从A表导数据到B表
与场景一类似,跨服务器需要定义多个 IFreeSql 对象,假设 A表所在服务器访问对象是 fsql1,B表使用 fsql2
1、分段插入
fsql2.Select<B>()
.Where(b => b.Time > DateTime.Parse("2022-08-01"))
.ToChunk(b => new A { field1 = b.Name, field2 = b.Code }, 1000, cb => {
fsql1.Insert(cb.Object).NoneParameter().ExecuteAffrows();
//如果数据库支持 BulkCopy 可以调用对应的 API 方法,如 SqlServer:
//fsql1.Insert(cb.Object).ExecuteSqlBulkCopy();
//这里也可以使用 BeginEdit 进行批量编辑功能,解决数据重复问题
});
2、分页插入
利用分页多次读取,分页可能造成新旧数据问题,尽量设置好分页排序并记录好偏移量,确保重复问题。(不推荐)
var pageNumber = 1;
while (true)
{
var list = fsql2.Select<B>()
.Where(b => b.Time > DateTime.Parse("2022-08-01"))
.Page(pageNumber++, 1000)
.OrderBy(b => b.Time)
.ToList(b => new A { field1 = b.Name, field2 = b.Code }, 1000);
if (list.Count == 0) break;
fsql1.Insert(cb.Object).NoneParameter().ExecuteAffrows();
//如果数据库支持 BulkCopy 可以调用对应的 API 方法,如 SqlServer:
//fsql1.Insert(cb.Object).ExecuteSqlBulkCopy();
//这里也可以使用 BeginEdit 进行批量编辑功能,解决数据重复问题
//停顿5秒后再插入,这个值可以根据需要自己调
Thread.CurrentThread.Join(TimeSpan.FromSeconds(5));
}
结束语
FreeSql 的功能强大,以及稳定性,我不想吹牛,也不喜欢吹牛,如果大家有什么好的想法可以一起讨论,毕竟我们只能遇到有限的场景,还有很多我不知道的场景需求不是吗?
希望这篇文章能帮助大家轻松理解并熟练掌握它,快速解决工作中遇到的数据导入问题,为企业的项目研发贡献力量。
开源地址:https://github.com/dotnetcore/FreeSql
作者是什么人?
作者是一个入行 18年的老批,他目前写的.net 开源项目有:
| 开源项目 | 描述 | 开源地址 | 开源协议 |
|---|---|---|---|
| FreeIM | 聊天系统架构 | https://github.com/2881099/FreeIM | MIT |
| FreeRedis | Redis SDK | https://github.com/2881099/FreeRedis | MIT |
| csredis | https://github.com/2881099/csredis | MIT | |
| FightLandlord | 斗DI主网络版 | https://github.com/2881099/FightLandlord | 学习用途 |
| FreeScheduler | 定时任务 | https://github.com/2881099/FreeScheduler | MIT |
| IdleBus | 空闲容器 | https://github.com/2881099/IdleBus | MIT |
| FreeSql | ORM | https://github.com/dotnetcore/FreeSql | MIT |
| FreeSql.Cloud | 分布式tcc/saga | https://github.com/2881099/FreeSql.Cloud | MIT |
| FreeSql.AdminLTE | 低代码后台生成 | https://github.com/2881099/FreeSql.AdminLTE | MIT |
| FreeSql.DynamicProxy | 动态代理 | https://github.com/2881099/FreeSql.DynamicProxy | 学习用途 |
需要的请拿走,这些都是最近几年的开源作品,以前更早写的就不发了。
FreeSql 导入数据的各种场景总结 [C#.NET ORM]的更多相关文章
- SQL SERVER导入数据到ORACLE的方法总结
我们偶尔会有将数据从SQL SERVER导入到ORACLE当中的这种需求,那么这种跨数据库导数有那些方法呢?这些方法又有那些利弊呢? 下面比较肤浅的总结了一些可行的方法. 1:生成SQL脚本然后去OR ...
- Elasticsearch —— bulk批量导入数据
在使用Elasticsearch的时候,一定会遇到这种场景--希望批量的导入数据,而不是一条一条的手动导入.那么此时,就一定会需要bulk命令! 更多内容参考我整理的Elk教程 bulk批量导入 批量 ...
- 批量导入数据到HBase
hbase一般用于大数据的批量分析,所以在很多情况下需要将大量数据从外部导入到hbase中,hbase提供了一种导入数据的方式,主要用于批量导入大量数据,即importtsv工具,用法如下: Us ...
- 随笔编号-09 批量导入数据(Mysql)报MySQL server has gone away 问题的解决方法
问题场景: 使用*.sql 脚本,批量导入数据到mysql实例中,使用DOS 界面导入的,期间,到最后一步 source D:\aaa.sql 回车后,系统提示 MySQL server has g ...
- 使用neo4j-import导入数据及关系
背景 上节我们了解了什么是图数据库,作为研究对象的neo4j的特点,优缺点以及基本的环境搭建. 现在我们要讲存储在csv中的通话记录数据导入到neo4j中去,并且可以通过cql去查询导入的数据及关系 ...
- 【HIVE】(1)建表、导入数据、外部表、导出数据
导入数据 1). 本地 load data local inpath "/root/example/hive/data/dept.txt" into table dept; 2). ...
- SQL Server导入数据时“启用标示插入”详解
在SQL Server中导入数据时,会有一个"启用标示插入"的选项,突然间懵逼了,这到底啥意思?我选与不选这个选项,结果好像没区别!不科学啊这,"存在即合理", ...
- MySQL 导入数据
MySQL中可以使用两种简单的方式来导入MySQL导出的数据. 使用 LOAD DATA 导入数据 MySQL 中提供了LOAD DATA INFILE语句来插入数据. 以下实例中将从当前目录中读取文 ...
- 导入数据经常使用的SQL语句整理整理
1.事务(基本) GO BEGIN TRY BEGIN TRAN -- 导入数据 COMMIT TRAN END TRY BEGIN CATCH ROLLBACK TRAN END CATCH GO ...
随机推荐
- Java中StringBuffer 简单学习,LeetCode中1323题运用
StringBuffer 学习 StringBuffer() 构造一个没有字符的字符串缓冲区,初始容量为16个字符. deleteCharAt(int index) 删除char在这个指定序列inde ...
- 静态代码块和数组工具类Arrays
静态代码块 静态代码块:定义在成员位置,使用static修饰的代码块{ }. ~位置:类中方法外. ~执行:随着类的加载而执行且执行一次,优先于main方法和构造方法的执行 格式: public cl ...
- dynamic + shardingsphere(4.1.1) 实现动态分库分表
1. 主要依赖: <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-da ...
- MySQL-过滤数据(WHERE语句)
1.使用WHERE子句 在SELECT语句中,数据根据WHERE子句中指定的搜索条件进行过滤.WHERE子句在表名( FROM子句)之后给出,如下所示: SELECT prod_name,prod_p ...
- APISpace 成语大全API接口 免费好用
成语有一个很大一部分是从古代相承沿用下来的,在用词方面往往不同于现代汉语,它代表一个故事或者典故.成语又是一种现成的话,跟习用语.谚语相近,但是也略有区别.成语是中华文化中的一颗璀璨的明珠. 成语 ...
- (一)java基础篇---第一个程序
先认识java的基础知识 1.变量命名规则 :1)变量名由数字字母下划线组成,2)不能使用java的关键字,比如public这种,3)遵循小驼峰命名法 2.数据类型 2.1基本数据类型有8种 其中分为 ...
- 用kubeadm简单部署k8s
一.环境准备 1.三台CentOS6.7虚拟机 master:192.168.0.54 注意:主节点最好是2颗cpu,否则在k8s控制平面初始化的时候会报错: node1:192.168.0.68 n ...
- 第十二天python3 匿名函数
python借助lambda表达式构建匿名函数: 参数列表不需要小括号: 冒号是用来分割参数列表和表达式的: 不需要使用return,表达式的值,就是匿名函数返回值: lambda表达式(匿名函数)只 ...
- Mpvue1.0+Python3.7+Django2.0.4实现微信小程序的支付功能
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_112 其实微信支付有很多种形式,刷脸,扫码,APP支付,小程序支付等,这边只说明小程序支付的实现,不过原理上都大同小异. 首先,需 ...
- react学习1-jsx语法注意点
* 1.定义虚拟DOM不要写引号 * 2.标签中使用js表达式的时候,要使用{} * 3.样式类名指定要使用className * 4.要使用内联样式的话,要使用style={{key:"v ...