【永恒的瞬間】
          ?Give me hapy ?

          第 11 章 與對象共事

          Hibernate是完整的對象/關系映射解決方案,它提供了對象狀態管理(state management)的功能,使開發者不再需要理會底層數據庫系統的細節。 也就是說,相對于常見的JDBC/SQL持久層方案中需要管理SQL語句,Hibernate采用了更自然的面向對象的視角來持久化Java應用中的數據。

          換句話說,使用Hibernate的開發者應該總是關注對象的狀態(state),不必考慮SQL語句的執行。 這部分細節已經由Hibernate掌管妥當,只有開發者在進行系統性能調優的時候才需要進行了解。

          11.1. Hibernate對象狀態(object states)

          Hibernate定義并支持下列對象狀態(state):

          • 瞬時(Transient) - 由new操作符創建,且尚未與Hibernate Session 關聯的對象被認定為瞬時(Transient)的。瞬時(Transient)對象不會被持久化到數據庫中,也不會被賦予持久化標識(identifier)。 如果程序中沒有保持對瞬時(Transient)對象的引用,它會被垃圾回收器(garbage collector)銷毀。 使用Hibernate Session可以將其變為持久(Persistent)狀態。(Hibernate會自動執行必要的SQL語句)

          • 持久(Persistent) - 持久(Persistent)的實例在數據庫中有對應的記錄,并擁有一個持久化標識(identifier)。 持久(Persistent)的實例可能是剛被保存的,或剛被加載的,無論哪一種,按定義對象都僅在相關聯的Session生命周期內的保持這種狀態。 Hibernate會檢測到處于持久(Persistent)狀態的對象的任何改動,在當前操作單元(unit of work)執行完畢時將對象數據(state)與數據庫同步(synchronize)。 開發者不需要手動執行UPDATE。將對象從持久(Persistent)狀態變成瞬時(Transient)狀態同樣也不需要手動執行DELETE語句。

          • 脫管(Detached) - 與持久(Persistent)對象關聯的Session被關閉后,對象就變為脫管(Detached)的。 對脫管(Detached)對象的引用依然有效,對象可繼續被修改。脫管(Detached)對象如果重新關聯到某個新的Session上, 會再次轉變為持久(Persistent)的(Detached其間的改動將被持久化到數據庫)。 這個功能使得一種編程模型,即中間會給用戶思考時間(user think-time)的長時間運行的操作單元(unit of work)的編程模型成為可能。 我們稱之為應用程序事務,即從用戶觀點看是一個操作單元(unit of work)。

          接下來我們來細致的討論下狀態(states)及狀態間的轉換(state transitions)(以及觸發狀態轉換的Hibernate方法)。

          11.2. 使對象持久化

          Hibernate認為持久化類(persistent class)新實例化的對象是瞬時(Transient)的。 我們可將瞬時(Transient)對象與session關聯而變為持久(Persistent)的。

          DomesticCat fritz = new DomesticCat();
          fritz.setColor(Color.GINGER);
          fritz.setSex('M');
          fritz.setName("Fritz");
          Long generatedId = (Long) sess.save(fritz);

          如果Cat的持久化標識(identifier)是generated類型的, 那么該標識(identifier)會自動在save()被調用時產生并分配給cat。 如果Cat的持久化標識(identifier)是assigned類型的,或是一個復合主鍵(composite key), 那么該標識(identifier)應當在調用save()之前手動賦予給cat。 你也可以按照EJB3 early draft中定義的語義,使用persist()替代save()

          此外,你可以用一個重載版本的save()方法。

          DomesticCat pk = new DomesticCat();
          pk.setColor(Color.TABBY);
          pk.setSex('F');
          pk.setName("PK");
          pk.setKittens( new HashSet() );
          pk.addKitten(fritz);
          sess.save( pk, new Long(1234) );

          如果你持久化的對象有關聯的對象(associated objects)(例如上例中的kittens集合) 那么對這些對象(譯注:pk和kittens)進行持久化的順序是任意的(也就是說可以先對kittens進行持久化也可以先對pk進行持久化), 除非你在外鍵列上有NOT NULL約束。 Hibernate不會違反外鍵約束,但是如果你用錯誤的順序持久化對象(譯注:在pk持久之前持久kitten),那么可能會違反NOT NULL約束。

          通常你不會為這些細節煩心,因為你很可能會使用Hibernate的 傳播性持久化(transitive persistence)功能自動保存相關聯那些對象。 這樣連違反NOT NULL約束情況都不會出現了 - Hibernate會管好所有的事情。 傳播性持久化(transitive persistence)將在本章稍后討論。

          11.3. 裝載對象

          如果你知道某個實例的持久化標識(identifier),你就可以使用Sessionload()方法 來獲取它。 load()的另一個參數是指定類的.class對象。 本方法會創建指定類的持久化實例,并從數據庫加載其數據(state)。

          Cat fritz = (Cat) sess.load(Cat.class, generatedId);
          // you need to wrap primitive identifiers
          long pkId = 1234;
          DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );

          此外, 你可以把數據(state)加載到指定的對象實例上(覆蓋掉該實例原來的數據)。

          Cat cat = new DomesticCat();
          // load pk's state into cat
          sess.load( cat, new Long(pkId) );
          Set kittens = cat.getKittens();

          請注意如果沒有匹配的數據庫記錄,load()方法可能拋出無法恢復的異常(unrecoverable exception)。 如果類的映射使用了代理(proxy),load()方法會返回一個未初始化的代理,直到你調用該代理的某方法時才會去訪問數據庫。 若你希望在某對象中創建一個指向另一個對象的關聯,又不想在從數據庫中裝載該對象時同時裝載相關聯的那個對象,那么這種操作方式就用得上的了。 如果為相應類映射關系設置了batch-size, 那么使用這種操作方式允許多個對象被一批裝載(因為返回的是代理,無需從數據庫中抓取所有對象的數據)。

          如果你不確定是否有匹配的行存在,應該使用get()方法,它會立刻訪問數據庫,如果沒有對應的行,會返回null。

          Cat cat = (Cat) sess.get(Cat.class, id);
          if (cat==null) {
          cat = new Cat();
          sess.save(cat, id);
          }
          return cat;

          你甚至可以選用某個LockMode,用SQL的SELECT ... FOR UPDATE裝載對象。 請查閱API文檔以獲取更多信息。

          Cat cat = (Cat) sess.get(Cat.class, id, LockMode.UPGRADE);

          注意,任何關聯的對象或者包含的集合都不會被以FOR UPDATE方式返回, 除非你指定了lock或者all作為關聯(association)的級聯風格(cascade style)。

          任何時候都可以使用refresh()方法強迫裝載對象和它的集合。如果你使用數據庫觸發器功能來處理對象的某些屬性,這個方法就很有用了。

          sess.save(cat);
          sess.flush(); //force the SQL INSERT
          sess.refresh(cat); //re-read the state (after the trigger executes)

          此處通常會出現一個重要問題: Hibernate會從數據庫中裝載多少東西?會執行多少條相應的SQLSELECT語句? 這取決于抓取策略(fetching strategy),會在第 20.1 節 “ 抓取策略(Fetching strategies) ”中解釋。

          11.4. 查詢

          如果不知道所要尋找的對象的持久化標識,那么你需要使用查詢。Hibernate支持強大且易于使用的面向對象查詢語言(HQL)。 如果希望通過編程的方式創建查詢,Hibernate提供了完善的按條件(Query By Criteria, QBC)以及按樣例(Query By Example, QBE)進行查詢的功能。 你也可以用原生SQL(native SQL)描述查詢,Hibernate提供了將結果集(result set)轉化為對象的部分支持。

          11.4.1. 執行查詢

          HQL和原生SQL(native SQL)查詢要通過為org.hibernate.Query的實例來表達。 這個接口提供了參數綁定、結果集處理以及運行實際查詢的方法。 你總是可以通過當前Session獲取一個Query對象:

          List cats = session.createQuery(
          "from Cat as cat where cat.birthdate < ?")
          .setDate(0, date)
          .list();
          List mothers = session.createQuery(
          "select mother from Cat as cat join cat.mother as mother where cat.name = ?")
          .setString(0, name)
          .list();
          List kittens = session.createQuery(
          "from Cat as cat where cat.mother = ?")
          .setEntity(0, pk)
          .list();
          Cat mother = (Cat) session.createQuery(
          "select cat.mother from Cat as cat where cat = ?")
          .setEntity(0, izi)
          .uniqueResult();

          一個查詢通常在調用list()時被執行,執行結果會完全裝載進內存中的一個集合(collection)。 查詢返回的對象處于持久(persistent)狀態。如果你知道的查詢只會返回一個對象,可使用list()的快捷方式uniqueResult()。

          11.4.1.1. 迭代式獲取結果(Iterating results)

          某些情況下,你可以使用iterate()方法得到更好的性能。 這通常是你預期返回的結果在session,或二級緩存(second-level cache)中已經存在時的情況。 如若不然,iterate()會比list()慢,而且可能簡單查詢也需要進行多次數據庫訪問: iterate()會首先使用1條語句得到所有對象的持久化標識(identifiers),再根據持久化標識執行n條附加的select語句實例化實際的對象。

          // fetch ids
          Iterator iter = sess.createQuery("from eg.Qux q order by q.likeliness").iterate();
          while ( iter.hasNext() ) {
          Qux qux = (Qux) iter.next();  // fetch the object
          // something we couldnt express in the query
          if ( qux.calculateComplicatedAlgorithm() ) {
          // delete the current instance
          iter.remove();
          // dont need to process the rest
          break;
          }
          }

          11.4.1.2. 返回元組(tuples)的查詢

          (譯注:元組(tuples)指一條結果行包含多個對象) Hibernate查詢有時返回元組(tuples),每個元組(tuples)以數組的形式返回:

          Iterator kittensAndMothers = sess.createQuery(
          "select kitten, mother from Cat kitten join kitten.mother mother")
          .list()
          .iterator();
          while ( kittensAndMothers.hasNext() ) {
          Object[] tuple = (Object[]) kittensAndMothers.next();
          Cat kitten  = tuple[0];
          Cat mother  = tuple[1];
          ....
          }

          11.4.1.3. 標量(Scalar)結果

          查詢可在select從句中指定類的屬性,甚至可以調用SQL統計(aggregate)函數。 屬性或統計結果被認定為"標量(Scalar)"的結果(而不是持久(persistent state)的實體)。

          Iterator results = sess.createQuery(
          "select cat.color, min(cat.birthdate), count(cat) from Cat cat " +
          "group by cat.color")
          .list()
          .iterator();
          while ( results.hasNext() ) {
          Object[] row = results.next();
          Color type = (Color) row[0];
          Date oldest = (Date) row[1];
          Integer count = (Integer) row[2];
          .....
          }

          11.4.1.4. 綁定參數

          接口Query提供了對命名參數(named parameters)、JDBC風格的問號(?)參數進行綁定的方法。 不同于JDBC,Hibernate對參數從0開始計數。 命名參數(named parameters)在查詢字符串中是形如:name的標識符。 命名參數(named parameters)的優點是:

          • 命名參數(named parameters)與其在查詢串中出現的順序無關

          • 它們可在同一查詢串中多次出現

          • 它們本身是自我說明的

          //named parameter (preferred)
          Query q = sess.createQuery("from DomesticCat cat where cat.name = :name");
          q.setString("name", "Fritz");
          Iterator cats = q.iterate();
          //positional parameter
          Query q = sess.createQuery("from DomesticCat cat where cat.name = ?");
          q.setString(0, "Izi");
          Iterator cats = q.iterate();
          //named parameter list
          List names = new ArrayList();
          names.add("Izi");
          names.add("Fritz");
          Query q = sess.createQuery("from DomesticCat cat where cat.name in (:namesList)");
          q.setParameterList("namesList", names);
          List cats = q.list();

          11.4.1.5. 分頁

          如果你需要指定結果集的范圍(希望返回的最大行數/或開始的行數),應該使用Query接口提供的方法:

          Query q = sess.createQuery("from DomesticCat cat");
          q.setFirstResult(20);
          q.setMaxResults(10);
          List cats = q.list();

          Hibernate 知道如何將這個有限定條件的查詢轉換成你的數據庫的原生SQL(native SQL)。

          11.4.1.6. 可滾動遍歷(Scrollable iteration)

          如果你的JDBC驅動支持可滾動的ResuleSetQuery接口可以使用ScrollableResults,允許你在查詢結果中靈活游走。

          Query q = sess.createQuery("select cat.name, cat from DomesticCat cat " +
          "order by cat.name");
          ScrollableResults cats = q.scroll();
          if ( cats.first() ) {
          // find the first name on each page of an alphabetical list of cats by name
          firstNamesOfPages = new ArrayList();
          do {
          String name = cats.getString(0);
          firstNamesOfPages.add(name);
          }
          while ( cats.scroll(PAGE_SIZE) );
          // Now get the first page of cats
          pageOfCats = new ArrayList();
          cats.beforeFirst();
          int i=0;
          while( ( PAGE_SIZE > i++ ) && cats.next() ) pageOfCats.add( cats.get(1) );
          }
          cats.close()

          請注意,使用此功能需要保持數據庫連接(以及游標(cursor))處于一直打開狀態。 如果你需要斷開連接使用分頁功能,請使用setMaxResult()/setFirstResult()

          11.4.1.7. 外置命名查詢(Externalizing named queries)

          你可以在映射文件中定義命名查詢(named queries)。 (如果你的查詢串中包含可能被解釋為XML標記(markup)的字符,別忘了用CDATA包裹起來。)

          <query name="eg.DomesticCat.by.name.and.minimum.weight"><![CDATA[
          from eg.DomesticCat as cat
          where cat.name = ?
          and cat.weight > ?
          ] ]></query>

          參數綁定及執行以編程方式(programatically)完成:

          Query q = sess.getNamedQuery("eg.DomesticCat.by.name.and.minimum.weight");
          q.setString(0, name);
          q.setInt(1, minWeight);
          List cats = q.list();

          請注意實際的程序代碼與所用的查詢語言無關,你也可在元數據中定義原生SQL(native SQL)查詢, 或將原有的其他的查詢語句放在配置文件中,這樣就可以讓Hibernate統一管理,達到遷移的目的。

          11.4.2. 過濾集合

          集合過濾器(filter)是一種用于一個持久化集合或者數組的特殊的查詢。查詢字符串中可以使用"this"來引用集合中的當前元素。

          Collection blackKittens = session.createFilter(
          pk.getKittens(),
          "where this.color = ?")
          .setParameter( Color.BLACK, Hibernate.custom(ColorUserType.class) )
          .list()
          );

          返回的集合可以被認為是一個包(bag, 無順序可重復的集合(collection)),它是所給集合的副本。 原來的集合不會被改動(這與“過濾器(filter)”的隱含的含義不符,不過與我們期待的行為一致)。

          請注意過濾器(filter)并不需要from子句(當然需要的話它們也可以加上)。過濾器(filter)不限定于只能返回集合元素本身。

          Collection blackKittenMates = session.createFilter(
          pk.getKittens(),
          "select this.mate where this.color = eg.Color.BLACK.intValue")
          .list();

          即使無條件的過濾器(filter)也是有意義的。例如,用于加載一個大集合的子集:

          Collection tenKittens = session.createFilter(
          mother.getKittens(), "")
          .setFirstResult(0).setMaxResults(10)
          .list();

          11.4.3. 條件查詢(Criteria queries)

          HQL極為強大,但是有些人希望能夠動態的使用一種面向對象API創建查詢,而非在他們的Java代碼中嵌入字符串。對于那部分人來說,Hibernate提供了直觀的Criteria查詢API。

          Criteria crit = session.createCriteria(Cat.class);
          crit.add( Expression.eq( "color", eg.Color.BLACK ) );
          crit.setMaxResults(10);
          List cats = crit.list();

          Criteria以及相關的樣例(Example)API將會再第 16 章 條件查詢(Criteria Queries) 中詳細討論。

          11.4.4. 使用原生SQL的查詢

          你可以使用createSQLQuery()方法,用SQL來描述查詢,并由Hibernate處理將結果集轉換成對象的工作。 請注意,你可以在任何時候調用session.connection()來獲得并使用JDBC Connection對象。 如果你選擇使用Hibernate的API, 你必須把SQL別名用大括號包圍起來:

          List cats = session.createSQLQuery(
          "SELECT {cat.*} FROM CAT {cat} WHERE ROWNUM<10",
          "cat",
          Cat.class
          ).list();
          List cats = session.createSQLQuery(
          "SELECT {cat}.ID AS {cat.id}, {cat}.SEX AS {cat.sex}, " +
          "{cat}.MATE AS {cat.mate}, {cat}.SUBCLASS AS {cat.class}, ... " +
          "FROM CAT {cat} WHERE ROWNUM<10",
          "cat",
          Cat.class
          ).list()

          和Hibernate查詢一樣,SQL查詢也可以包含命名參數和占位參數。 可以在第 17 章 Native SQL查詢找到更多關于Hibernate中原生SQL(native SQL)的信息。

          11.5. 修改持久對象

          事務中的持久實例(就是通過session裝載、保存、創建或者查詢出的對象) 被應用程序操作所造成的任何修改都會在Session刷出(flushed)的時候被持久化(本章后面會詳細討論)。 這里不需要調用某個特定的方法(比如update(),設計它的目的是不同的)將你的修改持久化。 所以最直接的更新一個對象的方法就是在Session處于打開狀態時load()它,然后直接修改即可:

          DomesticCat cat = (DomesticCat) sess.load( Cat.class, new Long(69) );
          cat.setName("PK");
          sess.flush();  // changes to cat are automatically detected and persisted

          有時這種程序模型效率低下,因為它在同一Session里需要一條SQL SELECT語句(用于加載對象) 以及一條SQL UPDATE語句(持久化更新的狀態)。 為此Hibernate提供了另一種途徑,使用脫管(detached)實例。

          請注意Hibernate本身不提供直接執行UPDATEDELETE語句的API。 Hibernate提供的是狀態管理(state management)服務,你不必考慮要使用的語句(statements)。 JDBC是出色的執行SQL語句的API,任何時候調用session.connection()你都可以得到一個JDBC Connection對象。 此外,在聯機事務處理(OLTP)程序中,大量操作(mass operations)與對象/關系映射的觀點是相沖突的。 Hibernate的將來版本可能會提供專門的進行大量操作(mass operation)的功能。 參考第 14 章 批量處理(Batch processing),尋找一些可用的批量(batch)操作技巧。

          11.6. 修改脫管(Detached)對象

          很多程序需要在某個事務中獲取對象,然后將對象發送到界面層去操作,最后在一個新的事務保存所做的修改。 在高并發訪問的環境中使用這種方式,通常使用附帶版本信息的數據來保證這些“長“工作單元之間的隔離。

          Hibernate通過提供使用Session.update()Session.merge()方法 重新關聯脫管實例的辦法來支持這種模型。

          // in the first session
          Cat cat = (Cat) firstSession.load(Cat.class, catId);
          Cat potentialMate = new Cat();
          firstSession.save(potentialMate);
          // in a higher layer of the application
          cat.setMate(potentialMate);
          // later, in a new session
          secondSession.update(cat);  // update cat
          secondSession.update(mate); // update mate

          如果具有catId持久化標識的Cat之前已經被另一Session(secondSession)裝載了, 應用程序進行重關聯操作(reattach)的時候會拋出一個異常。

          如果你確定當前session沒有包含與之具有相同持久化標識的持久實例,使用update()。 如果想隨時合并你的的改動而不考慮session的狀態,使用merge()。 換句話說,在一個新session中通常第一個調用的是update()方法,以便保證重新關聯脫管(detached)對象的操作首先被執行。

          希望相關聯的脫管對象(通過引用“可到達”的脫管對象)的數據也要更新到數據庫時(并且也僅僅在這種情況), 應用程序需要對該相關聯的脫管對象單獨調用update() 當然這些可以自動完成,即通過使用傳播性持久化(transitive persistence),請看第 11.11 節 “傳播性持久化(transitive persistence)”。

          lock()方法也允許程序重新關聯某個對象到一個新session上。不過,該脫管(detached)的對象必須是沒有修改過的!

          //just reassociate:
          sess.lock(fritz, LockMode.NONE);
          //do a version check, then reassociate:
          sess.lock(izi, LockMode.READ);
          //do a version check, using SELECT ... FOR UPDATE, then reassociate:
          sess.lock(pk, LockMode.UPGRADE);

          請注意,lock()可以搭配多種LockMode, 更多信息請閱讀API文檔以及關于事務處理(transaction handling)的章節。重新關聯不是lock()的唯一用途。

          其他用于長時間工作單元的模型會在第 12.3 節 “樂觀并發控制(Optimistic concurrency control)”中討論。

          11.7. 自動狀態檢測

          Hibernate的用戶曾要求一個既可自動分配新持久化標識(identifier)保存瞬時(transient)對象,又可更新/重新關聯脫管(detached)實例的通用方法。 saveOrUpdate()方法實現了這個功能。

          // in the first session
          Cat cat = (Cat) firstSession.load(Cat.class, catID);
          // in a higher tier of the application
          Cat mate = new Cat();
          cat.setMate(mate);
          // later, in a new session
          secondSession.saveOrUpdate(cat);   // update existing state (cat has a non-null id)
          secondSession.saveOrUpdate(mate);  // save the new instance (mate has a null id)

          saveOrUpdate()用途和語義可能會使新用戶感到迷惑。 首先,只要你沒有嘗試在某個session中使用來自另一session的實例,你應該就不需要使用update()saveOrUpdate(),或merge()。有些程序從來不用這些方法。

          通常下面的場景會使用update()saveOrUpdate()

          • 程序在第一個session中加載對象

          • 該對象被傳遞到表現層

          • 對象發生了一些改動

          • 該對象被返回到業務邏輯層

          • 程序調用第二個session的update()方法持久這些改動

          saveOrUpdate()做下面的事:

          • 如果對象已經在本session中持久化了,不做任何事

          • 如果另一個與本session關聯的對象擁有相同的持久化標識(identifier),拋出一個異常

          • 如果對象沒有持久化標識(identifier)屬性,對其調用save()

          • 如果對象的持久標識(identifier)表明其是一個新實例化的對象,對其調用save()

          • 如果對象是附帶版本信息的(通過<version><timestamp>) 并且版本屬性的值表明其是一個新實例化的對象,save()它。

          • 否則update() 這個對象

          merge()可非常不同:

          • 如果session中存在相同持久化標識(identifier)的實例,用用戶給出的對象的狀態覆蓋舊有的持久實例

          • 如果session沒有相應的持久實例,則嘗試從數據庫中加載,或創建新的持久化實例

          • 最后返回該持久實例

          • 用戶給出的這個對象沒有被關聯到session上,它依舊是脫管的

          11.8. 刪除持久對象

          使用Session.delete()會把對象的狀態從數據庫中移除。 當然,你的應用程序可能仍然持有一個指向已刪除對象的引用。所以,最好這樣理解:delete()的用途是把一個持久實例變成瞬時(transient)實例。

          sess.delete(cat);

          你可以用你喜歡的任何順序刪除對象,不用擔心外鍵約束沖突。當然,如果你搞錯了順序,還是有可能引發在外鍵字段定義的NOT NULL約束沖突。 例如你刪除了父對象,但是忘記刪除孩子們。

          11.9. 在兩個不同數據庫間復制對象

          偶爾會用到不重新生成持久化標識(identifier),將持久實例以及其關聯的實例持久到不同的數據庫中的操作。

          //retrieve a cat from one database
          Session session1 = factory1.openSession();
          Transaction tx1 = session1.beginTransaction();
          Cat cat = session1.get(Cat.class, catId);
          tx1.commit();
          session1.close();
          //reconcile with a second database
          Session session2 = factory2.openSession();
          Transaction tx2 = session2.beginTransaction();
          session2.replicate(cat, ReplicationMode.LATEST_VERSION);
          tx2.commit();
          session2.close();

          ReplicationMode決定數據庫中已存在相同行時,replicate()如何處理。

          • ReplicationMode.IGNORE - 忽略它

          • ReplicationMode.OVERWRITE - 覆蓋相同的行

          • ReplicationMode.EXCEPTION - 拋出異常

          • ReplicationMode.LATEST_VERSION - 如果當前的版本較新,則覆蓋,否則忽略

          這個功能的用途包括使錄入的數據在不同數據庫中一致,產品升級時升級系統配置信息,回滾non-ACID事務中的修改等等。 (譯注,non-ACID,非ACID;ACID,Atomic,Consistent,Isolated and Durable的縮寫)

          11.10. Session刷出(flush)

          每間隔一段時間,Session會執行一些必需的SQL語句來把內存中的對象的狀態同步到JDBC連接中。這個過程被稱為刷出(flush),默認會在下面的時間點執行:

          • 在某些查詢執行之前

          • 在調用org.hibernate.Transaction.commit()的時候

          • 在調用Session.flush()的時候

          涉及的SQL語句會按照下面的順序發出執行:

          1. 所有對實體進行插入的語句,其順序按照對象執行Session.save()的時間順序

          2. 所有對實體進行更新的語句

          3. 所有進行集合刪除的語句

          4. 所有對集合元素進行刪除,更新或者插入的語句

          5. 所有進行集合插入的語句

          6. 所有對實體進行刪除的語句,其順序按照對象執行Session.delete()的時間順序

          (有一個例外是,如果對象使用native方式來生成ID(持久化標識)的話,它們一執行save就會被插入。)

          除非你明確地發出了flush()指令,關于Session何時會執行這些JDBC調用是完全無法保證的,只能保證它們執行的前后順序。 當然,Hibernate保證,Query.list(..)絕對不會返回已經失效的數據,也不會返回錯誤數據。

          也可以改變默認的設置,來讓刷出(flush)操作發生的不那么頻繁。 FlushMode類定義了三種不同的方式。 僅在提交時刷出(僅當Hibernate的Transaction API被使用時有效), 按照剛才說的方式刷出, 以及除非明確使用flush()否則從不刷出。 最后一種模式對于那些需要長時間保持Session為打開或者斷線狀態的長時間運行的工作單元很有用。 (參見 第 12.3.2 節 “長生命周期session和自動版本化”).

          sess = sf.openSession();
          Transaction tx = sess.beginTransaction();
          sess.setFlushMode(FlushMode.COMMIT); // allow queries to return stale state
          Cat izi = (Cat) sess.load(Cat.class, id);
          izi.setName(iznizi);
          // might return stale data
          sess.find("from Cat as cat left outer join cat.kittens kitten");
          // change to izi is not flushed!
          ...
          tx.commit(); // flush occurs

          刷出(flush)期間,可能會拋出異常。(例如一個DML操作違反了約束) 異常處理涉及到對Hibernate事務性行為的理解,因此我們將在第 12 章 事務和并發中討論。

          11.11. 傳播性持久化(transitive persistence)

          對每一個對象都要執行保存,刪除或重關聯操作讓人感覺有點麻煩,尤其是在處理許多彼此關聯的對象的時候。 一個常見的例子是父子關系??紤]下面的例子:

          如果一個父子關系中的子對象是值類型(value typed)(例如,地址或字符串的集合)的,他們的生命周期會依賴于父對象,可以享受方便的級聯操作(Cascading),不需要額外的動作。 父對象被保存時,這些值類型(value typed)子對象也將被保存;父對象被刪除時,子對象也將被刪除。 這對將一個子對象從集合中移除是同樣有效:Hibernate會檢測到,并且因為值類型(value typed)的對象不可能被其他對象引用,所以Hibernate會在數據庫中刪除這個子對象。

          現在考慮同樣的場景,不過父子對象都是實體(entities)類型,而非值類型(value typed)(例如,類別與個體,或母貓和小貓)。 實體有自己的生命期,允許共享對其的引用(因此從集合中移除一個實體,不意味著它可以被刪除), 并且實體到其他關聯實體之間默認沒有級聯操作的設置。 Hibernate默認不實現所謂的可到達即持久化(persistence by reachability)的策略。

          每個Hibernate session的基本操作 - 包括 persist(), merge(), saveOrUpdate(), delete(), lock(), refresh(), evict(), replicate() - 都有對應的級聯風格(cascade style)。 這些級聯風格(cascade style)風格分別命名為 create, merge, save-update, delete, lock, refresh, evict, replicate。 如果你希望一個操作被順著關聯關系級聯傳播,你必須在映射文件中指出這一點。例如:

          <one-to-one name="person" cascade="persist"/>

          級聯風格(cascade style)是可組合的:

          <one-to-one name="person" cascade="persist,delete,lock"/>

          你可以使用cascade="all"來指定全部操作都順著關聯關系級聯(cascaded)。 默認值是cascade="none",即任何操作都不會被級聯(cascaded)。

          注意有一個特殊的級聯風格(cascade style) delete-orphan,只應用于one-to-many關聯,表明delete()操作 應該被應用于所有從關聯中刪除的對象。

          建議:

          • 通常在<many-to-one><many-to-many>關系中應用級聯(cascade)沒什么意義。 級聯(cascade)通常在 <one-to-one><one-to-many>關系中比較有用。

          • 如果子對象的壽命限定在父親對象的壽命之內,可通過指定cascade="all,delete-orphan"將其變為自動生命周期管理的對象(lifecycle object)

          • 其他情況,你可根本不需要級聯(cascade)。但是如果你認為你會經常在某個事務中同時用到父對象與子對象,并且你希望少打點兒字,可以考慮使用cascade="persist,merge,save-update"。

          可以使用cascade="all"將一個關聯關系(無論是對值對象的關聯,或者對一個集合的關聯)標記為父/子關系的關聯。 這樣對父對象進行save/update/delete操作就會導致子對象也進行save/update/delete操作。

          此外,一個持久的父對象對子對象的淺引用(mere reference)會導致子對象被同步save/update。 不過,這個隱喻(metaphor)的說法并不完整。除非關聯是<one-to-many>關聯并且被標記為cascade="delete-orphan", 否則父對象失去對某個子對象的引用不會導致該子對象被自動刪除。 父子關系的級聯(cascading)操作準確語義如下:

          • 如果父對象被persist(),那么所有子對象也會被persist()

          • 如果父對象被merge(),那么所有子對象也會被merge()

          • 如果父對象被save()update()saveOrUpdate(),那么所有子對象則會被saveOrUpdate()

          • 如果某個持久的父對象引用了瞬時(transient)或者脫管(detached)的子對象,那么子對象將會被saveOrUpdate()

          • 如果父對象被刪除,那么所有子對象也會被delete()

          • 除非被標記為cascade="delete-orphan"(刪除“孤兒”模式,此時不被任何一個父對象引用的子對象會被刪除), 否則子對象失掉父對象對其的引用時,什么事也不會發生。 如果有特殊需要,應用程序可通過顯式調用delete()刪除子對象。

          11.12. 使用元數據

          Hibernate中有一個非常豐富的元級別(meta-level)的模型,含有所有的實體和值類型數據的元數據。 有時這個模型對應用程序本身也會非常有用。 比如說,應用程序可能在實現一種“智能”的深度拷貝算法時, 通過使用Hibernate的元數據來了解哪些對象應該被拷貝(比如,可變的值類型數據), 那些不應該(不可變的值類型數據,也許還有某些被關聯的實體)。

          Hibernate提供了ClassMetadata接口,CollectionMetadata接口和Type層次體系來訪問元數據。 可以通過SessionFactory獲取元數據接口的實例。

          Cat fritz = ......;
          ClassMetadata catMeta = sessionfactory.getClassMetadata(Cat.class);
          Object[] propertyValues = catMeta.getPropertyValues(fritz);
          String[] propertyNames = catMeta.getPropertyNames();
          Type[] propertyTypes = catMeta.getPropertyTypes();
          // get a Map of all properties which are not collections or associations
          Map namedValues = new HashMap();
          for ( int i=0; i<propertyNames.length; i++ ) {
          if ( !propertyTypes[i].isEntityType() && !propertyTypes[i].isCollectionType() ) {
          namedValues.put( propertyNames[i], propertyValues[i] );
          }
          }
          }

          第 12 章 事務和并發

          Hibernate的事務和并發控制很容易掌握。Hibernate直接使用JDBC連接和JTA資源,不添加任何附加鎖定 行為。我們強烈推薦你花點時間了解JDBC編程,ANSI SQL查詢語言和你使用 的數據庫系統的事務隔離規范。Hibernate只添加自動版本管理,而不會鎖 定內存中的對象,也不會改變數據庫事務的隔離級別。基本上,使用 Hibernate就好像直接使用JDBC(或者JTA/CMT)來訪問你的數據庫資源。

          除了自動版本管理,針對行級悲觀鎖定,Hibernate也提供了輔助的API,它使用了 SELECT FOR UPDATE的SQL語法。本章后面會討論這個API。

          我們從Configuration層、SessionFactory層, 和 Session層開始討論Hibernate的并行控制、數據庫事務和應用 程序的長事務。

          12.1. Session和事務范圍(transaction scopes)

          一個SessionFactory對象的創建代價很昂貴,它是線程安全的對象,它被設計成可以 為所有的應用程序線程所共享。它只創建一次,通常是在應用程序啟動的時候,由一個 Configuraion的實例來創建。

          一個Session的對象是輕型的,非線程安全的,對于單個業務進程,單個的 工作單元而言,它只被使用一次,然后就丟棄。只有在需要的時候,Session 才會獲取一個JDBC的Connection(或一個Datasource) 對象。所以你可以放心的打開和關閉Session,甚至當你并不確定一個特定的請 求是否需要數據訪問時,你也可以這樣做。(一旦你實現下面提到的使用了請求攔截的模式,這就 變得很重要了。

          此外我們還要考慮數據庫事務。數據庫事務應該盡可能的短,降低數據庫鎖定造成的資源爭用。 數據庫長事務會導致你的應用程序無法擴展到高的并發負載。

          一個操作單元(Unit of work)的范圍是多大?單個的Hibernate Session能跨越多個 數據庫事務嗎?還是一個Session的作用范圍對應一個數據庫事務的范圍?應該何時打開 Session,何時關閉Session?,你又如何劃分數據庫事務的邊界呢?

          12.1.1. 操作單元(Unit of work)

          首先,別再用session-per-operation這種反模式了,也就是說,在單個線程中, 不要因為一次簡單的數據庫調用,就打開和關閉一次Session!數據庫事務也是如此。 應用程序中的數據庫調用是按照計劃好的次序,分組為原子的操作單元。(注意,這也意味著,應用程 序中,在單個的SQL語句發送之后,自動事務提交(auto-commit)模式失效了。這種模式專門為SQL控制臺操作設計的。 Hibernate禁止立即自動事務提交模式,或者期望應用服務器禁止立即自動事務提交模式。)

          在多用戶的client/server應用程序中,最常用的模式是 每個請求一個會話(session-per-request)。 在這種模式下,來自客戶端的請求被發送到服務器端(即Hibernate持久化層運行的地方),一 個新的Hibernate Session被打開,并且執行這個操作單元中所有的數據庫操作。 一旦操作完成(同時發送到客戶端的響應也準備就緒),session被同步,然后關閉。你也可以使用單 個數據庫事務來處理客戶端請求,在你打開Session之后啟動事務,在你關閉 Session之前提交事務。會話和請求之間的關系是一對一的關系,這種模式對 于大多數應用程序來說是很棒的。

          真正的挑戰在于如何去實現這種模式:不僅Session和事務必須被正確的開始和結束, 而且他們也必須能被數據訪問操作訪問。用攔截器來實現操作單元的劃分,該攔截器在客戶端請求達到服 務器端的時候開始,在服務器端發送響應(即,ServletFilter)之前結束。我們推薦 使用一個ThreadLocal 變量,把 Session綁定到處理客戶端請求的線 程上去。這種方式可以讓運行在該線程上的所有程序代碼輕松的訪問Session(就像訪問一 個靜態變量那樣)。你也可以在一個ThreadLocal 變量中保持事務上下文環境,不過這依賴 于你所選擇的數據庫事務劃分機制。這種實現模式被稱之為 ThreadLocal SessionOpen Session in View。你可以很容易的擴展本文前面章節展示的 HibernateUtil 輔助類來實現這種模式。當然,你必須找到一種實現攔截器的方法,并 且可以把攔截器集成到你的應用環境中。請參考Hibernate網站上面的提示和例子。

          12.1.2. 應用程序事務(Application transactions)

          session-per-request模式不僅僅是一個可以用來設計操作單元的有用概念。很多業務處理流程都需 要一系列完整的和用戶之間的交互,即用戶對數據庫的交叉訪問。在基于web的應用和企業 應用中,跨用戶交互的數據庫事務是無法接受的??紤]下面的例子:

          • 在界面的第一屏,打開對話框,用戶所看到的數據是被一個特定的 Session 和數據 庫事務載入(load)的。用戶可以隨意修改對話框中的數據對象。

          • 5分鐘后,用戶點擊“保存”,期望所做出的修改被持久化;同時他也期望自己是唯一修改這個信息的人,不會出現 修改沖突。

          從用戶的角度來看,我們把這個操作單元稱為應用程序長事務(application transaction)。 在你的應用程序中,可以有很多種方法來實現它。

          頭一個幼稚的做法是,在用戶思考的過程中,保持Session和數據庫事務是打開的, 保持數據庫鎖定,以阻止并發修改,從而保證數據庫事務隔離級別和原子操作。這種方式當然是一個反模式, 因為數據庫鎖定的維持會導致應用程序無法擴展并發用戶的數目。

          很明顯,我們必須使用多個數據庫事務來實現一個應用程序事務。在這個例子中,維護業務處理流程的 事務隔離變成了應用程序層的部分責任。單個應用程序事務通常跨越多個數據庫事務。如果僅僅只有一 個數據庫事務(最后的那個事務)保存更新過的數據,而所有其他事務只是單純的讀取數據(例如在一 個跨越多個請求/響應周期的向導風格的對話框中),那么應用程序事務將保證其原子性。這種方式比聽 起來還要容易實現,特別是當你使用了Hibernate的下述特性的時候:

          • 自動版本化 - Hibernate能夠自動進行樂觀并發控制 ,如果在用戶思考 的過程中發生并發修改沖突,Hibernate能夠自動檢測到。

          • 脫管對象(Detached Objects)- 如果你決定采用前面已經討論過的 session-per-request模式,所有載入的實例在用戶思考的過程 中都處于與Session脫離的狀態。Hibernate允許你把與Session脫離的對象重新關聯到Session 上,并且對修改進行持久化,這種模式被稱為 session-per-request-with-detached-objects。自動版本化被用來隔離并發修改。

          • 長生命周期的Session (Long Session)- Hibernate 的Session 可以在數據庫事務提交之后和底層的JDBC連接斷開,當一個新的客戶端請求到來的時候,它又重新連接上底層的 JDBC連接。這種模式被稱之為session-per-application-transaction,這種情況可 能會造成不必要的Session和JDBC連接的重新關聯。自動版本化被用來隔離并發修改。

          session-per-request-with-detached-objectssession-per-application-transaction 各有優缺點,我們在本章后面樂觀并發 控制那部分再進行討論。

          12.1.3. 關注對象標識(Considering object identity)

          應用程序可能在兩個不同的Session中并發訪問同一持久化狀態,但是, 一個持久化類的實例無法在兩個 Session中共享。因此有兩種不同的標識語義:

           

          數據庫標識

          foo.getId().equals( bar.getId() )

          JVM 標識

          foo==bar

           

          對于那些關聯到 特定Session (也就是在單個Session的范圍內)上的對象來說,這 兩種標識的語義是等價的,與數據庫標識對應的JVM標識是由Hibernate來保 證的。不過,當應用程序在兩個不同的session中并發訪問具有同一持久化標 識的業務對象實例的時候,這個業務對象的兩個實例事實上是不相同的(從 JVM識別來看)。這種沖突可以通過在同步和提交的時候使用自動版本化和樂 觀鎖定方法來解決。

           

          這種方式把關于并發的頭疼問題留給了Hibernate和數據庫;由于在單個線程內,操作單元中的對象識別不 需要代價昂貴的鎖定或其他意義上的同步,因此它同時可以提供最好的可伸縮性。只要在單個線程只持有一個 Session,應用程序就不需要同步任何業務對象。在Session 的范圍內,應用程序可以放心的使用==進行對象比較。

           

          不過,應用程序在Session的外面使用==進行對象比較可能會 導致無法預期的結果。在一些無法預料的場合,例如,如果你把兩個脫管對象實例放進同一個 Set的時候,就可能發生。這兩個對象實例可能有同一個數據庫標識(也就是說, 他們代表了表的同一行數據),從JVM標識的定義上來說,對脫管的對象而言,Hibernate無法保證他們 的的JVM標識一致。開發人員必須覆蓋持久化類的equals()方法和 hashCode() 方法,從而實現自定義的對象相等語義。警告:不要使用數據庫標識 來實現對象相等,應該使用業務鍵值,由唯一的,通常不變的屬性組成。當一個瞬時對象被持久化的時 候,它的數據庫標識會發生改變。如果一個瞬時對象(通常也包括脫管對象實例)被放入一 個Set,改變它的hashcode會導致與這個Set的關系中斷。雖 然業務鍵值的屬性不象數據庫主鍵那樣穩定不變,但是你只需要保證在同一個Set 中的對象屬性的穩定性就足夠了。請到Hibernate網站去尋求這個問題更多的詳細的討論。請注意,這不是一 個有關Hibernate的問題,而僅僅是一個關于Java對象標識和判等行為如何實現的問題。

           

          12.1.4. 常見問題

          決不要使用反模式session-per-user-session或者 session-per-application(當然,這個規定幾乎沒有例外)。請注意, 下述一些問題可能也會出現在我們推薦的模式中,在你作出某個設計決定之前,請務必理解該模式的應用前提。

          • Session 是一個非線程安全的類。如果一個Session 實例允許共享的話,那些支持并發運行的東東,例如HTTP request,session beans,或者是 Swing workers,將會導致出現資源爭用(race condition)。如果在HttpSession中有 Hibernate 的Session的話(稍后討論),你應該考慮同步訪問你的Http session。 否則,只要用戶足夠快的點擊瀏覽器的“刷新”,就會導致兩個并發運行線程使用同一個 Session。

          • 一個由Hibernate拋出的異常意味著你必須立即回滾數據庫事務,并立即關閉Session (稍后會展開討論)。如果你的Session綁定到一個應用程序上,你必 須停止該應用程序?;貪L數據庫事務并不會把你的業務對象退回到事務啟動時候的狀態。這 意味著數據庫狀態和業務對象狀態不同步。通常情況下,這不是什么問題,因為異常是不可 恢復的,你必須在回滾之后重新開始執行。

          • Session 緩存了處于持久化狀態的每個對象(Hibernate會監視和檢查臟數據)。 這意味著,如果你讓Session打開很長一段時間,或是僅僅載入了過多的數據, Session占用的內存會一直增長,直到拋出OutOfMemoryException異常。這個 問題的一個解決方法是調用clear()evict()來管理 Session的緩存,但是如果你需要大批量數據操作的話,最好考慮 使用存儲過程。在第 14 章 批量處理(Batch processing)中有一些解決方案。在用戶會話期間一直保持 Session打開也意味著出現臟數據的可能性很高。

          12.2. 數據庫事務聲明

          數據庫(或者系統)事務的聲明總是必須的。在數據庫事務之外,就無法和數據庫通訊(這可能會讓那些習慣于 自動提交事務模式的開發人員感到迷惑)。永遠使用清晰的事務聲明,即使只讀操作也是如此。進行 顯式的事務聲明并不總是需要的,這取決于你的事務隔離級別和數據庫的能力,但不管怎么說,聲明事務總歸有益無害。

          一個Hibernate應用程序可以運行在非托管環境中(也就是獨立運行的應用程序,簡單Web應用程序, 或者Swing圖形桌面應用程序),也可以運行在托管的J2EE環境中。在一個非托管環境中,Hibernate 通常自己負責管理數據庫連接池。應用程序開發人員必須手工設置事務聲明,換句話說,就是手工啟 動,提交,或者回滾數據庫事務。一個托管的環境通常提供了容器管理事務,例如事務裝配通過可聲 明的方式定義在EJB session beans的部署描述符中??删幊淌绞聞章暶鞑辉傩枰?,即使是 Session 的同步也可以自動完成。

          讓持久層具備可移植性是人們的理想。Hibernate提供了一套稱為Transaction的封裝API, 用來把你的部署環境中的本地事務管理系統轉換到Hibernate事務上。這個API是可選的,但是我們強烈 推薦你使用,除非你用CMT session bean。

          通常情況下,結束 Session 包含了四個不同的階段:

          • 同步session(flush,刷出到磁盤)

          • 提交事務

          • 關閉session

          • 處理異常

          session的同步(flush,刷出)前面已經討論過了,我們現在進一步考察在托管和非托管環境下的事務聲明和異常處理。

          12.2.1. 非托管環境

          如果Hibernat持久層運行在一個非托管環境中,數據庫連接通常由Hibernate的連接池機制 來處理。session/transaction處理方式如下所示:

          //Non-managed environment idiom
          Session sess = factory.openSession();
          Transaction tx = null;
          try {
          tx = sess.beginTransaction();
          // do some work
          ...
          tx.commit();
          }
          catch (RuntimeException e) {
          if (tx != null) tx.rollback();
          throw e; // or display error message
          }
          finally {
          sess.close();
          }

          你不需要顯式flush() Session - 對commit()的調用會自動觸發session的同步。

          調用 close() 標志session的結束。 close()方法重要的暗示是,session釋放了JDBC連接。

          這段Java代碼是可移植的,可以在非托管環境和JTA環境中運行。

          你很可能從未在一個標準的應用程序的業務代碼中見過這樣的用法;致命的(系統)異常應該總是 在應用程序“頂層”被捕獲。換句話說,執行Hibernate調用的代碼(在持久層)和處理 RuntimeException異常的代碼(通常只能清理和退出應用程序)應該在不同 的應用程序邏輯層。這對于你設計自己的軟件系統來說是一個挑戰,只要有可能,你就應該使用 J2EE/EJB容器服務。異常處理將在本章稍后進行討論。

          請注意,你應該選擇 org.hibernate.transaction.JDBCTransactionFactory (這是默認選項).

          12.2.2. 使用JTA

          如果你的持久層運行在一個應用服務器中(例如,在EJB session beans的后面),Hibernate獲取 的每個數據源連接將自動成為全局JTA事務的一部分。Hibernate提供了兩種策略進行JTA集成。

          如果你使用bean管理事務(BMT),可以通過使用Hibernate的 Transaction API來告訴 應用服務器啟動和結束BMT事務。因此,事務管理代碼和在非托管環境下是一樣的。

          // BMT idiom
          Session sess = factory.openSession();
          Transaction tx = null;
          try {
          tx = sess.beginTransaction();
          // do some work
          ...
          tx.commit();
          }
          catch (RuntimeException e) {
          if (tx != null) tx.rollback();
          throw e; // or display error message
          }
          finally {
          sess.close();
          }

          在CMT方式下,事務聲明是在session bean的部署描述符中,而不需要編程。 除非你設置了屬性hibernate.transaction.flush_before_completionhibernate.transaction.auto_close_sessiontrue, 否則你必須自己同步和關閉Session。Hibernate可以為你自動同步和關閉 Session。你唯一要做的就是當發生異常時進行事務回滾。幸運的是, 在一個CMT bean中,事務回滾甚至可以由容器自動進行,因為由session bean方法拋出的未處理的 RuntimeException異??梢酝ㄖ萜髟O置全局事務回滾。這意味著 在CMT中,你完全無需使用Hibernate的Transaction API 。

          請注意,當你配置Hibernate事務工廠的時候,在一個BMT session bean中,你應該選擇 org.hibernate.transaction.JTATransactionFactory,在一個 CMT session bean中選擇org.hibernate.transaction.CMTTransactionFactory。 記住,同時也要設置org.hibernate.transaction.manager_lookup_class。

          如果你使用CMT環境,并且讓容器自動同步和關閉session,你可能也希望在你代碼的不同部分使用 同一個session。一般來說,在一個非托管環境中,你可以使用一個ThreadLocal 變量來持有這個session,但是單個EJB方法調用可能會在不同的線程中執行(舉例來說,一個session bean調用另一個session bean)。如果你不想在應用代碼中被傳遞Session對 象實例的問題困擾的話,那么SessionFactory 提供的 getCurrentSession()方法就很適合你,該方法返回一個綁定到JTA事務 上下文環境中的session實例。這也是把Hibernate集成到一個應用程序中的最簡單的方法!這個“當 前的”session總是可以自動同步和自動關閉(不考慮上述的屬性設置)。我們的session/transaction 管理代碼減少到如下所示:

          // CMT idiom
          Session sess = factory.getCurrentSession();
          // do some work
          ...
          

          換句話來說,在一個托管環境下,你要做的所有的事情就是調用 SessionFactory.getCurrentSession(),然后進行你的數據訪問,把其余的工作 交給容器來做。事務在你的session bean的部署描述符中以可聲明的方式來設置。session的生命周期完全 由Hibernate來管理。

          after_statement連接釋放方式有一個警告。因為JTA規范的一個很愚蠢的限制,Hibernate不可能自動清理任何未關閉的ScrollableResults 或者Iterator,它們是由scroll()iterate()產生的。你must通過在finally塊中,顯式調用ScrollableResults.close()或者Hibernate.close(Iterator)方法來釋放底層數據庫游標。(當然,大部分程序完全可以很容易的避免在CMT代碼中出現scroll()iterate()。)

          12.2.3. 異常處理

          如果 Session 拋出異常 (包括任何SQLException), 你應該立即回滾數據庫事務,調用 Session.close() ,丟棄該 Session實例。Session的某些方法可能會導致session 處于不一致的狀態。所有由Hibernate拋出的異常都視為不可以恢復的。確保在 finally 代碼塊中調用close()方法,以關閉掉 Session

          HibernateException是一個非檢查期異常(這不同于Hibernate老的版本), 它封裝了Hibernate持久層可能出現的大多數錯誤。我們的觀點是,不應該強迫應用程序開發人員 在底層捕獲無法恢復的異常。在大多數軟件系統中,非檢查期異常和致命異常都是在相應方法調用 的堆棧的頂層被處理的(也就是說,在軟件上面的邏輯層),并且提供一個錯誤信息給應用軟件的用戶 (或者采取其他某些相應的操作)。請注意,Hibernate也有可能拋出其他并不屬于 HibernateException的非檢查期異常。這些異常同樣也是無法恢復的,應該 采取某些相應的操作去處理。

          在和數據庫進行交互時,Hibernate把捕獲的SQLException封裝為Hibernate的 JDBCException。事實上,Hibernate嘗試把異常轉換為更有實際含義 的JDBCException異常的子類。底層的SQLException可以 通過JDBCException.getCause()來得到。Hibernate通過使用關聯到 SessionFactory上的SQLExceptionConverter來 把SQLException轉換為一個對應的JDBCException 異常的子類。默認情況下,SQLExceptionConverter可以通過配置dialect 選項指定;此外,也可以使用用戶自定義的實現類(參考javadocs SQLExceptionConverterFactory類來了解詳情)。標準的 JDBCException子類型是:

          • JDBCConnectionException - 指明底層的JDBC通訊出現錯誤

          • SQLGrammarException - 指明發送的SQL語句的語法或者格式錯誤

          • ConstraintViolationException - 指明某種類型的約束違例錯誤

          • LockAcquisitionException - 指明了在執行請求操作時,獲取 所需的鎖級別時出現的錯誤。

          • GenericJDBCException - 不屬于任何其他種類的原生異常

          12.3. 樂觀并發控制(Optimistic concurrency control)

          唯一能夠同時保持高并發和高可伸縮性的方法就是使用帶版本化的樂觀并發控制。版本檢查使用版本號、 或者時間戳來檢測更新沖突(并且防止更新丟失)。Hibernate為使用樂觀并發控制的代碼提供了三種可 能的方法,應用程序在編寫這些代碼時,可以采用它們。我們已經在前面應用程序長事務那部分展示了 樂觀并發控制的應用場景,此外,在單個數據庫事務范圍內,版本檢查也提供了防止更新丟失的好處。

          12.3.1. 應用程序級別的版本檢查(Application version checking)

          未能充分利用Hibernate功能的實現代碼中,每次和數據庫交互都需要一個新的 Session,而且開發人員必須在顯示數據之前從數據庫中重 新載入所有的持久化對象實例。這種方式迫使應用程序自己實現版本檢查來確保 應用程序事務的隔離,從數據訪問的角度來說是最低效的。這種使用方式和 entity EJB最相似。

          // foo is an instance loaded by a previous Session
          session = factory.openSession();
          Transaction t = session.beginTransaction();
          int oldVersion = foo.getVersion();
          session.load( foo, foo.getKey() ); // load the current state
          if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();
          foo.setProperty("bar");
          t.commit();
          session.close();

          version 屬性使用 <version>來映射,如果對象 是臟數據,在同步的時候,Hibernate會自動增加版本號。

          當然,如果你的應用是在一個低數據并發環境下,并不需要版本檢查的話,你照樣可以使用 這種方式,只不過跳過版本檢查就是了。在這種情況下,最晚提交生效last commit wins)就是你的應用程序長事務的默認處理策略。 請記住這種策略可能會讓應用軟件的用戶感到困惑,因為他們有可能會碰上更新丟失掉卻沒 有出錯信息,或者需要合并更改沖突的情況。

          很明顯,手工進行版本檢查只適合于某些軟件規模非常小的應用場景,對于大多數軟件應用場景 來說并不現實。通常情況下,不僅是單個對象實例需要進行版本檢查,整個被修改過的關 聯對象圖也都需要進行版本檢查。作為標準設計范例,Hibernate使用長生命周期 Session的方式,或者脫管對象實例的方式來提供自動版本檢查。

          12.3.2. 長生命周期session和自動版本化

          單個 Session實例和它所關聯的所有持久化對象實例都被用于整個 應用程序事務。Hibernate在同步的時候進行對象實例的版本檢查,如果檢測到并發修 改則拋出異常。由開發人員來決定是否需要捕獲和處理這個異常(通常的抉擇是給用戶 提供一個合并更改,或者在無臟數據情況下重新進行業務操作的機會)。

          在等待用戶交互的時候, Session 斷開底層的JDBC連接。這種方式 以數據庫訪問的角度來說是最高效的方式。應用程序不需要關心版本檢查或脫管對象實例 的重新關聯,在每個數據庫事務中,應用程序也不需要載入讀取對象實例。

          // foo is an instance loaded earlier by the Session
          session.reconnect(); // Obtain a new JDBC connection
          Transaction t = session.beginTransaction();
          foo.setProperty("bar");
          t.commit(); // End database transaction, flushing the change and checking the version
          session.disconnect(); // Return JDBC connection 

          foo 對象始終和載入它的Session相關聯。 Session.reconnect()獲取一個新的數據庫連接(或者 你可以提供一個),并且繼續當前的session。Session.disconnect() 方法把session與JDBC連接斷開,把數據庫連接返回到連接池(除非是你自己提供的數據 庫連接)。在Session重新連接上數據庫連接之后,你可以對任何可能被其他事務更新過 的對象調用Session.lock(),設置LockMode.READ 鎖定模式,這樣你就可以對那些你不準備更新的數據進行強制版本檢查。此外,你并不需要 鎖定那些你準備更新的數據。

          假若對disconnect()reconnect()的顯式調用發生得太頻繁了,你可以使用hibernate.connection.release_mode來代替。

          如果在用戶思考的過程中,Session因為太大了而不能保存,那么這種模式是有 問題的。舉例來說,一個HttpSession應該盡可能的小。由于 Session是一級緩存,并且保持了所有被載入過的對象,因此 我們只應該在那些少量的request/response情況下使用這種策略。而且在這種情況下, Session 里面很快就會有臟數據出現,因此請牢牢記住這一建議。

          此外,也請注意,你應該讓與數據庫連接斷開的Session對持久層保持 關閉狀態。換句話說,使用有狀態的EJB session bean來持有Session, 而不要把它傳遞到web層(甚至把它序列化到一個單獨的層),保存在HttpSession中。

          12.3.3. 脫管對象(deatched object)和自動版本化

          這種方式下,與持久化存儲的每次交互都發生在一個新的Session中。 然而,同一持久化對象實例可以在多次與數據庫的交互中重用。應用程序操縱脫管對象實例 的狀態,這個脫管對象實例最初是在另一個Session 中載入的,然后 調用 Session.update(),Session.saveOrUpdate(), 或者 Session.merge() 來重新關聯該對象實例。

          // foo is an instance loaded by a previous Session
          foo.setProperty("bar");
          session = factory.openSession();
          Transaction t = session.beginTransaction();
          session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already
          t.commit();
          session.close();

          Hibernate會再一次在同步的時候檢查對象實例的版本,如果發生更新沖突,就拋出異常。

          如果你確信對象沒有被修改過,你也可以調用lock() 來設置 LockMode.READ(繞過所有的緩存,執行版本檢查),從而取 代 update()操作。

          12.3.4. 定制自動版本化行為

          對于特定的屬性和集合,通過為它們設置映射屬性optimistic-lock的值 為false,來禁止Hibernate的版本自動增加。這樣的話,如果該屬性 臟數據,Hibernate將不再增加版本號。

          遺留系統的數據庫Schema通常是靜態的,不可修改的?;蛘撸渌麘贸绦蛞部赡茉L問同一數據 庫,根本無法得知如何處理版本號,甚至時間戳。在以上的所有場景中,實現版本化不能依靠 數據庫表的某個特定列。在<class>的映射中設置 optimistic-lock="all"可以在沒有版本或者時間戳屬性映射的情況下實現 版本檢查,此時Hibernate將比較一行記錄的每個字段的狀態。請注意,只有當Hibernate能夠比 較新舊狀態的情況下,這種方式才能生效,也就是說, 你必須使用單個長生命周期Session模式,而不能使用 session-per-request-with-detached-objects模式。

          有些情況下,只要更改不發生交錯,并發修改也是允許的。當你在<class> 的映射中設置optimistic-lock="dirty",Hibernate在同步的時候將只比較有臟 數據的字段。

          在以上所有場景中,不管是專門設置一個版本/時間戳列,還是進行全部字段/臟數據字段比較, Hibernate都會針對每個實體對象發送一條UPDATE(帶有相應的 WHERE語句 )的SQL語句來執行版本檢查和數據更新。如果你對關聯實體 設置級聯關系使用傳播性持久化(transitive persistence),那么Hibernate可能會執行不必 要的update語句。這通常不是個問題,但是數據庫里面對on update點火 的觸發器可能在脫管對象沒有任何更改的情況下被觸發。因此,你可以在 <class>的映射中,通過設置select-before-update="true" 來定制這一行為,強制Hibernate SELECT這個對象實例,從而保證, 在更新記錄之前,對象的確是被修改過。

          12.4. 悲觀鎖定(Pessimistic Locking)

          用戶其實并不需要花很多精力去擔心鎖定策略的問題。通常情況下,只要為JDBC連接指定一下隔 離級別,然后讓數據庫去搞定一切就夠了。然而,高級用戶有時候希望進行一個排它的悲觀鎖定, 或者在一個新的事務啟動的時候,重新進行鎖定。

          Hibernate總是使用數據庫的鎖定機制,從不在內存中鎖定對象!

          LockMode 定義了Hibernate所需的不同的鎖定級別。一個鎖定 可以通過以下的機制來設置:

          • 當Hibernate更新或者插入一行記錄的時候,鎖定級別自動設置為LockMode.WRITE。

          • 當用戶顯式的使用數據庫支持的SQL格式SELECT ... FOR UPDATE 發送SQL的時候,鎖定級別設置為LockMode.UPGRADE

          • 當用戶顯式的使用Oracle數據庫的SQL語句SELECT ... FOR UPDATE NOWAIT 的時候,鎖定級別設置LockMode.UPGRADE_NOWAIT

          • 當Hibernate在“可重復讀”或者是“序列化”數據庫隔離級別下讀取數據的時候,鎖定模式 自動設置為LockMode.READ。這種模式也可以通過用戶顯式指定進行設置。

          • LockMode.NONE 代表無需鎖定。在Transaction結束時, 所有的對象都切換到該模式上來。與session相關聯的對象通過調用update() 或者saveOrUpdate()脫離該模式。

          "顯式的用戶指定"可以通過以下幾種方式之一來表示:

          • 調用 Session.load()的時候指定鎖定模式(LockMode)

          • 調用Session.lock()。

          • 調用Query.setLockMode()。

          如果在UPGRADE或者UPGRADE_NOWAIT鎖定模式下調 用Session.load(),并且要讀取的對象尚未被session載入過,那么對象 通過SELECT ... FOR UPDATE這樣的SQL語句被載入。如果為一個對象調用 load()方法時,該對象已經在另一個較少限制的鎖定模式下被載入了,那 么Hibernate就對該對象調用lock() 方法。

          如果指定的鎖定模式是READ, UPGRADEUPGRADE_NOWAIT,那么Session.lock()就 執行版本號檢查。(在UPGRADE 或者UPGRADE_NOWAIT 鎖定模式下,執行SELECT ... FOR UPDATE這樣的SQL語句。)

          如果數據庫不支持用戶設置的鎖定模式,Hibernate將使用適當的替代模式(而不是扔出異常)。 這一點可以確保應用程序的可移植性。



          攔截器與事件(Interceptors and events)

          應用程序能夠響應Hibernate內部產生的特定事件是非常有用的。這樣就允許實現某些通用的功能 以及允許對Hibernate功能進行擴展。

          13.1.  攔截器(Interceptors)

          Interceptor接口提供了從會話(session)回調(callback)應用程序(application)的機制, 這種回調機制可以允許應用程序在持久化對象被保存、更新、刪除或是加載之前,檢查并(或)修改其 屬性。一個可能的用途,就是用來跟蹤審核(auditing)信息。例如:下面的這個攔截器,會在一個實現了 Auditable接口的對象被創建時自動地設置createTimestamp屬性,并在實現了 Auditable接口的對象被更新時,同步更新lastUpdateTimestamp屬性。

          package org.hibernate.test;
          import java.io.Serializable;
          import java.util.Date;
          import java.util.Iterator;
          import org.hibernate.Interceptor;
          import org.hibernate.type.Type;
          public class AuditInterceptor implements Interceptor, Serializable {
          private int updates;
          private int creates;
          public void onDelete(Object entity,
          Serializable id,
          Object[] state,
          String[] propertyNames,
          Type[] types) {
          // do nothing
          }
          public boolean onFlushDirty(Object entity,
          Serializable id,
          Object[] currentState,
          Object[] previousState,
          String[] propertyNames,
          Type[] types) {
          if ( entity instanceof Auditable ) {
          updates++;
          for ( int i=0; i < propertyNames.length; i++ ) {
          if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) {
          currentState[i] = new Date();
          return true;
          }
          }
          }
          return false;
          }
          public boolean onLoad(Object entity,
          Serializable id,
          Object[] state,
          String[] propertyNames,
          Type[] types) {
          return false;
          }
          public boolean onSave(Object entity,
          Serializable id,
          Object[] state,
          String[] propertyNames,
          Type[] types) {
          if ( entity instanceof Auditable ) {
          creates++;
          for ( int i=0; i<propertyNames.length; i++ ) {
          if ( "createTimestamp".equals( propertyNames[i] ) ) {
          state[i] = new Date();
          return true;
          }
          }
          }
          return false;
          }
          public void postFlush(Iterator entities) {
          System.out.println("Creations: " + creates + ", Updates: " + updates);
          }
          public void preFlush(Iterator entities) {
          updates=0;
          creates=0;
          }
          ...
          }

          創建會話(session)的時候可以指定攔截器。

          Session session = sf.openSession( new AuditInterceptor() );

          你也可以使用Configuration來設置一個全局范圍的攔截器。

          new Configuration().setInterceptor( new AuditInterceptor() );

          13.2.  事件系統(Event system)

          如果需要響應持久層的某些特殊事件,你也可以使用Hibernate3的事件框架。 該事件系統可以用來替代攔截器,也可以作為攔截器的補充來使用。

          基本上,Session接口的每個方法都有相對應的事件。比如 LoadEvent,FlushEvent,等等(查閱XML配置文件 的DTD,以及org.hibernate.event包來獲得所有已定義的事件的列表)。當某個方 法被調用時,Hibernate Session會生成一個相對應的事件并激活所 有配置好的事件監聽器。系統預設的監聽器實現的處理過程就是被監聽的方法要做的(被監聽的方法所做的其實僅僅是激活監聽器, “實際”的工作是由監聽器完成的)。不過,你可以自由地選擇實現 一個自己定制的監聽器(比如,實現并注冊用來處理處理LoadEventLoadEventListener接口), 來負責處理所有的調用Sessionload()方法的請求。

          監聽器應該被看作是單例(singleton)對象,也就是說,所有同類型的事件的處理共享同一個監聽器實例,因此監聽器 不應該保存任何狀態(也就是不應該使用成員變量)。

          用戶定制的監聽器應該實現與所要處理的事件相對應的接口,或者從一個合適的基類繼承(甚至是從Hibernate自帶的默認事件監聽器類繼承, 為了方便你這樣做,這些類都被聲明成non-final的了)。用戶定制的監聽器可以通過編程使用Configuration對象 來注冊,也可以在Hibernate的XML格式的配置文件中進行聲明(不支持在Properties格式的配置文件聲明監聽器)。 下面是一個用戶定制的加載事件(load event)的監聽器:

          public class MyLoadListener extends DefaultLoadEventListener {
          // this is the single method defined by the LoadEventListener interface
          public Object onLoad(LoadEvent event, LoadEventListener.LoadType loadType)
          throws HibernateException {
          if ( !MySecurity.isAuthorized( event.getEntityClassName(), event.getEntityId() ) ) {
          throw MySecurityException("Unauthorized access");
          }
          return super.onLoad(event, loadType);
          }
          }

          你還需要修改一處配置,來告訴Hibernate以使用選定的監聽器來替代默認的監聽器。

          <hibernate-configuration>
          <session-factory>
          ...
          <listener type="load" class="MyLoadListener"/>
          </session-factory>
          </hibernate-configuration>

          看看用另一種方式,通過編程的方式來注冊它。

          Configuration cfg = new Configuration();
          cfg.getSessionEventListenerConfig().setLoadEventListener( new MyLoadListener() );

          通過在XML配置文件聲明而注冊的監聽器不能共享實例。如果在多個<listener/>節點中使用 了相同的類的名字,則每一個引用都將會產生一個獨立的實例。如果你需要在多個監聽器類型之間共享 監聽器的實例,則你必須使用編程的方式來進行注冊。

          為什么我們實現了特定監聽器的接口,在注冊的時候還要明確指出我們要注冊哪個事件的監聽器呢? 這是因為一個類可能實現多個監聽器的接口。在注冊的時候明確指定要監聽的事件,可以讓啟用或者禁用對某個事件的監聽的配置工作簡單些。

          13.3.  Hibernate的聲明式安全機制

          通常,Hibernate應用程序的聲明式安全機制由會話外觀層(session facade)所管理。 現在,Hibernate3允許某些特定的行為由JACC進行許可管理,由JAAS進行授權管理。 本功能是一個建立在事件框架之上的可選的功能。

          首先,你必須要配置適當的事件監聽器(event listener),來激活使用JAAS管理授權的功能。

          <listener type="pre-delete" class="org.hibernate.secure.JACCPreDeleteEventListener"/>
          <listener type="pre-update" class="org.hibernate.secure.JACCPreUpdateEventListener"/>
          <listener type="pre-insert" class="org.hibernate.secure.JACCPreInsertEventListener"/>
          <listener type="pre-load" class="org.hibernate.secure.JACCPreLoadEventListener"/>

          接下來,仍然在hibernate.cfg.xml文件中,綁定角色的權限:

          <grant role="admin" entity-name="User" actions="insert,update,read"/>
          <grant role="su" entity-name="User" actions="*"/>

          這些角色的名字就是你的JACC provider所定義的角色的名字。

          posted on 2007-05-15 15:09 ???MengChuChen 閱讀(1947) 評論(0)  編輯  收藏 所屬分類: hibernate
          主站蜘蛛池模板: 商丘市| 周至县| 普兰县| 林西县| 大渡口区| 龙里县| 松原市| 泰州市| 新宁县| 江孜县| 昌吉市| 苗栗市| 东丰县| 合肥市| 宝丰县| 望城县| 祁门县| 禹城市| 安阳县| 天台县| 响水县| 南康市| 航空| 宝鸡市| 桐乡市| 沙田区| 鹤山市| 繁峙县| 深圳市| 长治市| 大埔县| 定州市| 濮阳县| 大关县| 顺义区| 临漳县| 永安市| 铜山县| 万州区| 陆良县| 泸溪县|