天天看點

.net中的EF比你想象的更智能

作者:opendotnet
.net中的EF比你想象的更智能

盡管 EF 很受歡迎,但開發人員還是懶得閱讀文檔😬。結果,出現了大量額外的和大多數時候的備援代碼。

在今天的文章中,我們将探讨常見的代碼示例和改進它們的方法。你将了解如何使實體架構 (EF) 代碼更簡潔。此外,我們将介紹一些您可以與朋友😉分享和讨論的進階技術。

事不宜遲,讓我們開始吧

Domain

在下面的所有示例中,将使用以下實體:

public class User 
{ 
public int Id { get; set; } 
public string Name { get; set; } 
public ICollection\<Address> Addresses { get; set; } 
} 

public class Address 
{ 
public int Id { get; set; } 
public string Name { get; set; } 

public int UserId { get; set; } 
}
           

No need in DbSet

每個使用 EF 的人都知道,您需要在 .這樣,Entity Framework 将在資料庫中建立表,并将它們與相應的屬性進行比對。

public class ApplicationDbContext : DbContext 
{ 
public DbSet<User> Users { get; set; } 
public DbSet<Address> Addresses { get; set; } 
}
           

但是,您實際上并不需要這樣做。隻要配置了實體并在 EF 中注冊了配置,就可以确定應建立哪些表:DbContext

public class ApplicationDbContext : DbContext 
{ 
// public DbSet<User> Users { get; set; } 
// public DbSet<Address> Addresses { get; set; } 

protected override void OnModelCreating(ModelBuilder modelBuilder) 
 { 
// search for all FLuentApi configurations 
// modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext)); 

 modelBuilder.Entity<User>(); 
 modelBuilder.Entity<Address>(); 
 } 
}
           

稍後可以通過以下方法通路該表:

await using var dbContext = new ApplicationDbContext(); 

dbContext.Set<User>().AddAsync(new User()); 
           

. . .

當您想要限制對某些表的直接通路(這在 DDD 中很常見)或實作泛型操作時,這非常有用。DbContext

更新子項的集合

通常,UI 允許同時更改多個實體。更新父實體及其所有子實體是一項常見任務。例如,在下圖中,我們可以更改使用者名并在單個頁面上添加/更新/删除其位址。

.net中的EF比你想象的更智能

盡管這似乎不是最複雜的事情,但在實踐中,它讓許多開發人員摸不着頭腦。

這裡隻是我所看到的代碼的一個近似示例(請不要試圖了解它,隻需快速浏覽一下):

using (var dbContext = new AppDbContext()) 
{ 
// Retrieve the user and its addresses from the database 
var user = dbContext.Users.Include(u => u.Addresses).Find(userId); 

// Update the user's name 
 user.Name = newName; 

// Add/Update/Delete addresses 
var existingAddressNames = user.Addresses.Select(a => a.Name); 
var addressesToDelete = existingAddressNames.Except(newAddressNames).ToList(); 
var addressesToAdd = newAddressNames.Except(existingAddressNames).ToList(); 

// Remove addresses that are no longer in the updated list 
foreach (var addressName in addressesToDelete) 
 { 
var addressToDelete = user.Addresses.FirstOrDefault(a => a.Name == addressName); 
if (addressToDelete != ) 
 { 
 user.Addresses.Remove(addressToDelete); 
 dbContext.Addresses.Remove(addressToDelete); 
 } 
 } 

// Add new addresses 
foreach (var addressName in addressesToAdd) 
 { 
var newAddress = new Address { Name = addressName }; 
 user.Addresses.Add(newAddress); 
 dbContext.Addresses.Add(newAddress); 
 } 

// Update addresses 
 . . . 

// Save changes to the database 
 dbContext.SaveChanges(); 
} 
           

它可以通過大量使用 Linq 和其他類型的重構來優化。我很确定也有一些錯誤。但我的觀點是,這是相當多的工作。你可以通過觀察它有多大來意識到這一點。該代碼将變得更大,您擁有的實體越多。

一個更簡單的方法是删除所有位址并重新填充它們:

using (var dbContext = new AppDbContext()) 
{ 
// Retrieve the user and its addresses from the database 
var user = dbContext.Users.Include(u => u.Addresses).Find(userId); 

// Update the user's name 
 user.Name = newName; 

// Update addresses 
 user.Addresses.Clear(); 
 user.Addresses.Add(new Address 
 { 
 Id = 2, // this one has an Id and need to be updated 
 Name = "Rename Existing", 
 }); 
 user.Addresses.Add(new Address 
 { 
 Name = "Add New", 
 }); 

// Save changes to the database 
 dbContext.SaveChanges(); 
}
           

我希望 EF 删除所有使用者的位址并插入新🤔位址。然而,事實并非如此。

讓我們檢查生成的 SQL:

DELETE 
FROM [Address]
WHERE [Id] = 1;

UPDATE [Address] 
SET [Name] = 'Rename Existing'
WHERE [Id] = 2;

INSERT INTO [Address] ([Name], [UserId])
VALUES ('Add New', 1);
           

更新子實體從未如此簡單 😱

我們不必編寫任何複雜的算法來計算添加哪些實體、更新了哪些實體以及删除了哪些實體。Ef 足夠聰明,可以自己完成,謝天謝地 Change Tracker 😏 .

從其他範圍通路更改

好了,這次要了解問題所在,我希望你實際分析一下代碼,不過别擔心,我會幫助你的 🙂

想象一下,我們将注冊為 .我們還有兩個服務和.它們都對同一個使用者實體進行操作,并且都嘗試更新相同的屬性。我們假設原始使用者的名字是 John。現在樂趣開始了:

  • 您可以在注釋 No1 中看到,我們正在将使用者重命名為 Joe,但更改尚未送出到資料庫。然後被調用。它還會從資料庫加載同一使用者并更新其名稱。你能說出使用者的名字嗎?是還是?_nameService.UpdateUserName()question 1“John”“Joe”
  • 另請注意,在注釋 No2 中,我們将使用者重命名為 Jonathan,但是,這次被調用。您能說出當我們傳回到原始代碼時名稱字段中的值嗎?是還是?
class UserService
{
public void UpdateUser([FromServices] AppDbContext dbContext)
 {
var user = dbContext.Users.First(u => u.Id == 1);
 user.Name; // John
 user.Name = "Joe"; // 1

 _nameService.UpdateUserName();
 user.Name; // question 2 ???

 dbContext.SaveChanges();
 }
}

class NameService
{
public void UpdateUserName([FromServices] AppDbContext dbContext)
 {
var user= _dbContext.Users.First(p => p.Id == 1);
 user.Name; // question 1 ???
 user.Name = "Jonathan"; // 2

 dbContext.SaveChanges();
 }
}
           

如果我是一個不知道 EF 如何工作的人,我會猜測第一種情況和第二種情況。畢竟,這就是可變範圍的工作方式。但是,在這種情況下,EF 更智能。以下是實際結果:

class UserService
{
public void UpdateUser([FromServices] AppDbContext dbContext)
 {
var user = dbContext.Users.First(u => u.Id == 1);
 user.Name; // John
 user.Name = "Joe"; // 1

 _nameService.UpdateUserName();
 user.Name; // Jonathan

 user.Name = "Here's Johnny"; 

 dbContext.SaveChanges();
 }
}

class NameService
{
public void UpdateUserName([FromServices] AppDbContext dbContext)
 {
var user= dbContext.Users.First(p => p.Id == 1);

 user.Name; // Joe
 user.Name = "Jonathan"; // 2

 dbContext.SaveChanges();
 }
}
           

即使實體在另一個服務中重新加載,我們仍然可以通路其他代碼所做的更改。發生這種情況是因為它實際上不是一個新實體。

由于更改跟蹤器,EF 會提取實體,檢查其主鍵,并檢視已跟蹤的實體,是以傳回已跟蹤的實體。是以,在一個作用域中對實體所做的所有更改都存在于另一個作用域中。

這既有利也有弊。一方面,我們可以确定與我們合作的實體始終是最新的,無論通過其他方法進行何種操作。另一方面,我們可能會意外地送出我們不知道的更改。

Find() 與 First()

這是另一個例子。

我們有一個方法,可以加載一個使用者并更新其名稱,然後加載同一個使用者并更新其位址。

class UserService
{
public void UpdateUser(int userId, string userName, List<Address> addresses)
 {
UpdateName(userId, userName);
UpdateAddresses(userId, addresses);
 }

private void UpdateName(int userId, string userName)
 {
var user = _dbContext
 .Users
 .First(u => u.Id == userId);

 user.Name = userName; 

 _dbContext.SaveChanges();
 }

private void UpdateAddresses(int userId, List<Address> addresses)
 {
var user = _dbContext
 .Users
 .Include(u => u.Addresses)
 .First(u => u.Id == userId);

 user.Addresses = addresses;

 _dbContext.SaveChanges();
 }
}
           

您可以看到從資料庫中提取了兩次相同的使用者。正如我們已經知道的,在這兩種情況下,實體架構都将傳回相同的實體。但是,仍然存在一個問題,即使已經跟蹤了使用者,它也會建立兩個請求。我想 EF 畢竟😒沒那麼聰明SELECT

當然,我們可以有另一種方法來加載使用者,然後在 和 中使用它。但是,還有另一種解決方案。

為避免兩次擷取使用者,您可以使用代替 。

Find()按主鍵從資料庫中傳回實體。這是第一次向資料庫送出請求。然後跟蹤該實體,對于所有将來的調用,該實體将立即傳回到緩存中。

.net中的EF比你想象的更智能

它以這種方式工作,因為 是 EF 中的實際方法,并且 EF 的開發人員可以直接通路更改跟蹤器。相比之下,它隻是在任何字段(不一定是主鍵)上帶有謂詞的擴充方法。

abstract class DbSet<TEntity> 
{ 
 . . . 
public Task FindAsync(params object[] primaryKey); 
 . . . 
}
           

使用 TransactionScope 還原更改

經常發生的情況是,您的業務邏輯不僅僅是更新某些字段。您可以經常更新一些屬性,執行一些計算,然後再更新模型的其餘部分。想象一下,在計算過程中出了什麼問題:

class UserService
{
public void UpdateUser(int userId, string userName, List<Address> addresses)
 {
UpdateName(userId, userName);

// some business logic that can do this:
// throw new BusinessLogicException(💥)

UpdateAddresses(userId, addresses);
 }

private void UpdateName(int userId, string userName)
 {
var user = _dbContext
 .Users
 .Find(userId);

 user.Name = userName; 

 _dbContext.SaveChanges();
 }

private void UpdateAddresses(int userId, List<Address> addresses)
 {
var user = _dbContext
 .Users
 .Include(u => u.Addresses)
 .Find(userId);

 user.Addresses = addresses; 

 _dbContext.SaveChanges();
 }
}
           

這意味着有些資料被儲存,而有些則沒有。這會導緻資料不一緻。

當然,我們可以在這裡和那裡添加一些,編寫補償操作,使我們的代碼變得複雜,等等。但是我們為什麼要這樣做🤔呢?try/catches

使用 EF,隻需幾行即可使用:

class UserService 
{ 
public void UpdateUser(int userId, string userName, List<Address> addresses) 
 { 
using (var transaction = db.Database.BeginTransaction()) 
 { 
UpdateName(userId, userName); 
throw new BusinessLogicException(💥) 
UpdateAddresses(userId); 

 transaction.Commit(); 
 } 
 } 
 . . . 
}
           

EF 建立一個新事務并将其送出到 .但是,如果它看到已經有一個正在進行的事務,它将附加所有更改。

這次不會有任何資料不一緻。 可以随心所欲地調用,資料在點選之前不會被儲存。

Linq 連結

還有最後一個。我看到我的同僚寫了這樣的複雜查詢:

var result = _dbContext 
 .Users 
 .Where(u => u.Name.Contains("J") && u.Addresses.Count > 1 && u.Addresses.Count < 10) && 
 (idToSearch ==  || u.Id == idToSearch) 
 .ToList();
           

他試圖将所有條件放在單個語句中:.Where()

是以,我建議這樣重寫:

var query = _dbContext 
 .Users 
 .Where(u => u.Name.Contains("J")) 
 .Where(u => u.Addresses.Count > 1 && u.Addresses.Count < 10); 

if (idToSearch is not ) 
{ 
 query = query 
 .Where(u.Id == idToSearch); 
} 

var result = query 
 .ToList();
           

理由如下:

  • 将條件拆分為單獨的子句可使代碼更具可讀性,尤其是在處理複雜條件或長表達式時Where
  • 它的格式更好,允許開發人員單獨關注每個過濾器
  • 每個條件都是分開的,是以更容易了解每個過濾器的意圖
  • 使用單獨的子句,您可以獨立重用或修改條件。如果需要更改其中一個條件,可以在不影響其他條件的情況下進行更改。它在維護或改進代碼時提供了更大的靈活性Where
  • 更改為特定條件,會更好地顯示在 git 等版本控制系統中,使 PR 稽核過程窒息

他同意我的看法,但拒絕這樣做,因為這會降低性能。起初我很困惑。這怎麼可能🤔?直到那時我才意識到,他想到了那些 Linq 運算符,因為它們是針對正常集合執行的。

EF 不會按名稱篩選行,而是按位址篩選剩餘行,依此類推。它會将這些篩選器運算符轉換為 **SQL。**從技術上講,您使用哪種方法并不重要。

我還建議将這種方法也用于正常集合,因為:

  • 代碼庫是一緻的
  • 大多數時候,在記憶體集合中,我們正在處理的記憶體集合很小
  • 這兩種方法之間的性能差異可以忽略不計,因為即使是正常的 Linq 也無法以這種方式工作,但這是另一回事🙃

結論

事實證明,EF 是一個強大的工具,它提供了比眼睛看到的更多的功能。通過有效利用其功能,開發人員可以簡化複雜的任務、優化性能并確定資料一緻性。

了解 EF 的内部工作原理使我們能夠釋放其全部潛力并簡化我們的代碼,使開發過程更順暢、更高效。

如果你喜歡我的文章,請給我一個贊!謝謝