别跟我谈EF抵抗并发,敢问你到底会不会用EntityFramework

前言

一直以来写的博文都是比较温婉型的博文,今天这篇博文算是一篇批判性博文,有问题欢迎探讨,如标题,你到底会不会用EntityFramework啊。面试

你到底会不会用EntityFramework啊

  面试过三年至六年的同行,做为过面试者到现在做为面试官也算是老大对个人信任,对来面试的面试者的任何一位同行绝没有刁难之意,若还装逼那就没有什么意义。我也基本不看面试者的项目经历,由于我我的以为每一个面试者所在公司所作项目都不同,可能面试者项目所作的业务我一点都不知道,而我所关心的是项目当中所用到的技术,稍微看下了简历让面试者简单作个自我介绍,这算是基本流程吧。而后直接问面试者最擅长的技术是哪些?好比ASP.NET MVC、好比ASP.NET Web APi、好比EntityFramework,再好比数据库等等。若是面试者没有特别擅长的技术那我就简历上提出所熟悉和项目当中用到的技术进行提问。这里暂且不提其余技术,单单说EntityFramework,面试的面试者大部分都有用过EntityFramework,我就简单问了下,好比您用的EntityFramework版本是多少?答案是不知道,这个我理解,可能没去关心过这个问题,再好比我问您知道EntityFramework中有哪些继承策略,而后面试者要么是一脸懵逼,要么是不知道,要么回了句咱们不用。这个我也能理解,重点来了,我问您在EntityFramwork中对于批量添加操做是怎么作的,无一例外遍历循环一个一个添加到上下文中去,结果令我惊呆了,或许是只关注于实现,不少开发者只关注这个能实现就行了,这里不过多探讨这个问题,每一个人观点不同。数据库

  大部分人用EntityFramework时出现了问题,就吐槽EntityFramework啥玩意啊,啥ORM框架啊,各类问题,我只能说您根本不会用EntityFramework,甚至还有些人并发测试EntityFramework的性能,是的,没错,EntityFramework性能不咋的(这里咱们只讨论EF 6.x),或者说在您实际项目当中有了点并发发现EF出了问题,又开始抱怨EF不行了,同时对于轻量级、跨平台、可扩展的EF Core性能秒杀EF,即便你并发测试EF Core性能也就那么回事,我想说的是你并发测试EF根本没有任何意义,请好生理解EF做为ORM框架出现的意义是什么,不就是为了让咱们关注业务么,梳理好业务对象,在EF中用上下文操做对象就像直接操做表同样。而后咱们回到EF抵抗并发的问题,有的童鞋认为EF中给我提供了并发Token和行版本以及还有事务,这不就是为了并发么,童鞋对于并发Token和行版本这是对于少许的请求可能存在的并发EF团队提出的基本解决方案,对于事务不管是同一上文抑或是跨上下文也好只是为了保证数据一致性罢了。要是大一点的并发来了,您难道还让EF不顾一切冲上去么,这无疑是飞蛾扑火自取灭亡,你到底会不会用EntityFramework啊。EF做为概念上的数据访问层应该是处于最底层,若是咱们项目可预见没有所谓的并发问题,将上下文直接置于最上层好比控制器中并无什么问题,可是项目比较大,随着用户量的增长,咱们确定是可预知的,这个咱们须要从项目架构层面去考虑,此时在上下文上游一定还有其余好比C#中的并发队列或者Redis来进行拦截使其串行进行。架构

  有些人号称是对EntityFramwork很是了解,认为不就是增、删、该、查么,可是有的时候用出了问题就开始自我开解,我这么用没有任何问题啊,咱们都知道在EF 6.x中确实有不少坑,这个时候就借这个原因洗白了,这不是个人锅,结果EF背上了无名之锅,妄名之冤。是的,您没有说错,EF 6.x是有不少坑,您避开这些坑不就得了,我只能说这些人太浮于表面不了解基本原理就妄下结论,您到底会不会用EntityFramework啊。好了来,免说我纸上谈兵,我来举两个具体例子,您看本身到底会不会用。并发

EntityFramework 6.x查询

        static void Main(string[] args)
        {
            using (var ctx = new EfDbContext())
            {
                ctx.Database.Log = Console.WriteLine;

                var code = "Jeffcky";
                var order = ctx.Orders.FirstOrDefault(d => d.Code == code);           
            };
            Console.ReadKey();
        }

这样的例子用过EF 6.x的童鞋估计用烂了吧,而后查询出来的结果让咱们也很是满意至少是达到了咱们的预期,咱们来看看生成的SQL语句。app

 

请问用EF的您发现什么没有,在WHERE查询条件加上了一堆没有用的东西,我只是查询Code等于Jeffcky的实体数据,从生成的SQL来看可查询Code等于Jeffcky的也可查询Code等于空的数据,要是咱们以下查询,生成如上SQL语句我以为才是咱们所预期的对不对。框架

            using (var ctx = new EfDbContext())
            {
                ctx.Database.Log = Console.WriteLine;

                var code = "Jeffcky";
                var orders = ctx.Orders.Where(d => d.Code == null || d.Code == code).ToList();

            };

若是您真的会那么一点点用EntityFramework,那么请至少了解背后生成的SQL语句吧,这是其中之一,那要是咱们直接使用值查询呢,您以为是否和利用参数生成的SQL语句是同样的呢?函数

            using (var ctx = new EfDbContext())
            {
                ctx.Database.Log = Console.WriteLine;

                var order = ctx.Orders.FirstOrDefault(d => d.Code == "Jeffcky");

            };

出乎意料吧,利用值查询在WHERE条件上没有过多的条件过滤,而利用参数查询则是生成过多的条件筛选,到这里是否是就到此为止了呢,若是您对于参数查询不想生成对空值的过滤,咱们在上下文构造函数中可关闭这种所谓【语义可空】判断,以下:性能

    public class EfDbContext : DbContext
    {
        public EfDbContext() : base("name=ConnectionString")
        {
            Configuration.UseDatabaseNullSemantics = true;
        }
     }

// 摘要:
// 获取或设置一个值,该值指示当比较两个操做数,而它们均可能为 null 时,是否展现数据库 null 语义。默认值为 false。例如:若是 UseDatabaseNullSemantics
// 为 true,则 (operand1 == operand2) 将转换为 (operand1 = operand2);若是 UseDatabaseNullSemantics
// 为 false,则将转换为 (((operand1 = operand2) AND (NOT (operand1 IS NULL OR operand2
// IS NULL))) OR ((operand1 IS NULL) AND (operand2 IS NULL)))。
//
// 返回结果:
// 若是启用数据库 null 比较行为,则为 true;不然为 false。学习

在EF 6.x中对于查询默认状况下会进行【语义可空】筛选,经过如上分析,不知您们是否知道如上的配置呢。测试

EntityFramework 6.x更新

EF 6.x更新操做又是用熟透了吧,在EF中没有Update方法,而在EF Core中存在Update和UpdateRange方法,您是否以为更新又是如此之简单呢?咱们下面首先来看一个例子,看看您是否真的会用。

        static Customer GetCustomer()
        {
            var customer = new Customer()
            {
                Id = 2,
                CreatedTime = DateTime.Now,
                ModifiedTime = DateTime.Now,
                Email = "2752154844@qq.com",
                Name = "Jeffcky1"
            };
            return customer;
        }

如上实体如咱们请求传到后台须要修改的实体(假设该实体在数据库中存在哈),这里咱们进行写死模拟。接下来咱们来进行以下查询,您思考一下是否能正常更新呢?

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers.FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    ctx.Customers.Attach(customer);
                    ctx.Entry(customer).State = EntityState.Modified;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失败");
                    }
                }

            };

首先咱们根据传过来的实体主键去数据库中查询是否存在,若存在则将传过来的实体附加到上下文中(由于此时请求过来的实体还未被跟踪),而后将其状态修改成已被修改,最后提交,解释的是否是很是合情合理且合法,那是否是就打印更新成功了呢?

看到上述错误想必有部分童鞋一会儿就明白问题出在哪里,当咱们根据传过来的实体主键去数据库查询,此时在数据库中存在就已被上下文所跟踪,而后咱们又去附加已传过来的实体且修改状态,固然会出错由于在上下文已存在相同的对象,此时必然会产生已存在主键冲突。有的童鞋想了直接将传过来的实体状态修改成已修改不就得了么,以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                ctx.Entry(customer).State = EntityState.Modified;
                if (ctx.SaveChanges() > 0)
                {
                    Console.WriteLine("更新成功");
                }
                else
                {
                    Console.WriteLine("更新失败");
                }
            };

如此确定能更新成功了,我想都不会这么干吧,要是客户端进行传过来的主键在数据库中不存在呢(至少咱们得保证数据是已存在才修改),此时进行如上操做将抛出以下异常。

此时为了解决这样的问题最简单的方法之一则是在查询实体是否存在时直接经过AsNoTracking方法使其不能被上下文所跟踪,这样就不会出现主键冲突的问题。

 var dataBaseCustomer = ctx.Customers
                    .AsNoTracking()
                    .FirstOrDefault(d => d.Id == customer.Id);

咱们继续往下探讨 ,此时咱们将数据库Email修改成可空(映射也要对应为可空,不然抛出验证不经过的异常,你懂的),以下图:

 

而后将前台传过来的实体进行以下修改,不修改Email,咱们注释掉。

        static Customer GetCustomer()
        {
            var customer = new Customer()
            {
                Id = 2,
                CreatedTime = DateTime.Now,
                ModifiedTime = DateTime.Now,
                //Email = "2752154844@qq.com",
                Name = "Jeffcky1"
            };
            return customer;
        }

咱们接着再来进行以下查询试试看。

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .AsNoTracking()
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    ctx.Customers.Attach(customer);
                    ctx.Entry(customer).State = EntityState.Modified;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失败");
                    }
                }

            };

此时Email为可空,由于咱们设置实体状态为Modified,此时将对实体进行全盘更新,因此对于设置实体状态为Modified是针对全部列更新,要是咱们只想更新指定列,那这个就很差使了,此时咱们可经过Entry().Property()...来手动更新指定列,好比以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .AsNoTracking()
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    ctx.Customers.Attach(customer);
                    ctx.Entry(customer).Property(p => p.Name).IsModified = true;
                    ctx.Entry(customer).Property(p => p.Email).IsModified = true;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失败");
                    }
                }

            };

咱们继续往下走。除了上述利用AsNoTracking方法外使其查询出来的实体未被上下文跟踪而成功更新,咱们还可使用手动赋值的方式更新数据,以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    dataBaseCustomer.CreatedTime = customer.CreatedTime;
                    dataBaseCustomer.ModifiedTime = customer.ModifiedTime;
                    dataBaseCustomer.Email = customer.Email;
                    dataBaseCustomer.Name = customer.Name;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失败");
                    }
                }

            };

如上也能更新成功而不用将查询出来的实体未跟踪,而后将前台传过来的实体进行附加以及修改状态,下面咱们删除数据库中建立时间和修改时间列,此时咱们保持数据库中数据和从前台传过来的数据如出一辙,以下:

        static Customer GetCustomer()
        {
            var customer = new Customer()
            {
                Id = 2,
                Email = "2752154844@qq.com",
                Name = "Jeffcky1"
            };
            return customer;
        }

接下来咱们再来进行以下赋值修改,您会发现此时更新失败的:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    dataBaseCustomer.Email = customer.Email;
                    dataBaseCustomer.Name = customer.Name;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失败");
                    }
                }

            };

这是为什么呢?由于数据库数据和前台传过来的数据如出一辙,可是不会进行更新,毫无疑问EF这样处理是明智且正确的,无需画蛇添足更新,那咱们怎么知道是否有不同的数据进行更新操做呢,换句话说EF怎样知道数据未发生改变就不更新呢?咱们能够用上下文属性中的ChangeTacker中的HasChanges方法,若是上下文知道数据未发生改变,那么直接返回成功,以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    dataBaseCustomer.Email = customer.Email;
                    dataBaseCustomer.Name = customer.Name;
                    if (!ctx.ChangeTracker.HasChanges())
                    {
                        Console.WriteLine("更新成功");
                        return;
                    }
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失败");
                    }
                }

            };

好了到此为止咱们已经看到关于更新已经有了三种方式,别着急还有最后一种,经过Entry().CurrentValues.SetValues()方式,这种方式也是指定更新,将当前实体的值设置数据库中查询出来所被跟踪的实体的值。以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    ctx.Entry(dataBaseCustomer).CurrentValues.SetValues(customer); if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失败");
                    }
                }

            };

关于EF更新方式讲了四种,其中有关细枝末节就没有再细说可自行私下测试,不知道用过EF的您们是否四种都知道以及每一种对应的场景是怎样的呢?对于数据更新我通常直接经过查询进行赋值的形式,固然咱们也能够用AutoMapper,而后经过HasChanges方法来进行判断。

EntityFramework 6.x批量添加

对于批量添加已是EF 6.x中老掉牙的话题,可是依然有不少面试者不知道,我这里再从新讲解一次,对于那些私下不学习,不与时俱进的童鞋好歹也看看前辈们(不包括我)总经的经验吧,不知道为什么这样作,至少回答答案是对的吧。看到下面的批量添加数据代码是否是有点想打人。

           using (var ctx = new EfDbContext())
            {
                for (var i = 0; i <= 100000; i++)
                {
                    var customer = new Customer
                    {
                        Email = "2752154844@qq.com",
                        Name = i.ToString()
                    };
                    ctx.Customers.Add(customer);
                    ctx.SaveChanges();
                }
            };

至于缘由无需我过多解释,若是您这样操做,那您这一天的工做大概也就是等着数据添加完毕,等啊等。再不济您也将SaveChanges放在最外层一次性提交啊,这里我就再也不测试,浪费时间在这上面不必,只要您稍微懂点EF原理至少会以下这么使用。

            var customers = new List<Customer>();
            using (var ctx = new EfDbContext())
            {
                for (var i = 0; i <= 100000; i++)
                {
                    var customer = new Customer
                    {
                        Email = "2752154844@qq.com",
                        Name = i.ToString()
                    };
                    customers.Add(customer);
                }
                ctx.Customers.AddRange(customers);
                ctx.SaveChanges();
            };

若是您给个人答案如上,我仍是承认的,要是第一种真的说不过去了啊。通过如上操做依然有问题,咱们将全部记录添加到同一上下文实例,这意味着EF会跟踪这十万条记录, 对于刚开始添加的几个记录,会运行得很快,可是当越到后面数据快接近十万时,EF正在追踪更大的对象图,您以为恐怖不,这就是您不懂EF原理的代价,还对其进行诟病,吐槽性能能够,至少保证您写的代码没问题吧,咱们进一步优化须要关闭自调用的DetectChanges方法无需进行对每个添加的实体进行扫描。

            var customers = new List<Customer>();
            using (var ctx = new EfDbContext())
            {
                bool acd = ctx.Configuration.AutoDetectChangesEnabled;
                try
                {
                    ctx.Configuration.AutoDetectChangesEnabled = false;
                    for (var i = 0; i <= 100000; i++)
                    {
                        var customer = new Customer
                        {
                            Email = "2752154844@qq.com",
                            Name = i.ToString()
                        };
                        customers.Add(customer);
                    }
                    ctx.Customers.AddRange(customers);
                    ctx.SaveChanges();
                }
                finally
                {
                    ctx.Configuration.AutoDetectChangesEnabled = acd;
                }
            };

此时咱们经过局部关闭自调用DetectChanges方法,此时EF不会跟踪实体,这样将不会形成全盘扫描而使得咱们不会处于漫长的等待,如此优化将节省大量时间。若是在咱们了解原理的前提下知道添加数据到EF上下文中,随着数据添加到集合中也会对已添加的数据进行全盘扫描,那咱们何不建立不一样的上下文进行批量添加呢?未经测试在这种状况下是否比关闭自调用DetectChanges方法效率更高,仅供参考,代码以下:

    public static class EFContextExtensions
    {
        public static EfDbContext BatchInsert<T>(this EfDbContext context, T entity, int count, int batchSize) where T : class
        {
            context.Set<T>().Add(entity);

            if (count % batchSize == 0)
            {
                context.SaveChanges();
                context.Dispose();
                context = new EfDbContext();
            }
            return context;
        }
    }
        static void Main(string[] args)
        {
            var customers = new List<Customer>();
            EfDbContext ctx;
            using (ctx = new EfDbContext())
            {
                for (var i = 0; i <= 100000; i++)
                {
                    var customer = new Customer
                    {
                        Email = "2752154844@qq.com",
                        Name = i.ToString()
                    };
                    ctx = ctx.BatchInsert(customer, i, 100);
                }
                ctx.SaveChanges();
            };
            Console.ReadKey();
        }    

总结

不喜勿喷,敢问您到底会不会用EntityFramework啊,EF 6.x性能使人诟病可是至少得保证您写的代码没问题吧,对于复杂SQL查询能够EF很是鸡肋,可是咱们可结合Dapper使用啊,您又担忧EF 6.x坑太多,那请用EntityFramework Core吧,您值得拥有。谨以此篇批判那些不会用EF的同行,还将EF和并发扯到一块,EF不是用来抵抗并发,它的出现是为了让咱们将重心放在梳理业务对象,关注业务上,有关我对EF 6.x和EF Core 2.0理解所有集成到我写的书《你必须掌握的EntityFramework 6.x与Core 2.0》下个月可正式购买,想了解的同行可关注下,谢谢。

后续

看了不少前辈精彩的评论,我我的以为既然用了EF那就得提早知道这些基础知识或者基本原理,出了问题归结于EF,那就有点说不过去了,再者网上的前辈们在项目中总结的经验和老外的技术文档比比皆是,为什么不花点时间提早了解下是否知足项目需求呢。我在EF这方面不是专家,更谈不上精通,只不过常常看看国内和国外的技术文档,本身私下亲自实践罢了。最后总结起来一点则是选择适合本身项目的才是最好的,别太依赖EF,EF解决不了全部问题。

相关文章
相关标签/搜索