本篇主要講如何使用一句較復雜的SQL來加載整個聚合對象,以達到最小化數據庫連接次數。主要是解釋其中的原理。
LazyLoad及其缺點
相信越來越多的人已經開始使用富領域對象進行領域/業務層的實現了。而目前主流的數據庫依然還是關系型的。這中間的轉換,我們 叫它ORM。ORM的設計中,有一個常用的模式叫作“延遲加載(LazyLoad)”。基設計思想大致上是說,不要把所有的數據都加載進內存, 而是等到真正要使用數據的時候,再把它加載進內存。
例如以下這個聚合對象:
(為了和後面的代碼保持一致,這裡面使用的是GIX4項目中真實的類,可能會帶有一些領域特性,望讀者見諒。後面可能會繼續使用此 例,現大致對其進行解釋:其中,PBSType表示一套PBS模板/類型,一套模板由許多PBS組成。PBS是Project Breakdown Structure的簡稱 ,用於對某一個項目進行分解,這裡面一個PBS對象的實例其實只是結構中的一項,應該在後面加上Item,不過公司的人都習慣了,所以就 延用這個命名。每個PBS有許多屬性(PBSProperty),每個屬性又有許多可選值(PBSPropertyOptionalValue)。)
這個對象,在使用了LazyLoad對PBSType進行設計之後,客戶程序使用代碼如下:
var type = PBSType.Get(id);
//do something
//...
//lazily load a pbs list. data access occurs.
PBSList pbsList = type.PBSs;
//read from memory
var pbsListCount = type.PBSs.Count;
這裡一共產生了兩次數據訪問:獲取PBSType對象、獲取所有在該PBS模板下的PBS對象列表。此例說明了對集合對象使用LazyLoad,還 有一種比較常用的LazyLoad:對引用對象的LazyLoad。如下例:
文章對象引用一個用戶對象來表示其作者。這個外鍵引用的關系,常常也被設計為LazyLoad。
這一模式已經被廣泛地應用在各種ORM框架中,Linq to sql、EF等。這些ORM框架極大的方便了開發者,不需要再寫煩人的SQL,加快了 開發效率。但是如果不謹慎使用這一模式,很可能會造成過多的數據庫連接次數,導致性能低下。如果是分布式程序,則會是更耗時的遠 程連接。如:
IList<Article> articles = ArticleRepository.Get(new PagerInfo()
{
PageIndex = 1,
PageSize = 10
});
foreach (var article in articles)
{
//LazyLoad
User owner = article.Owner;
}
這段代碼中一共產生了 11 次數據訪問/遠程連接,相當的恐怖吧!
如何能保證又能降低連接次數,又不使用傳統的Table方案呢?這就是今天要說的,一個用於重構的模式:聚合對象SQL。
什麼是“聚合SQL”
要支持OO的領域對象,同時保證性能,我們的ORM就需要做到:獲取對象時,一次性獲取它指定的關系對象(集合/引用);同時,仍然 保留LazyLoad。
例如,當我們加載上述的Article及User時,可以調用類似ArticleRepository.Get_With_User的方法,使得一次性加載Article及其對 應的User。那麼,數據層訪問數據庫時,對應的SQL應該是把所有的數據都查詢出來,大致是:
select a.*, u.*
from Articles a inner join Users u on a.UserId = u.Id
然後在把整個Table映射為Article對象列表的過程中,在每一行中讀取並映射出User對象,然後對該行的Article對象的Owner屬性賦值 。
對應的,集合對象的一次性加載,要完成對數據的一次性加載,生成類似以下的SQL:
select * from PBSType t
left outer join PBS on t.Id = PBS.PBSTypeId
在應用中,當然不會那麼簡單,不過都是由以上兩種方式組合而成。如,在GIX4的項目PBS模塊中使用到這樣的一個SQL,其中關於SQL 的生成及格式定義,接下來我將會做更詳細的解釋:
private static readonly string SQL_GET_BY_PROJECT_WITH_PROPERTY_VALUES = string.Format(@"
select
{0},
{1},
{2},
{3}
from ProjectPBS pp
left outer join ProjectPBSPropertyValue v on pp.Id = v.ProjectPBSId
left outer join PBSProperty p on v.PBSPropertyID = p.Id
left outer join PBSPropertyOptionalValue ov on p.Id = ov.PBSPropertyId
where pp.ProjectId = '{{0}}'
order by pp.Id, v.Id, p.Id
", ProjectPBS.GetReadableColumnsSql("pp"),
ProjectPBSPropertyValue.GetReadableColumnsSql("v"),
PBSProperty.GetReadableColumnsSql("p"),
PBSPropertyOptionalValue.GetReadableColumnsSql("ov"));
今天先把理論寫一下。下一節主要講在目前的GIX4系統中,我們是如何引入聚合SQL來改善性能的。