ABP官方文件(十七)【倉儲】
3.3 ABP領域層 - 倉儲
倉儲定義:“在領域層和資料對映層的中介,使用類似集合的介面來存取領域物件”(Martin Fowler)。
實際上,倉儲被用於領域物件在資料庫上的操作(實體Entity和值物件Value types)。一般來說,我們針對不同的實體(或聚合根Aggregate Root)會建立相對應的倉儲。
3.3.1 IRepository介面
在ABP中,倉儲類要實現IRepository介面。最好的方式是針對不同倉儲物件定義各自不同的介面。
針對Person實體的倉儲介面宣告的示例如下所示:
public interface IPersonRepository : IRepository<Person> { }
IPersonRepository繼承自IRepository<TEntity>
,用來定義Id的型別為int(Int32)的實體。如果你的實體Id資料型別不是int,你可以繼承IRepository<TEntity, TPrimaryKey>
介面,如下所示:
public interface IPersonRepository : IRepository<Person, long> { }
對於倉儲類,IRepository定義了許多泛型的方法。比如: Select,Insert,Update,Delete方法(CRUD操作)。在大多數的時候,這些方法已足已應付一般實體的需要。如果這些方對於實體來說已足夠,我們便不需要再去建立這個實體所需的倉儲介面/類。在Implementation章節有更多細節。
1. 查詢(Query)
IRepository定義了從資料庫中檢索實體的常用方法。
取得單一實體(Getting single entity)
TEntity Get(TPrimaryKey id);
Task<TEntity> GetAsync(TPrimaryKey id);
TEntity Single(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 => o.Name == "Halil ibrahim Kalkan");
注意,Single方法會在給出的條件找不到實體或符合的實體超過一個以上時,都會丟擲異常。
FirstOrDefault也一樣,但是當沒有符合Lambda表示式或Id的實體時,會返回null(取代丟擲異常)。當有超過一個以上的實體符合條件,它只會返回第一個實體。
Load並不會從資料庫中檢索實體,但它會建立延遲執行所需的代理物件。如果你只使用Id屬性,實際上並不會檢索實體,它只有在你存取想要查詢實體的某個屬性時才會從資料庫中查詢實體。當有效能需求的時候,這個方法可以用來替代Get方法。Load方法在NHibernate與ABP的整合中也有實現。如果ORM提供者(Provider)沒有實現這個方法,Load方法執行的會和Get方法一樣。
ABP有些方法具有非同步(Async)版本,可以應用在非同步開發模型上(見Async方法相關章節)。
取得實體列表(Getting list of entities)
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 = _personRespository.GetAllList();
var somePeople = _personRepository.GetAllList(person => person.IsActive && person.Age > 42);
GetAll返回IQueryable<T>
型別的物件。因此我們可以在呼叫完這個方法之後進行Linq操作。示例:
var query = from person in _personRepository.GetAll()
where person.IsActive
orderby person.Name
select person;
var people = query.ToList();
List<Person> personList2 = _personRepository.GetAll().Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).Skip(40).Take(20).ToList();
如果呼叫GetAll方法,那麼幾乎所有查詢都可以使用Linq完成。甚至可以用它來編寫Join表示式。
說明:關於
IQueryable<T>
當你呼叫GetAll這個方法在Repository物件以外的地方,必定會開啟資料庫連線。這是因為IQueryable<T>
允許延遲執行。它會直到你呼叫ToList方法或在forEach迴圈上(或是一些存取已查詢的物件方法)使用IQueryable<T>
時,才會實際執行資料庫的查詢。因此,當你呼叫ToList方法時,資料庫連線必需是啟用狀態。我們可以使用ABP所提供的UnitOfWork特性在呼叫的方法上來實現。注意,Application Service方法預設都已經是UnitOfWork。因此,使用了GetAll方法就不需要如同Application Service的方法上新增UnitOfWork特性。
有些方法擁有非同步版本,可應用在非同步開發模型(見關於async方法章節)。
自定義返回值(Custom return value)
ABP也有一個額外的方法來實現IQueryable<T>
的延遲載入效果,而不需要在呼叫的方法上新增UnitOfWork這個屬性卷標。
T Query<T>(Func<IQueryable<Tentity>,T> queryMethod);
查詢方法接受Lambda(或一個方法)來接收IQueryable<T>
並且返回任何物件型別。示例如下:
var people = _personRepository.Query(q => q.Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).ToList());
因為是採用Lambda(或方法)在倉儲物件的方法中執行,它會在資料庫連線開啟之後才被執行。你可以返回實體集合,或一個實體,或一個具部份欄位(注: 非Select *)或其它執行查詢後的查詢結果集。
2. 新增(insert)
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);
新增方法會新增實體到資料庫並且返回相同的已新增實體。InsertAndGetId方法返回新增實體的識別符號(Id)。當我們採用自動遞增識別符號值且需要取得實體的新產生識別符號值時非常好用。InsertOfUpdate會新增或更新實體,選擇那一種是根據Id是否有值來決定。最後,InsertOrUpdatedAndGetId會在實體被新增或更新後返回Id值。
所有的方法都擁有非同步版本可應用在非同步開發模型(見關於非同步方法章節)
3. 更新(UPDATE)
IRepository定義一個方法來實現更新一個已存在於資料庫中的實體。它更新實體並返回相同的實體物件。
TEntity Update(TEntity entity);
Task<TEntity> UpdateAsync(TEntity entity);
4. 刪除(Delete)
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。
最後一個方法接受一個條件來刪除符合條件的實體。要注意,所有符合predicate表示式的實體會先被檢索而後刪除。因此,使用上要很小心,這是有可能造成許多問題,假如果有太多實體符合條件。
所有的方法都擁有async版本來應用在非同步開發模型(見關於非同步方法章節)。
5. 其它方法(others)
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<TEntity, bool>> predicate);
所有的方法都擁有async版本被應用在非同步開發模型(見關於非同步方法章節)。
6. 關於非同步方法(About Async methods)
ABP支援非同步開發模型。因此,倉儲方法擁有Async版本。在這裡有一個使用非同步模型的application service方法的示例:
public class PersonAppService : AbpWpfDemoAppServiceBase, IPersonAppService
{
private readonly IRepository<Person> _personRespository;
public PersonAppService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
}
public async Task<GetPersonOutput> GetAllPeople()
{
var people = await _personRepository.GetAllListAsync();
return new GetPeopleOutput {
People = Mapper.Map<List<PersonDto>>(people);
};
}
}
GetAllPeople方法是非同步的並且使用GetAllListAsync與await
關鍵字。Async
不是在每個ORM框架都有提供。上例是從EF所提供的非同步能力。如果ORM框架沒有提供Async的倉儲方法那麼它會以同步的方式操作。同樣地,舉例來說,InsertAsync
操作起來和EF的新增是一樣的,因為EF會直到單元作業(unit of work)完成之後才會寫入新實體到資料庫中(DbContext.SaveChanges
)。
3.3.2 倉儲的實現
ABP在設計上是採取不指定特定ORM框架或其它存取資料庫技術的方式。只要實現IRepository介面,任何框架都可以使用。
倉儲要使用NHibernate或EF來實現都很簡單。見實現這些框架在ABP倉儲物件上一文:
NHibernate
EntityFramework
當你使用NHibernate或EntityFramework,如果提供的方法已足夠使用,你就不需要為你的實體建立倉儲物件了。我們可以直接注入IRepositoryTEntity>
(或IRepository<TEntity, TPrimaryKey>
)。下面的示例為application service使用倉儲物件來新增實體到資料庫:
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方法。當你有需要為實體建立一個定製的倉儲方法,那麼你就應該建立一個倉儲類給指定的實體。
3.3.3 管理資料庫連線
資料庫連線的開啟和關閉,在倉儲方法中,ABP會自動化的進行連線管理。
當倉儲方法被呼叫後,資料庫連線會自動開啟且啟動事務。當倉儲方法執行結束並且返回以後,所有的實體變化都會被儲存, 事務被提交併且資料庫連線被關閉,一切都由ABP自動化的控制。如果倉儲方法丟擲任何型別的異常,事務會自動地回滾並且資料連線會被關閉。上述所有操作在實現了IRepository
介面的倉儲類所有公開的方法中都可以被呼叫。
如果倉儲方法呼叫其它倉儲方法(即便是不同倉儲的方法),它們共享同一個連線和事務。連線會由倉儲方法呼叫鏈最上層的那個倉儲方法所管理。更多關於資料庫管理,詳見UnitOfWork
檔案。
3.3.4 倉儲的生命週期
所有的倉儲物件都是暫時性的。這就是說,它們是在有需要的時候才會被建立。ABP大量的使用依賴注入,當倉儲類需要被注入的時候,新的類實體會由注入容器會自動地建立。見相依賴注入檔案有更多資訊。
3.3.5 倉儲的最佳實踐
對於一個T型別的實體,是可以使用
IRepository<T>
。但別任何情況下都建立定製化的倉儲,除非我們真的很需要。預定義倉儲方法已經足夠應付各種案例。假如你正建立定製的倉儲(可以實現
IRepository<TEntity>
)倉儲類應該是無狀態的。這意味著, 你不該定義倉儲等級的狀態物件並且倉儲方法的呼叫也不應該影響到其它呼叫。
當倉儲可以使用相依賴注入,儘可較少或是不相根據於其它服務。