项目

存储库

"使用类似集合的接口在领域层和数据映射层之间进行协调,以访问领域对象"(Martin Fowler)。

在实践中,存储库用于对领域对象执行数据库操作(参见 实体 )。通常,每个聚合根或实体使用一个单独的存储库。

泛型存储库

ABP可以为每个聚合根或实体提供一个默认的泛型存储库。您可以将 IRepository<TEntity, TKey> 注入 到您的服务中,并执行标准的CRUD操作。

数据库提供程序层必须正确配置才能使用默认泛型存储库。如果您是使用启动模板创建的项目,这已经完成。如果没有,请参考数据库提供程序文档( EF Core / MongoDB )进行配置。

默认泛型存储库的使用示例:

using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace Demo
{
    public class PersonAppService : ApplicationService
    {
        private readonly IRepository<Person, Guid> _personRepository;

        public PersonAppService(IRepository<Person, Guid> personRepository)
        {
            _personRepository = personRepository;
        }

        public async Task CreateAsync(CreatePersonDto input)
        {
            var person = new Person(input.Name);

            await _personRepository.InsertAsync(person);
        }

        public async Task<int> GetCountAsync(string filter)
        {
            return await _personRepository.CountAsync(p => p.Name.Contains(filter));
        }
    }
}

在这个例子中:

  • PersonAppService 在其构造函数中简单注入了 IRepository<Person, Guid>
  • CreateAsync 方法使用 InsertAsync 来保存新实体。
  • GetCountAsync 方法获取数据库中满足过滤条件的人员总数。

标准存储库方法

泛型存储库提供了一些开箱即用的标准CRUD功能:

  • GetAsync:通过其 Id 或谓词(lambda表达式)返回单个实体。
    • 如果未找到请求的实体,则抛出 EntityNotFoundException
    • 如果存在多个满足给定谓词的实体,则抛出 InvalidOperationException
  • FindAsync:通过其 Id 或谓词(lambda表达式)返回单个实体。
    • 如果未找到请求的实体,则返回 null
    • 如果存在多个满足给定谓词的实体,则抛出 InvalidOperationException
  • InsertAsync:将新实体插入数据库。
  • UpdateAsync:更新数据库中的现有实体。
  • DeleteAsync:从数据库中删除给定实体。
    • 此方法有一个重载,接受谓词(lambda表达式)以删除满足给定条件的多个实体。
  • GetListAsync:返回数据库中所有实体的列表。
  • GetPagedListAsync:返回有限的实体列表。接收 skipCountmaxResultCountsorting 参数。
  • GetCountAsync:获取数据库中所有实体的数量。

这些方法有多个重载。

  • 提供 UpdateAsyncDeleteAsync 方法,通过实体对象或其id来更新或删除实体。
  • 提供 DeleteAsync 方法,通过过滤器删除多个实体。

在存储库上进行查询 / LINQ

存储库提供了 GetQueryableAsync() 方法,返回一个 IQueryable<TEntity> 对象。您可以使用此对象对数据库中的实体执行LINQ查询。

示例:在存储库中使用LINQ

using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace Demo
{
    public class PersonAppService : ApplicationService
    {
        private readonly IRepository<Person, Guid> _personRepository;

        public PersonAppService(IRepository<Person, Guid> personRepository)
        {
            _personRepository = personRepository;
        }

        public async Task<List<PersonDto>> GetListAsync(string filter)
        {
            // 获取 IQueryable<Person>
            IQueryable<Person> queryable = await _personRepository.GetQueryableAsync();

            // 创建查询
            var query = from person in queryable
                where person.Name == filter
                orderby person.Name
                select person;

            // 执行查询以获取人员列表
            var people = query.ToList();

            // 转换为DTO并返回给客户端
            return people.Select(p => new PersonDto {Name = p.Name}).ToList();
        }
    }
}

您也可以使用LINQ扩展方法:

public async Task<List<PersonDto>> GetListAsync(string filter)
{
    // 获取 IQueryable<Person>
    IQueryable<Person> queryable = await _personRepository.GetQueryableAsync();

    // 执行查询
    var people = queryable
        .Where(p => p.Name.Contains(filter))
        .OrderBy(p => p.Name)
        .ToList();

    // 转换为DTO并返回给客户端
    return people.Select(p => new PersonDto {Name = p.Name}).ToList();
}

可以对从存储库返回的 IQueryable 使用任何标准的LINQ方法。

此示例使用了 ToList() 方法,但强烈建议使用异步方法来执行数据库查询,例如本例中的 ToListAsync()。参见 IQueryable & 异步操作 部分了解如何实现。

在存储库类外部暴露 IQueryable 可能会将您的数据访问逻辑泄漏到应用层。如果您想严格遵守分层架构原则,可以考虑实现一个自定义存储库类,并将数据访问逻辑包装在您的存储库类内部。您可以查看 自定义存储库 部分了解如何为您的应用程序创建自定义存储库类。

批量操作

有一些方法可以在数据库中执行批量操作:

  • InsertManyAsync
  • UpdateManyAsync
  • DeleteManyAsync

这些方法处理多个实体,如果底层数据库提供程序支持,可以利用批量操作的优势。

当您使用 UpdateManyAsyncDeleteManyAsync 方法时,可能无法进行乐观并发控制。

软删除 / 硬删除

如果实体是软删除实体(实现了 ISoftDelete),存储库的 DeleteAsync 方法不会删除该实体。软删除实体在数据库中被标记为“已删除”。数据过滤系统确保通常不会从数据库中检索到软删除的实体。

如果您的实体是软删除实体,您可以在需要时使用 HardDeleteAsync 方法从数据库中物理删除该实体。

有关软删除的更多信息,请参阅 数据过滤 文档。

直接删除

存储库的 DeleteDirectAsync 方法删除所有满足给定谓词的实体。它直接从数据库中删除实体,而无需先获取它们。

一些特性(如软删除、多租户和审计日志)将不起作用,因此在需要时请谨慎使用此方法。如果您需要这些特性,请使用 DeleteAsync 方法。

目前只有 EF Core 支持此功能 。对于不支持直接删除的ORM,我们将回退到现有的 DeleteAsync 方法。

确保实体存在

EnsureExistsAsync 扩展方法接受实体id或实体查询表达式,以确保实体存在,否则将抛出 EntityNotFoundException

启用 / 禁用变更跟踪

ABP提供了存储库扩展方法和属性,可用于控制底层数据库提供程序中查询实体的变更跟踪行为。

如果您从数据库中查询许多实体仅用于只读目的,禁用变更跟踪可以提高性能。查询单个或少量实体不会带来太大的性能差异,但您可以随时自由使用它。

如果底层数据库提供程序不支持变更跟踪,那么此系统将不会产生任何效果。例如,Entity Framework Core 支持变更跟踪,而 MongoDB 提供程序不支持。

用于变更跟踪的存储库扩展方法

变更跟踪是启用的,除非您显式禁用它。

示例:使用 DisableTracking 扩展方法

public class MyDemoService : ApplicationService
{
    private readonly IRepository<Person, Guid> _personRepository;

    public MyDemoService(IRepository<Person, Guid> personRepository)
    {
        _personRepository = personRepository;
    }

    public async Task DoItAsync()
    {
        // 此时变更跟踪已启用(默认)
        
        using (_personRepository.DisableTracking())
        {
            // 此时变更跟踪已禁用
            var list = await _personRepository.GetPagedListAsync(0, 100, "Name ASC");
        }
        
        // 此时变更跟踪已启用(默认)
    }
}

DisableTracking 扩展方法返回一个 IDisposable 对象,因此您可以在 using 块结束后安全地恢复变更跟踪行为到之前的状态。基本上,DisableTracking 方法确保在 using 块内部禁用变更跟踪,但不影响 using 块外部。这意味着,如果变更跟踪已经被禁用,DisableTracking 和可处理的返回值不会做任何事情。

EnableTracking() 方法与 DisableTracking() 方法的作用完全相反。您通常不需要使用它(因为默认情况下变更跟踪已启用),但在需要时它仍然存在。

用于变更跟踪的属性

您通常将 DisableTracking() 方法用于仅返回数据但不更改任何实体的应用服务方法。对于这种情况,您可以在方法/类上使用 DisableEntityChangeTracking 属性,作为禁用整个方法体内变更跟踪的快捷方式。

示例:在方法上使用 DisableEntityChangeTracking 属性

[DisableEntityChangeTracking]
public virtual async Task<List<PersonDto>> GetListAsync()
{
    /* 我们在此方法中禁用了变更跟踪,
       因为我们不会更改人员对象 */
    var people = await _personRepository.GetListAsync();
    return ObjectMapper.Map<List<Person>, List<PersonDto>(people);
}

EnableEntityChangeTracking 可用于相反的目的,它确保对给定方法启用变更跟踪。由于默认情况下变更跟踪已启用,EnableEntityChangeTracking 可能仅在您知道您的方法是从禁用变更跟踪的上下文中调用时才需要。

DisableEntityChangeTrackingEnableEntityChangeTracking 属性可以用于方法(影响类的所有方法)。

ABP使用动态代理使这些属性工作。这里有一些规则:

  • 如果您没有通过接口(如 IPersonAppService注入服务,则服务的方法必须是 virtual。否则,动态代理/拦截 系统无法工作。
  • 只有 async 方法(返回 TaskTask<T> 的方法)会被拦截。

变更跟踪行为不会影响从 InsertAsyncUpdateAsync 方法返回的实体对象的跟踪。从这些方法返回的对象始终被跟踪(如果底层提供程序具有变更跟踪功能),并且您对这些对象所做的任何更改都会保存到数据库中。

检查变更跟踪是否启用

如果您需要检查存储库对象上是否启用了变更跟踪,您可以简单地读取 IsChangeTrackingEnabled 属性。对于只读存储库(参见下面的只读存储库部分),它默认为 false。对于其他存储库对象,它默认为 null。如果为 null,则除非您显式使用了变更跟踪属性(参见本文档中的用于变更跟踪的属性部分),否则变更跟踪是启用的。

EntityName 属性

EntityName 属性用于在某些高级场景中在存储库对象上设置相关实体的名称。例如,您可以使用此属性来使用 Entity Framework Core 的 共享类型实体类型 功能(它允许您使用单个实体类来处理数据库中的多个表)。在其他情况下,您可以忽略它。

默认值为 null,除非您显式设置它。

使用示例:

IRepository<YourSharedType, Guid> _repository; // 您可以将其注入到您的类中
...
_repository.SetEntityName("Product"); // 在使用存储库之前设置实体名称
// 像往常一样使用 _repository 对象

ProviderName 属性

存储库对象的 ProviderName 属性返回底层数据库提供程序的名称。对于内置提供程序,它可能返回以下字符串值之一:

  • Volo.Abp.EntityFrameworkCore(来自常量 AbpEfCoreConsts.ProviderName 值)
  • Volo.Abp.MongoDB(来自常量 AbpMongoDbConsts.ProviderName 值)
  • Volo.Abp.MemoryDb(来自常量 AbpMemoryDbConsts.ProviderName 值)

在大多数情况下不使用此属性。它主要用于ABP框架的内部使用。

其他泛型存储库类型

标准的 IRepository<TEntity, TKey> 接口暴露了标准的 IQueryable<TEntity>,您可以自由地使用标准的LINQ方法进行查询。这对于大多数应用程序来说是可以的。然而,一些ORM提供程序或数据库系统可能不支持标准的 IQueryable 接口。如果您想使用这样的提供程序,就不能依赖 IQueryable

基础存储库

ABP提供了 IBasicRepository<TEntity, TPrimaryKey>IBasicRepository<TEntity> 接口来支持此类场景。您可以扩展这些接口(并可选地从 BasicRepositoryBase 派生)来为您的实体创建自定义存储库。

依赖 IBasicRepository 而不依赖 IRepository 的优势在于,即使数据源不支持 IQueryable,也可以使用所有数据源。

主要供应商,如Entity Framework、NHibernate或MongoDB已经支持 IQueryable。因此,对于典型应用程序,使用 IRepository推荐的方式。然而,可重用模块开发人员可以考虑使用 IBasicRepository 以支持更广泛的数据源。

只读存储库

还有 IReadOnlyRepository<TEntity, TKey>IReadOnlyBasicRepository<Tentity, TKey> 接口,适用于那些只想依赖存储库查询功能的人。

IReadOnlyRepository<TEntity, TKey> 继承自 IReadOnlyBasicRepository<Tentity, TKey>,并额外提供了以下属性和方法:

属性:

AsyncExecuter:一个用于不依赖实际数据库提供程序异步执行 IQueryable<T> 对象的服务。

方法:

  • GetListAsync()
  • GetQueryableAsync()
  • WithDetails() 1 个重载
  • WithDetailsAsync() 1 个重载

IReadOnlyBasicRepository<Tentity, TKey> 提供了以下方法:

  • GetCountAsync()
  • GetListAsync()
  • GetPagedListAsync()

它们都可以在下图中看到:

泛型存储库

Entity Framework Core 中只读存储库的行为

Entity Framework Core 的只读存储库实现使用了EF Core的无跟踪查询功能。这意味着从存储库返回的实体不会被EF Core变更跟踪器跟踪,因为预计您不会更新从只读存储库查询的实体。如果您需要跟踪实体,仍然可以在LINQ表达式上使用AsTracking()扩展方法,或者在存储库对象上使用 EnableTracking() 扩展方法(参见本文档中的启用 / 禁用变更跟踪部分)。

仅当存储库对象是通过只读存储库接口之一(IReadOnlyRepository<...>IReadOnlyBasicRepository<...>)注入时,此行为才有效。如果您注入了标准存储库(例如 IRepository<...>),然后将其转换为只读存储库接口,则此行为无效。

无主键的泛型存储库

如果您的实体没有Id主键(例如,它可能有复合主键),那么您不能使用上面定义的 IRepository<TEntity, TKey>(或基础/只读版本)。在这种情况下,您可以为您的实体注入并使用 IRepository<TEntity>

IRepository<TEntity> 缺少一些通常使用实体 Id 属性的方法。因为在这种情况下实体没有 Id 属性,所以这些方法不可用。一个例子是 Get 方法,它获取一个id并返回具有给定id的实体。但是,您仍然可以使用 IQueryable<TEntity> 功能通过标准LINQ方法查询实体。

自定义存储库

默认的泛型存储库在大多数情况下就足够了。但是,您可能需要为您的实体创建一个自定义存储库类。

自定义存储库示例

ABP不强制您为存储库实现任何接口或继承任何基类。它可以只是一个简单的POCO类。但是,建议继承现有的存储库接口和类,以便您的工作更轻松,并开箱即用地获得标准方法。

自定义存储库接口

首先,在您的领域层定义一个接口:

public interface IPersonRepository : IRepository<Person, Guid>
{
    Task<Person> FindByNameAsync(string name);
}

此接口扩展了 IRepository<Person, Guid>,以利用预构建的存储库功能。

自定义存储库实现

自定义存储库与您使用的数据访问工具类型紧密耦合。在此示例中,我们将使用Entity Framework Core:

public class PersonRepository : EfCoreRepository<MyDbContext, Person, Guid>, IPersonRepository
{
    public PersonRepository(IDbContextProvider<TestAppDbContext> dbContextProvider) 
        : base(dbContextProvider)
    {

    }

    public async Task<Person> FindByNameAsync(string name)
    {
        var dbContext = await GetDbContextAsync();
        return await dbContext.Set<Person>()
            .Where(p => p.Name == name)
            .FirstOrDefaultAsync();
    }
}

您可以直接访问数据访问提供程序(在本例中为 DbContext)来执行操作。

有关自定义存储库的更多信息,请参阅 EF CoreMongoDb 文档。

IQueryable & 异步操作

IRepository 提供了 GetQueryableAsync() 来获取一个 IQueryable,这意味着您可以直接对其使用LINQ扩展方法,如上面“在存储库上进行查询 / LINQ”部分的示例所示。

示例:使用 Where(...)ToList() 扩展方法

var queryable = await _personRepository.GetQueryableAsync();
var people = queryable
    .Where(p => p.Name.Contains(nameFilter))
    .ToList();

.ToListCount()... 是定义在 System.Linq 命名空间中的标准扩展方法( 参见所有方法 )。

您通常希望使用 .ToListAsync().CountAsync()... 来代替,以便能够编写真正的异步代码

然而,您会发现,当您使用标准 应用启动模板 创建新项目时,不能在您的应用或领域层使用所有异步扩展方法,因为:

  • 这些异步方法不是标准的LINQ方法,它们定义在 Microsoft.EntityFrameworkCore NuGet包中。
  • 标准模板没有引用领域层和应用层中的EF Core包,以独立于数据库提供程序。

根据您的需求和开发模式,您有以下选项来能够使用异步方法。

强烈建议使用异步方法!在执行数据库查询时不要使用同步LINQ方法,以便能够开发可扩展的应用程序。

选项1:引用数据库提供程序包

最简单的解决方案是直接从您想要使用这些异步方法的项目中添加EF Core包。

Volo.Abp.EntityFrameworkCore NuGet包添加到您的项目中,该包间接引用了EF Core包。这确保您使用与应用程序其余部分兼容的正确版本的EF Core。

将NuGet包添加到项目后,您就可以充分利用EF Core的扩展方法。

示例:添加EF Core包后直接使用 ToListAsync()

var queryable = await _personRepository.GetQueryableAsync();
var people = queryable
    .Where(p => p.Name.Contains(nameFilter))
    .ToListAsync();

此方法建议:

  • 如果您正在开发应用程序,并且不打算在未来更改EF Core,或者如果需要更改,您可以容忍。我们认为,如果您正在开发最终应用程序,这是合理的。

选项2:使用存储库的异步扩展方法

ABP为存储库提供了异步扩展方法,类似于异步LINQ扩展方法。

示例:在存储库上使用 CountAsyncFirstOrDefaultAsync 方法

var countAll = await _personRepository
    .CountAsync();

var count = await _personRepository
    .CountAsync(x => x.Name.StartsWith("A"));

var book1984 = await _bookRepository
    .FirstOrDefaultAsync(x => x.Name == "John");    

支持以下标准LINQ扩展方法:AllAsync, AnyAsync, AverageAsync, ContainsAsync, CountAsync, FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, LongCountAsync, MaxAsync, MinAsync, SingleAsync, SingleOrDefaultAsync, SumAsync, ToArrayAsync, ToListAsync

这种方法仍然有限制。您需要直接在存储库对象上调用扩展方法。例如,不支持以下用法:

var queryable = await _bookRepository.GetQueryableAsync();
var count = await queryable.Where(x => x.Name.Contains("A")).CountAsync();

这是因为此示例中的 CountAsync() 方法是在 IQueryable 接口上调用的,而不是在存储库对象上。对于这种情况,请参阅其他选项。

此方法建议尽可能使用

选项3:IAsyncQueryableExecuter

IAsyncQueryableExecuter 是一个用于不依赖实际数据库提供程序异步执行 IQueryable<T> 对象的服务。

示例:注入并使用 IAsyncQueryableExecuter.ToListAsync() 方法

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;

namespace AbpDemo
{
    public class ProductAppService : ApplicationService, IProductAppService
    {
        private readonly IRepository<Product, Guid> _productRepository;
        private readonly IAsyncQueryableExecuter _asyncExecuter;

        public ProductAppService(
            IRepository<Product, Guid> productRepository,
            IAsyncQueryableExecuter asyncExecuter)
        {
            _productRepository = productRepository;
            _asyncExecuter = asyncExecuter;
        }

        public async Task<ListResultDto<ProductDto>> GetListAsync(string name)
        {
            // 获取 IQueryable<T>
            var queryable = await _productRepository.GetQueryableAsync();
            
            // 创建查询
            var query = queryable
                .Where(p => p.Name.Contains(name))
                .OrderBy(p => p.Name);

            // 异步运行查询
            List<Product> products = await _asyncExecuter.ToListAsync(query);

            //...
        }
    }
}

ApplicationServiceDomainService 基类已经预注入了 AsyncExecuter 属性,无需显式构造函数注入即可使用。

ABP使用实际数据库提供程序的API异步执行查询。虽然这不是执行查询的常用方式,但它是在不依赖数据库提供程序的情况下使用异步API的最佳方式。

此方法建议:

  • 如果您希望开发应用程序代码而不依赖数据库提供程序。
  • 如果您正在构建一个可重用库,该库没有数据库提供程序集成包,但在某些情况下需要执行 IQueryable<T> 对象。

例如,ABP在 CrudAppService 基类中使用了 IAsyncQueryableExecuter(参见 应用服务 文档)。

选项4:自定义存储库方法

您始终可以创建自定义存储库方法,并使用数据库提供程序特定的API,例如这里的异步扩展方法。有关自定义存储库的更多信息,请参阅 EF CoreMongoDb 文档。

此方法建议:

  • 如果您想完全隔离您的领域层和应用层与数据库提供程序。
  • 如果您正在开发一个可重用的 应用模块,并且不想强制使用特定的数据库提供程序,这应该作为 最佳实践 来完成。

另请参阅

在本文档中