仓储

仓储

在领域层和数据映射层之间进行中介,使用类似集合的接口来操作领域对象。
实际上,仓储用于领域对象对数据库的操作, 通常每个 聚合根 或实体会创建各自对应的仓储.

通用仓储

ABP为每个聚合根和实体提供了默认的仓储。 你可以在服务中注入IRepository<TEntity, TKey>执行标准的CRUD操作。
默认通用仓储用法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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: 返回一个指定长度的实体列表。 他拥有 skipCount, maxResultCount and sorting 参数.
  • GetCountAsync: 获取数据库里所有实体的数量

这些方法还有还一些重载。

  • 提供 UpdateAsync 和 DeleteAsync 方法根据实体对象或者id来更新或者删除实体。
  • 提供 DeleteAsync 方法用来删除符合指定条件的多个实体。

在存储上使用LINQ

仓储提供了一个GetQueryableAsync方法来获取一个IQueryable<TEntity>对象。你可以通过这个对象来对实体执行LINQ查询以操作数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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扩展方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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方法自由查询。

批量操作

下面这些方法可以用来对数据库执行批量操作;

1
2
3
InsertManyAsync
UpdateManyAsync
DeleteManyAsync

这些方法可以操作多个实体,如果底层数据库提供程序支持,则可以进行批量操作。

软/硬删除

如果一个实体是软删除实体(即实现了ISoftDelete接口),则仓储的DeleteSync方法不会删除该实体,而是在数据库中标记为“已删除”。数据过滤器系统确保不会从数据库中正常检索软删除的实体。

如果您的实体是软删除实体,如果您需要物理删除这个实体,您可以使用HardDeleteAsync方法强制删除。

确保实体存在

EnsureExistsAsync扩展方法通过实体id或实体查询表达式来确保实体存在,如果其不存在,它将抛出EntityNotFoundException异常。

其他通用仓储类型

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>接口。

无主键的通用(泛型)仓储

如果你的实体没有id主键 (例如, 它可能具有复合主键) 那么你不能使用上面定义的 IRepository<TEntity, TKey>, 在这种情况下你可以仅使用实体(类型)注入 IRepository<TEntity>

IRepository<TEntity> 有一些缺失的方法, 通常与实体的 Id 属性一起使用. 由于实体在这种情况下没有 Id 属性, 因此这些方法不可用. 比如 Get 方法通过id获取具有指定id的实体. 不过, 你仍然可以使用IQueryable<TEntity>的功能通过标准LINQ方法查询实体。

自定义仓储

ABP不会强制你实现任何接口或从存储库的任何基类继承。它可以只是一个简单的POCO类。 但是建议继承现有的仓储接口和类,获得开箱即用的标准方法使你的工作更轻松。
首先在领域层定义一个仓储接口:

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

此接口扩展了 IRepository<Person, Guid> 以使用已有的通用仓储功能。
自定义存储库依赖于你使用的数据访问工具。 在此示例中, 我们将使用Entity Framework Core:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
}
}

IQueryable & 异步操作

IRepository提供GetQueryableAsync()来获取IQueryable,这意味着您可以直接在其上使用LINQ扩展方法

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

.ToList, Count()… 是在 System.Linq 命名空间下定义的扩展方法.
你通常想要使用 .ToListAsync(), .CountAsync()…. 来编写真正的异步代码.

但在你使用标准的应用程序启动模板时会发现无法在应用层或领域层使用这些异步扩展方法,因为:

  • 这里异步方法不是标准LINQ方法,它们定义在Microsoft.EntityFrameworkCoreNuget包中.

  • 标准模板应用层与领域层不引用EF Core 包以实现数据库提供程序独立.
    强烈建议使用异步方法! 在执行数据库查询时不要使用同步LINQ方法,以便能够开发可伸缩的应用程序.

    选项-1: 引用EF Core

    最简单的方法是在你想要使用异步方法的项目直接引用EF Core包.
    添加Volo.Abp.EntityFrameworkCoreNuGet包到你的项目间接引用EF Core包. 这可以确保你的应用程序其余部分兼容正确版本的EF Core.

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

    当以下情况时,这个方法是推荐的:

  • 如果你正在开发一个应用程序并且不打算在将来 更新FE Core,或者如果以后真的需要更改,你也能容忍它。我们认为,如果您正在开发最终应用程序,这是合理的。

    选项-2: 使用IRepository异步扩展方法

    ABP框架为仓储提供异步扩展方法,与异步LINQ扩展方法类似。

    1
    2
    3
    4
    5
    6
    7
    8
    var countAll = await _personRepository
    .CountAsync();

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

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

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

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

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

    选项-3: IAsyncQueryableExecuter

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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)
    {
    //Obtain the IQueryable<T>
    var queryable = await _productRepository.GetQueryableAsync();

    //Create the query
    var query = queryable
    .Where(p => p.Name.Contains(name))
    .OrderBy(p => p.Name);

    //Run the query asynchronously
    List<Product> products = await _asyncExecuter.ToListAsync(query);

    //...
    }
    }
    }

    ApplicationService 和 DomainService 基类已经预属性注入了 AsyncExecuter 属性,所以你可直接使用.

ABP框架使用实际数据库提供程序的API异步执行查询。虽然这不是执行查询的常见方式,但它是使用异步API而不依赖于数据库提供者的最佳方式。

当以下情况时,这个方法是推荐的:

  • 如果您想开发应用程序代码而不依赖数据库提供程序。
  • 如果你正在构建一个没有数据库提供程序集成包的可重用库,但是在某些情况下需要执行 IQueryable对象.
    例如,ABP框架在 CrudAppService 基类中(参阅应用程序文档)使用 IAsyncQueryableExecuter.

选项-4: 自定义仓储方法

当以下情况时,这个方法是推荐的:

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