C# EF 批量操作

C#

浏览数:1,210

2019-4-3

背景

源代码下载地址在最后
知识要求:ef code first
ef 批量操作是最近遇到的一个新问题,ef这个orm为我们解决了大量的curd操作,但是,对于批量操作,其性能一直没有很好的方案,不管是 foreach 方式,还是 addorupdate(这个扩展内部实现原理还是一个一个add),当数据量很大的时候,其性能简直是不能容忍,差不多1万多的数据,需要等半个小时左右!
于是开始着手寻找一个可以使用ef进行批量操作的的类库,开始用的是 zzz projects 的类库,但是其免费版,只有更新和删除,没有插入,使用非免费版,如果时间过期,会导致程序出问题(血的教训),于是有找啊找,皇天不负有心人,这里需要吐槽下百度,在百度上,基本上搜索到的答案,均是 zzz projects 的类库 或者 ef extend ,后者也没有 批量插入。
不得已,我们翻出去看看,不会翻墙的程序员不是好的搬砖工,在google一搜,搜到了很漂亮,很好用的一个类库:EntityFramework.Utilities,地址:https://github.com/MikaelEliasson/EntityFramework.Utilities, 关键这货还是免费开源的……
我们的故事就从这里开始!

创建项目

使用vs工具,创建一个控制台程序,并引入nuget 包:

图片.png

实体类如下:

using System.ComponentModel.DataAnnotations;

    public class TestEntity
    {
        [Key]
        public int Id { get; set; }

        public int RandomVlue { get; set; }
    }

上下文如下:

public class BatchDemoContext
        : DbContext
    {
        public BatchDemoContext() : 
            base("Default")
        {
        }

        public IDbSet<TestEntity> TestEntities { get; set; }
        
    }

有了以上代码,我们就可以使用code first 命令创建数据库,前提是,在配置文件里,添加了数据库连接字符串

创建数据源

有了以上内容,接下来,我们需要生产多条数据,比如,生产100000条数据,具体的代码如下

        /// <summary>
        ///     产生需要生产的数据
        /// </summary>
        /// <returns></returns>
        private static IEnumerable<TestEntity> GetInsertDatas()
        {
            // 线程安全的list
            ConcurrentBag<TestEntity> datas=new ConcurrentBag<TestEntity>();

            Parallel.For(0, 100000,
                (index, state) =>
                    {
                        Random rand = new Random();
                        var newData = new TestEntity { RandomVlue = rand.Next(1, 100) };
                        datas.Add(newData);
                    });

            return datas;
        }

为了生产数据快,使用了Parallel,不懂的可以自行 google,如果不能翻墙你就bing一下,或者是看《 C# 高级编程》 关于异步 的这章,这里,只需要知道,生产100000条数据,用parallel会很快的产生数据即可!

批量插入

我们获取了数据源,那么,如何对这些数据进行插入呢,看如下的测试代码:

        /// <summary>
        ///     批量插入
        /// </summary>
        private static void BatchInster()
        {
            var datas = GetInsertDatas();
            var testEntities
                = datas as IList<TestEntity> ?? datas.ToList();

            Stopwatch watch =new Stopwatch();
            
            Console.WriteLine("开始插入计时,总共数据:{0}条",testEntities.Count());
            watch.Start();

            using (var context=new BatchDemoContext())
            {
                EFBatchOperation.For(context,context.TestEntities)
                    .InsertAll(testEntities);
            }

            watch.Stop();
            Console.WriteLine("结束插入计时,工用时:{0}ms",watch.ElapsedMilliseconds);


            using (var context = new BatchDemoContext())
            {
                var count = context.TestEntities.Count();
                Console.WriteLine("数据库总共数据:{0}条",count);

                var minId = context.TestEntities.Min(c => c.Id);

                // 随机取十条数据进行验证

                for (int i = 1; i <= 10; i++)
                {
                    Random rand = new Random();
                    var id = rand.Next(minId, minId+ 100000);
                    
                    var testdata = context.TestEntities.FirstOrDefault(c => c.Id == id);
                    
                    Console.WriteLine("插入的数据 id:{0} randomvalue:{1}",testdata.Id,testdata.RandomVlue);

                }
                
            }

            Console.WriteLine("-----------------华丽的分割线   插入-------------------------");
        }

用 Stopwatch 监测 插入所执行的时间
通过使用代码

using (var context=new BatchDemoContext()) { EFBatchOperation.For(context,context.TestEntities) .InsertAll(testEntities); }

即可实现批量插入,然后,针对这10调数据,随机随十条进行验证,我们在生产数据的时候,默认的随机值不会超过100,如果这10条随机值,都是小于100的,可以认为插入成功。
使用
var minId = context.TestEntities.Min(c => c.Id);
是为了保证,id条件最小,不然第二次运行程序,会出错,因为id是自增的,删除数据,id也不会从1开始!
插入的运行结果如下:

图片.png

通过上图可以看出
需要插入的数据有 100000条,插入仅用了 3070ms,这比原生态的ef要快了不知道多少倍……
通过插入数据的id和randomvalue,可以看出,我们的数据,也的确是正确的插入到了数据库!

批量更新

通过上面的步骤,我们数据库里已经有100000条数据里,现在,我们将数据库里的数据,randomvalue 全部设置为 1000,于是我们需要,获取全部数据,然后并行运算,改randomvalue的值为100000,在然后批量更新修改后的数据
代码如下

        /// <summary>
        ///     批量更新
        /// </summary>
        private static void BatchUpdate()
        {
            IEnumerable<TestEntity> toUpdates=new List<TestEntity>();

            // 获取所有数据
            using (var context = new BatchDemoContext())
            {
                toUpdates = context.TestEntities.ToList();
            }

            // 所有的值 都为 1000
            Parallel.ForEach(toUpdates,
                (entity, state) =>
                    { entity.RandomVlue = 1000; });


            Stopwatch watch = new Stopwatch();

            Console.WriteLine("开始更新计时,总共数据:{0}条", toUpdates.Count());
            watch.Start();

            using (var context = new BatchDemoContext())
            {
                EFBatchOperation.For(context, context.TestEntities).UpdateAll(toUpdates, x => x.ColumnsToUpdate(c => c.RandomVlue));
            }

            watch.Stop();
            Console.WriteLine("结束更新计时,工用时:{0}ms", watch.ElapsedMilliseconds);


            using (var context = new BatchDemoContext())
            {
                var count = context.TestEntities.Count();
                Console.WriteLine("数据库总共数据:{0}条", count);

                var minId = context.TestEntities.Min(c => c.Id);

                // 随机取十条数据进行验证

                for (int i = 1; i <= 10; i++)
                {
                    Random Rand = new Random();
                    var id = Rand.Next(minId, minId+100000);

                    var testdata = context.TestEntities.FirstOrDefault(c => c.Id == id);
                    Console.WriteLine("更新的数据 id:{0} randomvalue:{1}", testdata.Id, testdata.RandomVlue);

                }

            }


            Console.WriteLine("-----------------华丽的分割线   更新-------------------------");

        }

通过

using (var context = new BatchDemoContext()) { EFBatchOperation.For(context, context.TestEntities).UpdateAll(toUpdates, x => x.ColumnsToUpdate(c => c.RandomVlue)); }

这条语句,告诉ef,需要更新哪个集合的randomvalue!
然后随机取10条数据,发现,randomvalue的值全部都是 1000,说明我们批量更新成功!
运行结果如下:

图片.png

查询更新

什么是查询更新呢?就是当我们满足什么条件的时候,对属性进行什么操作,类似于 update table set col=value where id=1 这样的 sql 语句,和批量更新有什么区别的?我这边的批量更新,是指从数据库中加载的多个实体到内存,在内存中改变了属性值,在将这一批数据,更新到数据库,而查询更新,无需查询到内存!
这里,我们将id大于等于 minid+10000的数据和 id 小于等于 minid+50000的数据进行改值,修改ran 的值为 500,
代码如下

        /// <summary>
        ///     将id >= 1w  小于 5w 的随机值等于 500
        /// </summary>
        private static void BatchUpdateQuery()
        {

            Stopwatch watch = new Stopwatch();

            Console.WriteLine("开始查询更新计时");
            watch.Start();

            using (var context = new BatchDemoContext())
            {

                var minId = context.TestEntities.Min(c => c.Id);

                EFBatchOperation.For(context, context.TestEntities)
                    .Where(c=>c.Id>= minId+10000 && c.Id<= minId+50000)
                    .Update(c=>c.RandomVlue,rv=>500);
            }

            watch.Stop();
            Console.WriteLine("结束查询更新计时,工用时:{0}ms", watch.ElapsedMilliseconds);

            using (var context = new BatchDemoContext())
            {
                var count = context.TestEntities.Count();
                Console.WriteLine("数据库总共数据:{0}条", count);

                var minId = context.TestEntities.Min(c => c.Id);
                // 随机取十条数据进行验证
                for (int i = 1; i <= 10; i++)
                {
                    Random rand = new Random();
                    var id = rand.Next(minId+10000, minId+ 50000);

                    var testdata = context.TestEntities.FirstOrDefault(c => c.Id == id);
                    Console.WriteLine("查询更新的数据 id:{0} randomvalue:{1}", testdata.Id, testdata.RandomVlue);

                }

            }


            Console.WriteLine("-----------------华丽的分割线  查询更新-------------------------");


        }


通过代码

                var minId = context.TestEntities.Min(c => c.Id);

                EFBatchOperation.For(context, context.TestEntities)
                    .Where(c=>c.Id>= minId+10000 && c.Id<= minId+50000)
                    .Update(c=>c.RandomVlue,rv=>500);

进行查询更新
然后取十条数据进行验证,具体的运行结果如下:

图片.png

通过结果,我们可以看出,查询更新也执行成功了

批量删除

类似的sql语句是 : delete from table where id=1
在ef里,删除只能是先获取,在remove,我们如何使用efUtilities进行批量删除呢?
看代码

        /// <summary>
        ///     删除所有数据
        /// </summary>
        private static void BatchDelete()
        {

            Stopwatch watch = new Stopwatch();

            Console.WriteLine("开始删除计时");
            watch.Start();

            using (var context = new BatchDemoContext())
            {
                EFBatchOperation.For(context, context.TestEntities)
                    .Where(c=>c.Id>=1).Delete();
            }

            watch.Stop();
            Console.WriteLine("结束删除计时,工用时:{0}ms", watch.ElapsedMilliseconds);

            using (var context = new BatchDemoContext())
            {
                var count = context.TestEntities.Count();
                Console.WriteLine("数据库总共数据:{0}条", count);
                
            }

            Console.WriteLine("-----------------华丽的分割线  删除-------------------------");

        }


使用代码

            using (var context = new BatchDemoContext())
            {
                EFBatchOperation.For(context, context.TestEntities)
                    .Where(c=>c.Id>=1).Delete();
            }

进行批量删除,当我们删除之后,数据库数据应该为空,即条目为0,为了验证是否删除,我们只需获取条目即可,运行结果如下

图片.png

实践证明,批量删除是成功的

总结

efUtilities地址:https://github.com/MikaelEliasson/EntityFramework.Utilities ,也可以从这里看到文档

相比 zzz projects ,其提供的功能还算是很全的,批量插入,批量更新,查询更新和批量删除,但是, ef utilities 是 免费开源的,免费开源的,免费开源的,重要的事情说五遍,开源的代码,我们可以学习甚至是改造,打造符合自己的代码!

相比 ef extend , ef utilities 提供的功能全面,基本上是 extend有的,utilities 有,extend 没有的,utilities 也有,(只针对批量操作,查询方面,还是extend 强大)

什么情况下,会用到批量操作?
我遇到的有:导入数据、录入多条数据、批量计算然后保存每一条数据等等………………

qq:1260825783
源代码:https://git.oschina.net/zhaord/EfBatchDemo.git
转载注明:http://www.jianshu.com/p/dff3c684a0e4

我的公总号