ABP框架教程 - 2.数据访问

一、定义实体

ABP 框架通过提供一些接口和基类来标准化实体的定义。在接下来的章节中,你将了解 ABP 框架的 AggregateRootEntity 基类(及其变体),使用这些类使用单个主键(PK)和复合主键(CPK),以及与全局唯一标识符(GUID)PK 协同工作。

AggregateRoot 类

聚合是一组由聚合根对象绑定在一起的对象(实体和值对象)的集合。

using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities;

namespace FormsApp
{
    public class Form : BasicAggregateRoot<Guid>
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public bool IsDraft { get; set; }
        public virtual ICollection<Question> Questions { get; set; }
        public virtual ICollection<FormManager> Managers { get; set; }
    }
}

BasicAggregateRoot 仅定义了一个 Id 属性作为主键,并将主键类型作为泛型参数。在这个例子中,Form 的主键类型是 Guid 。你可以使用任何类型作为主键(例如, int 、 string 等),只要底层数据库提供程序支持即可。
这里有一些其他的基础类,你可以从中派生出你的聚合根,具体如下:

  • AggregateRoot 类具有额外的属性以支持乐观并发和对象扩展功能。
  • CreationAuditedAggregateRoot 类继承自 AggregateRoot 类,并添加了 CreationTime ( DateTime ) 和 CreatorId ( Guid ) 属性以存储创建审计信息。
  • AuditedAggregateRoot 类继承自 CreationAuditedAggregateRoot 类,并添加了 LastModificationTime ( DateTime ) 和 LastModifierId ( Guid ) 属性以存储修改审计信息。
  • FullAuditedAggregateRoot 类继承自 AuditedAggregateRoot 类,并添加了 DeletionTime ( DateTime ) 和 DeleterId ( Guid ) 属性以存储删除审计信息。它还通过实现 ISoftDelete 接口 添加了 IsDeleted ( bool ),这使得实体可以进行软删除。
  • 乐观并发和对象扩展功能

实体类

Entity 基类类似于 AggregateRoot 类,但它们用于子集合实体而不是主(根)实体。例如,上一节中的 Form 聚合根示例有一个问题集合。

using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities;

namespace FormsApp
{
    public class Question : Entity<Guid>
    {
        public Guid FormId { get; set; }
        public string Title { get; set; }
        public bool AllowMultiSelect { get; set; }
        public ICollection<Option> Options { get; set; }
    }
}

具有 CPK 的实体

关系型数据库支持 CPKs,其中您的 PK 由多个值的组合组成。复合键对于具有多对多关系的关联表特别有用。
假设您想为表单对象设置多个管理器,并将集合属性添加到 Form 类中,如下所示:

public class Form : BasicAggregateRoot<Guid>
{
    ...
    public ICollection<FormManager> Managers { get; set; }
}

然后,您可以定义一个从非泛型的 Entity 类派生的 FormManager 类,如下所示:

using System;
using Volo.Abp.Domain.Entities;

namespace FormsApp
{
    public class FormManager : Entity
    {
        public Guid FormId { get; set; }
        public Guid UserId { get; set; }
        public Guid IsOwner { get; set; }

        public override object[] GetKeys()
        {
            return new object[] {FormId, UserId};
        }
    }
}

当您从非泛型的 Entity 类继承时,您必须实现 GetKeys 方法以返回键的数组。这样,ABP 可以在需要的地方使用 CPK 的值。例如,在这个例子中, FormId 和 UserId 是其他表的 FK(外键),它们构成了FormManager 实体的 CPK(复合主键)。

GUID PK

ABP 主要使用 GUIDs 作为预建实体的 PK 类型。GUIDs 通常与自增 ID(如 int 或 long ,由关系型数据库支持)进行比较。

ABP 提供了 IGuidGenerator 服务,该服务默认生成顺序的 Guid 值。虽然它生成顺序值,但算法生成的值仍然是通用和随机的。生成顺序值解决了聚集索引性能问题。

如果你手动设置实体的 Id 值,始终使用 IGuidGenerator 服务;永远不要使用 Guid.NewGuid() 。如果你没有为新的实体设置 Id 值并使用仓库将其插入数据库,仓库将自动使用 IGuidGenerator 服务设置它。

仓储模式

通用仓储

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;

namespace FormsApp
{
    public class FormService : ITransientDependency
    {
        private readonly IRepository<Form, Guid> _formRepository;

        public FormService(IRepository<Form, Guid> formRepository)
        {
            _formRepository = formRepository;
        }

        public async Task<List<Form>> GetDraftForms()
        {
            return await _formRepository
                .GetListAsync(f => f.IsDraft);
        }

        public async Task FooAsync()
        {
            // Insert a new entity
            var form = new Form(); // TODO: set the form properties
            await _formRepository.InsertAsync(form, autoSave: true);

            // Delete entities with a condition 
            await _formRepository.DeleteAsync(form => form.IsDraft);
        }

        public async Task<Form> GetAsync(string name)
        {
            return await _formRepository.GetAsync(form => form.Name == name);
        }

        public async Task<List<Form>> GetFormsAsync(string name)
        {
            return await _formRepository.GetListAsync(form => form.Name.Contains(name));
        }
    }
}

注入了 IRepository<Form, Guid> ,这是 Form 实体的默认通用仓库。然后,我们使用了 GetListAsync 方法从数据库中获取表单的筛选列表。通用的 IRepository 接口有两个泛型参数:实体类型(在本例中为 Form )和 PK 类型(在本例中为 Guid )。

非聚合根实体的仓储

默认情况下,通用仓储仅适用于 聚合根 实体,因为这是一种最佳实践,通过聚合根对象访问聚合。然而,如果你使用的是关系数据库,你可以为其他实体类型启用通用仓库。我们将在EF Core 集成 部分看到配置点。
通用仓储提供了许多内置方法来查询、插入、更新和删除实体。

插入、更新和删除实体

以下方法可用于在数据库中操作数据:

  • InsertAsync 用于插入新实体。
  • InsertManyAsync 用于在一次调用中插入多个实体。
  • UpdateAsync 用于更新现有实体。
  • UpdateManyAsync 用于在一次调用中更新多个实体。
  • DeleteAsync 用于删除现有实体。
  • DeleteManyAsync 用于在一次调用中插入多个实体。

ABP 框架的 UoW 系统会在当前 HTTP 请求成功完成后自动调用 SaveChanges 方法。如果您想立即将更改保存到数据库中,可以将 autoSave
数传递给仓库方法并设置为 true 。

var form = new Form(); // TODO: set the form properties
await _formRepository.InsertAsync(form, autoSave: true);

DeleteAsync 方法有一个额外的重载版本,用于删除满足给定条件的所有实体。以下示例展示了如何删除数据库中所有草稿表单:

await _formRepository.DeleteAsync(form => form.IsDraft);
关于取消令牌

所有仓库方法都接受一个可选的 CancellationToken 参数。取消令牌用于在需要时取消数据库操作。例如,如果用户关闭浏览器窗口,就没有必要继续长时间运行的数据库查询操作。大多数情况下,您不需要手动传递取消令牌,因为 ABP 框架在您没有明确传递取消令牌时,会自动捕获并使用 HTTP 请求中的取消令牌。

查询单个实体

以下方法可以用来获取单个实体:

  • GetAsync :通过其 Id 值或谓词表达式返回单个实体。如果请求的实体未找到,则抛出EntityNotFoundException 。
  • FindAsync :通过其 Id 值或谓词表达式返回单个实体。如果请求的实体未找到,则返回 null 。

如果您有自定义逻辑或回退代码,并且给定的实体在数据库中不存在,则应仅使用 FindAsync 方法。否则,请使用 GetAsync ,它会在 HTTP 请求中抛出一个已知的异常,导致返回 404 状态码给客户端。

以下示例使用 GetAsync 方法查询具有其 Id 值的 Form 实体:

public async Task<Form> GetFormAsync(Guid formId)
{
    return await _formRepository.GetAsync(formId);
}

两种方法都有重载,可以传递一个谓词表达式来查询具有给定条件的实体。以下示例使用 GetAsync 方法获取具有其唯一名称的 Form 实体:

public async Task<Form> GetFormAsync(string name)
{
    return await _formRepository
        .GetAsync(form => form.Name == name);
}

仅在你期望单个实体时使用这些重载。如果你的查询返回多个实体,则它们会抛出 InvalidOperationException

查询实体列表

通用存储库提供了许多从数据库查询实体的选项。以下方法可以直接用于获取实体列表:

  • GetListAsync :返回满足给定条件的所有实体或实体列表
  • GetPagedListAsync :用于分页查询实体

以下代码块显示了如何通过给定的名称获取过滤后的表单列表:

public async Task<List<Form>> GetFormsAsync(string name)
{
    return await _formRepository
        .GetListAsync(form => form.Name.Contains(name));
}

在存储库上使用 LINQ

存储库提供了 GetQueryableAsync() 方法,它返回一个 IQueryable<TEntity> 对象。然后你可以使用此对象在数据库中的实体上执行 LINQ。
以下示例使用 LINQ 操作在 Form 实体上获取按名称过滤和排序的表单列表:

public class FormService2 : ITransientDependency
{
    private readonly IRepository<Form, Guid> _formRepository;
    private readonly IAsyncQueryableExecuter _asyncExecuter;

    public FormService2(
        IRepository<Form, Guid> formRepository,
        IAsyncQueryableExecuter asyncExecuter)
    {
        _formRepository = formRepository;
        _asyncExecuter = asyncExecuter;
    }

    public async Task<List<Form>> GetOrderedFormsAsync(string name)
    {
        // Obtain an IQueryable<Form>
        var queryable = await _formRepository.GetQueryableAsync();

        // Write your LINQ
        var query = from form in queryable
            where form.Name.Contains(name)
            orderby form.Name
            select form;

        // Use IAsyncQueryableExecuter to execute it
        return await _asyncExecuter.ToListAsync(query);
    }
}

拥有一个 IQueryable 对象为你提供了 LINQ 的所有功能。你甚至可以在来自不同存储库的多个IQueryable 对象之间进行连接。
使用 IAsyncQueryableExecuter 服务可能对你来说有些奇怪。你可能期望直接在查询对象上调用ToListAsync 方法,如下所示:

return await query.ToListAsync();

不幸的是, ToListAsync 是由 EF Core 定义的扩展方法,位于Microsoft.EntityFrameworkCore NuGet 包内。如果你从应用程序层引用该包没有问题,那么你可以在代码中直接使用这些异步扩展方法。然而,如果你想保持应用程序层 ORM 依赖性,ABP 的IAsyncQueryableExecuter 服务提供了必要的抽象。

IRepository 异步扩展方法

ABP 框架为 IRepository 接口提供了所有标准的异步 LINQ 扩展方法: AllAsync , AnyAsync , AverageAsync , ContainsAsync , CountAsync , FirstAsync , FirstOrDefaultAsync , LastAsync , LastOrDefaultAsync , LongCountAsync , MaxAsync , MinAsync , SingleAsync , SingleOrDefaultAsync , SumAsync , ToArrayAsync , 和 ToListAsync 。您可以直接在存储库对象上使用这些方法中的任何一个。
以下示例使用 CountAsync 方法获取以 "A" 开头的表单的数量:

public async Task<int> GetCountAsync()
{
    return await _formRepository
        .CountAsync(x => x.Name.StartsWith("A"));
}

注意,这些扩展方法仅在 IRepository 接口上可用。

具有复合主键(CPK)的实体的泛型存储库

如果您的实体有一个复合主键(CPK),您不能使用 IRepository<TEntity, TKey> 接口,因为它获取一个单一的主键( Id )类型。在这种情况下,您可以使用 IRepository<TEntity> 接口。
例如,您可以使用 IRepository<FormManager> 获取给定表单的管理员,如下所示:

public class FormManagementService : ITransientDependency
{
    private readonly IRepository<FormManager> _formManagerRepository;

    public FormManagementService(IRepository<FormManager> formManagerRepository)
    {
        _formManagerRepository = formManagerRepository;
    }

    public async Task<List<FormManager>> GetManagersAsync(Guid formId)
    {
        return await _formManagerRepository
            .GetListAsync(fm => fm.FormId == formId);
    }
}
非聚合根实体的仓储

如本章中“泛型仓储”部分所述,默认情况下不能使用 IRepository<FormManager> ,因为 FormManager 不是一个聚合根实体。通常,您想要获取 Form 聚合根并访问其 Managers 集合以获取表单管理器。但是,如果您使用 EF Core,可以为不是聚合根的实体创建默认的泛型存储库。
没有提供 TKey 泛型参数的泛型仓储有一个限制,就是它们没有获取 Id 参数的方法,因为它们不知道 Id 的类型。然而,您仍然可以使用 LINQ 编写任何需要的查询。

自定义仓储 IFormRepository

您可以创建自定义仓库接口和类来访问底层数据库提供者的应用程序编程接口(API),封装您的 LINQ表达式,调用存储过程,等等。
要创建一个自定义仓库,首先,定义一个新的仓库接口。仓库接口在启动模板提供的 Domain 项目中定义。您可以从一个通用仓库接口继承以将标准方法包含在您的仓库接口中。以下代码片段展示了如何实现:

public interface IFormRepository : IRepository<Form, Guid>
{
    Task<List<Form>> GetListAsync(
        string name,
        bool includeDrafts = false
    );
}

IFormRepository 继承自 IRepository<Form, Guid> 并添加了一个新方法来获取具有某些筛选条件的表单列表。然后,您可以将 IFormRepository 注入到您的服务中,而不是使用通用仓储,并使用您自定义的方法。如果您不想包含标准仓库方法,只需从 IRepository (不带任何泛型参数)接口派生您的接口。这是一个空接口,用于标识您的接口作为仓储。
当然,我们必须在我们的应用程序的某个地方实现 IFormRepository 接口。ABP 启动模板为底层数据库提供者提供了集成项目,因此我们可以在数据库集成项目中实现自定义仓库接口。

二、EF Core 集成

[1] 配置 DBMS

微软的 EF Core 是.NET 的事实上的 ORM,您可以使用它与主要的数据库提供者一起工作,例如 SQL Server、Oracle、MySQL、PostgreSQL 和 Cosmos DB。当您使用 ABP 命令行界面(CLI)创建新的ABP 解决方案时,它是默认的数据库提供者。

启动模板默认使用SQL Server。如果您在创建新解决方案时更喜欢另一个 -dbms 参数,如下所示:

abp new DemoApp -dbms PostgreSQL

SqlServer 、 MySQL 、 SQLite 、 Oracle 和 PostgreSQL 直接支持。

我们使用 AbpDbContextOptions 在模块的 ConfigureServices 方法中配置 DBMS。以下示例配置使用 SQL Server 作为 DBMS:

Configure<AbpDbContextOptions>(options =>
{
    options.UseSqlServer();
});

当然,如果您选择了不同的 DBMS, UseSqlServer() 方法调用将会有所不同。我们不需要设置连接字符串,因为它会自动从 ConnectionStrings:Default 配置中获取。您可以在项目的appsettings.json 文件中查看并更改连接字符串。
我们已经配置了 DBMS,但尚未定义 DbContext 对象,这是在 EF Core 中与数据库工作所必需的。

[2] 定义 DbContext

namespace FormsApp
{
    public class FormsAppDbContext : AbpDbContext<FormsAppDbContext>
    {
        public DbSet<Form> Forms { get; set; }

        public FormsAppDbContext(DbContextOptions<FormsAppDbContext> options)
            : base(options)
        {
        }
    }
}

[3] 将 DbContext 注册到 DI

使用 AddAbpDbContext 扩展方法将 DbContext 类注册到 DI 系统中。您可以在模块的 ConfigureServices 方法中使用此方法(它位于启动解决方案中的 EntityFrameworkCore 项目内),如下面的代码块所示:

[DependsOn(
    typeof(FormsDomainModule),
    typeof(AbpEntityFrameworkCoreSqlServerModule)
)]
public class FormsEntityFrameworkCoreModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddAbpDbContext<FormsAppDbContext>(options =>
        {
            options.AddDefaultRepositories();
        });
    }
}

使用 AddDefaultRepositories() 启用与该 DbContext 相关的实体的默认泛型仓储。默认情况下,它只为聚合根实体启用泛型仓储,因为如果您想为其他实体类型也使用仓储,可以将 includeAllEntities 参数设置为 true ,如下所示:

options.AddDefaultRepositories(includeAllEntities: true);

使用此选项,您可以在应用程序代码中注入任何实体的 IRepository 服务。

启动模板中的 includeAllEntities 选项

ABP 启动模板将 includeAllEntities 选项设置为 true ,因为从事关系数据库开发的开发者习惯于查询所有数据库表。如果您想严格应用 DDD 原则,您应该始终使用聚合根来访问子实体。在这种情况下,您可以从 AddDefaultRepositories 方法调用中删除此选项。
我们已经看到了如何注册 DbContext 类。我们可以在 DbContext 类中注入并使用所有实体的 IRepository 接口。然而,我们首先应该配置实体的 EF Core 映射。

[4] 配置实体映射

EF Core 是一个对象关系映射器,它将你的实体映射到数据库表。我们可以通过以下两种方式配置这些映射的细节,如下所述:

  • 在你的实体类上使用数据注释属性
  • 通过重写 OnModelCreating 方法使用 Fluent API

使用数据注释属性会使你的领域层依赖于 EF Core。如果你不介意这个问题,你可以简单地遵循 EF Core 的文档来使用这些属性。在这本书中,我将使用 Fluent API 方法。
要使用 Fluent API 方法,你可以在你的 DbContext 类中重写 OnModelCreating 方法,如下面的代码块所示:

public class FormsAppDbContext : AbpDbContext<FormsAppDbContext>
{
    ...
    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        // TODO: configure entities...
    }
}

当你重写 OnModelCreating 方法时,始终要调用 base.OnModelCreating() ,因为 ABP 也会在该方法内部执行默认配置,这对于正确使用 ABP 功能(如审计日志和数据过滤器)是必要的。然后,你可以使用 builder 对象来执行你的配置。
例如,我们可以配置本章中定义的 Form 类的映射,如下所示:

public class FormsAppDbContext : AbpDbContext<FormsAppDbContext>
{
    public DbSet<Form> Forms { get; set; }

    public FormsAppDbContext(DbContextOptions<FormsAppDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<Form>(b =>
        {
            b.ToTable("Forms");
            b.ConfigureByConvention();
            b.Property(x => x.Name)
                .HasMaxLength(100)
                .IsRequired();
            b.HasIndex(x => x.Name);
        });

        builder.Entity<Question>(b =>
        {
            b.ToTable("FormQuestions");
            b.ConfigureByConvention();
            b.Property(x => x.Title)
                .HasMaxLength(200)
                .IsRequired();
            b.HasOne<Form>()
                .WithMany(x => x.Questions)
                .HasForeignKey(x => x.FormId)
                .IsRequired();
        });
    }
}

ConfigureByConvention

在重写 OnModelCreating 方法时调用 b.ConfigureByConvention() 方法很重要。如果实体是从 ABP 预定义的 Entity 或 AggregateRoot 类派生的,它将配置实体的基本属性。剩余的配置代码相当干净和标准,你可以从 EF Core 的文档中学习所有详细信息

ConfigureByConvention()ModelBuilder.Entity<T>() 生成的 EntityTypeBuilder<T> 扩展方法(来自 Volo.Abp.EntityFrameworkCore.Modeling 命名空间),作用是:

  • 自动为实体应用 ABP 框架的「默认数据库映射约定」,减少手动配置代码;

  • 兼容 ABP 领域模型的核心特性(如聚合根、实体、审计属性、软删除等),确保框架功能正常工作。

  • 约定内容 具体效果
    主键映射 自动识别实体的主键(如 Id 属性,支持 Guid/int/long 等类型),配置为主键列(PRIMARY KEY);若实体是聚合根(AggregateRoot<T>),自动确保主键配置正确。
    审计属性映射 若实体实现了审计接口(如 IAuditedObjectICreationAuditedObject),自动为 CreationTimeCreatorIdLastModificationTimeLastModifierId 等属性配置数据库列(类型、可空性等)。
    软删除映射 若实体实现 ISoftDelete 接口,自动为 IsDeleted 属性配置列,并默认添加「过滤已删除数据」的全局查询筛选器(确保查询时不返回软删除数据)。
    并发控制映射 若实体实现 IHasConcurrencyStamp 接口,自动为 ConcurrencyStamp 属性配置为并发控制列(ROWVERSION/TIMESTAMP 类型,支持乐观锁)。
    列名 / 表名默认规则 自动遵循 ABP 的命名约定(如实体名 Form → 表名 Forms,属性名 UserName → 列名 UserName,可通过 ABP 配置修改命名策略)。
    导航属性默认处理 对简单导航属性(如一对一、一对多)应用默认关联规则,减少手动配置外键的工作量。

[5] 实现自定义仓储 FormRepository

    public class FormRepository :
        EfCoreRepository<FormsAppDbContext, Form, Guid>,
        IFormRepository
    {
        public FormRepository(
            IDbContextProvider<FormsAppDbContext> dbContextProvider)
            : base(dbContextProvider)
        {
        }

        public async Task<List<Form>> GetListAsync(
            string name, bool includeDrafts = false)
        {
            var dbContext = await GetDbContextAsync();

            var query = dbContext.Forms
                .Where(f => f.Name.Contains(name));

            if (!includeDrafts)
            {
                query = query.Where(f => !f.IsDraft);
            }

            return await query.ToListAsync();
        }
    }

这个类是从 ABP 的 EfCoreRepository 类派生出来的。这样,我们就继承了所有标准仓库方法。
EfCoreRepository 类获取三个泛型参数: DbContext 类型实体类型实体类的 PK 类型
FormRepository 还实现了 IFormRepository ,它定义了一个自定义的 GetListAsync 方法。我们在该方法中使用 DbContext 实例来利用 EF Core API 的所有功能。

[1] 关于 WhereIf 的提示

条件过滤是一个广泛使用的模式,ABP 提供了一个很好的 WhereIf 扩展方法,可以简化我们的代码。
我们可以重写 GetListAsync 方法,如下面的代码块所示:

public async Task<List<Form>> GetList2Async(
    string name, bool includeDrafts = false)
{
    var dbContext = await GetDbContextAsync();
    return await dbContext.Forms
        .Where(f => f.Name.Contains(name))
        .WhereIf(!includeDrafts, f => !f.IsDraft)
        .ToListAsync();
}

于我们有 DbContext 实例,我们可以使用它来执行 结构化查询语言(SQL)命令或存储过程。以下方法执行一个原始 SQL 命令来删除所有草稿表单

public async Task DeleteAllDraftsAsync()
{
    var dbContext = await GetDbContextAsync();
    await dbContext.Database
        .ExecuteSqlRawAsync("DELETE FROM Forms WHERE IsDraft = 1");
}

[2] 覆盖默认仓储基方法,需要注册 AddRepository

一旦实现了 IFormRepository ,你就可以注入并使用它,而不是使用 IRepository<Form, Guid> ,如下所示:

public class FormService3 : ITransientDependency
{
    private readonly IFormRepository _formRepository;
    private readonly IAsyncQueryableExecuter _asyncExecuter;

    public FormService3(
        IFormRepository formRepository,
        IAsyncQueryableExecuter asyncExecuter)
    {
        _formRepository = formRepository;
        _asyncExecuter = asyncExecuter;
    }

    public async Task<List<Form>> GetFormsAsync(string name)
    {
        return await _formRepository
            .GetListAsync(name, includeDrafts: true); //这里使用的是 IFormRepository 的 GetListAsync,不是IRepository<Form, Guid>
    }
}

如果你在你的仓库中覆盖了 EfCoreRepository 类的基方法并进行了自定义,可能会出现一个潜在问题。在这种情况下,使用泛型仓库引用的服务将继续使用未覆盖的方法。为了防止这种碎片化,在将DbContext 注册到 DI 时使用 AddRepository 方法,如下所示:

            context.Services.AddAbpDbContext<FormsAppDbContext>(options =>
            {
                options.AddDefaultRepositories();
                //IAbpCommonDbContextRegistrationOptionsBuilder AddRepository<TEntity, TRepository>();
                options.AddRepository<Form, FormRepository>(); //使用此配置, AddRepository 方法将泛型仓库重定向到你的自定义仓库类。
            });

[6] 加载相关数据

如果你的实体有指向其他实体的导航属性或具有其他实体的集合,那么在处理主实体时,你将经常需要访问这些相关实体。例如,之前引入的 Form 实体有一个 Question 实体的集合,你可能需要在处理Form 对象时访问这些问题。

有多种方式访问相关实体:显式加载延迟加载预加载

EF CORE 对比维度 预加载(Include) 延迟加载(Lazy Loading) 显式加载(Explicit)
加载时机 主实体查询时同时加载 首次访问导航属性时加载 手动调用方法时加载
触发方式 Include 声明 自动触发(需 virtual 手动调用 LoadAsync 等方法
SQL 查询次数 1 次(联表查询) N+1 次(主实体 1 次 + 每次访问关联 1 次) 1(主实体)+ N(关联加载次数)次
依赖动态代理 是(导航属性需 virtual
开发便捷性 中等(需声明 Include 高(自动加载) 低(手动触发)
内存占用 可能较大(单次联表结果集) 较小(按需加载) 较小(按需加载)
避免 N+1 问题 不能(易触发) 能(手动控制)

显式加载

仓储提供了 EnsurePropertyLoadedAsyncEnsureCollectionLoadedAsync 扩展方法来显式加载导航属性或子集合。

方法 作用对象 关联关系类型 底层 EF Core 依赖
EnsurePropertyLoadedAsync 单个关联属性(标量 / 引用类型) 一对一(1:1)、多对一(N:1) EntityEntry.IsLoaded + EntityEntry.LoadAsync()
EnsureCollectionLoadedAsync 集合类型关联属性 一对多(1:N)、多对多(N:N) EntityEntry.Collection().IsLoaded + EntityEntry.Collection().LoadAsync()

例如,我们可以显式地加载表单的问题,如下面的代码块所示:

 public async Task<IEnumerable<Question>> GetQuestionsAsync(Guid formId)
 {
     var form = await _formRepository.GetAsync(formId);
     await _formRepository.EnsureCollectionLoadedAsync(form, f => f.Questions);
     return form.Questions;
 }

如果我们不在这里使用 EnsureCollectionLoadedAsync ,那么 form.Questions 集合可能会为空。如果我们不确定它是否已填充,我们可以使用 EnsureCollectionLoadedAsync 来确保它被加载。

如果相关的属性或集合已经加载, EnsurePropertyLoadedAsync 和 EnsureCollectionLoadedAsync 方法不会做任何事情,因此多次调用它们对性能没有问题。

延迟加载 (不建议使用)

懒加载是 EF Core 的一个功能,当你第一次访问它们时,它会加载相关的属性和集合。懒加载默认是禁
用的。如果你想为你的 DbContext 启用它,请按照以下步骤操作:

  1. 在你的 EF Core 层中安装 Microsoft.EntityFrameworkCore.Proxies NuGet 包。

  2. 在配置 AbpDbContextOptions 时使用 UseLazyLoadingProxies 方法,如下所示:

    [DependsOn(
        typeof(FormsDomainModule),
        typeof(AbpEntityFrameworkCoreSqlServerModule)
    )]
    public class FormsEntityFrameworkCoreModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            context.Services.AddAbpDbContext<FormsAppDbContext>(options =>
            {
                options.AddDefaultRepositories();
                options.AddRepository<Form, FormRepository>();
            });
            
            Configure<AbpDbContextOptions>(options =>
            {
                options.PreConfigure<FormsAppDbContext>(opts =>
                {
                    opts.DbContextOptions.UseLazyLoadingProxies();
                });
    
                options.UseSqlServer();
            });
        }
    }
  3. 确保你的实体中的导航属性和集合属性是虚拟的,如下所示

    public class Form : BasicAggregateRoot<Guid>
    {
        ...
        public virtual ICollection<Question> Questions { get; set; }
        public virtual ICollection<FormManager> Owners { get; set; }
    }
    

预加载 (还是不建议,体量小可以)

预加载是在首先查询主实体时加载相关数据的一种方式。
假设你已经创建了一个自定义仓库方法,在从数据库获取 Form 对象的同时加载相关的问题,如下所示:

public async Task<Form> GetWithQuestions(Guid formId)
{
    var dbContext = await GetDbContextAsync();
    return await dbContext.Forms
        .Include(f => f.Questions)
        .SingleAsync(f => f.Id == formId);
}

不要在ABP使用 Include

如果你创建了这样的自定义存储库方法,你可以使用完整的 EF Core API。然而,如果你正在使用 ABP的存储库,并且不希望在应用程序层依赖于 EF Core,你不能使用 EF Core 的 Include 扩展方法(用于预加载相关数据)

IRepository.WithDetailsAsync

IRepository 的 WithDetailsAsync 方法通过包含给定的属性或集合,返回一个 IQueryable 实例,

public async Task EagerLoadDemoAsync(Guid formId)
{
    var queryable = await _formRepository.WithDetailsAsync(f => f.Questions);
    var query = queryable.Where(f => f.Id == formId);
    var form = await _asyncExecuter.FirstOrDefaultAsync(query);
    foreach (var question in form.Questions)
    {
        //...
    }
}

WithDetailsAsync(f => f.Questions) 返回包含问题的 IQueryable<Form> ,因此我们可以安全地遍历 form.Questions 集合。 IAsyncQueryableExecuter 在本章的 通用仓储 部分中已经解释过。
如果需要包含多个属性, WithDetailsAsync 方法可以获取多个表达式。如果需要嵌套包含(EF Core中的 ThenInclude 扩展方法),则不能使用 WithDetailsAsync 。在这种情况下,创建一个自定义仓储方法。

聚合模式

为了提供一些简要信息,聚合被认为是一个单一单元;它作为一个单元读取和保存,包括所有子集合。这意味着你始终在加载表单时加载相关的问题。

ABP 很好地支持聚合模式,并允许你在全局点为实体配置预加载。我们可以在我们的模块类(在解决方案中的 EntityFrameworkCore 项目)的 ConfigureServices 方法中编写以下配置:

Configure<AbpEntityOptions>(options =>
{
    options.Entity<Form>(orderOptions =>
    {
        orderOptions.DefaultWithDetailsFunc = query => query
            .Include(f => f.Questions)
            .Include(f => f.Managers);
    });
});

建议包含所有子集合。一旦你配置了 DefaultWithDetailsFunc 方法,如下所示,那么以下情况将会发生:

  • 返回单个实体(例如 GetAsync )的存储库方法默认会预加载相关实体,除非你通过在方法调用中指定 includeDetails 参数为 false 来显式禁用该行为。
  • 返回多个实体(例如 GetListAsync )的存储库方法将允许预加载相关实体,但默认情况下不会进行预加载。

这里有一些示例。
获取包含子集合的单个表单如下:

var form = await _formRepository.GetAsync(formId);

获取不带子集合的单个表单如下

var form = await _formRepository.GetAsync(formId, includeDetails: false);

获取不带子集合的表单列表如下

var forms = await _formRepository.GetListAsync(f => f.Name.StartsWith("A"));

获取包含子集合的表单列表如下

var forms = await _formRepository.GetListAsync(f => f.Name.StartsWith("A"), includeDetails: true);

请注意,如果你真正实现了聚合模式,则不会使用导航属性(到其他聚合)。

[7] 理解 UoW(工作单元) 系统

UoW 是 ABP 用来启动、管理和释放数据库连接和事务的主要系统。UoW 系统是按照环境上下文模式设计的。这意味着当我们创建一个新的 UoW 时,它会创建一个范围上下文,该上下文由当前范围内执行的、共享相同上下文的全部数据库操作参与,并被视为一个单独的事务边界。在 UoW 中执行的所有操作要么(在成功时)提交,要么(在异常时)回滚。

虽然你可以手动创建 UoW 范围并控制事务属性,但大多数时候,它将无缝地按照你的期望工作。然而,如果你更改默认行为,它提供了一些选项。

UoW 和数据库操作

所有数据库操作必须在 UoW 范围内执行,因为 UoW 是管理 ABP 框架中数据库连接和事务的方式。否则,你会得到一个异常指示。

[7.1] 配置 UoW 选项

在默认设置下,在 ASP.NET Core 应用程序中,一个 HTTP 请求被视为 UoW 范围。ABP 在请求开始时启动 UoW,如果请求成功完成,则将更改保存到数据库。如果请求因异常失败,则回滚 UoW。

ABP 根据 HTTP 请求类型确定数据库事务的使用。HTTP GET 请求不创建数据库事务。UoW 仍然工作,但在这种情况下不使用数据库事务。如果你没有其他配置,所有其他 HTTP 请求类型( POST 、 PUT 、DELETE 和其他)都将使用数据库事务。

HTTP GET 请求和事务

不在 GET 请求中更改数据库是一个最佳实践。如果你在 GET 请求中执行多个写操作,并且请求以某种方式失败,你的数据库状态可能会处于不一致的状态,因为 ABP 不会为 GET 请求创建数据库事务。在这种情况下,你可以使用 AbpUnitOfWorkDefaultOptions 为 GET 请求启用事务,或者像下一节中描述的那样手动控制 UoW。

如果你想更改 UoW 选项,请在你的模块(数据库集成项目中)的 ConfigureServices 方法中使用 AbpUnitOfWorkDefaultOptions ,如下所示:

[DependsOn(
    typeof(AbpDddDomainModule)
)]
public class FormsDomainModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure<AbpUnitOfWorkDefaultOptions>(options =>
        {
            options.TransactionBehavior = UnitOfWorkTransactionBehavior.Enabled;
            options.Timeout = 300000; // 5 minutes
            options.IsolationLevel = IsolationLevel.Serializable;
        });
    }
}

TransactionBehavior 可以取以下三个值:

  • Auto (默认): 自动确定是否使用数据库事务(非 GET HTTP 请求启用事务)
  • Enabled : 总是使用数据库事务,即使是 HTTP GET 请求
  • Disabled : 从不使用数据库事务

Auto 行为是默认值,适用于大多数应用。 IsolationLevel 仅对关系型数据库有效。如果你没有指定,ABP 将使用底层提供者的默认值。最后, Timeout 选项允许你将事务的默认超时值设置为毫秒。如果UoW 操作在给定超时值内未完成,将抛出超时异常。

在本节中,我们学习了如何配置所有 UoW 的默认选项。如果你手动控制,也可以为单个 UoW 配置这些 值。

[7.2] 手动控制 UoW

对于 Web 应用,你很少需要手动控制 UoW 系统。然而,对于后台工作者或非 Web 应用,你可能需要自己创建 UoW 作用域。你可能还需要控制 UoW 系统以创建内部事务作用域。
创建 UoW 作用域的一种方式是在你的方法上使用 [UnitOfWork] 属性,如下所示:

[UnitOfWork(isTransactional: true)]
public async Task DoItAsync()
{
    await _formRepository.InsertAsync(new Form() { ... });
    await _formRepository.InsertAsync(new Form() { ... });
}

UoW 系统使用环境上下文模式。如果已经存在一个周围的 UoW,你的 UnitOfWork 属性将被忽略,你的方法将参与周围的 UoW。否则,ABP 在进入 DoItAsync 方法之前启动一个新的事务性 UoW,如果没有抛出异常,则提交事务。如果该方法抛出异常,则回滚事务。
如果你想要精细控制 UoW 系统,你可以注入并使用 IUnitOfWorkManager 服务,如下面的代码块所示:

public async Task DoItAsync()
{
    using (var uow = _unitOfWorkManager.Begin(
        requiresNew: true,
        isTransactional: true,
        timeout: 15000))
    {
        await _formRepository.InsertAsync(new Form() { });
        await _formRepository.InsertAsync(new Form() { });
        await uow.CompleteAsync();
    }
}

在此示例中,我们以 15 秒作为 timeout 参数值的值启动一个新的事务性 UoW 作用域。使用此用法( requiresNew: true ),即使存在周围的 UoW,ABP 也会始终启动一个新的 UoW。如果一切顺利,始终调用 uow.CompleteAsync() 方法。如果你想回滚当前事务,可以使用 uow.RollbackAsync()方法。

如前所述,UoW 使用环境作用域。你可以使用 IUnitOfWorkManager.Current 属性在任何此作用域中访问当前 UoW。如果没有正在进行的 UoW,它可以是 null 。
以下代码片段使用 SaveChangesAsync 方法与 IUnitOfWorkManager.Current 属性:

await _unitOfWorkManager.Current.SaveChangesAsync();

我们已将所有挂起的更改保存到数据库中。然而,如果这是一个事务性 UoW,如果你回滚 UoW 或在该作用域中抛出任何异常,这些更改也将被回滚。

posted @ 2025-10-31 11:29  【唐】三三  阅读(4)  评论(0)    收藏  举报