使用Hibernate批量获取
如果需要從Java處理大型數據庫結果集,則可以選擇JDBC,以提供所需的低級控制。 另一方面,如果您已在應用程序中使用ORM,則回退到JDBC可能意味著額外的麻煩。 在域模型中導航時,您將失去樂觀鎖定,緩存,自動獲取等功能。 幸運的是,大多數ORM,例如Hibernate,都有一些選項可以幫助您。 盡管這些技術不是新技術,但有兩種可能可供選擇。
一個簡化的例子; 假設我們有一個表(映射到類'DemoEntity'),具有100.000條記錄。 每個記錄都由一個列(映射到DemoEntity中的屬性“ property”)組成,其中包含一些大約2KB的隨機字母數字數據。
JVM與-Xmx250m一起運行。 假設250MB是可以分配給系統上JVM的總最大內存。 您的工作是讀取表中當前的所有記錄,進行一些未進一步指定的處理,最后存儲結果。 我們假設批量操作產生的實體沒有被修改。 首先,我們將首先嘗試顯而易見的方法,即執行查詢以簡單地檢索所有數據:
幾秒鐘后:
Exception in thread 'main' java.lang.OutOfMemoryError: GC overhead limit exceeded
顯然,這不會削減。 為了解決這個問題,我們將切換到Hibernate可滾動結果集,這可能是大多數開發人員都知道的。 上面的示例指示hibernate執行查詢,將整個結果映射到實體并返回它們。 使用滾動結果集時,記錄一次轉換為一個實體:
new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {@Overridepublic Void doInTransaction(TransactionStatus status) {Session session = sessionFactory.getCurrentSession();ScrollableResults scrollableResults = session.createQuery('from DemoEntity').scroll(ScrollMode.FORWARD_ONLY);int count = 0;while (scrollableResults.next()) {if (++count > 0 && count % 100 == 0) {System.out.println('Fetched ' + count + ' entities');}DemoEntity demoEntity = (DemoEntity) scrollableResults.get()[0];//Process and write result}return null;} });運行之后,我們得到:
... Fetched 49800 entities Fetched 49900 entities Fetched 50000 entities Exception in thread 'main' java.lang.OutOfMemoryError: GC overhead limit exceeded
盡管我們使用的是可滾動的結果集,但每個返回的對象都是一個附加對象,并成為持久性上下文(也稱為會話)的一部分。 結果實際上與我們使用“ session.createQuery('from DemoEntity')。list() '的第一個示例相同。 但是,采用這種方法,我們無法控制。 一切都在幕后發生,如果休眠完成了工作,您將獲得包含所有數據的列表。 另一方面,使用可滾動的結果集使我們迷上了檢索過程,并允許我們在需要時釋放內存。 如我們所見,它不會自動釋放內存,您必須指示Hibernate實際執行此操作。 存在以下選項:
- 處理對象后將其從持久性上下文中逐出
- 偶爾清除整個會話
我們將選擇第一個。 在上面的示例的第13行( // Process和write result )下,我們將添加:
session.evict(demoEntity);重要:
- 如果您要對實體(或與它有關聯的實體進行級聯逐出的實體)進行任何修改,請確保在逐出或清除之前刷新會話,否則由于Hibernate的回寫而導致的查詢將不會發送到數據庫
- 逐出或清除不會將實體從二級緩存中刪除。 如果啟用了二級緩存并正在使用它,并且還希望將其刪除,請使用所需的sessionFactory.getCache()。evictXxx()方法
- 從您退出實體的那一刻起,該實體將不再附加(不再與會話關聯)。 在該階段對實體所做的任何修改將不再自動反映到數據庫中。 如果您使用的是延遲加載,則訪問驅逐之前未加載的任何屬性都會產生著名的org.hibernate.LazyInitializationException。 因此,基本上,在逐出或清除之前,請確保已完成對該實體的處理(或至少已初始化以進一步滿足需要)
再次運行該應用程序后,我們看到它現在已成功執行:
... Fetched 99800 entities Fetched 99900 entities Fetched 100000 entities
順便說一句; 您還可以將查詢設置為只讀,以允許休眠狀態執行一些其他優化:
ScrollableResults scrollableResults = session.createQuery('from DemoEntity').setReadOnly(true).scroll(ScrollMode.FORWARD_ONLY);這樣做只會在內存使用方面產生很小的差異,在此特定的測試設置中,它使我們能夠在給定的內存量下額外讀取約300個實體。 就我個人而言,我不會僅將此功能僅用于內存優化,而僅當它適合您的整體不變性策略時才使用。 使用休眠模式,您可以使用不同的選項將實體設置為只讀:在實體本身上,整個會話為只讀,依此類推。 分別對查詢設置只讀為false可能是最不推薦的方法。 (例如,之前在會話中加載的實體將保持不受影響,可能可修改。即使查詢返回的根對象是只讀的,惰性關聯也將可修改地加載)。
好的,我們能夠處理我們的100.000條記錄,生活很美好。 但是事實證明,Hibernate對于批量操作還有另一個選擇:無狀態會話。 您可以從無狀態會話中獲取可滾動結果集,方法與從普通會話中獲取方法相同。 無狀態會話直接位于JDBC之上。 Hibernate將在幾乎“所有功能禁用”模式下運行。 這意味著沒有持久上下文,沒有第二級緩存,沒有臟檢測,沒有延遲加載,基本上什么也沒有。 從javadoc:
/*** A command-oriented API for performing bulk operations against a database.* A stateless session does not implement a first-level cache nor interact with any * second-level cache, nor does it implement transactional write-behind or automatic * dirty checking, nor do operations cascade to associated instances. Collections are * ignored by a stateless session. Operations performed via a stateless session bypass * Hibernate's event model and interceptors. Stateless sessions are vulnerable to data * aliasing effects, due to the lack of a first-level cache. For certain kinds of * transactions, a stateless session may perform slightly faster than a stateful session.** @author Gavin King*/它唯一要做的就是將記錄轉換為對象。 這可能是一個有吸引力的選擇,因為它可以幫助您擺脫手動驅逐/沖洗的麻煩:
new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {@Overridepublic Void doInTransaction(TransactionStatus status) {sessionFactory.getCurrentSession().doWork(new Work() {@Overridepublic void execute(Connection connection) throws SQLException {StatelessSession statelessSession = sessionFactory.openStatelessSession(connection);try {ScrollableResults scrollableResults = statelessSession.createQuery('from DemoEntity').scroll(ScrollMode.FORWARD_ONLY);int count = 0;while (scrollableResults.next()) {if (++count > 0 && count % 100 == 0) {System.out.println('Fetched ' + count + ' entities');}DemoEntity demoEntity = (DemoEntity) scrollableResults.get()[0];//Process and write result }} finally {statelessSession.close();}}});return null;} });
除了無狀態會話具有最佳的內存使用情況外,使用它還會帶來一些副作用。 您可能已經注意到,我們正在打開一個無狀態會話并顯式關閉它:既沒有sessionFactory.getCurrentStatelessSession()也沒有(在撰寫本文時)任何用于管理無狀態會話的Spring集成。打開無狀態會話會分配一個新的Java。默認情況下sql.Connection(如果使用openStatelessSession() )執行其工作,因此間接產生第二個事務。 您可以通過使用Hibernate work API來減輕這些副作用,如提供當前Connection并將其傳遞給openStatelessSession(Connection connection)的示例中所示。 最后關閉會話對物理連接沒有影響,因為它是由Spring基礎結構捕獲的:打開無狀態會話時,僅關閉邏輯連接句柄,并創建新的邏輯連接句柄。
還要注意,您必須自己關閉無狀態會話,并且上面的示例僅適用于只讀操作。 從您打算使用無狀態會話進行修改的那一刻起,還有一些警告。 如前所述,休眠模式在“所有功能都已禁用”模式下運行,因此直接導致實體以分離狀態返回。 對于您修改的每個實體,您都必須顯式調用: statelessSession.update(entity) 。 首先,我嘗試使用此方法來修改實體:
new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {@Overridepublic Void doInTransaction(TransactionStatus status) {sessionFactory.getCurrentSession().doWork(new Work() {@Overridepublic void execute(Connection connection) throws SQLException {StatelessSession statelessSession = sessionFactory.openStatelessSession(connection);try {DemoEntity demoEntity = (DemoEntity) statelessSession.createQuery('from DemoEntity where id = 1').uniqueResult();demoEntity.setProperty('test');statelessSession.update(demoEntity);} finally {statelessSession.close();}}});return null;} });這個想法是我們用現有的數據庫Connection打開一個無狀態會話。 正如StatelessSession javadoc指示不會發生任何回寫一樣,我確信無狀態會話執行的每個語句都將直接發送到數據庫。 最終,當提交事務(由TransactionTemplate開始)時,結果將在數據庫中可見。 但是,hibernate使用無狀態會話來執行BATCH語句。 我不是100%知道批處理和回寫之間有什么區別,但是結果是相同的,因此與javadoc的字典相反,因為語句在以后排入隊列并刷新。 因此,如果您沒有做任何特別的事情,批處理的語句將不會被刷新,這就是我的情況:'statelessSession.update(demoEntity);' 被分批處理,從不沖洗。 強制刷新的一種方法是使用休眠事務API:
StatelessSession statelessSession = sessionFactory.openStatelessSession(); statelessSession.beginTransaction(); ... statelessSession.getTransaction().commit(); ...在這種情況下,您可能不希望僅因為使用無狀態會話而開始以編程方式控制交易。 此外,由于沒有傳遞我們的Connection,因此我們再次在第二個事務場景中運行無狀態會話工作,因此將獲得新的數據庫連接。 我們無法通過外部連接的原因是,如果我們提交內部事務(“無狀態會話事務”),并且它將使用與外部事務相同的連接(由TransactionTemplate開始),則會破壞外部事務事務的原子性,因為將外部事務發送到數據庫的語句與內部事務一起提交。 因此,不通過連接意味著打開一個新的連接,從而創建第二筆交易。 更好的選擇是觸發Hibernate刷新無狀態會話。 但是,statelessSession沒有“ flush”方法來手動觸發刷新。 解決方案是稍微依賴Hibernate內部API。 該解決方案使手動事務處理和第二個事務處理變得過時:所有語句成為我們(唯一的)外部事務的一部分:
StatelessSession statelessSession = sessionFactory.openStatelessSession(connection);try {DemoEntity demoEntity = (DemoEntity) statelessSession.createQuery('from DemoEntity where id = 1').uniqueResult();demoEntity.setProperty('test');statelessSession.update(demoEntity);((TransactionContext) statelessSession).managedFlush();} finally {statelessSession.close(); }幸運的是,最近在Spring jira上發布了一個更好的解決方案: https : //jira.springsource.org/browse/SPR-2495這還不是Spring的一部分,但是工廠bean的實現非常簡單: StatelessSessionFactoryBean。使用此Java時,您可以簡單地注入StatelessSession:
@Autowired private StatelessSession statelessSession;它將注入一個無狀態的會話代理,這等效于正常的“當前”會話的工作方式(稍有不同的是,您注入一個SessionFactory并且每次都需要獲取currentSession)。 調用代理時,它將查找綁定到正在運行的事務的無狀態會話。 如果已經不存在,它將創建一個與普通會話相同的連接(就像我們在示例中所做的那樣),并為無狀態會話注冊自定義事務同步。 提交事務后,由于同步,將刷新無狀態會話,并最終將其關閉。 使用此方法,您可以直接注入無狀態會話,并將其用作當前會話(或與注入JPA PeristentContext相同的方式)。 這使您不必處理無狀態會話的打開和關閉,而不必處理一種或多種方法以使其變得暢通無阻。 該實現是針對JPA的,但是JPA部分僅限于在getPhysicalConnection()中獲得物理連接。 您可以輕松地省略EntityManagerFactory并直接從Hibernate會話獲取物理連接。
非常謹慎的結論:最好的方法顯然取決于您的情況。 如果您使用普通會話,則在讀取或保留實體時必須自行解決。 如果您有一個混合事務,那么除了必須手動執行操作之外,還可能影響會話的進一步使用。 你們都在同一筆交易中執行“批量”和“正常”操作。 如果繼續進行正常操作,您將在會話中分離實體,這可能會導致意外結果(因為臟檢測將不再起作用,依此類推)。 另一方面,您仍將具有主要的休眠好處(只要不驅逐該實體),例如延遲加載,緩存,臟檢測等。 在編寫本文時使用無狀態會話需要額外注意管理它(打開,關閉和刷新),這也容易出錯。 假設您可以繼續使用建議的工廠bean,那么您將擁有一個非常裸露的會話,該會話與正常會話是分開的,但仍參與同一事務。 使用此工具,您無需執行內存管理即可擁有執行批量操作的強大工具。 缺點是您沒有其他可用的休眠功能。
參考:在Koen Serneels – Technology博客博客上,從我們的JCG合作伙伴 Koen Serneels 與Hibernate進行批量獲取 。
翻譯自: https://www.javacodegeeks.com/2013/03/bulk-fetching-with-hibernate.html
總結
以上是生活随笔為你收集整理的使用Hibernate批量获取的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2000元音箱加电脑(2000元左右电脑
- 下一篇: 编年史与微云