使用ORM提取数据很容易! 是吗?
介紹
幾乎任何系統都以某種方式與外部數據存儲一起運行。 在大多數情況下,它是一個關系數據庫,并且數據獲取通常委托給某些ORM實現。 ORM涵蓋了很多例程,并帶來了一些新的抽象作為回報。
Martin Fowler寫了一篇有關ORM的有趣文章 ,其中的主要思想之一是:“ ORM幫助我們處理大多數企業應用程序中非常實際的問題。 …他們不是很好的工具,但是他們解決的問題也不是很可愛。 我認為他們應該得到更多的尊重和更多的理解。”
在CUBA框架中,我們非常頻繁地使用ORM,并且由于它在世界范圍內有各種各樣的項目,所以對它的局限性了解很多。 有很多事情可以討論,但我們將集中討論其中之一:懶惰與急切的數據獲取。 我們將討論數據獲取的不同方法(主要是在JPA API和Spring中),我們如何在CUBA中處理數據以及我們為提高CUBA中的ORM層所做的RnD工作。 我們將研究一些基本要素,這些要素可能會幫助開發人員避免使用ORM帶來糟糕的性能問題。
取數據:懶惰還是渴望?
如果您的數據模型僅包含一個實體,那么使用ORM不會有任何問題。 讓我們看一個例子。 我們有一個具有ID和名稱的用戶:
public class User { @Id @GeneratedValue private int id; private String name; //Getters and Setters here }要獲取它,我們只需要很好地詢問EntityManager即可:
EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User. class , id);當實體之間存在一對多關系時,事情就會變得有趣起來:
public class User { @Id @GeneratedValue private int id; private String name; @OneToMany private List<Address> addresses; //Getters and Setters here }如果要從數據庫中獲取用戶記錄,則會出現一個問題:“我們也應該獲取地址嗎?”。 正確的答案將是:“取決于”。 在某些用例中,我們可能需要其中一些地址-不需要。 通常,ORM提供兩個用于獲取數據的選項:惰性和渴望。 它們中的大多數默認情況下都設置了惰性提取模式。 而當我們編寫以下代碼時:
EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User. class , 1 ); em.close(); System.out.println(user.getAddresses().get( 0 ));我們得到了所謂的“LazyInitException” ,這使ORM新手非常困惑。 在這里,我們需要解釋“附加”和“分離”對象上的概念,并講述數據庫會話和事務。
然后,將一個實體實例附加到會話,這樣我們就可以獲取詳細信息屬性。 在這種情況下,我們遇到了另一個問題–交易時間越來越長,因此陷入僵局的風險增加了。 而且,由于短查詢的數量增加,將我們的代碼拆分為一系列短事務可能會導致數據庫“百萬蚊子死亡”。
如前所述,您可能需要也可能不需要獲取Addresses屬性,因此僅在某些用例中需要“觸摸”集合,從而添加更多條件。 嗯... 看起來越來越復雜。
好的,另一種提取類型會有所幫助嗎?
public class User { @Id @GeneratedValue private int id; private String name; @OneToMany (fetch = FetchType.EAGER) private List<Address> addresses; //Getters and Setters here }好吧,不完全是。 我們將擺脫煩人的懶惰init異常,并且不應該檢查實例是已附加還是已分離。 但是我們遇到了性能問題,因為同樣,我們并不需要所有情況的地址,而是始終選擇它們。 還有其他想法嗎?
Spring JDBC
一些開發人員對ORM感到非常惱火,以至于他們使用Spring JDBC切換到“半自動”映射。 在這種情況下,我們為唯一的用例創建唯一的查詢,并返回包含僅對特定用例有效的屬性的對象。
它給了我們極大的靈活性。 我們只能得到一個屬性:
String name = this .jdbcTemplate.queryForObject( "select name from t_user where id = ?" , new Object[]{1L}, String. class );或整個對象:
User user = this .jdbcTemplate.queryForObject( "select id, name from t_user where id = ?" , new Object[]{1L}, new RowMapper<User>() { public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setName(rs.getString( "name" )); user.setId(rs.getInt( "id" )); return user; } });您也可以使用ResultSetExtractor來獲取地址,但是它涉及編寫一些額外的代碼,并且您應該知道如何編寫SQL聯接以避免n + 1 select問題 。
好吧,它又變得越來越復雜。 您可以控制所有查詢并可以控制映射,但是您必須編寫更多代碼,學習SQL并知道如何執行數據庫查詢。 盡管我認為了解SQL基礎知識對于幾乎每個開發人員都是必不可少的技能,但其中一些人并不這么認為,因此我不會與他們爭論。 如今,了解x86匯編器也不是每個人都至關重要的技能。 讓我們考慮一下如何簡化開發。
JPA實體圖
讓我們退后一步,嘗試了解我們將要實現什么? 似乎我們需要做的就是準確告訴我們要在不同用例中獲取哪些屬性。 那我們做吧! JPA 2.1引入了新的API –實體圖。 該API背后的想法很簡單–您只需編寫一些注釋來描述應獲取的內容。 讓我們看一個例子:
@Entity @NamedEntityGraphs ({ @NamedEntityGraph (name = "user-only-entity-graph" ), @NamedEntityGraph (name = "user-addresses-entity-graph" , attributeNodes = { @NamedAttributeNode ( "addresses" )}) }) public class User { @Id @GeneratedValue private int id; private String name; @OneToMany (fetch = FetchType.LAZY) private Set<Address> addresses; //Getters and Setters here }對于這個實體,我們描述了兩個實體圖– user-only-entity-graph不獲取Addresses屬性(標記為惰性),而第二個圖則指示ORM選擇地址。 如果我們將屬性標記為渴望,則實體圖設置將被忽略,并且將獲取該屬性。
因此,從JPA 2.1開始,您可以通過以下方式選擇實體:
EntityManager em = entityManagerFactory.createEntityManager(); EntityGraph graph = em.getEntityGraph( "user-addresses-entity-graph" ); Map<String, Object> properties = Map.of( "javax.persistence.fetchgraph" , graph); User user = em.find(User. class , 1 , properties); em.close();這種方法極大地簡化了開發人員的工作,無需“接觸”惰性屬性并創建長事務。 很棒的事情是,實體圖可以應用于SQL生成級別,因此不會從數據庫中獲取額外的數據到Java應用程序。 但是仍然有一個問題。 我們不能說獲取了哪些屬性,哪些沒有。 有一個API,可以使用PersistenceUnit類檢查屬性:
PersistenceUtil pu = entityManagerFactory.getPersistenceUnitUtil(); System.out.println( "User.addresses loaded: " + pu.isLoaded(user, "addresses" "User.addresses loaded: " + pu.isLoaded(user, "addresses" ));但這很無聊。 我們可以簡化一下,只是不顯示未提取的屬性嗎?
Spring預測
Spring Framework提供了一個很棒的工具,稱為Projections (它與Hibernate的Projections不同)。 如果我們只想獲取實體的某些屬性,則可以指定一個接口,Spring將從數據庫中選擇接口“實例”。 讓我們看一個例子。 如果我們定義以下接口:
interface NamesOnly { String getName(); }然后定義一個Spring JPA存儲庫以獲取我們的User實體:
interface UserRepository extends CrudRepository<User, Integer> { Collection<NamesOnly> findByName(String lastname); }在這種情況下,調用findByName方法后,我們將無法訪問未提取的屬性! 同樣的原則也適用于詳細實體類。 因此,您可以通過這種方式獲取主記錄和明細記錄。 此外,在大多數情況下,Spring會生成“適當的” SQL,并且僅獲取投影中指定的屬性,即,投影的工作方式類似于實體圖描述。
這是一個非常強大的概念,您可以使用SpEL表達式,使用類而不是接口等。如果您有興趣,可以在文檔中查看更多信息。
投影的唯一問題是在幕后將它們實現為地圖,因此是只讀的。 因此,考慮到您可以為投影定義setter方法,則既不能使用CRUD存儲庫也不能使用EntityManager保存更改。 您可以將投影視為DTO,并且必須編寫自己的DTO到實體的轉換代碼。
CUBA實施
從CUBA框架開發的開始,我們就嘗試優化可與數據庫一起使用的代碼。 在框架中,我們使用EclipseLink來實現數據訪問層API。 關于EclipseLink的好處-它從一開始就支持部分實體加載,這就是為什么我們首先選擇它而不是Hibernate的原因。 在此ORM中,您可以指定在JPA 2.1成為標準之前應確切加載哪些屬性。 因此,我們將類似內部“實體圖”的概念添加到我們的框架CUBA Views中 。 視圖非常強大-您可以擴展它們,合并等等。CUBA視圖創建背后的第二個原因-我們想使用短事務,并專注于主要處理分離對象,否則,我們將無法快速,快速地響應豐富的Web UI 。
在CUBA視圖中,描述存儲在XML文件中,如下所示:
<view class = "com.sample.User" extends = "_local" name= "user-minimal-view" > <property name= "name" /> <property name= "addresses" view= "address-street-only-view" /> </property> </view>該視圖指示CUBA DataManager提取具有其本地名稱屬性的User實體,并應用地址僅街道視圖來獲取地址,同時在查詢級別獲取它們(重要!)。 定義視圖后,可以使用DataManager類將其應用于獲取實體:
List<User> users = dataManager.load(User. class ).view( "user-edit-view" ).list();它的工作原理很像,并且由于不加載未使用的屬性而節省了大量網絡流量,但是像在JPA Entity Graph中一樣,存在一個小問題:我們無法說出用戶實體的哪些屬性已加載。 在CUBA中,我們有令人討厭的“IllegalStateException: Cannot get unfetched attribute [...] from detached object” 。 像在JPA中一樣,您可以檢查是否未提取屬性,但是為每個要提取的實體編寫這些檢查是一項無聊的工作,并且開發人員對此不滿意。
CUBA View Interfaces PoC
如果我們能充分利用兩個世界的優勢,該怎么辦? 我們決定使用Spring的方法來實現所謂的實體接口,但是這些接口在應用程序啟動期間會轉換為CUBA視圖,然后可以在DataManager中使用。 這個想法很簡單:定義一個指定實體圖的接口(或一組接口)。 它看起來像Spring Projections,并且像Entity Graph一樣工作:
interface UserMinimalView extends BaseEntityView<User, Integer> { String getName(); void setName(String val); List<AddressStreetOnly> getAddresses(); interface AddressStreetOnly extends BaseEntityView<Address, Integer> { String getStreet(); void setStreet(String street); } }請注意,如果僅在一種情況下使用,則AddressStreetOnly接口可以嵌套。
在CUBA應用程序啟動期間(實際上,大多數情況是Spring Context初始化),我們為CUBA視圖創建了程序化表示并將其存儲在Spring上下文中的內部存儲庫bean中。
之后,我們需要調整DataManager,以便它除了可以接受CUBA View字符串名稱之外,還可以接受類名稱,然后我們只需傳遞接口類即可:
List<User> users = dataManager.loadWithView(UserMinimalView. class ).list();我們為每個從數據庫獲取的實例生成代理來實現實體視圖,就像冬眠一樣。 而且,當您嘗試獲取屬性的值時,代理會將調用轉發給真實實體。
通過這種實現,我們試圖用一塊石頭殺死兩只鳥:
- 接口中未聲明的數據不會加載到Java應用程序代碼中,從而節省了服務器資源
- 開發人員僅使用獲取的屬性,因此不會再出現“ UnfetchedAttribute”錯誤(在Hibernate中也稱為LazyInitException )。
與Spring Projections相比,實體視圖包裝實體并實現CUBA的實體接口,因此可以將它們視為實體:您可以更新屬性并將更改保存到數據庫。
這里的“第三只鳥” –您可以定義一個僅包含吸氣劑的“只讀”接口,從而完全防止實體在API級別進行修改。
另外,我們可以對分離的實體執行一些操作,例如將該用戶的名稱轉換為小寫:
@MetaProperty default String getNameLowercase() { return getName().toLowerCase(); }在這種情況下,所有計算出的屬性都可以從實體模型中移出,因此您不必將數據獲取邏輯與用例特定的業務邏輯混合在一起。
另一個有趣的機會–您可以繼承接口。 這使您可以準備具有不同屬性集的多個視圖,然后根據需要將它們混合。 例如,您可以有一個包含用戶名和電子郵件的界面,以及另一個包含名稱和地址的界面。 而且,如果您需要第三個視圖接口,其中應該包含名稱,電子郵件和地址,則可以通過將二者結合起來來實現–這要歸功于Java中接口的多重繼承。 請注意,您可以將此第三個接口傳遞給使用第一個或第二個接口的方法,OOP原理照常在這里工作。
我們還實現了視圖之間的實體轉換–每個實體視圖都有reload()方法,該方法接受另一個視圖類作為參數:
UserFullView userFull = userMinimal.reload(UserFullView. class );UserFullView可能包含其他屬性,因此該實體將從數據庫中重新加載。 實體重新加載是一個懶惰的過程,僅當您嘗試獲取實體屬性值時才執行。 我們之所以這樣做是因為在CUBA中,我們有一個“網絡”模塊,可呈現豐富的UI,并可能包含自定義的REST控制器。 在此模塊中,我們使用相同的實體,并且可以將其部署在單獨的服務器上。 因此,每個實體重新加載都會通過核心模塊(aka中間件)向數據庫發出附加請求。 因此,通過引入惰性實體重新加載,我們節省了一些網絡流量和數據庫查詢。
PoC可以從GitHub下載-隨時使用。
結論
ORM將在不久的將來在企業應用程序中大量使用。 我們只需要提供一些將數據庫行轉換為Java對象的工具即可。 當然,在復雜的高負載應用程序中,我們將繼續看到獨特的解決方案,但是ORM的生存時間將與RD??BMSes一樣長。
在CUBA框架中,我們試圖簡化ORM的使用,以使開發人員盡可能地輕松。 在下一版本中,我們將引入更多更改。 我不確定這些接口是視圖接口還是其他接口,但是我可以肯定的是,使用CUBA在下一版本中使用ORM將得到簡化。
翻譯自: https://www.javacodegeeks.com/2019/09/fetching-data-with-orm-easy.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的使用ORM提取数据很容易! 是吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 显示器1440*900是多少寸
- 下一篇: 逃出房间的游戏 谁能过关 写攻略