前言
和大家脫離了一段時間,有時候總想着時間擠擠總是會有的,但是並非人願,后面會借助周末的時間來打理博客,如有問題可以在周末私信我或者加我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.