本节内容:
- 默认仓储
- 自定义仓储
- 自定义仓储接口
- 自定义仓储实现
- 基仓储方法
- 查询
- 获取单个实体
- 获取实体列表
- 关于 IQueryable
- 自定义返回值
- 插入
- 更新
- 删除
- 其它
- 关于异步方法
- 查询
- 管理数据库连接
- 一个仓储的生命周期
- 仓储最佳实践
领域和映射层之间的媒介使用一种类似集合的接口来访问实体。通常地,每个实体(或聚合根)使用一个分离的仓储。
默认仓储
在ABP里,一个仓储类实现IRepository<TEntity,TPrimaryKey>接口。ABP默认地为每个实体类型自动创建一个默认仓储。你可以直接注入IRepository<TEntity>(或IRepository<TEntity,TPrimaryKey>)。一个应用服务使用仓储把一个实体插入数据库的例子:
public class PersonAppService : IPersonAppService { private readonly IRepository<Person> _personRepository; public PersonAppService(IRepository<Person> personRepository) { _personRepository = personRepository; } public void CreatePerson(CreatePersonInput input) { person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; _personRepository.Insert(person); } }
PersonAppService构造器注入IRepository<Person>并使用Insert方法。
自定义仓储
只有当实体需要创建一个自定义的仓储方法时,才需要你创建一个仓储类。
自定义仓储接口
如下示例,为一个Person实体定义一个仓储:
public interface IPersonRepository : IRepository<Person> { }
IPersonRepository扩展了IRepository<TEntity>,它用来定义具有int(Int32)类型Id属性的实体。如果你的实体键不是int,你可以扩展IRepository<TEntity,TPrimaryKey>接口,如下所示:
public interface IPersonRepository : IRepository<Person, long> { }
自定义仓储实现
ABP设计成与ORM(对象/关系映射)框架分离、或其它访问数据库技术分离。仓储开箱即用地实现了NHibernate和EntityFramework。查看ABP对这些框架的实现的相关文档:
- NHibernate integration
- EntityFramework integration
基仓储方法
每个仓储包含一些通用的来自IRepository<TEntity>接口的方法,我们在此把它们的大部分方法,研究一下。
查询
获取一个单独实体
TEntity Get(TPrimaryKey id); Task<TEntity> GetAsync(TPrimaryKey id); TEntity Single(Expression<Func<TEntity, bool>> predicate); Task<TEntity> SingleAsync(Expression<Func<TEntity, bool>> predicate); TEntity FirstOrDefault(TPrimaryKey id); Task<TEntity> FirstOrDefaultAsync(TPrimaryKey id); TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate); Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate); TEntity Load(TPrimaryKey id);
Get方法用来获取一个给定主键(Id)的实体。如果无法从数据库中找到给定Id的实体,将抛出异常。Single方法类似于Get方法,但接受一个表达式,而不是一个Id,所以你可以写一个lambda表达式来获取一个实体,用法示例:
var person = _personRepository.Get(42); var person = _personRepository.Single(p => p.Name == "Halil ?brahim Kalkan");
注意:Single会在无法获取符合表达式的实体,或是有多个符合表达式的实体时,抛出异常。
FirstOrDefault类似,但在找不到给定Id的实体时,返回null(代替抛出异常)。如果找到多个实体,则返回第一个。
Load不从数据库获取实体,但为延迟加载创建一个代理对象。如果你只是使用Id属性,那么实质上,不会从数据库中获取实体,只有当你访问实体的其它属性时,它才从数据库中获取实体。出于性能考虑,这个方法用来代替Get。它已经在NHibernate中实现了。如果ORM供应器没有实现它,Load方法就跟Get方法是一样的。
获取一个实体列表
List<TEntity> GetAllList(); Task<List<TEntity>> GetAllListAsync(); List<TEntity> GetAllList(Expression<Func<TEntity, bool>> predicate); Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate); IQueryable<TEntity> GetAll();
GetAllList用来获取数据库中的所有实体。它的重载可以过滤实体,例如:
var allPeople = _personRepository.GetAllList(); var somePeople = _personRepository.GetAllList(person => person.IsActive && person.Age > 42);
GetAll返回IQueryable<T>,所以你在这个方法后可以添加Linq方法,例如:
//Example 1 var query = from person in _personRepository.GetAll() where person.IsActive orderby person.Name select person; var people = query.ToList(); //Example 2: List<Person> personList2 = _personRepository.GetAll().Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).Skip(40).Take(20).ToList();
通过实例GetAll,几乎可以把写所有查询写在Linq里,甚至它可以用在一个json表达式里。
关于 IQueryable<T>
当你在一个仓储方法之外调用GetAll(),必须有一个打开的数据库连接,这是因为IQueryable<T>是延迟执行的。它不会执行数据库的查询,除非你调用ToList()方法或在一个foreach循环(或其它方式访问查询里的项)。所以,当你调用ToList()方法时,数据库连接必须可用。对于一个Web项目,在部分情况你不必关心这个,因为Mvc控制器方法默认都是工作单元,且数据库连接在整个请求里都是可用的。为更好地理解它,请查看工作单元文档。
自定义返回值
还有一个另外的方法提供更强大的IQueryable,可以用在工作单元之外。
T Query<T>(Func<IQueryable<TEntity>, T> queryMethod);
Query方法接受一个lambda表达式(或方法),该表达式(或方法)接收IQueryable<T>并返回任何类型的对象。例如:
var people = _personRepository.Query(q => q.Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).ToList());
由于给定的lambda(或方法)在仓储方法在执行,当数据库连接可用时,它被执行。你可以在执行查询后,返回实体列表、单个实体、一个投射或其它。
插入
IRepository接口定义了把实体插入数据库的方法:
TEntity Insert(TEntity entity); Task<TEntity> InsertAsync(TEntity entity); TPrimaryKey InsertAndGetId(TEntity entity); Task<TPrimaryKey> InsertAndGetIdAsync(TEntity entity); TEntity InsertOrUpdate(TEntity entity); Task<TEntity> InsertOrUpdateAsync(TEntity entity); TPrimaryKey InsertOrUpdateAndGetId(TEntity entity); Task<TPrimaryKey> InsertOrUpdateAndGetIdAsync(TEntity entity);
Insert方法简单地把一个新实体插入到数据库,并返回此插入的实体。InsertAndGetId方法为新插入的实体返回Id,当Id是自增时非常有用。InsertOrUpdate根据Id值执行插入或更新操作。最后,InsertOrUpdateAndGetId在插入或更新实体后,返回它的Id值。
更新
IRepository定义了更新一个已存在于数据库的实体的方法,它获取一个需要更新的实体,返回相同的实体。
TEntity Update(TEntity entity); Task<TEntity> UpdateAsync(TEntity entity);
大部分时间,你不需要显式地调用Update方法,因为工作单元会在完成时调用Update方法。见工作单元文档获取更多信息。
删除
IRepository定义了从数据库删除一个已存在的实体的方法。
void Delete(TEntity entity); Task DeleteAsync(TEntity entity); void Delete(TPrimaryKey id); Task DeleteAsync(TPrimaryKey id); void Delete(Expression<Func<TEntity, bool>> predicate); Task DeleteAsync(Expression<Func<TEntity, bool>> predicate);
第一个方法接受一个已存在的实体,第二个接受要删除实体的Id。最后一个根据给定条件删除所有符合的实体,注意:所有匹配谓词的实体可能会从数据库中先获取到内存,然后再删除(看仓储如何实现了),所以使用它要小心了,这在有大量符合条件的实体时,可能引起性能问题。
其它
IRepository同时也提供了获取一个表的实体数量的方法
int Count(); Task<int> CountAsync(); int Count(Expression<Func<TEntity, bool>> predicate); Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate); long LongCount(); Task<long> LongCountAsync(); long LongCount(Expression<Func<TEntity, bool>> predicate); Task<long> LongCountAsync(Expression<Func<TEntity, bool>> predicate);
关于异步方法
ABP支持异常编程模式,所以仓储方法有异步版本。如下例子为一个应用服务方法使用异常模式:
public class PersonAppService : AbpWpfDemoAppServiceBase, IPersonAppService { private readonly IRepository<Person> _personRepository; public PersonAppService(IRepository<Person> personRepository) { _personRepository = personRepository; } public async Task<GetPeopleOutput> GetAllPeople() { var people = await _personRepository.GetAllListAsync(); return new GetPeopleOutput { People = Mapper.Map<List<PersonDto>>(people) }; } }
GetAllPeople方法是一个异步方式,并使用关键字await调用GetAllListAsync。
可能不是所有的ORM框架都支持异步。EntityFramework支持。如果不支持,异步方法以同步的方式工作。同样的,例如,在EntityFramework中InsertAsync和Insert工作方式相同,因为EF直到工作单元完成前(也就是DbContext.SaveChanges),代码不写入新的实体。
管理数据库连接
在一个仓储方法里,它不打开或关闭数据库连接,ABP自动管理数据库连接。
当进入一个仓储方法,ABP自动打开一个数据库连接并开始一个事务,当这个方法结束并返回时,所有的变化被保存,事务提交后关闭数据库连接。如果你的仓储方法抛出任何类型的异常,自动回滚事务并关闭数据库连接。这适用于所有实现IRepository接口的类的公开方法。
如果一个仓储方法调用另一个仓储方法(即使是一个不同仓储的方法),它们共享相同的连接和事务,第一个方法管理数据库的连接(打开/关闭)。获取更多的数据库连接管理信息,请查阅工作单元文档。
一个仓储的生命周期
所有仓储实例都是短暂的,它的意思是:它们为每次的使用都进行实例化。查阅依赖注入文档获取更多信息。
仓储最佳实践
- 为一个T类型的实体,尽可能地使用IRepository<T>。不要创建自定义的仓储,除非确实需要。预定义的仓储方法可满足大部分情况。
- 如果你正在创建一个自定义仓储(通过扩展IRepository〈TEntity>):
- 仓储类应该没有状态,也就是说:你不应该定义一个仓储级别状态的对象且一个仓储方法的调用不应该影响另一个调用。
- 自定义仓储方法不应该包含业务逻辑或应用逻辑。它应该只是执行数据相关或ORM相关的任务。
- 虽然仓储可以使用依赖注入,但尽可能少或不定义对其它服务的依赖。