首页 > 代码库 > ABP官方文档翻译 3.3 仓储

ABP官方文档翻译 3.3 仓储

 仓储

  • 默认仓储
  • 自定义仓储
    • 自定义仓储接口
    • 自定义仓储实现
  • 基础仓储方法管理数据库连接
    • 查询
      • 获取单个实体
      • 获取实体列表
    • 关于IQueryable
      • 自定义返回值
    • 插入
    • 更新
    • 删除
    • 其他
    • 关于异步方法
  • 管理数据库连接
  • 仓储生命周期
  • 仓储最佳实践

  协调领域和数据映射层,使用类集合接口访问领域对象。"(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实现的仓储都是开箱即用的。可参见这些框架的相关文档:

  • 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。所以,使用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编写。即使它被用在连接表达式。

关于IQueryable

  当调用仓储的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特定的任务。
    • 当仓储可以使用依赖注入时,尽量少或不依赖于其他服务。

返回主目录

ABP官方文档翻译 3.3 仓储