本篇目錄
Martin Fowler對倉儲的定義
位於領域層和數據映射層之間,使用類似集合的接口來訪問領域對象。
在實踐中,倉儲是執行領域對象(實體和值對象)的數據庫操作。一般地,一個分離的倉儲用於一個實體(或者聚合根)。
IRepository接口###
在ABP中,一個倉儲類應該實現一個IRepository接口。為每一個倉儲定義一個接口是一個好的做法。
一個Person實體的倉儲定義如下:
public interface IPersonRepository : IRepository<Person>
{
}
IPersonRepository擴展了IRepository
public interface IPersonRepository : IRepository<Person, long>
{
}
IRepository為倉儲類定義了最通用的方法,如select,insert,update和delete方法(CRUD操作)。大多數情況下,這些方法對於簡單的實體是足夠了。如果這些方法對於一個實體來說已經足夠了,那么就沒有必要為這個實體創建倉儲接口和倉儲類了。看下面。
查詢
IRepository定義了通用的方法,從數據庫中檢索實體。
獲得單個實體
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)的實體。如果在數據庫中沒有找到這個實體,就會拋出異常。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屬性,那么Entity實際上並沒有檢索到。只有你訪問實體的其他屬性,才會從數據庫中檢索。考慮到性能因素,這個就可以替換Get方法。這在NHiberbate中也實現了。如果ORM提供者沒有實現它,那么Load方法會和Get方法一樣地工作。
一些方法有用於async編程模型的異步(async)版本。
獲得實體的列表
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
//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
的延遲執行。直到調用ToList()方法或者在foreach循環中使用IQueryable (或者訪問查詢到的元素)時,才會執行數據庫查詢操作。因此,當調用ToList()方法時。數據庫連接必須打開。這可以通過ABP中的 UnitOfWork特性標記調用者方法來實現。注意:應用服務方法默認已經是UnitOfWork,因此,即使沒有為應用服務層方法添加UnitOfWork特性,GetAll()方法也會正常工作。
這些方法也存在用於異步編程模型的asyn版本。
自定義返回值
也存在提供了IQueryable的額外方法,在調用的方法中不需要使用UnitOfWork。
T Query<T>(Func<IQueryable<TEntity>, T> queryMethod);
Query方法接受一個接收IQueryable
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是自動增長的並且需要最新插入實體的Id,那么該方法很有用。InsertOrUpdate方法通過檢查Id的值插入或更新給定的實體。最后,當插入或者更新之后,InsertOrUpdateAndGetId返回該實體的值。
所有的方法都存在用於異步編程模型的async版本。
更新
IRepository定義了一個方法來更新數據庫中已存在的實體。它可以獲得要更新的實體並返回相同的實體對象。
TEntity Update(TEntity entity);
Task<TEntity> UpdateAsync(TEntity entity);
刪除
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支持異步編程模型(APM)。因此,倉儲方法有異步版本。下面是一個使用了異步模型的應用服務方法樣例:
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框架都支持Async,但是EntityFramework支持。如果不支持,異步倉儲方法就會同步進行。比如,在EF中,InsertAsync和Insert是等效的,因為直到工作單元完成(Dbcontext.SaveChanges),EF才會將新的實體寫入數據庫。
倉儲實現###
ABP的設計獨立於一個特定的ORM(對象/關系映射)框架或者訪問數據庫的其他技術。通過實現倉儲接口,可以使用任何框架。
ABP使用NHibernate和 EntityFramework實現了開箱即用的倉儲。關於這兩個ORM框架可以關注后面的文檔。
當使用NHibernate或EntityFramework時,如果標准方法是足夠使用的話,那么不必為實體類創建倉儲了。你可以直接注入IRepository
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
管理數據庫連接###
在倉儲方法中,數據庫連接是沒有打開的或是關閉的。ABP對於數據庫連接的管理是自動處理的。
當將要進入一個倉儲方法時,數據庫連接會自動打開,並且事務自動開始。當倉儲方法結束並返回的時候,ABP會自動完成:保存所有的更改,完成事務的提交和關閉數據庫連接。如果倉儲方法拋出任何類型的異常,那么事務會自動回滾並關閉數據庫。這對於所有的實現了IRepository接口的類的公共方法都是成立的。
如果一個倉儲方法調用了其他的倉儲方法,那么它們會共享相同的連接和事務。進入倉儲的第一個方法會管理數據庫的連接。更多信息,請留意后面博客的工作單元。
一篇不錯的數據庫連接博客:細說數據庫連接
倉儲的生命周期###
所有的倉儲實例都是Transient(每次使用時都會實例化)的。ABP強烈推薦使用依賴注入技術。當一個倉儲類需要注入時,依賴注入的容器會自動創建該類的新實例。
倉儲最佳實踐###
- 對於一個T類型的實體,使用IRepository
倉儲接口。除非真的需要,否則不要創建自定義的倉儲。預定義的倉儲方法對於很多情況足夠用了。 - 如果你正在創建一個自定義的倉儲(通過擴展IRepository
): - 倉儲類應該是無狀態的。這意味着,你不應該定義倉儲級別的狀態對象,而且一個倉儲方法調用不應該影響其他的調用。
- 自定義倉儲方法不應該包含業務邏輯或者應用邏輯,而應該只執行數據相關的或者orm特定的任務。
- 當倉儲使用依賴注入時,給其他服務定義更少的或者不要定義依賴。