# 仓储 "*在领域层和数据映射层之间进行中介,使用类似集合的接口来操作领域对象.*" (Martin Fowler). 实际上,仓储用于领域对象在数据库(参阅[实体](Entities.md))中的操作, 通常每个 **聚合根** 或不同的实体创建对应的仓储. ## 通用(泛型)仓储 ABP为每个聚合根或实体提供了 **默认的通用(泛型)仓储** . 你可以在服务中[注入](Dependency-Injection.md) `IRepository` 使用标准的**CRUD**操作. > 数据库提供程序层应正确配置为能够使用默认的通用存储库. 如果你已经使用启动模板创建了项目,则这些配置 **已经完成**了. 如果不是,请参考数据库提供程序文档([EF Core](Entity-Framework-Core.md) / [MongoDB](MongoDB.md))进行配置. **默认通用仓储用法示例:** ````C# 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 _personRepository; public PersonAppService(IRepository personRepository) { _personRepository = personRepository; } public async Task CreateAsync(CreatePersonDto input) { var person = new Person(input.Name); await _personRepository.InsertAsync(person); } public async Task GetCountAsync(string filter) { return await _personRepository.CountAsync(p => p.Name.Contains(filter)); } } } ```` 在这个例子中; * `PersonAppService` 在它的构造函数中注入了 `IRepository` 。 * `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`对象。你可以通过这个对象来对实体执行LINQ查询以操作数据库。 **示例: 在仓储中使用LINQ表达式** ````csharp 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 _personRepository; public PersonAppService(IRepository personRepository) { _personRepository = personRepository; } public async Task> GetListAsync(string filter) { // 获取 IQueryable IQueryable 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扩展方法: ````csharp public async Task> GetListAsync(string filter) { // 获取 IQueryable IQueryable 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 & 异步操作** 小节来学习如何做到这一点。 ### 批量操作 下面这些方法可以用来对数据库执行批量操作; * `InsertManyAsync` * `UpdateManyAsync` * `DeleteManyAsync` 这些方法可以操作多个实体,如果底层数据库提供程序支持,则可以进行批量操作。 > 使用`UpdateManyAsync`和`DeleteManyAsSync`方法时,乐观锁可能会失效。 ### 软 / 硬 删除 如果一个实体是**软删除**实体(即实现了`ISoftDelete`接口),则仓储的`DeleteSync`方法不会删除该实体,而是在数据库中标记为“已删除”。数据过滤器系统确保不会从数据库中正常检索软删除的实体。 如果您的实体是软删除实体,如果您需要物理删除这个实体,您可以使用`HardDeleteAsync`方法强制删除。 > 阅读 [数据过滤](Data-Filtering.md) 文档以了解更多关于软删除。 ### 确保实体存在 `EnsureExistsAsync`扩展方法通过实体id或实体查询表达式来确保实体存在,如果其不存在,它将抛出`EntityNotFoundException`异常。 ## 其他通用仓储类型 `IRepository` 接口扩展了标准 `IQueryable` 你可以使用标准LINQ方法自由查询。这对于大多数应用程序都很好。但是,某些ORM提供程序或数据库系统可能不支持`IQueryable`接口。如果您想使用这样的提供者,就不能依赖`IQueryable`。 ### 基础仓储 ABP提供了 `IBasicRepository` 和 `IBasicRepository` 接口来支持这样的场景. 你可以扩展这些接口(并可选择性地从`BasicRepositoryBase`派生)为你的实体创建自定义存储库. 依赖于 `IBasicRepository` 而不是依赖 `IRepository` 有一个优点, 即使它们不支持 `IQueryable` 也可以使用所有的数据源。 但主要的供应商, 像 Entity Framework, NHibernate 或 MongoDb 已经支持了 `IQueryable`. 因此, 使用 `IRepository` 是典型应用程序的 **建议方法**. 但是可重用的模块开发人员可能会考虑使用 `IBasicRepository` 来支持广泛的数据源. ### 只读仓储 对于想要使用只读仓储的开发者,我们提供了`IReadOnlyRepository` 与 `IReadOnlyBasicRepository`接口。 ### 无主键的通用(泛型)仓储 如果你的实体没有id主键 (例如, 它可能具有复合主键) 那么你不能使用上面定义的 `IRepository`, 在这种情况下你可以仅使用实体(类型)注入 `IRepository`。 > `IRepository` 有一些缺失的方法, 通常与实体的 `Id` 属性一起使用. 由于实体在这种情况下没有 `Id` 属性, 因此这些方法不可用. 比如 `Get` 方法通过id获取具有指定id的实体. 不过, 你仍然可以使用`IQueryable`的功能通过标准LINQ方法查询实体。 ### 自定义仓储 对于大多数情况, 默认通用仓储就足够了。但是, 你可能会需要为实体创建自定义仓储类。 #### 自定义仓储示例 ABP不会强制你实现任何接口或从存储库的任何基类继承。它可以只是一个简单的POCO类。 但是建议继承现有的仓储接口和类,获得开箱即用的标准方法使你的工作更轻松。 #### 自定义仓储接口 首先在领域层定义一个仓储接口: ```c# public interface IPersonRepository : IRepository { Task FindByNameAsync(string name); } ``` 此接口扩展了 `IRepository` 以使用已有的通用仓储功能。 #### 自定义仓储实现 自定义存储库依赖于你使用的数据访问工具。 在此示例中, 我们将使用Entity Framework Core: ````C# public class PersonRepository : EfCoreRepository, IPersonRepository { public PersonRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) { } public async Task FindByNameAsync(string name) { var dbContext = await GetDbContextAsync(); return await dbContext.Set() .Where(p => p.Name == name) .FirstOrDefaultAsync(); } } ```` 你可以直接使用数据库访问提供程序 (本例中是 `dbContext` ) 来执行操作. > 请参阅[EF Core](Entity-Framework-Core.md)或[MongoDb](MongoDB.md)了解如何自定义仓储. ## IQueryable & 异步操作 `IRepository`提供`GetQueryableAsync()`来获取`IQueryable`,这意味着您可以**直接在其上使用LINQ扩展方法**,如上面的“*在实体中使用LINQ”部分所示。 **示例: 使用 `Where(...)` 和 `ToList()` 扩展方法** ````csharp var queryable = await _personRepository.GetQueryableAsync(); var people = queryable .Where(p => p.Name.Contains(nameFilter)) .ToList(); ```` `.ToList`, `Count()`... 是在 `System.Linq` 命名空间下定义的扩展方法. ([参阅所有方法](https://docs.microsoft.com/en-us/dotnet/api/system.linq.queryable)). 你通常想要使用 `.ToListAsync()`, `.CountAsync()`.... 来编写**真正的异步代码**. 但在你使用标准的[应用程序启动模板](Startup-Templates/Application.md)时会发现无法在应用层或领域层使用这些异步扩展方法,因为: * 这里异步方法**不是标准LINQ方法**,它们定义在[Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore)Nuget包中. * 标准模板应用层与领域层**不引用**EF Core 包以实现数据库提供程序独立. 根据你的需求和开发模式,你可以根据以下选项使用异步方法. > 强烈建议使用异步方法! 在执行数据库查询时不要使用同步LINQ方法,以便能够开发可伸缩的应用程序. ### 选项-1: 引用EF Core **最简单的方法**是在你想要使用异步方法的项目直接引用EF Core包. > 添加[Volo.Abp.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore)NuGet包到你的项目间接引用EF Core包. 这可以确保你的应用程序其余部分兼容正确版本的EF Core. 当你添加NuGet包后,你可以使用全功能的EF Core扩展方法. **示例: 直接使用 `ToListAsync()`** ````csharp var queryable = await _personRepository.GetQueryableAsync(); var people = queryable .Where(p => p.Name.Contains(nameFilter)) .ToListAsync(); ```` 当以下情况时,这个方法是推荐的: * 如果你正在开发一个应用程序并且**不打算在将来** 更新FE Core,或者如果以后真的需要更改,你也能**容忍**它。我们认为,如果您正在开发最终应用程序,这是合理的。 #### MongoDB 如果使用的是MongoDB,则需要将[Volo.Abp.MongoDB] NuGet包添加到项目中. 但在这种情况下你也不能直接使用异步LINQ扩展(例如`ToListAsync`),因为MongoDB不提供 `IQueryable`的异步扩展方法,而是提供 `IMongoQueryable`. 你需要先将查询强制转换为 `IMongoQueryable` 才能使用异步扩展方法. **示例: 转换Cast `IQueryable` 为 `IMongoQueryable` 并且使用 `ToListAsync()`** ````csharp var queryable = await _personRepository.GetQueryableAsync(); var people = ((IMongoQueryable) queryable .Where(p => p.Name.Contains(nameFilter))) .ToListAsync(); ```` ### 选项-2: 使用IRepository异步扩展方法 ABP框架为仓储提供异步扩展方法,与异步LINQ扩展方法类似。 **示例: 在仓储中使用 `CountAsync` 和 `FirstOrDefaultAsync` 方法 ** ````csharp 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*. 这种方法仍有**局限性**。您需要直接在存储库对象上调用扩展方法。例如,以下用法**不受支持**: ```csharp var queryable = await _bookRepository.GetQueryableAsync(); var count = await queryable.Where(x => x.Name.Contains("A")).CountAsync(); ``` 这是因为本例中的`CountAsync()`方法是在`IQueryable`接口上调用的,而不是在存储库对象上调用的。请参见此类情况的其他选项。 建议 **尽可能使用此方法**. ### 选项-3: IAsyncQueryableExecuter `IAsyncQueryableExecuter` 是一个用于异步执行 `IQueryable` 对象的服务,**不依赖于实际的数据库提供程序**. **示例: 注入并使用 `IAsyncQueryableExecuter.ToListAsync()` 方法** ````csharp 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 _productRepository; private readonly IAsyncQueryableExecuter _asyncExecuter; public ProductAppService( IRepository productRepository, IAsyncQueryableExecuter asyncExecuter) { _productRepository = productRepository; _asyncExecuter = asyncExecuter; } public async Task> GetListAsync(string name) { //Obtain the IQueryable 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 products = await _asyncExecuter.ToListAsync(query); //... } } } ```` > `ApplicationService` 和 `DomainService` 基类已经预属性注入了 `AsyncExecuter` 属性,所以你可直接使用. ABP框架使用实际数据库提供程序的API异步执行查询。虽然这不是执行查询的常见方式,但它是使用异步API而不依赖于数据库提供者的最佳方式。 当以下情况时,这个方法是推荐的: * 如果您想开发应用程序代码**而不依赖**数据库提供程序。 * 如果你正在构建一个没有数据库提供程序集成包的**可重用库**,但是在某些情况下需要执行 `IQueryable`对象. 例如,ABP框架在 `CrudAppService` 基类中(参阅[应用程序](Application-Services.md)文档)使用 `IAsyncQueryableExecuter`. ### 选项-4: 自定义仓储方法 你始终可以创建自定义仓储方法并使用特定数据库提供程序的API,比如这里的异步扩展方法. 有关自定义存储库的更多信息,请参阅[EF Core](Entity-Framework-Core.md)或[MongoDb](MongoDB.md)文档. 当以下情况时,这个方法是推荐的: * 如果你想**完全隔离**你的领域和应用层和数据库提供程序. * 如果你开发可**重用的[应用模块](Modules/Index.md)**,并且不想强制使用特定的数据库提供程序,这应该作为一种[最佳实践](Best-Practices/Index.md).