EF Core CRUD
上篇文章中,我們已經基本入門了EFCore,搭建了一個簡單的EFCore專案,本文開始簡單使用下EF,做增刪改查的相關操作;
一、資料新增操作(C)
public static async void Insert_測試新增資料1()
{
var myDbContext = new MyDbContext();
if (myDbContext.TestTables.Any(p => p.Id == 1)) return;
var newEntity = new TestTable
{
Id = 1,
Name = "主表資料1"
};
await myDbContext.TestTables.AddAsync(newEntity);
myDbContext.SaveChanges();
Console.WriteLine($"TestTable Insert Success");
Console.WriteLine($"------------------------");
}
是不是很簡單的程式碼?so eays ...
我們還是來分析下整體程式碼的執行;
1.首先我們自己在程式碼段中新增了一個實體物件,並對其欄位做賦值,然後通過Add方法加入到DbSet中
2.通過DbContext.SaveChanges提交資料到資料庫儲存
那EF是如何偷偷的在背後幫我們完成這一切的呢?
EFCore實體四狀態, 如下四種狀態;
public enum EntityState
{
/// <summary>
/// The entity is not being tracked by the context.
/// </summary>
Detached = 0,
/// <summary>
/// The entity is being tracked by the context and exists in the database. Its property
/// values have not changed from the values in the database.
/// </summary>
Unchanged = 1,
/// <summary>
/// The entity is being tracked by the context and exists in the database. It has been marked
/// for deletion from the database.
/// </summary>
Deleted = 2,
/// <summary>
/// The entity is being tracked by the context and exists in the database. Some or all of its
/// property values have been modified.
/// </summary>
Modified = 3,
/// <summary>
/// The entity is being tracked by the context but does not yet exist in the database.
/// </summary>
Added = 4
}
Detached : 實體未被跟蹤
Unchanged:未修改
Deleted : 刪除狀態
Modified:修改狀態
Added:新增狀態
Detached 未被跟蹤狀態,很多同學可能無法理解了,EFCore會預設自動跟蹤實體資訊,用來維護實體狀態,也是方便後續提交時的處理;EFCore提供兩種查詢方法,跟蹤查/非跟蹤查,跟蹤查得到的資料是Unchanged,而非跟蹤查的到的資料是Detached,這兩種方式我們後面詳細說明,這裡先簡單描述下;
EFCore管理記憶體實體
檢視DbContext原始碼中的Add方法,跟蹤方法,發現Add方法會呼叫到 EntityReferenceMap.cs 類中的Update方法 (下面的原始碼內容),此方法中EFCore會在記憶體中維護我們操作的實體資訊,將我們操作的實體資訊管理到記憶體中(我們的增刪改查操作,EFCore都會再記憶體維護,方法中只是對實體狀態維護,SaveChanges才會提交);
public virtual void Update(
[NotNull] InternalEntityEntry entry,
EntityState state,
EntityState? oldState)
{
var mapKey = entry.Entity ?? entry;
var entityType = entry.EntityType;
if (_hasSubMap && entityType.HasDefiningNavigation())
{
if (_dependentTypeReferenceMap == null)
{
_dependentTypeReferenceMap = new Dictionary<IEntityType, EntityReferenceMap>();
}
if (!_dependentTypeReferenceMap.TryGetValue(entityType, out var dependentMap))
{
dependentMap = new EntityReferenceMap(hasSubMap: false);
_dependentTypeReferenceMap[entityType] = dependentMap;
}
dependentMap.Update(entry, state, oldState);
}
else
{
if (oldState.HasValue)
{
Remove(mapKey, entityType, oldState.Value);
}
if (!oldState.HasValue || state != EntityState.Detached)
{
switch (state)
{
case EntityState.Detached:
if (_detachedReferenceMap == null)
{
_detachedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
}
_detachedReferenceMap[mapKey] = entry;
break;
case EntityState.Unchanged:
if (_unchangedReferenceMap == null)
{
_unchangedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
}
_unchangedReferenceMap[mapKey] = entry;
break;
case EntityState.Deleted:
if (_deletedReferenceMap == null)
{
_deletedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
}
_deletedReferenceMap[mapKey] = entry;
break;
case EntityState.Modified:
if (_modifiedReferenceMap == null)
{
_modifiedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
}
_modifiedReferenceMap[mapKey] = entry;
break;
case EntityState.Added:
if (_addedReferenceMap == null)
{
_addedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
}
_addedReferenceMap[mapKey] = entry;
break;
}
}
}
}
程式碼中就是針對不同狀態的實體,EF採用不同的集合進行維護,按照我們的測試程式碼,會將新增實體放入上面的_addedReferenceMap 集合中,方便EFCore做提交時的操作;
EFCore SaveChanges
然後來看下將實體加入到記憶體集合後,提交操作到底做了什麼
首先跟蹤到 DbContext的 SaveChanges方法,其內部會呼叫StateManger.SaveChanges方法,程式碼如下:
public virtual int SaveChanges(bool acceptAllChangesOnSuccess)
{
if (ChangedCount == 0)
{
return 0;
}
var entriesToSave = GetEntriesToSave(cascadeChanges: true);
if (entriesToSave.Count == 0)
{
return 0;
}
try
{
var result = SaveChanges(entriesToSave);
if (acceptAllChangesOnSuccess)
{
AcceptAllChanges((IReadOnlyList<IUpdateEntry>)entriesToSave);
}
return result;
}
catch
{
foreach (var entry in entriesToSave)
{
((InternalEntityEntry)entry).DiscardStoreGeneratedValues();
}
throw;
}
}
首先 GetEntriesToSave 方法,這個方法就是和上面的Add呼應,將EFCore加入到記憶體集合中管理的實體再次讀取出來,得到一個entriesToSave集合,也就是需要儲存的實體集合,看下其中的部分程式碼就是將記憶體集合中的資料得到
if (!hasDependentTypes)
{
var numberOfStates
= (returnAdded ? 1 : 0)
+ (returnModified ? 1 : 0)
+ (returnDeleted ? 1 : 0)
+ (returnUnchanged ? 1 : 0);
if (numberOfStates == 1)
{
if (returnUnchanged)
{
return _unchangedReferenceMap.Values;
}
if (returnAdded)
{
return _addedReferenceMap.Values;
}
if (returnModified)
{
return _modifiedReferenceMap.Values;
}
if (returnDeleted)
{
return _deletedReferenceMap.Values;
}
}
if (numberOfStates == 0)
{
return Enumerable.Empty<InternalEntityEntry>();
}
}
可以看到將不同狀態的實體集合返回,得到了一個需要儲存的實體集合資料,那得到需要儲存的實體之後,就需要執行資料庫命令了;
整體的儲存入口方法還是由 DbContext 提供,DbContext.SaveChanges 方法會呼叫到 BatchExecutor.cs類中的Execute方法
private int Execute(DbContext _, (IEnumerable<ModificationCommandBatch>, IRelationalConnection) parameters)
{
var commandBatches = parameters.Item1;
var connection = parameters.Item2;
var rowsAffected = 0;
IDbContextTransaction startedTransaction = null;
try
{
if (connection.CurrentTransaction == null
&& (connection as ITransactionEnlistmentManager)?.EnlistedTransaction == null
&& Transaction.Current == null
&& CurrentContext.Context.Database.AutoTransactionsEnabled)
{
startedTransaction = connection.BeginTransaction();
}
else
{
connection.Open();
}
foreach (var batch in commandBatches)
{
batch.Execute(connection);
rowsAffected += batch.ModificationCommands.Count;
}
startedTransaction?.Commit();
}
finally
{
if (startedTransaction != null)
{
startedTransaction.Dispose();
}
else
{
connection.Close();
}
}
return rowsAffected;
}
上訴程式碼中,根據得到的命令集合,迴圈執行命令來執行命令,最後通過事務統一來提交操作,也是確保DbContext內的事務一致性;
至此,我們EfCore的新增操作就簡單分析完了,通過EFCore的DbContext來新增實體物件,通過Add方法,此時物件會加入到EFCore的記憶體管理集合中,不同狀態物件不同的管理集合,呼叫SaveChanges方法儲存,此時EFCore會將記憶體的實體物件讀取出來,然後通過資料庫事務統一提交;EFCore在整個操作中給我們加入了一層資料快取層,也就是記憶體管理(後面會慢慢交流這一層的內容);
二、資料查詢 (R)
資料查詢的內容上一篇入門文章中我已經說了相關方法,這裡再把呼叫方式同步下
EF 的資料查詢分為兩種,跟蹤查詢和非跟蹤查詢;
1.跟蹤查詢是預設方式,預設EF查詢出來的資料是跟蹤模式(也可以手動調整),用於對資料做更新等資料庫操作;
2.非跟蹤查詢模式,此模式用於單純的資料查詢,後續不需要對資料做相關修改,因為不需要對實體做調整的監測,所以會比跟蹤查詢相對快一些;
兩種方式各有利弊,需要根據自己的業務實際需要來選擇;
兩種模式的文件說明(https://docs.microsoft.com/zh-cn/ef/core/querying/tracking)
var myDbContext = new MyDbContext();
var list = myDbContext.TestTables.ToList();
Console.WriteLine($"TestTable Count: {list.Count}");
if (!list.Any()) return;
Console.WriteLine($"TestTable Detail ---------------- ");
foreach (var item in list)
{
Console.WriteLine($"ID : {item.Id} , Name : {item.Name}");
}
Console.WriteLine($"------------------------");
三、資料更新 (U)
資料更新操作,必須使用跟蹤查詢得到資料,然後修改得到的實體資訊,再通過DbContext的SaveChanges提交資料即可;
三部曲:
1.跟蹤查詢得到資料
2.修改實體資料
3.DbContext.SaveChanges儲存資料
var myDbContext = new MyDbContext();
var list = myDbContext.TestTables.AsTracking().ToList();
var firstEntity = list.FirstOrDefault(p => p.Id == 1);
if (firstEntity != null) firstEntity.Name = $"{firstEntity.Name} Query_跟蹤查詢";
myDbContext.SaveChanges();
Console.WriteLine($"------------------------");
四、資料刪除 (D)
1.使用跟蹤查詢,將資料查詢出來
2.通過DbSet將其Remove(也是再記憶體集合中做了標記,將其放入了remove集合中)
3.SaveChanges提交儲存
var myDbContext = new MyDbContext();
var entity = myDbContext.TestTables.FirstOrDefault(p => p.Id == 1);
if (entity != null)
myDbContext.TestTables.Remove(entity);
myDbContext.SaveChanges();
有沒有發現問題,(⊙o⊙)? 刪除個資料還要查詢出來?業務場景中,很多都是前端傳入主鍵ID,執行執行相關表的資料刪除,那是否可以使用非跟蹤查詢模式呢?
驗證下 .....
var myDbContext = new MyDbContext();
var entity = myDbContext.TestTables.AsNoTracking().FirstOrDefault(p => p.Id == 1);
if (entity != null)
myDbContext.TestTables.Remove(entity);
myDbContext.SaveChanges();
結果當然是可以刪除啦,?
那既然是非跟蹤可以刪除,也意味著自己構建的實體可以刪除,那是否只需要主鍵就可以刪除了 ?
var myDbContext = new MyDbContext();
var entity = new TestTable()
{
Id = 1
};
myDbContext.TestTables.Remove(entity);
myDbContext.SaveChanges();
如上的程式碼,確實也是可以的,刪除成功 !!!
至此我們完成了EFCore的入門簡單操作,基本都是簡單的單表操作,只是為了演示整個EFCore的程式碼結構,大家可以自己上手嘗試下,後續我們將開始EFCore相關的騷操作以及一些進階操作