天天看點

java orm 開源架構_Reladomo簡介-企業開源Java ORM,包括電池!

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的一些功能。 首先,關于寵物的非時間模型:

java orm 開源架構_Reladomo簡介-企業開源Java ORM,包括電池!

第二,教科書分類帳的模型:

java orm 開源架構_Reladomo簡介-企業開源Java ORM,包括電池!

在此模型中,帳戶交易證券(産品),并且該産品具有任意數量的辨別符(稱為同義詞)。 累計餘額保留在餘額對象中。 餘額可以表示有關該帳戶的任意數量的累計值,例如數量,應納稅所得額,利息等。您可以在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 開源架構