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

a3481e26a46ac321b1fe74fa3ead1270.png

尽管 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 允许同时更改多个实体。更新父实体及其所有子实体是一项常见任务。例如,在下图中,我们可以更改用户名并在单个页面上添加/更新/删除其地址。

4e6a42d5d2f4f172d99dd06f07b67480.png

尽管这似乎不是最复杂的事情,但在实践中,它让许多开发人员摸不着头脑。

这里只是我所看到的代码的一个近似示例(请不要试图理解它,只需快速浏览一下):

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 != null)  
        {  
            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()按主键从数据库中返回实体。这是第一次向数据库发出请求。然后跟踪该实体,对于所有将来的调用,该实体将立即返回到缓存中。

d08f811f6424e0f369966049dc458725.png

它以这种方式工作,因为 是 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 == null || 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 null)  
{  
    query = query  
      .Where(u.Id == idToSearch);  
}  
  
var result = query  
    .ToList();

理由如下:

  • 将条件拆分为单独的子句可使代码更具可读性,尤其是在处理复杂条件或长表达式时Where

  • 它的格式更好,允许开发人员单独关注每个过滤器

  • 每个条件都是分开的,因此更容易理解每个过滤器的意图

  • 使用单独的子句,您可以独立重用或修改条件。如果需要更改其中一个条件,可以在不影响其他条件的情况下进行更改。它在维护或改进代码时提供了更大的灵活性Where

  • 更改为特定条件,会更好地显示在 git 等版本控制系统中,使 PR 审核过程窒息

他同意我的看法,但拒绝这样做,因为这会降低性能。起初我很困惑。这怎么可能🤔?直到那时我才意识到,他想到了那些 Linq 运算符,因为它们是针对常规集合执行的。

EF 不会按名称筛选行,而是按地址筛选剩余行,依此类推。它会将这些筛选器运算符转换为 **SQL。**从技术上讲,您使用哪种方法并不重要。

我还建议将这种方法也用于常规集合,因为:

  • 代码库是一致的

  • 大多数时候,在内存集合中,我们正在处理的内存集合很小

  • 这两种方法之间的性能差异可以忽略不计,因为即使是常规的 Linq 也无法以这种方式工作,但这是另一回事🙃

结论

事实证明,EF 是一个强大的工具,它提供了比眼睛看到的更多的功能。通过有效利用其功能,开发人员可以简化复杂的任务、优化性能并确保数据一致性。

了解 EF 的内部工作原理使我们能够释放其全部潜力并简化我们的代码,使开发过程更顺畅、更高效。

如果你喜欢我的文章,请给我一个赞!谢谢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
.NET Core,Entity Framework Core(EF Core)是一种广泛使用的对象关系映射(ORM)框架,用于与数据库进行交互。它提供了一组语法和API,用于操作数据库、定义实体模型和执行查询等操作。 下面是一些常见的EF Core语法: 1. 数据库上下文(DbContext):数据库上下文是与数据库交互的主要入口点。通过继承`DbContext`类并指定实体模型,可以创建自定义的数据库上下文类。 2. 实体(Entity):实体是映射到数据库表的对象模型。在EF Core,可以使用POCO(Plain Old CLR Object)类作为实体。 3. 数据迁移(Data Migration):EF Core提供了数据迁移工具,用于管理数据库模式和结构的变化。通过命令行工具或API,可以创建、应用和回滚数据库迁移。 4. LINQ查询:通过使用LINQ(Language-Integrated Query)语法,可以在EF Core执行强类型的查询操作。LINQ提供了一组丰富的操作符和方法,用于筛选、排序和投影数据。 5. 关系映射:EF Core支持多种关系映射类型,如一对一、一对多和多对多等。可以使用数据注解或Fluent API来配置实体之间的关系。 6. 查询跟踪(Query Tracking):默认情况下,EF Core会跟踪查询结果并自动新上下文的实体。可以使用`.AsNoTracking()`方法来禁用查询跟踪。 7. 异步操作:EF Core提供了异步的API,用于执行数据库操作。通过使用`async`和`await`关键字,可以在异步环境执行数据库查询和保存操作。 这只是EF Core语法的一小部分。如果您有具体的EF Core问题或需要详细的信息,请告诉我。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值