EntityFramework Core问题处理集锦(一)

前言

和大家脱离了一段时间,有时候总想着时间挤挤总是会有的,但是并非人愿,后面会借助周末的时间来打理博客,如有问题可以在周末私信我或者加我QQ皆可,欢迎和大家一起探讨,本节我们来讨论EF Core中的一些问题后面陆陆续续会将EF Core中需要注意的地方补充上来,有些是我一直以来比较疏忽的地方,不喜勿喷。用在实际项目中的时候才发现和平时所学有很大差异,靠着项目才能检验出真理。

EntityFramework Core问题集锦

更新单个实体

更新单个实体的方式有两种:

(1)查询出实体进行赋值更新

说的更专业一点则是已被跟踪的实体进行赋值更新,此时实体已被快照,此时进行更新时只需要调用SaveChanges或者SaveChangesAsync,当已赋值属性与快照中值不同时,此时调用SaveChangesAsync或者SaveChanges方法时会将此属性的状态即(IsModified)修改为True,否则为False。代码大概如下:

        public async Task<bool> UpdateStatus(int id, byte status)
        {
            var blog = _efCoreContext.Blogs.Find(id);
            blog.Status = status;
            var effectRows = await _efCoreContext.SaveChangesAsync(CancellationToken.None);
            if (effectRows > 0)
            {
                return true;
            }
            return false;
        }

但是如上又带来一个问题,我们通过影响行数来获取是否更新成功,如果想更新某一列,但是此列的值未进行改变,此时与快照中的值一致,则受影响行数为0,结果返回的更新失败,如下所示:

在这种情况就需要一个扩展方法来显式指定更新属性即使值未发生改变也将其属性状态IsModified修改为True,这样才不会导致值未改变但是更新失败的情况,即如下:

 _efCoreContext.Entry(blog).Property(d => d.Status).IsModified = true;

(2)未查询出实体进行赋值更新。

当此实体未进行查询,此时需要调用Update来将此实体状态中所有属性的IsModified修改为True,此时代码大概如下:

        public async Task<bool> UpdateStatus(Blog blog)
        {
            _efCoreContext.Blogs.Update(blog);
            var effectRows = await _efCoreContext.SaveChangesAsync(CancellationToken.None);
            if (effectRows > 0)
            {
                return true;
            }
            return false;
        }

也就是说如果是明确实体所有属性都会更改则可以利用Update方法来更新所有属性,否则不需要更新的属性比如常见场景:数据库中表中数据创建时间下次进行更新时是不需要更新,如若调用Update方法,如果对创建时间赋值会进行覆盖,未赋值则会显示DateTime默认时间。

批量更新之表达式树

批量更新的场景大有,在我们项目中选择多个产品将产品的状态更新为下架状态,下面我们来还原场景。创建批量更新接口,此时数据库中数据如下:

Task<bool> UpdateStatus(int[] ids);

我们将Blog中状态中为0的行更新为1,此时接口则如下:

        public async Task<bool> UpdateStatus(int[] ids)
        {
            var blogs = _efCoreContext.Blogs.Where(d => ids.Contains(d.Id));

            blogs.Select(b => new Blog() { Id = b.Id, Status = 1 }).ToList();

            if (await _efCoreContext.SaveChangesAsync(CancellationToken.None) > 0)
            {
                return true;
            }
            return false;
        }

此时更新肯定不能正确更新,其原因不必多讲,由于是更新集合中的指定属性,此时我写了关于单个和集合更新指定属性的扩展方法,如下:

    public static class EfCoreUpdateExe
    {
        public static void Update<T>(this EFCoreContext context, T entity, params Expression<Func<T, object>>[] properties) where T : class, new()
        {
            var dbEntityEntry = context.Entry(entity);
            if (properties.Any())
            {
                foreach (var property in properties)
                {
                    dbEntityEntry.Property(property).IsModified = true;
                }
            }
            else
            {
                foreach (var rawProperty in dbEntityEntry.Entity.GetType().GetTypeInfo().DeclaredProperties)
                {
                    var originalValue = dbEntityEntry.Property(rawProperty.Name).OriginalValue;
                    var currentValue = dbEntityEntry.Property(rawProperty.Name).CurrentValue;
                    foreach (var property in properties)
                    {
                        if (originalValue != null && !originalValue.Equals(currentValue))
                            dbEntityEntry.Property(property).IsModified = true;
                    }

                }
            }
        }



        public static void UpdateRange<TEntity>(this EFCoreContext context, IEnumerable<TEntity> entities, bool isNoTracking = true, params Expression<Func<TEntity, object>>[] properties) where TEntity : class, new()
        {
            foreach (var entity in entities)
            {
                var dbEntityEntry = context.Entry(entity);
                //Notice that:当更新实体指定属性时,若实体从数据库中查询而出,此时实体已被跟踪,则无需处理,若实例化对象而更新对象指定属性,此时需要将其状态修改为Unchanged即需要附加
                if (!isNoTracking) { dbEntityEntry.State = EntityState.Unchanged; }
                if (properties.Any())
                {
                    foreach (var property in properties)
                    {
                        dbEntityEntry.Property(property).IsModified = true;
                    }
                }
                else
                {
                    foreach (var rawProperty in dbEntityEntry.Entity.GetType().GetTypeInfo().DeclaredProperties)
                    {
                        var originalValue = dbEntityEntry.Property(rawProperty.Name).OriginalValue;
                        var currentValue = dbEntityEntry.Property(rawProperty.Name).CurrentValue;
                        foreach (var property in properties)
                        {
                            if (originalValue != null && !originalValue.Equals(currentValue))
                                dbEntityEntry.Property(property).IsModified = true;
                        }

                    }
                }
            }
        }
    }

然后代码更新代码修改如下:

        public async Task<bool> UpdateStatus(int[] ids)
        {
            var blogs = _efCoreContext.Blogs
                .Where(d => ids.Contains(d.Id));

            var updateProductList = blogs.Select(b => new Blog() { Id = b.Id, Status = 1 }).ToList();

            _efCoreContext.UpdateRange(updateProductList, true, d => d.Status);
            
            if (await _efCoreContext.SaveChangesAsync(CancellationToken.None) > 0)
            {
                return true;
            }
            return false;
        }

此时与数据库会进行两次连接,一次是查询,一次是更新指定属性字段,通过SQL跟踪我们能看到如下语句:

SELECT [d].[Id]
FROM [Blog] AS [d]
WHERE [d].[Id] IN (2, 3, 5, 6)
exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Blog] SET [Status] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;

UPDATE [Blog] SET [Status] = @p2
WHERE [Id] = @p3;
SELECT @@ROWCOUNT;

UPDATE [Blog] SET [Status] = @p4
WHERE [Id] = @p5;
SELECT @@ROWCOUNT;

UPDATE [Blog] SET [Status] = @p6
WHERE [Id] = @p7;
SELECT @@ROWCOUNT;

',N'@p1 int,@p0 tinyint,@p3 int,@p2 tinyint,@p5 int,@p4 tinyint,@p7 int,@p6 tinyint',@p1=2,@p0=1,@p3=3,@p2=1,@p5=5,@p4=1,@p7=6,@p6=1

最终正确更新如下:

除了上述通过写反射扩展方法来更新外属性外,一直在想着其中会进行两次数据库链接,进行一次数据库链接比较耗时,这个时候想到的只能执行SQL命令了。

批量更新之SQL命令

利用WHERE ....IN来进行更新,此时SQL更新代码则如下:

        public async Task<bool> UpdateStatus(int[] ids)
        {
            var testIds = string.Join(",", ids);

            var effctRow = await _efCoreContext.Database.ExecuteSqlCommandAsync("update dbo.Blog set [Status] = 1 where id in ({0})", CancellationToken.None, testIds);
            if (effctRow > 0)
            {
                return true;
            }
            return false;
        }

不知道各位看客发现什么没有,上述的代码是有问题的,哪里有问题,不知道的请看如下动态演示。

 

正常情况下WHERE...IN(2,3,5,6)而非上述“2,3,5,6”,此时我将上述代码修改为如下:

        public async Task<bool> UpdateStatus(int[] ids)
        {
            var blogIds = string.Empty;
            foreach (var id in ids)
            {
                blogIds += $"{id},";
            }
            blogIds = blogIds.TrimEnd(',');
            var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync("update dbo.Blog set [Status] = 1 where id in ({0})", CancellationToken.None, blogIds);
            if (effectRows > 0)
            {
                return true;
            }
            return false;
        }

此时再来看看演示效果:

此时则报NVARCHAR转换到INT失败,那么粗暴一点将id转换为NVARCHAR:

 var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync("update dbo.Blog set [Status] = 1 where cast(id as nvarchar(max)) in ({0})", CancellationToken.None, blogIds);

当然如上涉及到索引,通过函数转换不会走索引,我们正常情况下应该是定义一个变量将id进行转换,然后利用变量来进行包含。此时再来看演示效果:

此时压根都没去更新,我也是醉了,最后我们再来看一种情况,我们写SQL命令通过拼接的形式来进行,如下:

 var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync($"update dbo.Blog set [Status] = 1 where id in ({blogIds})", CancellationToken.None);

此时居然更新成功了,其实我们利用上述字符串拼接的方式进行如下两种转换都会更新成功:

             //转换方式一
            //var blogIds = string.Join(",", ids);

            //转换方式二
            var blogIds = string.Empty;
            foreach (var id in ids)
            {
                blogIds += $"{id},";
            }
            blogIds = blogIds.TrimEnd(',');
            var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync($"update dbo.Blog set [Status] = 1 where id in ({blogIds})", CancellationToken.None);

但是利用$符号本质无非是简化了string.format的书写罢了,容易导致SQL注入的问题,但是利用参数化SQL对于WHERE....IN情况就是无法进行更新,对于删除亦是如此,上述未曾演示利用SqlParameter来进行更新,如果你这样做了,结果依然一样不好使:

            var blogIds = string.Empty;
            foreach (var id in ids)
            {
                blogIds += $"{id},";
            }
            blogIds = blogIds.TrimEnd(',');
            var parameters = new SqlParameter[]
            {
                new SqlParameter("@ids",System.Data.SqlDbType.NVarChar,400){ Value = blogIds }
            };
            var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync("update dbo.Blog set [Status] = 1 where id in (@ids)", CancellationToken.None, parameters);

上述是对于更新的主键为INT的情况,若是主键为字符串,此时这种情况更加突出,因为对于字符串形式需要这样的格式IN('A','B','C'),此时我们将上述id看作为字符串,我们进行如下转换:

var blogIds = "'" + string.Join("','", ids) + "'";

然后去进行更新,参数正确,格式也正确,但是就是无法进行更新。最终统一得出的结论是:

进行批量更新或者删除的情况利用WHERE....IN参数化SQL无法进行更新或者删除,利用$或者string.format进行拼接却好使,但是会导致SQL注入。

上述演示EF Core版本为1.1.2,遇到这样的问题是在进行批量删除时,有人反问了批量删除不是有RemoveRange么,但是其中涉及到多表查询然后进行批量删除,就是期望达到一步到位的效果,最终没有办法,我采用LINQ的方法利用两步来进行批量删除,看到此文的你对于EF Core中利用SQL(WHERE....IN)命令来进行批量删除或者更新的情况见解是怎样,是否有遇到这样的问题,如果利用参数化SQL解决了问题的话望告知。

2017-08-07利用WHERE...IN参数化SQL批量更新或删除

这两天人感冒,什么都不想干,回来太早又没事干,于是乎再次回顾了下这个问题,我天真的以为在ADO.NET中利用WHERE...IN用SQL的方式来进行批量更新呢或者删除是好使的,结果一试居然一样不好使,有人想了为何不利用存储过程解决何必纠结于此,想了想就一句话的事情,没必要还搞个存储过程而且还要打开数据库操作(我懒)。最终还是利用原生的方式来解决这个问题,在WHERE...IN中将IN中的所有需要更新或者需要删除的数据生成参数的方式来解决即可,请往下看。

        public async Task<bool> UpdateStatus(string idsStr)
        {
            var ids = idsStr.Split(',');

            var parms = ids.Select((s, i) => "@p" + i.ToString()).ToArray();

            var inclause = string.Join(",", parms);

            var parameters = new SqlParameter[parms.Length];

            for (int i = 0; i < ids.Length; i++)
            {
                parameters[i] = new SqlParameter()
                {
                    Value = ids[i],
                    ParameterName = parms[i],
                    SqlDbType = SqlDbType.VarChar,
                    Size = 50
                };
            }

            var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync($"UPDATE dbo.Blog SET [Status] = 1 WHERE Id in({inclause})", CancellationToken.None, parameters);
            if (effectRows > 0)
            {
                return true;
            }
            return false;
        }

 接下来进行调用更新:

        [HttpGet("[action]")]
        public async Task<IActionResult> Index()
        {
            var ids = "2,4,6,7";
            var result = await _blogRepository.UpdateStatus(ids);

            return Ok();
        }

数据库原始数据如下:

至此成功进行更新,上述代码则无需一一进行解释,简单易懂。为了方便调用,对于利用WHERE...IN利用进行批量更新或删除将其进行如下封装。

第一步:构造WHERE...IN中的参数

        public static string BuildWhereInClause<T>(string partialClause, string paramPrefix, IEnumerable<T> parameters)
        {
            string[] parameterNames = parameters.Select(
                (paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString())
                .ToArray();

            string inClause = string.Join(",", parameterNames);
            string whereInClause = string.Format(partialClause.Trim(), inClause);

            return whereInClause;
        }

第二步:构造参数化Parameter

        public static SqlParameter[] Parameter<T>(string paramPrefix, IEnumerable<T> parameters)
        {
            string[] parameterValues = parameters.Select((paramText) => paramText.ToString()).ToArray();

            string[] parameterNames = parameterValues.Select(
                (paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString()
                ).ToArray();
            var param = new SqlParameter[parameterNames.Length];
            for (int i = 0; i < parameterNames.Length; i++)
            {
                param[i] = new SqlParameter()
                {
                    Value = parameterValues[i],
                    ParameterName = parameterNames[i],
                    SqlDbType = SqlDbType.VarChar
                };
            }

            return param;
        }

最终定义一个静态类来调用如上两个方法:

    public static class SqlWhereInParameterBuilder
    {
        public static string BuildWhereInClause<T>(string partialClause, string paramPrefix, IEnumerable<T> parameters)
        {
            string[] parameterNames = parameters.Select(
                (paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString())
                .ToArray();

            string inClause = string.Join(",", parameterNames);
            string whereInClause = string.Format(partialClause.Trim(), inClause);

            return whereInClause;
        }

        public static SqlParameter[] Parameter<T>(string paramPrefix, IEnumerable<T> parameters)
        {
            string[] parameterValues = parameters.Select((paramText) => paramText.ToString()).ToArray();

            string[] parameterNames = parameterValues.Select(
                (paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString()
                ).ToArray();
            var param = new SqlParameter[parameterNames.Length];
            for (int i = 0; i < parameterNames.Length; i++)
            {
                param[i] = new SqlParameter()
                {
                    Value = parameterValues[i],
                    ParameterName = parameterNames[i],
                    SqlDbType = SqlDbType.VarChar
                };
            }

            return param;
        }
    }

此时上述调用则进行如下调用:

            var sql = SqlWhereInParameterBuilder.BuildWhereInClause("UPDATE dbo.Blog SET [Status] = 1 WHERE Id in({0})", "Id", ids);
            var parameters = SqlWhereInParameterBuilder.Parameter("id", ids);
            var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync(sql, CancellationToken.None, parameters);

一切从简,想要批量删除或者更新一步到位,你get到没有!遗留一个问题,上述只是针对单表而言,如果是多表,还有其他判断条件的参数,那么上述方法则不再适用,那又该如何改造呢?容我想想!

彩蛋

EntityFramework Core Shadow Property(狭隘属性)

在EF Core系列中介绍过EF Core中几个新特性比如可选键作为除主键外的唯一约束,BackFileds,关于BackFieds未曾用到也差不多忘记了,本节我们介绍一下EF Core漏掉的狭隘属性。

狭隘属性不是实体类的一部分,所以不存在于实体类中但是存在于实体模型中,那么到底该如何使用狭隘属性呢?使用狭隘属性主要在以下两个场景。

(1)当不想对实体类作出更改,但是需要添加一些字段到实体模型中。

(2)明确知道该属性是上下文中的一部分,但是不希望暴露这些属性。

例如在Blog实体类中存在如何字段和导航属性。

    public class Blog : IEntityBase
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public byte Status { get; set; }
        public IEnumerable<Post> Posts { get; set; }
    }

常见场景:现在我们需要添加一个属性创建时间作为狭隘属性,此创建时间只有在实体添加状态时才有其值,其他状态值不发生改变且无需对外暴露,此时我们在映射中进行配置保持实体类洁净,如下:

        public override void Map(EntityTypeBuilder<Blog> b)
        {
            b.ToTable("Blog");
           
            b.HasKey(k => k.Id);

            b.Property(p => p.Url);
            b.Property(p => p.Name);
            b.Property(p => p.Status).HasColumnType("TINYINT").IsRequired();

            b.Property<DateTime>("CreatedTime");
        }

那么如何对CreatedTime进行设置值和获取值呢?Change Tracker API负责维护狭隘属性,当我们创建Blog时为其狭隘属性赋值,如下:

        public async Task<bool> Create()
        {
            var blog = new Blog() { Name = "Jeffcky", Status = 0, Url = "http://www.cnblogs.com/CreateMyself" };

            _efCoreContext.Entry(blog).Property("CreatedTime").CurrentValue = DateTime.Now;

            if (await _efCoreContext.SaveChangesAsync(CancellationToken.None) > 0)
            {
                return true;
            }
            return false;
        }

由于对于大部分情况下都有其创建时间这一列,我们放在SaveChanges方法中并将其重写,如下:

        public override int SaveChanges()
        {
            var modifiedEntries = ChangeTracker
                                .Entries().Where(x => x.State == EntityState.Added);

            foreach (var item in modifiedEntries)
            {
                item.Property("CreatedTime").CurrentValue = DateTime.Now;
            }
            return base.SaveChanges();
        }

        public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            var modifiedEntries = ChangeTracker
                                .Entries().Where(x => x.State == EntityState.Added);

            foreach (var item in modifiedEntries)
            {
                item.Property("CreatedTime").CurrentValue = DateTime.Now;
            }
            return await base.SaveChangesAsync();
        }

此时创建Blog则改写为如下:

        public async Task<bool> Create()
        {
            var blog = new Blog() { Name = "Jeffcky", Status = 0, Url = "http://www.cnblogs.com/CreateMyself" };

            _efCoreContext.Add(blog);

            if (await _efCoreContext.SaveChangesAsync(CancellationToken.None) > 0)
            {
                return true;
            }
            return false;
        }

 

那么问题来了,如果配置的狭隘属性在实体类中已存在那么是否会抛出异常呢?不会,自动将已存在的实体类中同名的名称配置成狭隘属性。当然我们也可以通过如下来起别名:

b.Property<DateTime>("CreatedTime").HasColumnName("CreatedDate");

结论:狭隘属性应是对已存在的实体类添加但是不会去修改狭隘属性值。

那么最后一个问题又来了,在LINQ中如何引用狭隘属性进行查询呢?如下:通过EF.Property<>实现引用狭隘属性:

var cList = _efCoreContext.Blogs
        .OrderBy(b => EF.Property<DateTime>(b, "CreatedTime")).ToList();

总结 

有一段时间没写博客感觉有点生硬,后面会陆陆续续捡起来并将项目中遇到的问题进行总结,如有疑问或言论不对之处,请指教。see u.

posted @ 2017-08-06 15:57  Jeffcky  阅读(6240)  评论(27编辑  收藏  举报