ABP官方文档翻译 3.3 仓储
仓储
协调领域和数据映射层,使用类集合接口访问领域对象。"(Martin Fowler)
实际上,仓储用来执行领域对象的数据库操作(实体和值类型)。通常,每个对象(或聚合根)使用单独的仓储。
在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)类型主键的实体。如果实体的主键不是int类型,可以继承IRepository<TEntity,TPrimaryKey>接口,如下所示:
public interface IPersonRepository : IRepository<Person, long> { }
ABP设计为独立于特定的ORM(对象/关系映射)框架或其他访问数据库的技术。在NHibernate和EntityFramework实现的仓储都是开箱即用的。可参见这些框架的相关文档:
每个仓储类有些来自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。所以,使用Single可以写一个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编写。即使它被用在连接表达式。
当调用仓储的GetAll()方法时,必须有一个打开的数据库连接。这是因为IQueryable<T>是延迟执行的。在调用ToList()方法或在foreach循环(或访问查询项时)里使用IQueryable<T>之前,它不会执行数据库查询。所以,当调用ToList()方法时,数据库连接必须是可用的。对于Web应用,不用关心数据库连接的问题,因为MVC控制器方法默认为一个工作单元,数据库连接在整个请求期间都是可用的。参见UnitOfWork文档了解更多。
有一个额外的方法,可以使IQueryable在工作单元外使用。
T Query<T>(Func<IQueryable<TEntity>, T> queryMethod);
Query方法接收一个接收IQeryable<T>的lambda表达式(或方法)并返回对象的任何类型。
var people = _personRepository.Query(q => q.Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).ToList());
因为给定的lambda(或方法)在仓储方法内执行,当数据库连接可用时执行。可以执行这个查询返回实体列表、单个实体、一个投射或其他的东西。
IRepository接口定义了insert方法插入实体到数据库:
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是自增的且需要新插入实体的Id的时候这将是非常有用的。InsertOrUpdate方法通过检查id值插入或更新给定的实体。最后,InsertOrUpdateAndGetId返回插入或更新后实体的Id。
IRepository定义了更新数据库中已存在实体的方法。它获取需要更新的实体并返回这个实体对象。
TEntity Update(TEntity entity); Task<TEntity> UpdateAsync(TEntity entity);
大多数时候不需要显示的调用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是支持的。如果不支持,异步仓储方法将按同步方式执行。例如,在EF中InsertAsync与Insert方法执行方式相同,因为EF只有直到工作单元完成时才会写入新实体到数据库(a.k.a DbContext.SaveChanges)。
数据库连接不会在仓储方法中打开或关闭。连接管理由ABP自动管理。
当进入仓储方法时,数据库连接自动打开并开始一个事务。当方法结束并返回时,所有的更改被保存,事务提交、关闭数据库连接,这些由ABP自动完成。如果仓储方法抛出任何类型的异常,事务自动回滚并关闭数据库连接。所有实现IRepository接口类的公共方法都是这样的。
如果一个仓储方法调用另一个仓储方法(甚至是不同仓储的方法),这些方法将共享同样的连接和事务。数据库连接由进入仓储的第一个方法管理(打开或关闭)。关于数据库连接管理的更多信息,参见工作单元文档。
所有的仓储接口都是临时的。意味着,每次使用都会实例化。参见依赖注入文档了解更多信息。
- 对于泛型T的实体,尽可能使用IRepository<T>接口。除非真的需要不要创建自定义仓储。预定义的仓储方法足够满足大多数场景。
- 如果创建了一个自定义仓储(通过扩展IRepository<TEntity>接口实现):
- 仓储类应该是无状态的。意味着,不应该定义仓储级别状态的对象,并且一个仓储方法的调用不能影响另一个仓储方法的调用。
- 自定义仓储方法不应该包含业务逻辑或应用逻辑。它应该仅仅执行数据相关或orm特定的任务。
- 当仓储可以使用依赖注入时,尽量少或不依赖于其他服务。