java orm 開源架構
重要要點
- Reladomo是由高盛開發的企業級Java ORM,并于2016年作為開源項目釋出。
- Reladomo提供了許多獨特而有趣的功能,例如強類型查詢語言,分片,時間支援,實際可測試性和高性能緩存。
- Reladomo是一個自以為是的架構,基于指導其發展的一系列核心價值觀。
- 本文中的示例說明了Reladomo的可用性和可程式設計性。
早在2004年,我們就面臨着艱巨的挑戰。 我們需要一種方法來抽象出Java應用程式中的資料庫互動,而該互動不适合任何現有架構。 該應用程式具有以下正常解決方案之外的需求:
- 資料被高度分割。 有100多個具有相同模式但資料不同的資料庫。
- 資料是基于時間的(我們将在本文的第2部分中對此進行解釋,敬請期待!)。
- 針對資料的查詢不一定是靜态的,某些查詢必須從一組複雜的使用者輸入中動态建立。
- 資料模型相當複雜-數百張表。
我們從2004年開始Reladomo的開發。那年晚些時候進行了首次生産部署,此後一直定期釋出。 在随後的幾年中,Reladomo已被高盛(Goldman Sachs)廣泛采用,并且使用它的應用程式指導了我們添加的主要新功能。 現在,它已用于多種分類帳,中層辦公室貿易處理,資産負債表處理以及許多其他應用程式。 高盛(Goldman Sachs)在2016年根據Apache 2.0許可釋出了Reladomo(關系域對象的縮寫)作為開源項目。
為什麼要建立另一個ORM?
很簡單,現有解決方案無法滿足我們的核心要求,而傳統的ORM存在需要解決的問題。
我們決定消除代碼級樣闆和痕迹結構。 在Reladomo中,沒有用于擷取,關閉,洩漏或沖洗的連接配接。 沒有會議。 沒有EntityManager。 沒有LazyInitializationException。 API以兩種基本方式提供:在域對象本身上,并通過強類型List實作高度增強。
Reladomo查詢語言對我們來說是另一個關鍵點。 基于字元串的語言不适合我們的應用程式,也通常不适合面向對象的代碼。 除了最瑣碎的查詢之外,将字元串連接配接在一起以形成動态查詢是行不通的。 基于字元串連接配接來維護這些動态查詢是一項令人沮喪的工作。
分片是我們需要完整的本機支援的另一個領域。 Reladomo中的分片非常靈活,可以處理出現在不同分片中,指向不同對象的相同主鍵值。 分片查詢文法自然适合查詢語言。
時态(單時态和雙時态)支援可以幫助資料庫設計人員記錄和推理有關時态的資訊,Richard Snodgrass在他的書《用SQL開發面向時間的資料庫應用程式》中描述的是Reladomo的真正獨特功能。 它适用于許多地方,從各種會計系統到參考資料,到需要完全可重複性的任何地方。 即使是簡單的應用程式(例如項目協作工具),也可以從單時間表示中受益,使使用者界面可以像時光機一樣工作,并顯示事物的變化。
真正的可測試性在所有要做的事情上都很高,我們很早就決定,做到這一點的唯一方法就是自己做飯:絕大多數Reladomo測試都是使用Reladomo自己的測試實用程式編寫的! 我們有務實的測試觀點。 我們希望測試可以增加長期價值。 Reladomo測試易于設定,并且可以針對記憶體資料庫執行所有生産代碼,進而可以進行連續內建測試。 這些測試可幫助開發人員了解與資料庫的互動,而無需使用已安裝的資料庫配置開發環境。
最後,我們不想在性能上有所妥協。 Reladomo最重要,技術上最複雜的部分之一是它的緩存。 這是一個無鍵,多索引,事務性對象緩存。 将對象作為對象緩存,并確定其資料占用單個記憶體引用。 對象緩存由引用相同對象的查詢緩存增強。 查詢緩存很聰明-它不會傳回陳舊的結果。 當多個JVM使用Reladomo向同一資料寫入時,緩存可以正常工作。 可以在啟動時将其配置為按需緩存或完整緩存。 對于正确的資料和應用程式,甚至可以将對象存儲在堆外,以便通過複制進行大規模緩存。 我們正在生産中運作的緩存超過200GB。
原則發展
Reladomo被定位為架構,而不是庫。 架構超越了庫所提供的功能,它通過規定和考慮哪些編碼模式合适以及哪些不合适。 Reladomo還會生成代碼,并且所生成的API有望在其餘的代碼中廣泛使用。 是以,當務之急是,架構代碼和應用程序代碼必須具有統一的觀點。
我們定義我們的核心價值,以便我們的潛在使用者可以确定Reladomo是否适合他們:
- 目标代碼要在生産中運作多年甚至數十年。
- 不要重複自己。
- 使代碼更改變得容易。
- 以基于域的面向對象的方式編寫代碼。
- 不要妥協正确性和一緻性。
這些核心價值觀及其後果在我們的《 哲學與願景》文檔中進行了詳細說明。
可用性和可程式設計性
我們将使用幾個小型領域模型來示範Reladomo的一些功能。 首先,關于寵物的非時間模型:

第二,教科書分類帳的模型:
在此模型中,帳戶交易證券(産品),并且該産品具有任意數量的辨別符(稱為同義詞)。 累計餘額保留在餘額對象中。 餘額可以表示有關該帳戶的任意數量的累計值,例如數量,應納稅所得額,利息等。您可以在github上檢視這些模型的代碼 。
我們将很快看到,這是一個雙時态模型的示例。 現在,我們将忽略時間位,它們不會妨礙您。
通過為每個概念對象建立Reladomo對象定義并使用它們生成一些類來定義模型。 我們希望您定義的域類可以用作您的真實業務域。 初始生成後,域中的具體類将永遠不會被覆寫。 每當模型或Reladomo版本更改時,都會生成它們的抽象超類。 您可以-并且應該-将方法添加到這些具體類中,然後将其檢入版本控制系統中。
Reladomo提供的大多數API都在生成的類上:我們的pet示例中為
PetFinder, PetAbstract
和
PetListAbstract
。
PetFinder
具有正常的get / set方法和其他一些用于持久性的方法。 API真正有趣的部分位于Finder和List上。
顧名思義,特定于類的Finder(例如PersonFinder)用于查找事物。 這是一個簡單的示例:
Person john = PersonFinder.findOne(PersonFinder.personId().eq(8));
請注意,沒有要擷取和關閉的連接配接或會話。 在所有上下文中,檢索到的對象都是有效的引用。 您可以自由地将其傳遞給不同的線程,并使它參與事務性工作單元。 如果傳回多個對象,則findOne會引發異常。
讓我們分解一下這個表達式。
PersonFinder. firstName ()
PersonFinder. firstName ()
是
Attribute
。 它是類型化的(它是一個
StringAttribute
):您可以調用
rstName
().eq( "John" )
,但是不能
firstName ().eq(8)
或
firstName ().eq(someDate)
。 它還具有在其他類型的屬性上找不到的特殊方法,例如:
PersonFinder.firstName().toLowerCase().startsWith("j")
如T方法
oLowerCase(), startsWith()
和其他許多人不提供比方說,一個
IntegerAttribute
,它有自己的一套專門的方法。
所有這些都建立了兩個重要的可用性點:首先,您的IDE可以幫助您編寫正确的代碼。 其次,當您對模型進行更改時,編譯器将找到所有需要更改的位置。
屬性上具有建立操作的方法,例如
eq(), greaterThan()
等。Reladomo中的操作用于通過
Finder. findOne or Finder. findMany
檢索對象
Finder. findOne or Finder. findMany
Finder. findOne or Finder. findMany
Finder. findOne or Finder. findMany
。 操作實作是不變的。 它們可以與
and()
和
or()
組合在一起:
Operation op = PersonFinder.firstName().eq("John");
op = op.and(PersonFinder.lastName().endsWith("e"));
PersonList johns = PersonFinder.findMany(op);
執行大量IO的應用程式傾向于批量加載資料。 這可能意味着使用條款。 如果我們構造此操作:
Set<String> lastNames = ... // a large set, say 10K elements
PersonList largeList =
PersonFinder.findMany(PersonFinder.lastName().in(lastNames));
在背景,Reladomo分析您的
Operation
并生成相應SQL。 對于大型子句将生成什麼sql? 在Reladomo中,答案是:“取決于”。 Reladomo可以選擇發出多個從句,或者根據目标資料庫使用臨時表聯接。 從使用者角度看,該選擇是透明的。 Reladomo的實作将根據操作和資料庫有效地傳回正确的結果。 如果配置發生更改,開發人員不必做出必然會出錯的選擇,也不必編寫複雜的代碼來應對變化。 附送電池 !
主鍵
Reladomo中的主鍵是對象屬性的任意組合。 無需定義鍵類或将這些屬性差別對待。 我們的理念是複合鍵在所有模型中都是非常自然的,使用它們應該沒有障礙。 在我們的簡單交易模型中,
ProductSynonym
類具有自然的組合鍵:
<Attribute name="productId"
javaType="int"
columnName="PRODUCT_ID"
primaryKey="true"/>
<Attribute name="synonymType"
javaType="String"
columnName="SYNONYM_TYPE"
primaryKey="true"/>
當然,合成鍵在某些情況下很有用。 我們支援使用基于表的高性能方法來生成合成密鑰。 合成密鑰是按批,異步和按需生成的。
人際關系
類之間的關系在模型中定義:
<Relationship name="pets"
relatedObject="Pet"
cardinality="one-to-many"
relatedIsDependent="true"
reverseRelationshipName="owner">
this.personId = Pet.personId
</Relationship>
定義關系提供了三種讀取功能:
- 對象上的get方法,如果通過reverseRelationshipName屬性将關系标記為雙向,則可能使用相關對象上的get方法,例如
person.getPets()
- 導航查找器上的關系,例如
PersonFinder. pets ()
。PersonFinder. pets ()
- 能夠在每個查詢的基礎上深度擷取關系。
深度擷取是一種有效地檢索相關對象的能力,可以避免衆所周知的
N+1 query problem
。 如果檢索某些人對象,則可以要求有效地加載他們的寵物對象。
PersonList people = ...
people.deepFetch(PersonFinder.pets());
或更有趣的例子:
TradeList trades = ...
trades.deepFetch(TradeFinder.account()); // Fetch accounts for these trades
trades.deepFetch(TradeFinder.product()
.cusipSynonym()); // Fetch the products and the
// products’ CUSIP synonym (a type of identifier) for these trades
trades.deepFetch(TradeFinder.product()
.synonymByType("ISN")); // Also fetch the products’ ISN
// synonym (another identifier).
可以指定可達圖的任何部分。 請注意,這是如何不作為模型的一部分實作的。 該模型沒有“渴望”或“懶惰”的概念。 這是指定此問題的特定代碼段。 是以,更改模型不可能徹底改變現有代碼的IO和性能,進而使模型更加靈活。
建立
Operation
時可以使用關系:
Operation op = TradeFinder
.account()
.location()
.eq("NY"); // Find all trades
// belonging to NY accounts.
op = op.and(TradeFinder.product()
.productName()
.in(productNames)); // … and whose product name
// is included in the supplied list
TradeList trades2 = TradeFinder.findMany(op);
關系在Reladomo中沒有實際引用地實作。 這使得在記憶體和IO方面添加關系成為免費的。
Reladomo中的關系非常靈活。 考慮一個具有許多不同類型的同義詞(例如CUSIP,Ticker等)的Product對象的教科書示例。 我們已經在交易模型中定義了這個例子。 從
Product
到
ProductSynonym
的傳統一對多關系幾乎不再有用:
<Relationship name="synonyms"
relatedObject="ProductSynonym"
cardinality="one-to-many">
this.productId = ProductSynonym.productId
</Relationship>
這樣做的原因是,很少要在查詢中傳回産品的所有同義詞。 兩種類型的進階關系使這個常見示例更加有用。 具有常量表達式的關系可以在模型中表示重要的業務概念。 例如,如果我們想按名稱通路産品的CUSIP同義詞,則添加以下關系:
<Relationship name="cusipSynonym"
relatedObject="ProductSynonym"
cardinality="one-to-one">
this.productId = ProductSynonym.productId and
ProductSynonym.synonymType = "CUS"
</Relationship>
請注意,我們如何在上面的
deepFetch
和query示例中使用此
cusipSynonym
關系。 這具有三個好處:首先,我們不必在代碼中重複“ CUS ”。 其次,如果我們想要的隻是CUSIP,我們無需支付檢索所有同義詞的IO成本。 第三,查詢的可讀性和書寫習慣更加豐富。
可組合性
基于字元串的查詢的最大問題之一是它們很難編寫。 通過使用類型安全的,基于域的面向對象的查詢語言,我們将可組合性提高到了新的水準。 為了說明這一點,讓我們看一個有趣的例子。
在我們的交易模型中,交易對象和餘額對象都與帳戶和産品都有關系。 假設您有一個GUI,可以通過過濾帳戶和産品來檢索交易。 一個不同的視窗允許通過過濾帳戶和産品來檢索餘額。 自然,因為我們要處理的是相同的實體,是以過濾器是相同的。 使用Reladomo,可以輕松地在兩者之間共享代碼。 我們已經将産品和帳戶業務邏輯抽象為幾個GUI元件類 ,然後使用它們:
public BalanceList retrieveBalances()
{
Operation op = BalanceFinder.businessDate().eq(readUserDate());
op = op.and(BalanceFinder.desk().in(readUserDesks()));
Operation refDataOp = accountComponent.getUserOperation(
BalanceFinder.account());
refDataOp = refDataOp.and(
productComponent.getUserOperation(BalanceFinder.product()));
op = op.and(refDataOp);
return BalanceFinder.findMany(op);
}
這将發出以下SQL:
select t0.ACCT_ID,t0.PRODUCT_ID,t0.BALANCE_TYPE,t0.VALUE,t0.FROM_Z,
t0.THRU_Z,t0.IN_Z,t0.OUT_Z
from BALANCE t0
inner join PRODUCT t1
on t0.PRODUCT_ID = t1.PRODUCT_ID
inner join PRODUCT_SYNONYM t2
on t1.PRODUCT_ID = t2.PRODUCT_ID
inner join ACCOUNT t3
on t0.ACCT_ID = t3.ACCT_ID
where t1.FROM_Z <= '2017-03-02 00:00:00.000'
and t1.THRU_Z > '2017-03-02 00:00:00.000'
and t1.OUT_Z = '9999-12-01 23:59:00.000'
and t2.OUT_Z = '9999-12-01 23:59:00.000'
and t2.FROM_Z <= '2017-03-02 00:00:00.000'
and t2.THRU_Z > '2017-03-02 00:00:00.000'
and t2.SYNONYM_TYPE = 'CUS'
and t2.SYNONYM_VAL in ( 'ABC', 'XYZ' )
and t1.MATURITY_DATE < '2020-01-01'
and t3.FROM_Z <= '2017-03-02 00:00:00.000'
and t3.THRU_Z > '2017-03-02 00:00:00.000'
and t3.OUT_Z = '9999-12-01 23:59:00.000'
and t3.CITY = 'NY'
and t0.FROM_Z <= '2017-03-02 00:00:00.000'
and t0.THRU_Z > '2017-03-02 00:00:00.000'
and t0.OUT_Z = '9999-12-01 23:59:00.000'
ProductComponent和AccountComponent類可完全重用于貿易(請參見BalanceWindow和TradeWindow)。 但是可組合性并不止于此。 假設業務需求已更改,并且僅對于“餘額”視窗,使用者希望使用适合帳戶篩選器或産品篩選器的餘額。 使用Reladomo,那将是一行代碼更改:
refDataOp = refDataOp.or(
productComponent.getUserOperation(BalanceFinder.product()));
發出SQL現在非常不同:
select t0.ACCT_ID,t0.PRODUCT_ID,t0.BALANCE_TYPE,t0.VALUE,t0.FROM_Z,
t0.THRU_Z,t0.IN_Z,t0.OUT_Z
from BALANCE t0
left join ACCOUNT t1
on t0.ACCT_ID = t1.ACCT_ID
and t1.OUT_Z = '9999-12-01 23:59:00.000'
and t1.FROM_Z <= '2017-03-02 00:00:00.000'
and t1.THRU_Z > '2017-03-02 00:00:00.000'
and t1.CITY = 'NY'
left join PRODUCT t2
on t0.PRODUCT_ID = t2.PRODUCT_ID
and t2.FROM_Z <= '2017-03-02 00:00:00.000'
and t2.THRU_Z > '2017-03-02 00:00:00.000'
and t2.OUT_Z = '9999-12-01 23:59:00.000'
and t2.MATURITY_DATE < '2020-01-01'
left join PRODUCT_SYNONYM t3
on t2.PRODUCT_ID = t3.PRODUCT_ID
and t3.OUT_Z = '9999-12-01 23:59:00.000'
and t3.FROM_Z <= '2017-03-02 00:00:00.000'
and t3.THRU_Z > '2017-03-02 00:00:00.000'
and t3.SYNONYM_TYPE = 'CUS'
and t3.SYNONYM_VAL in ( 'ABC', 'XYZ' )
where ( ( t1.ACCT_ID is not null )
or ( t2.PRODUCT_ID is not null
and t3.PRODUCT_ID is not null ) )
and t0.FROM_Z <= '2017-03-02 00:00:00.000'
and t0.THRU_Z > '2017-03-02 00:00:00.000'
and t0.OUT_Z = '9999-12-01 23:59:00.000'
請注意,此SQL與先前SQL在結構上有所不同。 需求從“和”更改為“或”,我們将代碼從“和”更改為“或”,并且可以正常工作。 包括電池! 如果使用基于字元串的查詢或公開“聯接”的任何查詢機制來實作,則需要将需求從“和”更改為“或”。
CRUD和工作機關
Reladomo的CRUD API在對象和清單實作中。 該對象具有諸如insert()和delete()之類的方法,而清單具有批量方法。 沒有“儲存”或“更新”方法。 在持久對象上設定值将更新資料庫。 預期大多數寫入将在事務中執行,該事務通過指令模式實作:
MithraManagerProvider.getMithraManager().executeTransactionalCommand(
tx ->
{
Person person = PersonFinder.findOne(PersonFinder.personId().eq(8));
person.setFirstName("David");
person.setLastName("Smith");
return person;
});
UPDATE PERSON
SET FIRST_NAME='David', LAST_NAME='Smith'
WHERE PERSON_ID=8
對資料庫的寫入進行合并和批處理,唯一的限制是正确性。
PersonList對象具有許多有用的方法,這些方法可提供基于集合的API。 例如,您可以執行以下操作:
Operation op = PersonFinder.firstName().eq("John");
op = op.and(PersonFinder.lastName().endsWith("e"));
PersonList johns = PersonFinder.findMany(op);
johns.deleteAll();
從所有方面來看,您可能會認為這首先解析了清單,然後一個一個地删除了個人記錄,但事實并非如此。 相反,它将發出以下(事務性)查詢:
DELETE from PERSON
WHERE LAST_NAME like '%e' AND FIRST_NAME = 'John'
很好,但這不是真正的生産應用程式所需的唯一批量删除類型。 考慮應用程式需要清除舊資料的情況。 顯然,該資料已不再使用,是以不需要在整個資料集中進行整體交易。 可能需要在背景過程中以盡力而為的方式删除資料。 為此,您可以使用:
johns.deleteAllInBatches(1000);
這會根據目标資料庫發出不同類型的查詢:
MS-SQL:
delete top(1000) from PERSON
where LAST_NAME like '%e' and FIRST_NAME = 'John'
PostgreSQL:
delete from PERSON
where ctid = any (array(select ctid
from PERSON
where LAST_NAME like '%e'
and FIRST_NAME = 'John'
limit 1000))
而且,它非常努力地完成工作,處理臨時故障并在一切完成後傳回。 這就是我們所說的“包含電池”的含義-常見的圖案易于烘烤且易于烘焙。
易于整合
我們對Reladomo進行了結構設計,以使其易于與您的代碼內建。
首先,Reladomo具有很少的依賴性。 在運作時,類路徑上隻有六個jar(主庫jar和五個淺依賴性)。 對于完整的生産部署,您需要一個驅動程式類,一個slf4j日志實作和您自己的代碼。 這給了您極大的自由,可以随意放入其他任何東西,而不必擔心jar沖突。
第二,我們緻力于在Reladomo中提供向後相容性。 您應該能夠在不破壞代碼的情況下更新Reladomo的版本。 如果我們計劃進行一項更改,進而導緻向後不相容的更改,那麼我們将確定您至少有一年的時間才能切換到新API。
結論
盡管我們非常重視可用性(“包括電池!”),但我們認識到存在許多不同的用例,并且試圖将所有人的所有東西都用不上。
困擾傳統ORM的問題之一是抽象洩漏。 如果正确實施,我們的核心價值觀将建立一個非常引人注目的系統,進而避免了這些滲漏的抽象。 Reladomo中沒有本機查詢或存儲過程支援,這并非偶然。 我們非常努力地不寫說明“如果基礎資料庫支援Y,則支援功能X”的文檔。
Reladomo的功能比我們這裡未介紹的要多。 随時在Github上通路我們,看看文檔和Katas (我們的學習Reladomo的教程集)。 在本文的第二部分(六月),我們将展示Reladomo的一些性能,可測試性和企業功能。
翻譯自: https://www.infoq.com/articles/Reladomo-Open-Source-ORM/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1
java orm 開源架構