low level programmer

Main | Next page »
星期四 八月 21, 2008

測試用的 GeneralEAO, EntityManagerHelper

雖然不知道這樣算不算 GeneralEAO, 不過這兩個 class 已經寫好幾次拿來做測試了, 很方便用.
到後來乾脆直接摳來摳去, 反正要測試的時候就可以用, 看習慣.
因為很方便, 所以記一下, 不用以後又重寫. 順便如果有不足的再改.

首先是 ThreadLocal 的 EntityManagerHelper

public class EntityManagerHelper {

    private static final ThreadLocal<EntityManager> threadLocal = new ThreadLocal<EntityManager>();
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    
    public static EntityManager getEntityManager() {
        EntityManager em = threadLocal.get();
        if ( em == null ) {
            em = emf.createEntityManager();
            threadLocal.set(em);
        }
        return em;
    }
    
}
然後是 GeneralEAO
public class GeneralEAO {

    private static final Logger logger = Logger.getLogger(GeneralEAO.class);

    public synchronized static  void create(T t) {
        logger.info("create " + t);
        EntityManager em = EntityManagerHelper.getEntityManager();
        em.getTransaction().begin();
        em.persist(t);
        em.getTransaction().commit();
    }

    public synchronized static  T find(Class cls, Object key) {
        logger.info("find " + cls + " where id = " + key);
        return (T) EntityManagerHelper.getEntityManager().find(cls, key);
    }

    public synchronized static  void update(T t) {
        logger.info("update " + t );
        EntityManager em = EntityManagerHelper.getEntityManager();
        em.getTransaction().begin();
        em.merge(t);
        em.getTransaction().commit();
    }

    public synchronized static  void updateCommit(Query q) {
        logger.info("update with query " + q );
        EntityManager em = EntityManagerHelper.getEntityManager();
        em.getTransaction().begin();
        q.executeUpdate();
        em.getTransaction().commit();
    }

    public synchronized static  void delete(T t) {
        logger.info("delete " + t );
        EntityManager em = EntityManagerHelper.getEntityManager();
        em.getTransaction().begin();
        em.remove( em.merge(t) );
        em.getTransaction().commit();
    }

    public synchronized static  T undo(T t) {
        logger.info("undo " + t );
        EntityManager em = EntityManagerHelper.getEntityManager();
        t = em.merge(t);
        em.refresh( t );
        return t;
    }

    public synchronized static Query namedQuery(String name) {
        logger.info("namedQuery " + name );
        return EntityManagerHelper.getEntityManager().createNamedQuery(name);
    }

    public synchronized static Query query(String queryString) {
        logger.info("query: " + queryString );
        return EntityManagerHelper.getEntityManager().createQuery(queryString);
    }

    public synchronized static void detachAll() {
        logger.info("detach all");
        EntityManagerHelper.getEntityManager().clear();
    }

}

星期六 八月 09, 2008

SCBCD5 passed

啊? 過了? 拿到成績單還有點不敢相信.

這兩天因為考試券要到期了, 上網找看有沒有考題,
好不容易找到一份看起來還不錯的下載來看完.
裡面用 EJB3 規格書出了大概一百二十題 (結果只考一題 XD).
藉由這份考題讓我有機會去多看一下規格書,
才覺得 EJB3 in Action 完全是作者特地將 EJB3 的部份用簡單的話來描述,
實際上 EJB3 包含的東西更多更廣.
隨著看考題練習也愈來愈感覺到自己學習到的部份只是九牛一毛,
九牛一毛我還忘掉大半才慘.

EJB3 範圍這麼大, 要濃縮成 61 題選擇真的是很難.
所以裡面都在考觀念, 光是觀念就考不完了, 細節沒考很多.
所以在忘記大半的情況下, 還是用推想怎樣才合理的方式過了.

嗯...就這樣

星期五 八月 08, 2008

EJB3 - Tuning

description

屬於效能調整, 不過因為很多部份都要看 vender 有無支援, 而我對要看 vender 支援的部分暫時沒興趣, 所以一些地方就跳過.

reference

EJB3 in Action - CH13 - Taming wild EJBs : performance and scalability

entity lock

  • 在 concurrent 系統中有幾個議題 (T1, T2 表示 transaction 1, transaction 2..)
    1. Dirty read: T1 讀取資料並修改後, 在 commit 前 T2 讀取修改過的資料, 結果 T1 決定 rollback, 使得 T2 取得的資料成為並不存在的資料.
    2. Nonrepeatable read: T1 取得一筆資料準備做處理, 結果同時 T2 卻把同一筆資料刪除了, 這時候 T1 要做的修改導致錯誤發生
    3. Phantom read: T1 做一次 query, T2 改了資料, T1 再做一次 query 得到與第一次 query 不同的結果.
  • 在高度 transactional system 必須有 lock 的機制確保一個 user 不會影響其他 user 的資料. 所以就有了 pessimistic locking 和 optimistic locking.
    1. pessimistic locking : JPA 沒規定 vender 要支援 pessimistic locking.
    2. pessimistic locking user 可能用兩種 lock : write 與 read.
    3. 當 user 用 write lock 時會不允許其他人 read, update, delete 該筆被 lock 住的資料.
    4. read lock 則允許其他人 read 但不能 update, delete.
    5. optimistic locking : JPA 支援透過 version 欄位作 optimistic locking
    6. optimistic locking 的作法是在取得資料後, 處理的是取出資料的備份, 等改完要存之前再檢查資料有沒有被改過, 沒改過就 commit, 否則丟 exception (OptimisticLockingException).
    7. optimistic locking 的實做方式通常是增加一個 version 欄位, 用數字或 timestamp 的方式判斷是否被改過. 數字型態的 version 欄位是比較常用也合理的.
    8. JPA spec 沒規定在無 version 欄位的情況下還要支援 optimistic locking
    9. 用了 version 欄位, 在 update 的時候會自動增加 version 值, provider 也會在更新前檢查是否目前準備更新的資料已經過時了, 如果 version 已經是 1 但要 update 的還是 0 就會 OptimisticLockException
    10. private Long version;
      @Version
      @Column(name="VERSION")
      public Long getVersion() {
          return version;
      }
      
      public void setVersion(Long version) {
          this.version = version;
      }            
              
    11. EntityManager 有 lock method, 如果沒有 version 欄位就不能保證 vender 如何做到 optimistic locking, 如果不能 optimistic, lock 的時候會出現 PersistenceException
    12. EntityManager.lock 會自動轉成 database lock.
    13. 使用 LockModeType.READ 能確保不會有 dirty read 或 nonrepeatable read.
    14. 使用 LockModeType.WRITE 確保其他人不能對 entity 執行 UPDATE 或 DELETE 的操作, 包括其他 transaction 也一樣. 如果其他 user 試圖 UPDATE DELETE lock 住的 entity 會出現 OptimisticLockException
  • Entity 的效能
  • 合併 table : 比方說 BillingInfo 內有一個 @OneToOne 的 Address, 這樣會有兩個 table, 每次讀都要 join, 所以就把 BillingInfo 內的 Address 改為 @Embedded, 然後 Address 改為 @Embeddable. 在實際 table 中把兩個 entity 的欄位放在一起就不用多的 join 了.
  • 分開 table : 比方說 Item 裡面有一個 picture 的 @Lob 欄位, 雖然說 @Lob 可加上 @Basic 設定為 lazy, 但由於 @Lob 的 lazy 效果不是每個 provider 都有 (我測試連 hibernate 也沒有預設支援, 好像要設定), 所以就可以把 @Lob 的欄位分開到另一個 table, 然後用 @OneToOne(fetch=LAZY) 的方式處理. 這樣可以不用每次去設定太多每個 provider 的特別設定, 也因為 @OneToOne 等的 lazy 效果是比較多 provider 支援的而能達到好好控制 @Lob lazy 效果的目的.
  • 處理繼承關係 entity 的時候, 使用 SINGLE_TABLE 的 strategy 是效能最好的, 因為不用 join. (不過也要注意如果繼承的 class 多, 相差異的 column 又多會有很多 null value 的情形.)
    @Entity
    @Inheritance(strategy=InheritanceType.SINGLE_TABLE)
    @DiscriminatorColumn(name="EMPLOYEE_TYPE", discriminatorType=DiscriminatorType.STRING)
    public abstract class Employee implements Serializable {
    
        private static final long serialVersionUID = 1L;
        private Long id;
        private String name;
        private String type;
        
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
    }
    
    @Entity
    @DiscriminatorValue(value="P")
    public class Programmer extends Employee implements Serializable {
    
        private static final long serialVersionUID = 1L;
        private String programmerDesciption;
        
        public String getProgrammerDesciption() {
            return programmerDesciption;
        }
    
        public void setProgrammerDesciption(String programmerDesciption) {
            this.programmerDesciption = programmerDesciption;
        }
    }
    
    @Entity
    @DiscriminatorValue(value="S")
    public class Sales extends Employee implements Serializable {
    
        private static final long serialVersionUID = 1L;
        private String salesDescription;
    
        public String getSalesDescription() {
            return salesDescription;
        }
    
        public void setSalesDescription(String salesDescription) {
            this.salesDescription = salesDescription;
        }
        
    }
    
            
  • JDBC layer tuning
    1. 記得調整 connection pool size
    2. 設定 catch statement : 可在 data-source 下設定不同 provider 的 catch statement.
    3. 在設定 catch statement 之後要記得使用 parameter binding 才能得到 catch statement 的好處
      沒有 parameter binding
      private void queryAllEmployee() {
          String name = "PDP";
          Query q = GeneralEAO.query("from Employee e where e.name = '" + name + "'");
          for (Object object : q.getResultList()) {
              System.out.println(object);
          }
      }                
                  
      有 parameter binding
      private void queryAllEmployee() {
          String name = "PDP";
          Query q = GeneralEAO.query("from Employee e where e.name = :name");
          q.setParameter("name", name);
          for (Object object : q.getResultList()) {
              System.out.println(object);
          }
      }
                  
    4. 使用 named query : 因為 named query 會先 prepare 一次然後重複使用. 我覺得可以想像在處理 query string 的時候, 第一件事就是去 parse query string 轉成 JDBC statement, 這個 parse 的動作會很花成本, 所以與其用 dynamic query string 每次去 parse, 還不如一次就 parse 好再重複利用.
      @Entity
      @NamedQueries({@NamedQuery(name="findAllEmployee", query="from Employee")})
      @Inheritance(strategy=InheritanceType.SINGLE_TABLE)
      @DiscriminatorColumn(name="EMPLOYEE_TYPE", discriminatorType=DiscriminatorType.STRING)
      public abstract class Employee implements Serializable {
      
      private void queryAllEmployee() {
          Query q = GeneralEAO.namedQuery("findAllEmployee");
          for (Object object : q.getResultList()) {
              System.out.println(object);
          }
      }
      
      public static Query namedQuery(String name) {
          return EntityManagerHelper.getEntityManager().createNamedQuery(name);
      }
                  
    5. 避免在 read only 的時候使用 transaction : 因為 transaction 管理是很麻煩的, 所以 transaction 的工作愈少愈好, 例如 readonly 如 query 的動作可以不用 transaction 就不要用. (可在 read only 的 method 中 @TransactionAttribute 設定為 NOT_REQUIRED)
  • 減少 DB 操作
    1. 選擇 fetch mode : 使用 lazy 會有多個 select statement, 好處是能在需要的時候再取得 entity; 使用 eager 不會多個 select statement 而是有 join, 不過可能有很多不需要的資訊. 雖然 @OneToMany, @ManyToMany 通常是 LAZY; @ManyToOne, @OneToOne 通常是 EAGER, 但可能你 @OneToOne 的欄位真的很少用到, 那就可以設定成 lazy, 而 @OneToMany 的欄位每次都會用到, 就可以設定成 EAGER. 不過還是要小心就是了. 有一種情形是你可以在 entity 中設定為 LAZY, 但是因為你真的需要那個 LAZY 的屬性, 就可以在 query string 加上 join fetch 直接在 query 的時候就先幫你把 lazy 屬性設定進去
      private void testFetchQuery( Object calendarId ) {
          Query q = GeneralEAO.query("select c from XCalendar c join fetch c.calendarLob join fetch c.calendarDetails where c.id = :cid");
          q.setParameter("cid", calendarId);
          XCalendar c = (XCalendar) q.getSingleResult();
          GeneralEAO.detachAll();
          System.out.println(c.getCalendarLob());
          for (XCalendarDetail xCalendarDetail : c.getCalendarDetails()) {
              System.out.println(xCalendarDetail.getName());
          }
      }
                  
    2. 把 database update 延到 transaction 結束之前才做
      1. 預設上 flush model 為 AUTO, 會在transaction 結束時或 query 執行之前 flush 確保 query 的結果都是最新的.
      2. 透過呼叫 EntityManager.flush 可提前對 datebase update, 這樣會造成多餘的 DB 操作, 所以建議除非別無選擇, 否則別使用 flush.
      3. 呼叫 EntityManager.setFlushMode( FlushModeType.COMMIT ) 可將 flush mode 改為 COMMIT, 就是不會在 query 之前先 flush, 不過這樣會導致 query 的結果並非最新的資料.
      4. 也可以在執行 query 之前才把 flush mode 改為 COMMIT. (不過其實不太清楚這樣做有什麼好處 ?)
    3. 使用最低的 lock 模式 : 即使 provider 有提供服務也不要用 pessmisitic locking (除非真的需要), 而 optimistic locking 也用 read locking (選一個最低的 lock) 就好.
    4. 適當使用 DELETE_CASCADE (用 DB DELETE CASCADE CONSTRAINT) : @OneToMany(cascade=CascadeType.REMOVE) 的時候可以在 master 刪除時也將 detail 刪除, 不過這是比較耗效能的. 其實在很多 DB 中可以設定 DELETE CASCADE 的 CONSTRAINT, 使用這樣的 CONSTRAINT 能夠增加很多效能.
    5. 適當使用 cascade 屬性 : cascade 可設定為 ALL, PERSIST, MERGE... , 如果沒設定好會導致許多不必要的 SQL statement. (比方說永遠不會被 update 到的卻每次因為相關的 entity 更新了而一起 update.)
    6. Bulk updates :
      當一次要 update 多筆資料時, 可能會採取以下作法
      public void test() {       
          String name = "TESTNAME";
          List< Employee > list = eao.query("from Employee").getResultList();
          for (Employee employee : list) {
              employee.setName(name);
              eao.update(employee);
          }
      }
                   
      可是這種作法會導致很多個 update SQL statement.
      比較好的作法是一次就 update 完.
      public void test() {
          String name = "TESTNAME";
          Query q = eao.query("update Employee e set e.name = :name");
          q.setParameter("name", name);
          q.executeUpdate();
      }
                   
    7. 使用 @JoinTable 可建立維持關聯性的 table, 使用這種 table 需要透過額外的 SQL statement 來 join. 如果不是必須的, 避免使用維持關聯性的 table 能增進效能.
  • query 效能
    1. 不要 full-table scan. 這大家都知道吧, 不要 select * from table, 要加 where 或其他的篩選條件
    2. 在 DB 適當欄位加 index, 這應該也是大家都知道.
      1. 如果會 from Item i where i.name = :name, 就在 name 欄位設 index.
      2. 在 relationship fields 加 index. from ItemB b join ItemA a where a.itemId = b.itemId 就在 ItemB.itemId 設 index
      3. 透過 @OrderBy 可在搜尋資料的時候照自己要的順序取得資料, 如下所示
        @OneToMany(cascade=CascadeType.ALL, mappedBy="owner")
        @OrderBy(value="price asc")
        public List< Certification > getCerts() {
            if ( certs == null ) {
                certs = new ArrayList< Certification >();
            }
            return certs;
        }
                            
        這樣在 SQL statement 可建立 order by certs0_.price asc 的結果. 可是 order by 的效果很花資源, 所以如果不是必需的就不要用 @OrderBy, 或是情況允許就在 order by 的欄位加上 index.
    3. 在 JPQL 的 where 中使用 function 時要注意, 一旦使用了 function, DB 就不會使用 indexed scan. 所以要盡量避免在 where 使用 function. 以 upper function 來說, 如果每次比較都需要用 upper, 那可以考慮在存的時候就先變大寫再存
  • Caching : Caching 不是萬靈丹, 用不好反而導致效率差, 所以使用前要先了解 Caching 的機制
    1. EJB3 JPA 的 caching type 分為三類 :
      1. Transactional Cache : 由 persistence provider 提供在 transaction 中的 cache 以減少 DB round trip
      2. Extended Persistence Context : 給 stateful session bean 使用的 cache 機制
      3. Persistence Unit Level Cache : 在所有 client 間共用的 caching
    2. Transaction cache 用在當 cache 中同一個物件被 request 時會直接回傳. 例如一個 entity 被 query 過後就留在 transaction cache 中, 此時 EntityManager.find 要找同個 object 就直接回傳
    3. 一個 transactional cache 的好處�����所���對 entity 的 update 會延遲到 transaction 結束的時候才執行.
    4. transactional cache 只在一個 transaction 有效, 可是當需要在不同 method call 間維持 state, 就會有很多 round-trip 只為了存取同一個 entity. 這時候就可以使用 extended persistence context 來避免這種情形. 這也是 stateful session bean 適用的 cache. 這種 cache 讓對 entity 的操作會一直延遲到 persistence context 結束
    5. 使用 extended cache 可在 stateful session bean 中 @TransactionAttribute 設定為 NOT_REQUIRED, 然後在 @Remove method 設定為 REQUIRED 就可以讓 entity 只在 @Remove 的時候 update DB.
    6. transactional cache 與 persistence context caching 都是只針對單一 client, 但真的對效能有差的常是不同 client 間的 entity 分享. 這就是 PersistenceUnit cache. 要使用這個 cache 就要去看 provider 的文件, 大部分 PersistenceUnit cache 的用法就是要使用此 cache 的效果就是透過 EntityManager.find 的方式取得 entity, 這是因為 provider 應該用 identity 做 cache, 除此之外就是要在程式或設定檔裡面設定 cache 的數量.
    7. cacheing best practice 之一是使用 read-only entity, 就是評估 AP 中的 entity 哪些可作為 read-only entity 然後註冊為 read-only entity, 可惜的是 read-only entity 與 cache 機制一樣都是看 provider 有沒有做. 通常 read-only entity 會被載入 PersistenceUnit cache 然後就不再變動.
    8. cache 適合用在很少變動的 entity 上, 注意確保 table data 只由 cache 的 AP 更新. 因為一旦有外部 AP 更新 cache 裡面擁有的資料, cache 處理的就會變成舊資料 (就變 garbage 了).
    9. 檢查 provider 如何支援 entity 與 query 的 cache, 因為每個 provider cache 的支援方式不同.
    10. 測試使用 cache 前後的不同再決定如何使用 cache.
  • EJB3 components performance
    1. 只要能使用 @Local 就不要用 @Remote, 因為 @Remote 是 RMI call, 耗資源. (不過注意 GlassFish V.2 的 stateful session bean 只能是 @Remote)
    2. 只在必要的時候使用 stateful session bean.
    3. 對 EJB component (特別是 Remote session bean)的多個 method call 改成 facade.
    4. 如果用不到 transaction, 就把 @TransactionAttribute 改為 NOT_SUPPORTED
    5. 調整 session bean pool(包括 MDB) 不太多也不太少
    6. 記得在 stateful session bean 加上 @Remove method.
    7. stateful session bean 在 passivate 的時候會把物件 serialize, 如果 serialize 的物件太大會消耗太多 CPU 與記憶體. 可以把不用 serialize 的變數宣告為 transient
    8. 包括 MDB 也是, 用 @PostConstruct 與 @PreDestroy 建立與清除佔資源的物件
  • 星期二 八月 05, 2008

    只用 transactional cache 可讀 stale data 的方式

    description

    使用 transactional cache 是很平常的事情, Hibernate 也預設支援的樣子 (因為我都沒設定就有效果).
    可是晚上練習的時候練習到一種可以讀取 stale data 的方式.
    主要就是先起一個 thread A 等著 update entity, 等 thread B 將 entity 起始, detach, 再 query 而記在 transactional cache 後再由 thread A update.
    等 thread A update 完後再由 thread B 用 EntityManager.find 取得資料, 這時候就發現因為 transactional cache 減少 DB round trip 的關係而讀到 cache 住的資料, 也是舊資料.
    為了確定資料的正確性, 還在程式結束之後看 DB 內容, 的確是被 thread A update 過的資料沒錯.
    由此可見 JPA 真的是一個需要非常透徹了解, 設計, 測試後再使用的東西.
    否則你以為每個 EntityManager.find 都有 cache 而取得減少 round trip 的好處, 事實上你的資料根本都錯了.
    對於每一個讀取資料的 method, 都要非常注意你的資料是否有任何可能被其他人修改, 只要有可能, 大該都需要重新撈一次 DB 吧. (不過當然還有 lock 機制在, 反正都要考慮到)
    想到 ORM 的 cache 可不止 transactional cache, 例如 Persistencen Unit Level Cache, 可是記住 Persistence Unit Level Cache 並不是 JPA 規定支援的, 所以 provider 要不要做還很難說, 而且覺得實務上對一個 DB 用了多個 persistence unit 的情況還不少.
    小心切記!

    codes

    public class TestMain {
        
        private static final Logger logger = Logger.getLogger(TestMain.class.getName());
        
        public static void main(String[] args) {
            TestMain test = new TestMain();
            test.test();
        }
        
        boolean cached = false;
        boolean cachedAndUpdated = false;
        
        private void test() {
            Long id = prepareData();
            
            // new thread to wait variable cached, if true then update Employee
            newThreadToUpdateEmployee( id ); 
            
            // query will trigger transactional cache
            queryAll(); 
            
            // now can see there is transactional cache and have no DB round trip
            findEmployee( id ); 
            
            // let Update-Employee thread to update Employee
            logger.info( "Set up cached to true." );
            cached = true; 
            logger.info( "Wait for cachedAndUpdated.." );
            
            // Update-Employee will set to true after Employee updated, then find Employee again
            while ( true ) {
                if ( cachedAndUpdated ) { 
                    logger.info( "cachedAndUpdated is true.." );
                    // now it's stale data
                    logger.info( GeneralEAO.retrieve(Employee.class, id).toString() );
                    break;
                }
            }
            
            // This way can read correct data
            GeneralEAO.detachAll();
            logger.info(GeneralEAO.retrieve(Employee.class, id).toString());
        }
        
        private void newThreadToUpdateEmployee(final Long id) {
            new Thread( "Update-Employee" ) {
                @Override
                public void run() {
                    while ( true ) {
                        if ( cached ) {
                            logger.info("cached, start find..");
                            Employee e = GeneralEAO.retrieve(Employee.class, id);
                            e.setName("TESTESTEST");
                            GeneralEAO.update(e);
                            GeneralEAO.transactionCommit();
                            cachedAndUpdated = true;
                            break;
                        }
                    }
                }
            }.start();
        }
        
        private void findEmployee(Long id) {
            logger.info( "start findEmployee.." );
            logger.info( GeneralEAO.retrieve(Employee.class, id).toString() );
        }
        
        private void queryAll() {
            logger.info( "start queryAll.." );
            Query q = GeneralEAO.query("from Employee");
            for (Object object : q.getResultList()) {
                logger.info(object.toString());
            }
        }
        
        private Long prepareData() {
            Employee e = new Employee();
            e.setName("Rock");
            
            Certification scjp = new Certification();
            scjp.setName("scjp");
            scjp.setPrice(Double.valueOf(Math.random() * 100));
            scjp.setOwner(e);
    
            Certification scwcd = new Certification();
            scwcd.setName("scwcd");
            scwcd.setPrice(Double.valueOf(Math.random() * 100));
            scwcd.setOwner(e);
            
            Certification scbcd = new Certification();
            scbcd.setPrice(Double.valueOf(Math.random() * 100));
            scbcd.setName("scbcd");
            scbcd.setOwner(e);
            
            e.getCerts().add(scjp);
            e.getCerts().add(scwcd);
            e.getCerts().add(scbcd);
            
            
            GeneralEAO.create(e);
            GeneralEAO.transactionCommit();
            GeneralEAO.detachAll();
            return e.getId();
        }
        
    }
    

    星期一 七月 28, 2008

    EJB3 - Transactions

    description

    在 JPA 都看完之後再回過頭來看 transaction.

    reference

    EJB3 in Action - CH6 - Transactions and security

    Focal points

  • ACID: 這是廣為人知的了
    1. Atomicity: 不是全做完, 就是不做
    2. Consistency: 做完或復原之後, 完整性約束沒破壞
    3. Isolation: 兩件事不互相干擾
        Isolation levels
      1. Read uncommitted: 可能會讀到舊資料
      2. Read committed: 不會讀到舊資料, 這是大部分 DB 的預設值
      3. Repeatable read: 直到 transaction 結束為止, 保證多個讀取同筆紀錄的動作會得到相同的資料
      4. Serializable: 最高等級的 isolation level, 保證你用到的 table 在 transaction 中不會有人用到.
    4. Durability: 事情做完之後, 不會又被回復.
  • transaction management
    1. transaction manager 管理的是 resource manager, 這個 resource manager 的 resource 不只 DB, 還包括 message 或甚至其他的系統.
    2. 只使用一個 resource 的叫做 local transaction.
    3. 從 AP 的角度來看, transaction manager 擔任一個負責協調 resource manager 和 AP program 的角色(或提供這項服務). Program 要求 transaction manager begin, commit, rollback. transaction manager 再去決定哪個 resource manager 需要做什麼事情.
  • two-phase commit: 就是所有的 resource manager 都看能不能正確的 commit, 如果全都 ok 才 commit, 否則就全部 rollback.
  • JTA ( Java Transaction API )
    1. JTA 的兩種用法, 一是宣告為 CMT ( container-managed transaction ), 一是宣告為 BMT ( bean-managed transaction )
    2. CMT 只需要 annotation 或部署檔就可以讓 container 幫你處理 transaction, BMT 則是要自己用程式控制 transaction.
    3. 在 EJB3 中只有 session bean 與 MDB 支援 CMT 或 BMT. JPA 不直接依賴 CMT 或 BMT, 而是當部屬於 Java EE container 時可以外掛在 transaction 環境上.
  • CMT - container-managed transactions
    1. CMT 在 EJB business method 被呼叫時 start, 在 EJB business method 中 rollback 或 commit.
    2. 使用 CMT 只需 annotation 或部署檔宣告如何使用, 就可以告訴 container 何時要 rollback. 預設上 CMT 假設每個 EJB business method 都需要 transaction.
    3. 以下這個例子, 因為是 CMT 所以沒有 transaction.commit 得呼叫也可以正常的把 entity persist 進 DB
      @TransactionManagement(TransactionManagementType.CONTAINER)
      @Stateless
      public class TestStatelessBean implements TestStatelessLocal {
      
          @PersistenceContext
          private EntityManager em;
          
          
          public void createPerson() {
              Person p = new Person();
              p.setName("PERSON " + Calendar.getInstance().getTimeInMillis());
              em.persist(p);
          }
      
      }            
              
      但這個例子就不行了, 使用了 BEAN 就需要自己控制 transaction, 所以沒有 transaction.begin 會出現 javax.persistence.TransactionRequiredException
      @TransactionManagement(TransactionManagementType.BEAN)
      @Stateless
      public class TestStatelessBean implements TestStatelessLocal {
      
          @PersistenceContext
          private EntityManager em;
          
          public void createPerson() {
              Person p = new Person();
              p.setName("PERSON " + Calendar.getInstance().getTimeInMillis());
              em.persist(p);
          }
      
      }            
              
    4. @TransactionAttribute
      1. 雖然 CMT 可幫你管 transaction, 但你還是要告訴 container 應該如何管理, 因為會有 client code 是否有 transaction, 你的程式是否需要 transaction, 以及如何與 client code transaction 搭配的問題. 這些設定就透過 @TransactionAttribute 進行
      2. @TransactionAttribute的類別與說明如下
        # 用 t 代表 transaction
        # 用 c 代表 caller
        REQUIRED        c 無 t 則建 t, c 有 t 就 join t
        REQUIRED_NEW    c 無 t 則建 t, c 有 t 就先 suspended 後建一 t 自己用
        SUPPORTS        c 無 t 則不用 t, c 有 t 就 join t
        MANDATORY       c 無 t 則丟 EJBTransactionRequiredException, c 有 t 就 join t
        NOT_SUPPORTED   c 無 t 則不用 t, c 有 t 就 suspended 然後不用 t 執行自己的程式
        NEVER           c 無 t 則不用 t, c 有 t 就丟 EJBException
                            
      3. 使用 REQUIRED 的時候, 如果發生錯誤需要 rollback, 除了會把整個 transaction rollback 以外還會丟一個 javax.transaction.RollbackException 給 client 讓 client 知道有 client 呼叫的某個 method rollback 了.
      4. 使用 REQUIRED_NEW 的動作是先把 caller 的 transaction 暫停, 然後起始一個 transaction 執行自己的程式, 如果成功就 commit 自己的 transaction 發生錯誤也只 rollback 自己的 transaction, 最後再讓 caller 的 transaction 繼續運作.
        使用 REQUIRED_NEW 的時機在當你不希望自己的動作 transaction 影響 caller, 也不希望自己的 transaction 被 caller 影響的時候. 這個效果就是你的程式 rollback 了 caller 不會怎樣, 同樣的你的程式 rollback 了 caller 也沒差.
      5. 使用 SUPPORTS 就是逆來順受, 有就用沒也 OK. 這個動作用意在避免無謂的 transaction 被 suspended. 這個屬性常用在 read-only operation.
      6. 使用 MANDATORY 在當你要確保自己 rollback 的時候 client 也要 fail.
      7. 使用 NOT_SUPPORTED 會讓你的 method 用不到 transaction, 就算 client 有 transaction 進來也 suspend. 這個屬性可用在 MDB 在某些情況的使用沒有 rollback message delivery 的需求就可以用這個屬性
      8. 使用 NEVER 在當你的 method 做的事情不支援 transaction 如處理文字檔的時候, 你又希望 client 知道這個 method 不需要 transaction 的這件事情可用 NEVER.
      9. MDB 只支援 REQUIRED 與 NOT_SUPPORTED, 雖然你可以指定任意的屬性, 但只有這兩種是支援的. 這是因為 MDB 不是由 client code 呼叫, 而是由 container 呼叫, 所以我們只要告訴 container 這個 MDB 是否需要 transaction. 其他如 REQUIRED_NEW, SUPPORTS, MANDATORY, NEVER 都因為和 client 的 transaction 無關所以用不到 (NEVERE 的時候也不用特地告訴 container 這時候處理的事情不用 transaction, 因為 container 根本不管).
    5. 使用 CMT 藥 rollback 的時候, container 不是立刻 rollback, 而是先做個記號表示 transaction 要 rollback, 再在 transaction 結束之後執行 rollback 的動作.
      例如在 REQUIRED 而 client 無 transaction 的情況下, 進入 method 後 container 起始一個 transaction, 然後在 method return 之前檢查是否能 commit, 如果我們設定了 rollback 就會 rollback.
      EJBContext 有做 transaction proxy, 所以使用 EJBContext.setRollBackOnly 的時候要確定有 transaction 才呼叫, 例如狀態為 REQUIRED, REQUIRED_NEW, MANDATORY 的時候. 如果在無 transaction 的時候呼叫會出現 java.lang.IllegalStateException. 另外也可以透過 EJBContext.getRollbackOnly 得知目前是否為要 rollback 的狀態.
      setRollbackOnly, getRollbackOnly 只能在 CMT + transaction 屬性為 REQUIRED, REQUIRED_NEW, MANDATORY 的時候使用, 否則會 IllegalStateException
      @TransactionManagement(TransactionManagementType.CONTAINER)
      @Stateless
      public class TestStatelessBean implements TestStatelessLocal {
      
          @PersistenceContext
          private EntityManager em;
          
          @Resource
          SessionContext ctx;
          
          public void createPerson() {
              try {
                  Person p = new Person();
                  p.setName("PERSON " + Calendar.getInstance().getTimeInMillis());
                  em.persist(p);
                  throw new Exception();
              } catch (Exception e) {
                  e.printStackTrace();
                  ctx.setRollbackOnly(); // 沒這行就會資料進 DB
              }
          }
      
      }                
                  
    6. 使用@ApplicationException
      1. 由於每個 exception 都可能需要 rollback, 造成每個 catch 下都會有 setRollbackOnly 的呼叫, 所以就用 @javax.ejb.ApplicationException 來定義每個 exception 對 transaction 的處理為何.
      2. @ApplicationException 就是把 checked 或 unchecked exception 定義為 application exception. 一般來說, 除了 java.rmi.RemoteException 以外所有的 checked exception 都被期望是 application exception, 而所有繼承 java.rmi.RemoteException 或 java.lang.RuntimeException 的都被期望是 system exception, 而 system exception 則是不被 client 所預期要處理的, 一旦遇到這種 system exception 就不會直接傳給 client, 而會被 container 包成 javax.ejb.EJBException.
      3. 雖然說繼承 RuntimeException 會被當成 system exception 而被包成 EJBException, 不過在加上 @ApplicationException 後, 就會被當成 application exception 了.
      4. rollback 預設為 false, 設定為 true 的話會在 exception 傳回 client 之前就先把 transaction rollback.
      5. 當遇到真的沒預期的 RuntimeException 如 NullPointerException, container 也會 rollback, 不過在此同時 container 也會認為這個 session bean 已經是不能持續下去的狀態而將此 session bean destroy 掉. (RuntimeException -> Destroy), 由於不必要的 destroy session bean 是昂貴的, 所以不要隨便使用 system exception (就是沒有 @ApplicationException 又繼承 RuntimeException)
      6. @ApplicationException(rollback=false)
        public class NotRollbackException extends Exception {}
        
        @ApplicationException(rollback=true)
        public class RollbackException extends Exception {}
        
        @TransactionManagement(TransactionManagementType.CONTAINER)
        @Stateless
        public class TestStatelessBean implements TestStatelessLocal {
        
            @PersistenceContext
            private EntityManager em;
            
            @Resource
            SessionContext ctx;
            
            public String testInterceptor() {
                return "interceptor passed.";
            }
            
            @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
            public void show() throws NotRollbackException {
                Person p = new Person();
                p.setName("PERSON IN SHOW " + Calendar.getInstance().getTimeInMillis());
                em.persist(p);
                throw new NotRollbackException(); // 丟了還是 commit
            }
        
            @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
            public void createPerson() throws RollbackException {
                Person p = new Person();
                p.setName("PERSON IN CREATE " + Calendar.getInstance().getTimeInMillis());
                em.persist(p);
                throw new RollbackException(); // 丟了就不會 commit
            }
        
        }                        
                        
  • BMT - Bean-managed transaction
    1. 使用 CMT 有個限制就是必須由 container 管理 transaction. 而使用 BMT 就是可以用程式明確的指定要如何操作 transaction. (CMT: container 管; BMT: 自己寫程式管) 不過雖然是自己控制 transaction, 實際上還是由 container 替你產生底層的 transaction 實體以及處理底層的細節. 不過使用了 BMT 還是要多知道一些 JTA transaction API, 主要是 javax.transaction.UserTransaction.
    2. 使用 BMT 首先要設定 @TransactionManagement 為 @TransactionManagement(TransactionManagementType.BEAN)
    3. 使用 BMT 的時候就不用設定 @TransactionAttribute, 因為 @TransactionAttribute 只給 CMT 使用
    4. 取得 UserTransaction 的方式
      1. 用 @Resource
        @Resource
        private UserTransaction userTransaction
                            
      2. JNDI lookup java:comp/UserTransaction, 當必須在 EJB 外使用到 UserTransaction 的時候就用這種方式.
        private UserTransaction userTransaction;
        
        @PostConstruct
        private void postConstruct() {
            try {
                Context context = new InitialContext();
                userTransaction = (UserTransaction) context.lookup("java:comp/UserTransaction");
                System.out.println("look up UserTransaction: " + userTransaction);
            } catch (NamingException ex) {
                Logger.getLogger(TestStatelessBean.class.getName()).log(Level.SEVERE, "No such name: java:comp/UserTransaction", ex);
            }
        }
                            
      3. EJBContext.getUserTransaction. 當你已經為了某個需求注入 EJBContext, 就不要再另外注入 UserTransaction 了.
        EJBContext.getUserTransaction 只能在 BMT 的環境下使用, 在 CMT 下使用會 IllegalStateException
        EJBContext.getRollbackOnly, EJBContext.setRollbackOnly 只能在 CMT 用, 在 BMT 用會出現 IllegalStateException.
        @Resource
        private EJBContext context;
        @Resource
        private UserTransaction userTransaction;
        
        @PostConstruct
        private void postConstruct() {
            userTransaction = context.getUserTransaction();
        }
                            
    5. @TransactionManagement(TransactionManagementType.BEAN)
      @Stateless
      public class TestStatelessBean implements TestStatelessLocal {
      
          @Resource
          private UserTransaction userTransaction;
          @PersistenceContext
          private EntityManager em;
      
          public void changePersonName() throws RollbackException {
              try {
                  try {
                      userTransaction.begin();
                      if (personExists()) {
                          Person p = findPerson();
                          changeName(p);
                      }
                      userTransaction.commit();
                  } catch (Exception ex) {
                      userTransaction.rollback();
                      Logger.getLogger(TestStatelessBean.class.getName()).log(Level.SEVERE, null, ex);
                  }
              } catch (Exception ex) {
                  Logger.getLogger(TestStatelessBean.class.getName()).log(Level.SEVERE, null, ex);
              }
          }
      
          private void changeName(Person p) {
              p.setName("KKK 123");
              System.out.println(p.getName());
              em.merge(p);
              em.flush();
          }
      
          private Person findPerson() {
              return em.find(Person.class, Long.valueOf(36));
          }
      
          private boolean personExists() {
              return ((Long) em.createQuery("select COUNT(p) from Person p").getSingleResult()) > 0;
          }
      }            
              
    6. UserTransaction methods 說明
      1. begin: 建立與目前 thread 綁定的 low-level transaction, 由於 Java EE 不支援 nested transaction, 所以在 commit 或 rollback 之前連續呼叫兩次 commit 會出現 NotSupportedException.
      2. commit, rollback, 移除透過 begin 與目前 thread 綁定的 transaction. 一旦 commit 送出 "success" 記號給 transaction manager, rollback 就會捨棄目前的 transaction.
      3. setRollbackOnly 在這看起來比較多餘, 因為已經有 rollback 這個 method 了就已經不需要做一個 rollback 記號. 這是因為有可能我們在 BMT 的環境下使用了 CMT 的 transaction, 為了把 rollback 的訊息傳給 CMT, 就要透過 setRollbackOnly
      4. getStatus 則是更詳細的 getRollbackOnly, getStatus 的值放在 javax.transaction.Status 中. 比較特別的狀態有
        STATUS_PREPARED     :   因為 two phase commit 的關係, 有個階段是所有 resource manager 都準備好要 commmit 了.
        STATUS_PREPARING    :   想要 commit 了, 在等待 resource manager 的 response.
                            
      5. setTransactionTimeout 指定的是 "秒", 在幾秒內 transaction 必須結束, 每個 server 的預設時間不同 (因為 UserTransaction 是 BMT 在用, 如果要設定 BMT 的 transactionTimeout 就要透過 vender-specific 部署設定檔)
  • BMT 的優缺點
    1. BMT 應少用, 因為複雜難維護, 不過 BMT 的 transaction 不用在一個 method call 內完成 begin 與 commit, 所以使用 stateful session bean 且需要在多個 method 間維持同一個 transaction 就必須使用 BMT. 不過因為使用 BMT 就要自己操作 transaction 而有太多細節要注意容易出錯, 所以建議使用 CMT 就好.
    2. 一個使用 BMT 的好處就是可以調整 transaction 的綁定使程式所拿住資料的時間能愈短愈好, 不過書上認為這種做法只是沉溺於最佳化
    3. BMT 永遠不會 join 已存在的 transaction, 使用 BMT 的時候會先 suspend 現有的 transaction, 這樣明顯限制了 component 的重複利用.
  • 星期日 七月 20, 2008

    EJB3 - 打包EJB3

    description

    就是要了解一下EJB3的資料夾怎麼部署的, 以及相關資訊. 不過由於自己對 xml 設定實在沒什麼興趣, 所以就沒試很多.

    reference

    EJB3 in Action - CH11 - Packaging EJB3 applications

    Focal points

  • WAR檔放web application
  • EAR放所有archive, 所以包成EAR檔後只要部署這一個檔案即可. AP會scan EAR檔的內容然後部署.
  • archive 類型
      type    desc                            部署檔                   contexts
      CAR     Client application archives     application-client.xml  Java client for EJBs
      EAR     Enterprise application archives application.xml         Java modules
      EJB-JAR EJB Java archives               ejb-jar.xml             Session beans, MDBs, entities, persistence.xml
      RAR     Resource adapter archives       ra.xml                  Resource adapters
      WAR     Web application archives        web.xml                 Web app. persistence.xml
              
  • entity可放在大部分的archives, RAR不支援放entity.
  • 一個EAR可能有的檔案
    META-INF/application.xml
    TestEJB3_June2008-ejb.jar
    TestEJB3_June2008-war.war
    lib/derbyclient.jar
        
  • EAR檔的application.xml用來描述EAR裡面的module如何被部署. 當部屬EAR檔到AP SERVER, AP SERVER會使用部署描述檔部署modules. Java EE 5可以不用這個描述檔, 只要檔案位置都符合Guidelines, Patterns, and code for end-to-end Java applications, SERVER就會自動偵測要部署的檔案.
    <?xml version="1.0" encoding="UTF-8"?>
    <application version="5" xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/application_5.xsd">
      <display-name>TestEJB3_June2008</display-name>
      <module>
        <ejb>TestEJB3_June2008-ejb.jar</ejb>
      </module>
      <module>
        <web>
          <web-uri>TestEJB3_June2008-war.war</web-uri>
          <context-root>/TestEJB3_June2008-war</context-root>
        </web>
      </module>
    </application>        
        
  • AP載入module的方式依vender實做而不同, 可是雖然實作不同, 大致上還是依照一個規定好的演算法.
    if ( 為 .jar ) {
        if ( 有ejb-jar.xml 或 class使用EJB3 annotation ) {
            判斷為EJB-JAR (EJB-JAR Module)
        } else {
            if ( 是否有application-client.xml 或 manifest 中指定 Main-Class ) {
                判斷為CAR (Client Application Module)
            } else {
                error! 未知的 module 型態
            }
        }
    } else {
        if ( 為 .war ) {
            判斷為 WAR (Web Module)
        } else {
            if ( 為 .rar ) {
                判斷為 RAR (Resource Adapter Module) 
            } else {
                error! 未知的 module 型態
            }
        }
    }
        
  • Class Loading
    1. class不是一開始就全部載入, 是在需要的時候才runtime動態載入
    2. class loader執行有預設順序如下
      1. 第一步先由Bootstrap Class Loader載%JAVAHOME/jre/lib/rt.jar, 就是載java.lang, java.util等package. 也可用bootclasspath設定起始class loader去載其他jar檔的class.
      2. 再來由Extension Class Loader載%JAVAHOME/jre/lib/ext/*.jar 或 System property -Djava.ext.dir 中的jar檔, 預設上這裡載的是security classes.
      3. 再來由System Class Loader載AP指定的classes, 也就是AP class loader. 可用多種機制指定要載的class, 例如指定環境變數classpath, 也可以指定classpath裡jar檔中manifest的Class-Path(用相對於jar檔的位置指定).
      4. 最後就是User Defined Class Loader, 就是程式內自己做的class loader載classes.
    3. class loader載class有標準順序如下(Parent First Delegation Model)
      1. 開始載class
      2. 判斷是否已經載入, 是的話就取得class
      3. 判斷是否在parent class loader有這個class, 有就去parent class loader載.
      4. 判斷是否在classpath能載到此class, 可以的話就載
      5. 都找不到, 丟 ClassNotFoundException
    4. 標準 class loading 關係如下, 不過實際上怎麼做看vender.
      1. Bootstrap Class Loader (%JAVAHOME/jre/lib/rt.jar)
      2. Extension Class Loader (%JAVAHOME/jre/lib/ext/*.jar)
      3. System Class Loader (%CLASSPATH)
      4. Application Server Class Loader (%APP_SERVER_HOME/lib)
      5. EAR Class Loader
      6. EJB-JAR Class Loader
      7. WAR Class Loader
    5. deployment descriptor 可 override annotation 對 session bean 的設定
    6. 不常變動的部份用annotation, 常變動的部份用 xml
    7. EJB-JAR 的 application.xml 中, ejb-jar tag 的 version 很重要, 設定為 3.0 才會有 EJB3 的效果出來.
    8. 如果用 xml 指定 session bean name, 要指定和 annotation 的一樣, 如果 annotation 沒指定 session bean name, 就用 bean class name.
    9. interceptor 執行的順序, 取決於 interceptor 設定順序
    10. 如果把 entity 放 EJB-JAR, 則 EJB-JAR 就會有一個 persistence.xml . 目錄結構如下:
        TestEJB3-ejb.jar
      1. META-INF/persistence.xml
      2. orm.xml (optional)
      3. packageA/
          classA
      4. packageB/
          classB
          secondORM.xml
    11. persistence.xml 是唯一必須存在的 xml.
    12. persistence.xml 的 mapping-file tag 可指定 orm.xml
    13. persistence.xml 的 jar-file 可指定其他的 entity jar
    14. persistence.xml 的 properties tag 可指定 vender specific property
    15. 每個 persistence unit 都要在一個 Java EE module 中 unique 的 name. container 用這個 name 來建立 EntityManagerFactory 來建立此 persistence unit 的 EntityManager
    16. 可以在 persistence.xml 替一個 AP module 定義多個 persistence unit.
    17. 如果 persistence unit 定義在一個 module, 如一個 WAR 或 一個 EJB-JAR, 則該 persistence unit 只會在設定的 module 被看到, 如果 persistence unit 的 jar 檔放 EAR, 則此 persistence unit 會整個 AP 都看到.
    18. 如果 EAR 和 EJB-JAR (module level) 都有一樣的 persistence unit, 則 module 的 persistence unit 會採用, 如果要指定注入 EAR 的 persistence unit, 可宣告為
      @PersistenceUnit(unitName = "lib/kk.jar#kk")
      private EntityManagerFactory emf;
                  
    19. persistence.xml 的 transaction-type 預設為 JTA, 如果在 container 外就要設定為 RESOURCE_LOCAL
    20. 在 persistence.xml 設定 datasource 要先在 server 上設定 datasource, datasource 通常和 connection pool 在一起, connection pool 有連線的資訊.
      <?xml version="1.0" encoding="UTF-8"?>
      <persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
      http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
        <persistence-unit name="oraclePU" transaction-type="JTA">
          <provider>org.hibernate.ejb.HibernatePersistence</provider>
          <jta-data-source>jdbc/oracle</jta-data-source>
          <properties>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect"/>
          </properties>
        </persistence-unit>
      </persistence>                
                  
    21. 可在 META-INF 下用 orm.xml 也可在 persistence.xml 指定 orm.xml 的路徑 (mapping-file tag)
    22. 避免 library 重複部署在多個地方, 可將 util library 放在全部 module 都可看見的地方.
    23. 除非是需求的唯一解, 否則別用 vender-specific 的 API 或 tag.
    24. orm 用 deployment descriptor 可幫助改變 orm 不用改程式
    25. 若要載資源就用 Thread.current.getContextClassLoader().getResourceAsStream()
    26. 注意 class loader 的議題, 有可能需要的 class 放另一個 class loader 造成 NoClassDefFoundException
    27. 注意 class loader 的議題, 有可能由不同 class loader 載的同名 class 無法 cast
  • 星期日 七月 13, 2008

    EJB3 - MessageDrivenBean

    description

    MDB重點在loose coupling與非同步元件間的溝通, 是很方便的東西.

    reference

    EJB3 in Action - CH4 - Messaging and developing MDBs

    focal points

  • 在messaging架構中, 負責不同原件間傳遞訊息的叫MOM (Messaging-oriented middleware), 送出訊息的叫producer, 存放訊息的地方叫destination.
  • 任何對destination中訊息有興趣的都可以去取訊息, 這接收訊息的叫consumers.
  • messaging中一種模式是點對點模式(PTP), 就是producer送出訊息後, 訊息會存放在Queue中, 注意這個Queue不保證訊息有照順序, 然後一旦有consumer取走一個message, 那個message就沒人取的到了. 若有多個consumer, 誰取哪個message是隨機的.
  • 另一種模式為Publish-subscribe (pub-sub), 就是一個producer送出訊息後由當時連線在destination上的consumer不限數量的接收訊息, pub-sub的destination稱為topic, consumer稱為subscriber.
  • 還有一種是在message中留下足夠的訊息讓consumer能夠送reply回producer.
  • 送出訊息
    1. 先準備ConnectionFactory和Destination
    2. 用ConnectionFactory建立Connection
    3. 用Connection建立Session
    4. 用Session建立Message, 把message放Message物件中.
    5. 用Session建立要送到哪個destination的MessageProducer
    6. 把Session建立的Message放在MessageProducer中send出
    7. 關閉Session和Connection
    8. @Stateless(name="SimpleStatelessBean1")
      public class SimpleStatelessBean1 implements SimpleStatelessLocal {
      
          @Resource(name="TestQueueConnectionFactory")
          private ConnectionFactory connectionFactory;
          
          @Resource(name="jms/TestQueueDestination")
          private Destination destination;
          
          public String simpleShow() {
              try {
                  Connection conn = connectionFactory.createConnection();
                  Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
                  MessageProducer messageProducer = session.createProducer(destination);
                  TextMessage message = session.createTextMessage();
                  message.setText("This is text message");
                  messageProducer.send(message);
                  messageProducer.close();
                  session.close();
                  conn.close();
              } catch (JMSException ex) {
                  throw new RuntimeException( ex );
              } 
              return this + ":This is simple show";
          }
      }
              
  • 因為開啟一個Connection需要比較多資源, 因此Connection設計為thread-safe且可分享的
  • Session提供single-thread, 任務導向的context去送與接收message.
  • Connection.createSession可設定是否需要transaction, 若設定為true則message會到session commit且close之後才被realized. 反之, 若transaction設為false, 則MessageProducer.send呼叫後訊息就立刻送出.
  • Connection.createSession還可設定通知模式且只在接收message時影響無transaction的session.
  • 很重要的一點是Connection和Session都要在結束之後用close釋放資源, 因為在transactional session中, 在Session close而commit之前是不會有message被送出的.
  • 用MDB處理接收到的訊息
    1. MDB支援multithread, 當consumer傳送多個內含未處理multithread bean的message到destination, MDB會從pool被取出後拿來個別處理message.
    2. MDB一定要直接或間接實做message listener interface, 實作的method一定要public且不能static或final.
    3. MDB一定要是public concrete class, 不能是abstract或final
    4. MDB一定要試POJO, 且不可以是其他MDB的subclass.
    5. MDB一定要有no-argument constructor, container用來建新instance.
    6. MDB不可以有finalize methoc, 要用@PreDestroy來代替.
    7. MDB不可以丟RemoteException或RuntimeException, 如果丟了RuntimeException會造成MDB停止.
    8. MDB也有lifecycle callback @PostConstruct, @PreDestroy
    9. @MessageDriven的屬性name用來指定MDB name, 如果name留空白預設為MDB的class name.
    10. @MessageDriven的屬性messageListenerInterface用來指定MDB實做哪個MessageListener, 如果指定了這個屬性在class上就可以不用寫implements MessageListener而只要有個onMessage method即可, 另一種不用寫implements MessageListener的方式就是透過設定檔.
      為什麼需要可以指定interface是因為如果你要使用符合JCA機制的messaging如JAXM(一種SOAP-based XML messaging API), 就可以不用改程式而只要改變實作的interface即可(比方說可改成javax.jaxm.OneWayMessageListener).
    11. @MessageDriven的屬性activationConfig用來設定系統指定的properties
      1. activationConfig的值為@ActivationConfigProperty陣列, 每個@ActivationConfigProperty可設定name-value.
      2. 基本的name-value pair為destinationType, connectionFactoryJndiName, destinationName
      3. 有個設定是acknowledgeMode, 用來決定通知queue去remove掉已經接收的message. 以下四種mode是JMS的mode, MDB只支援AUTO_ACKNOWLEDGE與DUPS_OK_ACKNOWLEDGE. 我用GlassFish的時候只能設定為Auto-acknowledge, 如果設定DUPS_OK_ACKNOWLEDGE或Auto_acknowledge都會出現如
        java.lang.IllegalArgumentException: MQJMSRA_AS4001: setAcknowledgeMode:Invalid acknowledgeMode=DUPS_OK_ACKNOWLEDGE
                            
        的錯誤
    12. Queue的MDB
      @MessageDriven(mappedName = "jms/TestQueueDestination", activationConfig =  {
              @ActivationConfigProperty(propertyName = "destinationName", propertyValue = "TestQueueDestination"),
              @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
              @ActivationConfigProperty(propertyName = "connectionFactoryJndiName", propertyValue = "TestQueueConnectionFactory"),
              @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge")
          })
      public class SimpleStatelessBeanMDBBean implements MessageListener {
          
          @PostConstruct
          private void postConstruct() {
              System.out.println("create.." + this);
          }
          
          public void onMessage(Message message) {
              try {
                  TextMessage msg = (TextMessage) message;
                  System.out.println("Receive message:" + msg.getText());
                  message.acknowledge();
              } catch (JMSException ex) {
                  throw new RuntimeException( ex );
              }
          }
          
          @PreDestroy
          private void preDestroy() {
              System.out.println("destroy.." + this);
          }
          
      }
                  
    13. Topic的destination會把訊息通知到關注此destination的MDB中正連線在topic上的MDB.
    14. Topic的MDB可設定為durable或nondurable. durable就是雖然預設是只送message到連現在topic上的MDB, 但可透過設定等待未連線的MDB連線後再送message過去.
    15. Topic的MDB
      @MessageDriven(mappedName = "TestTopicDestination", activationConfig =  {
              @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge"),
              @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Topic"),
              @ActivationConfigProperty(propertyName = "subscriptionDurability", propertyValue = "Durable"),
              @ActivationConfigProperty(propertyName = "clientId", propertyValue = "TopicMDBBean1"),
              @ActivationConfigProperty(propertyName = "subscriptionName", propertyValue = "TopicMDBBean1")
          })
      public class TopicMDBBean1 implements MessageListener {
          
          public void onMessage(Message message) {
              try {
                  TextMessage msg = (TextMessage) message;
                  System.out.println(this + " receive " + msg.getText());
              } catch (JMSException ex) {
                  Logger.getLogger(TopicMDBBean1.class.getName()).log(Level.SEVERE, null, ex);
              }
          }
          
      }
      
      @MessageDriven(mappedName = "TestTopicDestination", activationConfig =  {
              @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge"),
              @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Topic"),
              @ActivationConfigProperty(propertyName = "subscriptionDurability", propertyValue = "Durable"),
              @ActivationConfigProperty(propertyName = "clientId", propertyValue = "TopicMDBBean2"),
              @ActivationConfigProperty(propertyName = "subscriptionName", propertyValue = "TopicMDBBean2")
          })
      public class TopicMDBBean2 implements MessageListener {
          
          public void onMessage(Message message) {
              try {
                  TextMessage msg = (TextMessage) message;
                  System.out.println(this + " receive " + msg.getText());
              } catch (JMSException ex) {
                  Logger.getLogger(TopicMDBBean2.class.getName()).log(Level.SEVERE, null, ex);
              }
          }
          
      }                
                  
    16. messageSelector可用來篩選要處理的message, 書上表示message selector的語法和SQL 92中的WHERE雨法相同, 但由於光用一個IS就出現錯誤, 所以這部份還要實做才知道.
      在best practices小節中指出, 雖然messageSelector可讓一個destination有多種用途, 但較好的作法還是將destination分開來處理message.
      1. 此例要設定message的property name為flag, property value為5才能觀察. 雖然書上表示message IS 是一個message selector允許的語法, 但真的指定了 "flag is true"則出現了
        com.sun.messaging.jmq.util.selector.SelectorFormatException: Unknown operator: [25,is]: "booleanFlag is true"
        的訊息.
        // MessageConsumer codes start
        ...
        message.setLongProperty("flag", Long.valueOf(5));
        ...
        // MessageConsumer codes end
                                
        @MessageDriven(mappedName = "TestTopicDestination", activationConfig =  {
                @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge"),
                @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Topic"),
                @ActivationConfigProperty(propertyName = "subscriptionDurability", propertyValue = "Durable"),
                @ActivationConfigProperty(propertyName = "clientId", propertyValue = "TopicMDBBean3"),
                @ActivationConfigProperty(propertyName = "subscriptionName", propertyValue = "TopicMDBBean3"),
                @ActivationConfigProperty(propertyName = "messageSelector", propertyValue = "flag = 5")
            })
        public class TopicMDBBean3 implements MessageListener {
            
            public void onMessage(Message message) {
                try {
                    TextMessage msg = (TextMessage) message;
                    System.out.println(this + " receive " + msg.getText());
                } catch (JMSException ex) {
                    Logger.getLogger(TopicMDBBean3.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            
        }                    
                            
    17. MDB的生命週期(MDB有@PostConstruct與@PreDestroy)
      1. 建立MDB instance
      2. 注入資源, 包含MessageContext
      3. 把MDB放在pool中
      4. 當message送到的時候把MDB從pool取出來處理message
      5. MDB執行onMessage
      6. onMessage執行完就把MDB放回pool
      7. 在需要的時候destroy MDB
    18. MDB可依需求再送message到其他地方
      @MessageDriven(mappedName = "TestTopicDestination", activationConfig =  {
              @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge"),
              @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Topic"),
              @ActivationConfigProperty(propertyName = "subscriptionDurability", propertyValue = "Durable"),
              @ActivationConfigProperty(propertyName = "clientId", propertyValue = "TopicMDBBean4"),
              @ActivationConfigProperty(propertyName = "subscriptionName", propertyValue = "TopicMDBBean4")
          })
      public class TopicMDBBean4 implements MessageListener {
          
          @Resource(name="TestQueueConnectionFactory")
          private ConnectionFactory connectionFactory;
          
          @Resource(name="jms/TestQueueDestination")
          private Destination destination;
          
          public void onMessage(Message message) {
              try {
                  TextMessage msg = (TextMessage) message;
                  System.out.println(this + " receive " + msg.getText());
                  sendAgain();
              } catch (JMSException ex) {
                  Logger.getLogger(TopicMDBBean4.class.getName()).log(Level.SEVERE, null, ex);
              }
          }
          
          private void sendAgain() throws JMSException {
              Connection connection = connectionFactory.createConnection();
              Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
              MessageProducer messageProducer = session.createProducer(destination);
              TextMessage message = session.createTextMessage();
              message.setText("Send message again from " + this);
              messageProducer.send(message);
              messageProducer.close();
              session.close();
              connection.close();        
          }
          
      }                
                  
    19. MDB也支援transaction, 預設上onMessage開始執行時transaction也開始, onMessage return時transaction commit. 同樣在onMessage也可控制transaction的rollback (就像一般的transaction處理), 可在MDB注入MessageDrivenContext便可呼叫setRollbackOnly
    20. 有一種message叫做poison messages. 這是因為當MDB的onMessage出現exception時, 由於MDB未正常結束, 使得container會rollback並將message重新送回queue中, 因為出現錯誤的MDB仍在listen此queue所以又接收了message因而又出現一次exception, 如此無窮盡的處理一個message就叫poison messages. 不過這個問題幾乎都由vender透過計算同一個message重新傳送的次數或是透過一個error queue紀錄錯誤message的方式避免掉poison messages的問題. 只不過避免poison messages的實做並不是spec公規定的, 所以還是要注意poison messages的問題.
    21. 和session bean一樣, MDB也有pool, 所以也要設定好pool size, 否則也會有資源浪費或不足的問題.
  • 星期二 七月 08, 2008

    EJB3 - Session bean

    description

    其實session bean是最一開始就看的, 回過頭來看再記重點有點心浮氣躁.

    reference

    EJB3 in Action - CH3 - Building business logic with session beans

    Focal Points

  • session bean一定要有一個以上的interface與一個實作
  • 一個session bean可以有多個interface, 所以當user呼叫一個@Local的interface, 就是使用local的session bean. 使用@Remote或@WebService就是用remote或web service的session bean.
  • session bean一定要是concrete class, 一定要有無參數建構子, 不能是abstract或final.
    session bean的實作不能是abstract classes, 如果宣告為abstract class, ide會警告但不會compile error. 但runtime會有javax.ejb.EJBException: nested exception is: javax.ejb.EJBException: nested exception is: javax.ejb.CreateException: Could not create stateless EJB
    不能宣告abstract class 實作business interface又沒宣告@Stateless會起始錯誤如Deploying application in domain failed; Error loading deployment descriptors for module [TestEJB3_June2008] -- Cannot resolve reference Unresolved Ejb-Ref servlets.stateless.SimpleStatelessServlet/simpleStatelessLocal@jndi: @null@stateless.SimpleStatelessLocal@Session@null
  • session bean可以是其他session bean或POJO的subclass.
  • session bean的business method與lifecycle callback可定在super class或session bean class裡.
  • session bean的annotation的繼承是有條件的, 就是class level比方說@Stateless, @Stateful會被忽略, 不過lifecycle callback會被繼承下來.
  • session bean的business method name不能以ejb開頭, 比方說不能是ejbDoit()
  • session bean的method必須是public且不能是static或final
  • 使用一個remote business interface要注意argument與return type必須實作Serizable
  • 所有session bean都有的生命週期是creation / destruction
  • stateful bean比stateless bean又多了passivation / activation
  • stateless bean的lifecycle callback為@PostConstruct, @PreDestroy
  • stateful bean的lifecycle callback為@PostConstruct, @PreDestroy, @PostActivate, @PrePassivate
  • @PostConstruct, @PreDestroy比較簡單就是container起始session bean後呼叫@PostContruct, container移除session bean前會呼叫@PreDestroy
  • @PostActivate, @PrePassivate比較特別. stateful bean是每個client都會有一個, 所以在container上就會有很多個stateful bean, 一旦container判斷一個stateful bean停用了而決定要暫時讓這個session bean失去效用, 這個動作叫passivation. 而container讓已經失效的stateful bean再度生效就叫activation. 所以@PostActivate就是activation後呼叫的method, @PrePassivate就是passivation前呼叫的method.
  • lifecycle callback可以是public, private, protected, package-protected
  • lifecycle callback主要用來替session bean準備起始後需要的資源以及從container移除前要釋放的資源.
  • lifecycle callback除了放在session bean以外也可放在分開的interceptor class
  • stateless session bean有pool, 也就是說有一定數量的stateless session bean在container的pool中
  • @Stateless的定義
    1. 屬性有name, mappedName, description
    2. name屬性用來指定bean的name, 有的container用來和JNDI綁定. 如果name沒有設定就會是bean的class name.
    3. mappedName是vender-specific, 也就是依不同container有不同的情形. 以GlassFish來說是將mappedName的值綁定到JNDI name.
  • @Local: stateless session bean的local interface, local interface表示這個session bean和client放在同一個JVM上執行.
  • @Remove: 當client存在於container外的JVM時就必須使用@Remote
    1. 一個@Remote interface可以繼承java.rmi.Remote
       public interface TestRemote extends Remote { ... } 
      就算程式裡面沒寫繼承Remote, container還是會在byte code階段插入繼承Remote的動作
    2. 沒有程式上繼承java.rmi.Remote的好處就是不用處理java.rmi.RemoteException
    3. @Remote business interface有個需求就是所有的參數與回傳值都必須是Serializable, 因為這樣才能通過RMI
  • @WebService: 透過@WebService可讓session bean成為SOAP-based web service. 唯一要做的就是在interface上加上@WebService.
  • 不能讓一個business interface同時@Local又@Remote或是又@WebService, 不過可以透過interface的繼承改變要使用的是@Remote session bean還是@Local的 session bean.
  • 放session bean的lifecycle callback
  • 可以有多個@PostConstruct, @PreDestroy (不過我試起來一個session bean就只能一個lifecycle callback有效, 頂多除了callback以外還指定interceptor, 就加上interceptor)的一個lifecycle callback有效.)
  • lifecycle callback要符合pattern: void < METHOD >()
  • interceptor內的lifecycle callback要符合pattern: Object < METHOD >(InvocationContext) throws Exception, 然後記得回傳InvocationContext.proceed, 除非打算不繼續執行. (可參考EJB3 Interceptor)
  • session bean的lifecycle callback不可有checked exception, interceptor的則可以.
  • session bean的lifecycle callback不可有傳入參數, interceptor則要傳入InvocationContext, 否則有java.lang.IllegalArgumentException: wrong number of arguments
  • stateless session bean與stateful session bean的差別主要在於container管理的方式, stateless session bean起始之後會被container放進pool, 等client要使用的時候再從pool取出, 用完再放回pool. stateful session bean則是讓一個client擁有一個stateful session bean直到client離開或stateful session bean destroy為止. stateful session bean與client是one to one的關係.
  • stateful session bean需要付出代價, stateless session bean由於所有client共用session bean比較能節省資源, stateful session bean則因為與client是one to one的關係所以比較耗資源. 一旦container判斷消耗資源太多或佔用資源太久就會開始執行passivate的動作.
  • 由於stateful session bean比較耗資源, 所以注意要在stateful session bean加上@Remove method, 當呼叫此method, container就會負責將此method destroy以節省資源.
  • 由於stateful session bean在passivate的時候會做serialize的動作, 所以注意stateful session bean的class member必須實做Serializable或必須是primitive. 否則在passivate的時候會出現例如[NRU-stateful.SimpleStatefulBean]: passivateEJB(), Exception caught 的exception, 就是因為無法serialize該object的關係. 如果要使用不須serialize的class member只要用transient宣告該class member或在@PrePassivate把class member改成null再於@PostActivate設定回來即可.
  • 使用stateful session bean的方式幾乎和stateless session bean的方式幾乎一樣, 唯一不一樣的是stateful session bean的business interface只能使用@Local與@Remote而不能用@WebService. 因為SOAP-based web Service本來就不是stateful因此無法使用.
  • stateful session bean的生命週期中有重要的一點就是container在destroy一個passivate的時候會先將該stateful session bean先activate再passivate.
  • @Stateful(name="SimpleStatefulBean", mappedName="ejb/SimpleStatefulBean")
    public class SimpleStatefulBean implements SimpleStatefulRemote {
        
        private Logger logger = Logger.getLogger("SimpleStatefulBean");
        
        private byte[] b = new byte[100000];
        
        {
            for ( int i = 0; i < b.length; i++ ) {
                b[i] = (byte) 100;
            }
        }
        
        public String simpleShow() {
            return this + ":This is simple show" + b;
        }
    
        @PostConstruct
        public void postConstruct() {
            logger.info("create " + this);
        }
        
        @PreDestroy
        public void preDestroy() {
            logger.info("destroy " + this);
        }
        
        @PostActivate
        public void postActivate() {
            logger.info("activate " + this);
        }
        
        @PrePassivate
        public void prePassivate() {
            logger.info("passivate " + this);
        }
        
        @Remove
        public void remove() {
            logger.info("remove " + this);
        }
    }        
        
    放interceptor的lifecycle callback
    @Stateless
    @Interceptors(value={SimpleInterceptor.class})
    public class SimpleStatelessBean implements SimpleStatelessLocal {
    
        private Logger logger = Logger.getLogger("SimpleStatelessBean");
        
        @Resource(name="TestQueueConnectionFactory")
        private ConnectionFactory connectionFactory;
        
        @Resource(name="jms/TestQueueDestination")
        private Destination destination;
        
        public String simpleShow() {
            try {
                Connection conn = connectionFactory.createConnection();
                Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
                MessageProducer messageProducer = session.createProducer(destination);
                TextMessage message = session.createTextMessage();
                message.setText("This is text message");
                messageProducer.send(message);
                messageProducer.close();
                session.close();
                conn.close();
            } catch (JMSException ex) {
                throw new RuntimeException( ex );
            } 
            return this + ":This is simple show";
        }
    
    }
    public class SimpleInterceptor {
    
        Logger logger = Logger.getLogger("SimpleStatefulBeanInterceptor");
    
        @PostConstruct
        public void onCreate(InvocationContext ic) {
            try {
                logger.info("create " + this);
                ic.proceed();
            } catch (Exception ex) {
                Logger.getLogger(SimpleInterceptor.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        
        @AroundInvoke
        public Object aroundInvoke(InvocationContext ctx) throws Exception {
            logger.info(ctx + " is invoked.");
            return ctx.proceed();
        }
    
        @PreDestroy
        public void onDestroy(InvocationContext ic) {
            try {
                logger.info("destroy " + this);
                ic.proceed();
            } catch (Exception ex) {
                Logger.getLogger(SimpleInterceptor.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        
    }        
        
    stateful bean的@PrePassivate, @PostActivate也可放interceptor
    @Interceptors(value={SimpleInterceptor.class})
    @Stateful(name="SimpleStatefulBean", mappedName="ejb/SimpleStatefulBean")
    public class SimpleStatefulBean implements SimpleStatefulRemote {
        
        private byte[] b = new byte[100000];
        
        {
            for ( int i = 0; i < b.length; i++ ) {
                b[i] = (byte) 100;
            }
        }
        
        public String simpleShow() {
            return this + ":This is simple show" + b;
        }
        
        @Remove
        public void remove() {
            Logger.getLogger("SimpleStatefulBean").info("remove " + this);
        }
    }
    
    public class SimpleInterceptor {
    
        @PostConstruct
        public void onCreate(InvocationContext ic) {
            try {
                Logger.getLogger(SimpleInterceptor.class.getName()).info("create " + this);
                ic.proceed();
            } catch (Exception ex) {
                Logger.getLogger(SimpleInterceptor.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        
        @PostActivate
        public void onActivate(InvocationContext ic) {
            try {
                Logger.getLogger(SimpleInterceptor.class.getName()).info("activate " + this);
                ic.proceed();
            } catch (Exception ex) {
                Logger.getLogger(SimpleInterceptor.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        
        @PrePassivate
        public void onPassivate(InvocationContext ic) {
            try {
                Logger.getLogger(SimpleInterceptor.class.getName()).info("passivate " + this);
                ic.proceed();
            } catch (Exception ex) {
                Logger.getLogger(SimpleInterceptor.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        
        @AroundInvoke
        public Object aroundInvoke(InvocationContext ctx) throws Exception {
            Logger.getLogger(SimpleInterceptor.class.getName()).info(ctx + " is invoked.");
            return ctx.proceed();
        }
    
        @PreDestroy
        public void onDestroy(InvocationContext ic) {
            try {
                Logger.getLogger(SimpleInterceptor.class.getName()).info("destroy " + this);
                ic.proceed();
            } catch (Exception ex) {
                Logger.getLogger(SimpleInterceptor.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        
    }
        
  • @EJB用來注入session bean到client code. @EJB有幾個屬性: name, beanInterface, beanName. name用來指定JNDI name, beanName則是當一個interface有兩個實作時用來決定要注入哪個實作.
  • 使用@EJB注入的時候如果沒有指定JNDI name, container就會用interface name當成JNDI name注入.
  • 如果要注入同個interface不同的實作可透過指定JNDI name或beanName
  • @Stateless(name="SimpleStatelessBean1")
    public class SimpleStatelessBean1 implements SimpleStatelessLocal { ... }
    
    @Stateless(name="SimpleStatelessBean2")
    public class SimpleStatelessBean2 implements SimpleStatelessLocal { ... }
    
    public class SimpleStatelessServlet extends HttpServlet {
       
        @EJB(beanName="SimpleStatelessBean1")
        private SimpleStatelessLocal simpleStatelessLocal1;
        
        @EJB(beanName="SimpleStatelessBean2")
        private SimpleStatelessLocal simpleStatelessLocal2;
    
        ...
    }
        
  • 可注入stateless bean或stateful bean到其他的stateful bean. 但不能注入stateful bean到stateless bean, 因為這樣stateful session bean就會被所有client分享.
  • 注入stateful bean到另一個stateful bean時, 一旦持有注入的stateful bean destroy了, 被持有的stateful bean也會一起destroy.
  • 如果不用stateful bean可將狀態放在DB中或放在server side的檔案或放HttpSession, 不過要注意清除不必要的資源.
  • 儲存conversation state的時候要注意儲存的值愈小愈好, 例如primitive.
  • stateful bean要記得定義@Remove method.
  • 調整server到stateful bean效能最佳的狀態, 小心頻繁的passivate / activate造成效能變差太多.
  • 星期日 六月 29, 2008

    servlet + session bean & MDB

    Description

    想要在servlet使用EJB3 session bean.
    由於servlet會大家一起共用, 因此在servlet中用@EJB宣告一個stateless bean大家共用還沒問題.
    stateful bean就不行了, 因為stateful bean是一個user一個stateful bean.
    根據EJB3 in Action論壇所提示, 在servlet中試用了stateless bean(書上的例子)和stateful bean(論壇的提示).

    Reference

    EJB3 in Action - CH2 - A first taste of EJB
    EJB3 in Action論壇

    Codes

    stateless bean + MDB & servlet
    @Local
    public interface SimpleStatelessLocal {
        String simpleShow();
    }
    @Stateless
    public class SimpleStatelessBean implements SimpleStatelessLocal {
        public String simpleShow() {
            return this + ":This is simple show";
        }
    }    
    @MessageDriven(mappedName = "jms/TestQueueDestination", activationConfig =  {
            @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge"),
            @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")
        })
    public class SimpleStatelessBeanMDBBean implements MessageListener {
        public void onMessage(Message message) {
            try {
                TextMessage msg = (TextMessage) message;
                System.out.println("Receive message:" + msg.getText());
            } catch (JMSException ex) {
                throw new RuntimeException( ex );
            }
        }
    }
    public class SimpleStatelessServlet extends HttpServlet {
        @EJB
        private SimpleStatelessLocal simpleStatelessLocal;
        /** 
        * Processes requests for both HTTP GET and POST methods.
        * @param request servlet request
        * @param response servlet response
        */
        protected void processRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
            response.setContentType("text/html;charset=UTF-8");
            request.setAttribute("result", "sessionID:" + request.getSession().getId() + ":" + simpleStatelessLocal.simpleShow() );
            RequestDispatcher dispatcher = request.getRequestDispatcher("index.jsp");
            dispatcher.forward(request, response);
        } 
        // doGet, doPost call processRequest
    }
    

    stateful bean & servlet.
    注意在GlassFish上的stateful bean interface如果用@Local, servlet的lookup會找不到, 要用@Remote才可以.
    @Remote
    public interface SimpleStatefulRemote {
        String simpleShow();
    }    
    @Stateful(name="SimpleStatefulBean", mappedName="ejb/SimpleStatefulBean")
    public class SimpleStatefulBean implements SimpleStatefulRemote {
        public String simpleShow() {
            return this + ":This is simple show";
        }
    }
    public class SimpleStatefulServlet extends HttpServlet {
       /** 
        * Processes requests for both HTTP GET and POST methods.
        * @param request servlet request
        * @param response servlet response
        */
        protected void processRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
            try {
                response.setContentType("text/html;charset=UTF-8");
                InitialContext ctx = new InitialContext();
                SimpleStatefulRemote simpleStatefulLocal = (SimpleStatefulRemote) ctx.lookup("ejb/SimpleStatefulBean");
                request.setAttribute("result", "sessionID:" + request.getSession().getId() + ":" + simpleStatefulLocal.simpleShow());
                RequestDispatcher dispatcher = request.getRequestDispatcher("index.jsp");
                dispatcher.forward(request, response);
            } catch (NamingException ex) {
                throw new ServletException( ex );
            }
        } 
        //doGet, doPost call processRequest
    }
    

    星期六 六月 28, 2008

    簡單測試session bean的HttpClient

    description

    之前用到HttpClient, 覺得非常方便又好用.
    剛好在試用EJB3的時候, 不同的user可能有同個stateless bean, 卻會有不同的stateful bean.
    如果自己用同個瀏覽器都會測到同個session, 要測不同的session都比較麻煩.
    不過用HttpClient的話很方便, 都會是不同的session.
    所以就用一個簡單的HttpClient讓自己測試, 雖然顯示都是文字模式, 對我來說卻已經足夠.
    如果以後會不夠再說.

    Reference

    HttpClient

    Codes

    package testhttpclient;
    
    import java.awt.BorderLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.KeyAdapter;
    import java.awt.event.KeyEvent;
    import java.util.logging.Logger;
    import javax.swing.DefaultComboBoxModel;
    import javax.swing.JButton;
    import javax.swing.JComboBox;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JScrollPane;
    import javax.swing.JTextArea;
    import javax.swing.JTextField;
    import javax.swing.WindowConstants;
    import org.apache.commons.httpclient.HttpClient;
    import org.apache.commons.httpclient.methods.PostMethod;
    import org.apache.commons.io.IOUtils;
    import org.apache.commons.lang.math.NumberUtils;
    
    /**
     *
     * @author Isaac
     */
    public class TestHttpClient {
    
        // Used to add urls you want to test
        private String[] urls = new String[]{
            "http://localhost:8081/June2008-war/SimpleStatefulServlet",
            "http://localhost:8081/June2008-war/SimpleStatelessServlet"
        };
    
        public static void main(String[] args) {
            TestHttpClient test = new TestHttpClient();
            test.test();
        }
    
        private void test() {
            JTextArea jTextAreaResponse = new JTextArea(30, 80);
            JPanel jPanelUrl = prepareJPanelUrl(jTextAreaResponse);
            JPanel jPanelMain = prepareJPanelMain(jPanelUrl, jTextAreaResponse);
            show(jPanelMain);
        }
    
        private JPanel prepareJPanelMain(JPanel jPanelUrl, JTextArea jTextAreaResponse) {
            JPanel jPanelMain = prepareJPanel();
            JScrollPane jScrollPane = new JScrollPane();
            jScrollPane.setViewportView(jTextAreaResponse);
            jPanelMain.add(jPanelUrl, BorderLayout.NORTH);
            jPanelMain.add(jScrollPane, BorderLayout.CENTER);
            return jPanelMain;
        }
    
        private JPanel prepareJPanelUrl(JTextArea jTextAreaResponse) {
            JComboBox jComboBoxUrl = new JComboBox();
            JTextField jTextFieldRequestTimes = new JTextField(5);
            JButton jButtonSubmit = new JButton("Submit");
            
            jTextFieldRequestTimes.addKeyListener(new FormSubmitHandler(jComboBoxUrl, jTextAreaResponse, jTextFieldRequestTimes));
            
            jComboBoxUrl.setEditable(true);
            jComboBoxUrl.setModel(new DefaultComboBoxModel(urls));
            jComboBoxUrl.addActionListener(new FormSubmitHandler(jComboBoxUrl, jTextAreaResponse, jTextFieldRequestTimes));
            
            jButtonSubmit.addActionListener(new FormSubmitHandler(jComboBoxUrl, jTextAreaResponse, jTextFieldRequestTimes));
            
            JPanel jPanelBtn = prepareJPanel();
            jPanelBtn.add(jTextFieldRequestTimes, BorderLayout.WEST);
            jPanelBtn.add(jButtonSubmit, BorderLayout.EAST);
            
            JPanel jPanelUrl = prepareJPanel();
            jPanelUrl.add(jComboBoxUrl, BorderLayout.CENTER);
            jPanelUrl.add(jPanelBtn, BorderLayout.EAST);
            return jPanelUrl;
        }
    
        private JPanel prepareJPanel() {
            JPanel jPanel = new JPanel();
            jPanel.setLayout(new BorderLayout());
            return jPanel;
        }
    
        private void show(JPanel jPanelMain) {
            JFrame f = new JFrame();
            f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            f.getContentPane().setLayout(new BorderLayout());
            f.getContentPane().add(jPanelMain, BorderLayout.CENTER);
            f.pack();
            f.setVisible(true);
        }
    }
    
    
    class FormSubmitHandler extends KeyAdapter implements ActionListener {
    
        private JComboBox jComboBoxUrl;
        private JTextArea jTextAreaResponse;
        private JTextField jTextFieldRequestTimes;
    
        public FormSubmitHandler(final JComboBox jComboBoxUrl, final JTextArea jTextAreaResponse, final JTextField jTextFieldRequestTimes) {
            if ( jComboBoxUrl == null || jTextAreaResponse == null || jTextFieldRequestTimes == null ) {
                throw new IllegalArgumentException("All parameters can't be null.");
            }
            this.jComboBoxUrl = jComboBoxUrl;
            this.jTextAreaResponse = jTextAreaResponse;
            this.jTextFieldRequestTimes = jTextFieldRequestTimes;
        }
    
        public void actionPerformed(ActionEvent e) {
            doSubmit();
        }
    
        @Override
        public void keyPressed(KeyEvent e) {
            if ( e.getKeyCode() == KeyEvent.VK_ENTER ) {
                doSubmit();
            }
        }
        
        private void doSubmit() {
            String requestTimesText = jTextFieldRequestTimes.getText();
            int requestTimes = NumberUtils.isNumber(requestTimesText) ? NumberUtils.toInt(requestTimesText) : 1;
            jTextAreaResponse.setText(HttpRequester.request(jComboBoxUrl.getSelectedItem().toString(), requestTimes));        
        }
    }
    
    class HttpRequester {
    
        public static String request(String url, int requestTimes) {
            String response = "";
            for (int i = 0; i < requestTimes; i++) {
                Logger.getLogger(HttpRequester.class.getName()).info("Run " + (i + 1) + " times");
                try {
                    HttpClient client = new HttpClient();
                    PostMethod method = new PostMethod(url);
                    client.executeMethod(method);
                    response = IOUtils.toString(method.getResponseBodyAsStream());
                    method.releaseConnection();
                    client = null;
                } catch (Exception ex) {
                    Logger.getLogger(HttpRequester.class.getName()).info(HttpRequester.class.getName() + " has exception " + ex + " and break for loop.");
                    ex.printStackTrace();
                    break;
                }
            }
            return response;
        }
    }
    
    

    星期五 六月 27, 2008

    EJB3 - JPA - Query

    Description

    JPA的Query是很重要又方便的東西, 不過要注意盡量不要把Query和一般的entity處理方式放一起.
    因為JPA沒要求persistence provider必須將Query和persistence context同步, 所以現在就常看到一些因為使用Query導致使用者需要先flush才能Query的現象.

    Reference

    EJB3 in Action - CH10 - Using the query and JPQL to retrieve entities

    Notes

  • Query物件由EntityManager建立
    1. createNamedQuery可用JPQL也可放native SQL
    2. createNativeQuery(String sqlString)用來 UPDATE 或 DELETE
    3. createNativeQuery(String sqlString, Class result-class)用來取得single entity type
    4. createNativeQuery(String sqlString, String result-setMapping)用來取得multiple entity types
  • named query可用annotation定義在entity上或在orm xml上, 只要是多個地方會使用到的query都適合用作named query
  • named query 可增加效能, 因為他準備一次之後就可有效率的重複使用
  • 使用query不需要transaction, 在沒有transaction的情況下使用query會讓取得的entity detach.
  • 一個named query的name在persistence unit中必須是唯一的, 所以要注意命名
  • 用@NamedQuery或@NamedQueries
    @Entity
    @NamedQuery(name=Programmer.JPQL_FIND_ALL_PROGRAMMER, query="from Programmer")
    public class Programmer implements Serializable {...}
            
    @Entity
    @NamedQueries(
        @NamedQuery(name=Programmer.JPQL_FIND_ALL_PROGRAMMER, query="from Programmer")
    )
    public class Programmer implements Serializable {...}
    
    @Entity
    @NamedQueries({
        @NamedQuery(name=Programmer.JPQL_FIND_ALL_PROGRAMMER, query="from Programmer"),
        @NamedQuery(name=Programmer.JPQL_FIND_ALL_PROGRAMMER, query="from Programmer")
    })
    public class Programmer implements Serializable {...}
        
  • 使用named query的方式
    Query q = em.createNamedQuery(Programmer.JPQL_FIND_ALL_PROGRAMMER);
  • 使用dynamic query的方式
    Query q = em.createQuery("from Programmer");
  • 取得Query物件後, 較特別的method
    1. setParameter(String name, Object value)設定named paramater
    2. setParameter(String name, Date value, TemporalType temporalType)設定Date型態的named parameter
    3. setParameter(String name, Calendar value, TemporalType temporalType)設定Calendar型態的named parameter
    4. setParameter(int position, Object value)設定特定位置parameter的值
    5. setParameter(int position, Date value, TemporalType temporalType)設定特定位置Date型態parameter的值
    6. setParameter(int position, Calendar value, TemporalType temporalType)設定特定位置Calendar型態parameter的值
  • 使用position的參數設定方式, 建議用named parameter比較好debug
    private void useParametericQuery() {
        EntityManager em = EntityManagerHelper.getEntityManager();
        Query q = em.createQuery("select p from Programmer p where p.id >= ?100 AND p.id <= ?70");
        /* 如果指定錯誤, 例如放65.
         * 會出現java.lang.IllegalArgumentException: 
         * org.hibernate.QueryParameterException: 
         * could not locate named parameter [65] */ 
        q.setParameter(70, Long.valueOf(100));
        q.setParameter(100, Long.valueOf(50));
        List< Programmer > resultList = q.getResultList();
        for (Programmer programmer : resultList) {
            System.out.println(programmer.getId() + ":" + programmer.getName());
        }
    }
        
  • 使用named parameter的Query (建議用法)
    private void useNamedParametericQuery() {
        EntityManager em = EntityManagerHelper.getEntityManager();
        Query q = em.createQuery("select p from Programmer p " + 
                        "where p.id >= :lowerID and p.id <= :higherID");
        q.setParameter("lowerID", Long.valueOf(30));
        q.setParameter("higherID", Long.valueOf(80));
        List< Programmer > resultList = q.getResultList();
        for (Programmer programmer : resultList) {
            System.out.println(programmer.getId() + ":" + programmer.getName());
        }
    }
        
  • 使用getSingleResult取得一個entity.
    1. 如果query沒有entity會丟NoResultException, 若有多筆會NonUniqueResultException. 所以使用時要確定只會搜尋到一筆.
    2. 由於NonUniqueResultException與NoResultException不會roll back transaction, 所以若有處理transaction要記得處理exception的話怎麼辦.
    3. private void useNoResultExceptionQuery(String pname) {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("select p from Programmer p where p.name = :pname");
          q.setParameter("pname", pname);
          try {
              System.out.println(q.getSingleResult());
          } catch ( NoResultException nre ) {
              System.out.println("no result");
              nre.printStackTrace();
          } catch ( NonUniqueResultException nure ) {
              System.out.println("not unique result");
              nure.printStackTrace();
          }
      }
              
  • 用getResultList取得多筆entities
    1. 如果沒有符合的就回傳空List, 不會有exception.
    2. 透過setMaxResults可設定一頁最大query量
    3. 透過setFirstResult可設定要從哪一筆開始查
    4. private void usePaginationQuery() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createNamedQuery(Programmer.JPQL_FIND_ALL_PROGRAMMER);
          q.setMaxResults(10);
      
          int resultCount = 0;
          while ( true ) {
              q.setFirstResult(resultCount);
              List< Programmer > resultList = q.getResultList();
              if ( resultList.size() == 0) {
                  System.out.println("result end..");
                  break;
              }
              resultCount += resultList.size();
              System.out.println("getResultList...");
              for (Programmer programmer : resultList) {
                  System.out.println(programmer.getName());
              }
              System.out.println("next page...");
          }
      }
              
  • 設定Query的FlushMode
    1. FlushModeType.AUTO: 這是預設值, 會讓Query執行前先把EntityManager中對entity的處理commit再query.
      private void useAutoCommit() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          // 如果沒有begin就不會馬上有AUTO COMMIT的效果, 就是有 INSERT 的 statement
          em.getTransaction().begin(); 
          System.out.println("transaction begin");
          prepareProgrammer(em, 999); // 在這新增一筆Programmer但沒commit
          System.out.println("prepare programmer finished");
          Query q = em.createQuery("from Programmer");
          System.out.println("prepare Query instance");
          
          // 到 getResultList 才會 auto-commit 然後 select
          // 這時候 query 的結果會是處理完entity的結果
          List< Programmer > resultList = q.getResultList();
          System.out.println("getResultList");
          for (Programmer programmer : resultList) {
              System.out.println(programmer.getName());
          }
          
          // 可是如果最後沒執行commit還是不會把entity的處理放進DB
          em.getTransaction().commit();
      }                
                  
    2. FlushModeType.COMMIT: 這種mode沒有規定provider要怎麼實做. 測試Hibernate是Query的結果不會包含處理過的entity.
      private void useNotAutoCommit() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          em.getTransaction().begin();
          System.out.println("transaction begin");
          prepareProgrammer(em, 999);
          System.out.println("prepare programmer finished");
          Query q = em.createQuery("from Programmer");
          q.setFlushMode(FlushModeType.COMMIT);
          System.out.println("prepare Query instance");
          // 這時候query的結果不會包含新增的Programmer, 因為FlushMode改為COMMIT
          List< Programmer > resultList = q.getResultList();
          System.out.println("getResultList");
          for (Programmer programmer : resultList) {
              System.out.println(programmer.getName());
          }
      }
                  
  • 用Query執行update
    1. JPQL 的 update 格式
      UPDATE entityName indentifierVariable 
      SET single_value_path_expression1 = value1, ... 
      single_value_path_expressionN = valueN 
      WHERE where_clause
                  
    2. 使用JPQL update
      private void useUpdateQuery() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("update Programmer p set " + 
                          "p.name = 'QQ 100' where p.name = 'QQ 999'");
      
          // 執行Query的update需要transaction
          em.getTransaction().begin();
          q.executeUpdate();
          em.getTransaction().commit();
      }
                  
  • 使用Query執行delete
    1. JPQL的delete格式
      DELETE entityName indentifierVariable
      WHERE where_clause
                  
    2. 使用JPQL delete
      private void useDeleteQuery() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("delete Certification c where c.name like 'SCWCD%'");
      
          // 沒有transaction會
          // javax.persistence.TransactionRequiredException: Executing an update/delete query
          em.getTransaction().begin();
          q.executeUpdate();
          em.getTransaction().commit();
      }
                  
  • 替entity 命名
    1. @Entity可以命名, 如果沒有替entity命名, 名稱就會跟entity class name一樣, 注意如果沒有替entity的table命名, table名稱也會跟entity name一樣, 所以沒用@Table與替entity命名的話, 一旦改變entity class name就會出現找不到table的exception
    2. 在一個persistence unit中, entity name必須是唯一的, 否則在deploy階段就會出現錯誤
    3. 下面這樣的named query會取得Certification的資料, 雖然entity name取得差很多
      @Entity(name="Programmer2")
      @Table(name="CERTIFICATION")
      @NamedQueries({
          @NamedQuery(name=Certification.JPQL_FIND_ALL_CERT, query="from Certification as c")
      })
      public class Certification implements Serializable { ... }            
                  
  • 使用identifier variable
    1. 使用JPQL時會用到如
      FROM Certification AS c
      , 其中的 c 就是identifier variable
    2. identifier variable不能是同個persistence unit的entity name也不能是JPQL的保留字
    3. JPQL有保留字如下
      SELECT, UPDATE, DELETE, FROM, WHERE, GROUP, HAVING, ORDER, BY, ASC, DESC
      
      JOIN, OUTER, INNER, LEFT, FETCH
      
      DISTINCT, OBJECT, NULL, TRUE, FALSE, NOT, AND, OR, BETWEEN, LIKE, IN, AS, 
      UNKNOWN, EMPTY, MEMBER, OF, IS, NEW, EXISTS, ALL, ANY, SOME
      
      AVG, MAX, MIN, SUM, COUNT, MOD, UPPER, LOWER, TRIM, POSITION, CHARACTER_LENGTH, 
      CHAR_LENGTH, BIT_LENGTH, CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP
                  
  • path expression
    1. from Programmer p where p.name = 'KK' 的 p.name就是一個path expression
    2. path expression就是identifier variable和persistence或相關欄位間用"."連起來
    3. 用"."連起來的相關變數可以是一個單一值(one-to-one, many-to-one)或一個collection(many-to-many, one-to-many).
    4. many-to-many, one-to-many的用法為
      from Programmer p where p.certs is not empty
    5. single-value中one-to-one的用法就是直接在"."之後加上欄位即為single-value
    6. 無法在many-to-one的情形下使用"."取得欄位值, 比方說certificationCenter.sentCertifications.programmer, 因為sentCertifications是collection, 會不知道要取得的programmer是哪一個而發生錯誤
  • where clause
    1. 使用where的時候幾乎所有的java literal都可用, 但是八進和十六進位的數字不行, byte[] 或 char[]也不行.
  • Conditional與operator
    1. JPQL支援的operator如下
      .
      
      +, - 
      
      +, -, *, /
      
      =, >, >=, <=, <>, 
      [not] bewteen, [not] like, [not] in, is [not] null, is [not] empty, [not] member [of]
      
      NOT, AND, OR
                  
    2. 使用between的兩個屬性必須是同一個型別
      private void useQueryBetween() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("from Certification c where c.createDate between ?0 and ?1");
          Calendar date1 = Calendar.getInstance(); date1.set(1998, 0, 1);
          Calendar date2 = Calendar.getInstance(); date2.set(2000, 0, 1);
          q.setParameter(0, date1);
          q.setParameter(1, date2);
          List< Certification > list = q.getResultList();
      
          q = em.createQuery("select count(c) from Certification c " + 
                          "where c.createDate between ?0 and ?1");
          q.setParameter(0, date1);
          q.setParameter(1, date2);
          Long count = (Long) q.getSingleResult();
      
          System.out.println(Long.valueOf(list.size()).equals(count));  // true
      }
                  
    3. 使用in時可用('A','B'), 就是把要判斷的值放進括號中或是在括號內放進sub query內. 當一個query有subquery時, 會先執行subquery再執行query
      private void useQueryIn() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("from Certification c where" + 
                          " c.name in ('SCJP 0', 'SCJP 1', 'SCJP 2')");
          List< Certification > list = q.getResultList();
          for (Certification certification : list) {
              System.out.println(certification.getName());
          }
      
          q = em.createQuery("from Certification c " + 
                      "where c.name not in ('SCJP 0', 'SCJP 1', 'SCJP 2')");
          list = q.getResultList();
          for (Certification certification : list) {
              System.out.println(certification.getName());
          }
      
          q = em.createQuery("from Certification c " + 
                      "where c.programmer in (from Programmer p where p.id > 600)");
          list = q.getResultList();
          for (Certification certification : list) {
              System.out.println(certification.getName() + 
                          ":" + certification.getProgrammer().getId());
          }
      
          q = em.createQuery("from Certification c where " + 
                      "c.name in (select c.name from c where c.id > 600)");
          list = q.getResultList();
          for (Certification certification : list) {
              System.out.println(certification.getId() + ":" + certification.getName());
          }
      }
                  
    4. 使用like判斷一個欄位是否符合某種字串格式, 可用底線(_)代表一個字或用百分號(%)代表任意個數的字
      private void useQueryLike() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("from Certification c where c.name like '%_P_9_'");
          List< Certification > list = q.getResultList();
          for (Certification cert : list) {
              System.out.println(cert.getName());
          }
      
          q = em.createQuery("from Certification c where c.name not like '%_P_9_'");
          list = q.getResultList();
          for (Certification cert : list) {
              System.out.println(cert.getName());
          }
      }                
                  
    5. JPQL定義的null和empty string是不同的, 但不是所有DB都這樣, 使用DB前要先測試確認清楚.
      當conditional expression遇到null結果會是 null 或 unknown.
      不能用is null來判斷一個collection是否為空collection. is null也不會判斷一個collection是否為空.
      對於collection, JPQL是透過empty來判斷是否為空collection.
      由於SQL中並沒有is empty這種東西, 所以JPQL的is empty其實是透過JOIN的方式去找看相關聯的entity collection是否為空.
      TRUE  AND null = UNKNOWN
      FALSE AND null = FALSE
      Null  AND null = UNKNOWN
      TRUE  OR  null = TRUE
      Null  OR  null = UNKNOWN
      FALSE OR  null = UNKNOWN
            NOT null = UNKNOWN
                  
      private void useQueryNull() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("from Certification c where c.name is null");
          List< Certification > list = q.getResultList();
          for (Certification certification : list) {
              System.out.println(certification.getName());
          }
      
          q = em.createQuery("from Programmer p where p.certs is empty");
          List< Programmer > programmers = q.getResultList();
          for (Programmer programmer : programmers) {
              System.out.println(programmer.getName() + ":" + programmer.getCerts().size());
          }
      }
                  
    6. member的用法是
      entity_expression [NOT] MEMBER [OF] collection_value_path_expression
      , 如下所示
      private void useQueryMember() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Certification c = em.find(Certification.class, Long.valueOf(114));
          Query q = em.createQuery("from Programmer p where ?0 member of p.certs");
          q.setParameter(0, c);
          List< Programmer > list = q.getResultList();
          for (Programmer programmer : list) {
              System.out.println(programmer.getCerts().contains(c));
          }
      }
                  
  • 使用JPQL中字串處理的function. 因為java處理字串比放到DB才處理更快, 所以建議在JPQL處理字串.
      private void useJPQLStringFunction() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("from Certification c where " + 
                          "CONCAT('scwcd ', '14') = LOWER(c.name) ");
          List< Certification > list = q.getResultList();
          for (Certification certification : list) {
              System.out.println(certification.getName());
          }
      
          // substring第一個字母index就是一, 不像java從0開始
          q = em.createQuery("from Certification c where " + 
                      "UPPER(SUBSTRING('abcscwcd 14123', 4, 8)) = UPPER(c.name)");
          list = q.getResultList();
          for (Certification cert : list) {
              System.out.println(cert.getName());
          }
      
          q = em.createQuery("from Certification c where " + 
                      "UPPER(TRIM(BOTH ' ' FROM '   scwcd 14  ')) = UPPER(c.name)");
          list = q.getResultList();
          for (Certification cert : list) {
              System.out.println(cert.getName());
          }
      
          q = em.createQuery("from Certification c where " + 
                      "UPPER(TRIM(LEADING ' ' FROM '   scwcd 14')) = UPPER(c.name)");
          list = q.getResultList();
          for (Certification cert : list) {
              System.out.println(cert.getName());
          }
      
          q = em.createQuery("from Certification c where " + 
                      "UPPER(TRIM(TRAILING ' ' FROM 'scwcd 14  ')) = UPPER(c.name)");
          list = q.getResultList();
          for (Certification cert : list) {
              System.out.println(cert.getName());
          }
      
          q = em.createQuery("from Certification c where " + 
                      "UPPER(TRIM(BOTH 'a' FROM 'aaaascwcd 14aaaaaaaaa')) = UPPER(c.name)");
          list = q.getResultList();
          for (Certification cert : list) {
              System.out.println(cert.getName());
          }
      
          q = em.createQuery("from Certification c where LENGTH(c.name) = 8");
          list = q.getResultList();
          for (Certification cert : list) {
              System.out.println(cert.getName());
          }
      
          // LOCATE第一個字index也是1
          q = em.createQuery("from Certification c where LOCATE('wcd', LOWER(c.name)) = 3");
          list = q.getResultList();
          for (Certification cert : list) {
              System.out.println(cert.getName());
          }
      
      }            
              
  • 使用JPQL的arithmetic function
    1. 有ABS(arithmetic_expression), SQRT(arithmetic_expression), MOD(num, div), SIZE(collection_value_path_expression)
      private void useJPQLArithmeticFunction() {
          EntityManager em = EntityManagerHelper.getEntityManager();
      
          // ABS 絕對值
          Query q = em.createQuery("from Certification c where ABS(c.id) = 100");
          List< Certification > list = q.getResultList();
          for (Certification certification : list) {
              System.out.println(certification.getName());
          }
      
          // SQRT 平方根
          q = em.createQuery("from Certification c where SQRT(c.id) = 10");
          list = q.getResultList();
          for (Certification certification : list) {
              System.out.println(certification.getName());
          }
      
          // MOD 餘數
          q = em.createQuery("from Certification c where c.id = MOD(1100, 1000)");
          list = q.getResultList();
          for (Certification certification : list) {
              System.out.println(certification.getName());
          }
      
          // SIZE 量
          q = em.createQuery("from Programmer p where SIZE(p.certs) > 2");
          List< Programmer > programmers = q.getResultList();
          for (Programmer programmer : programmers) {
              System.out.println(programmer.getName());
          }
      }                
                  
  • 使用JPQL的temporal function
    1. 有 CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP, 不過測試無法使用CURRENT_TIME
      private void useJPQLTemporalFunction() {
          EntityManager em = EntityManagerHelper.getEntityManager();
      
          // 從DB 取DATE
          Query q = em.createQuery("select distinct CURRENT_DATE from Certification");
          System.out.println(q.getSingleResult());
      
          // 無法使用CURRENT_TIME, 會有
          //java.sql.SQLException: ORA-00904: "CURRENT_TIME": invalid identifier
          q = em.createQuery("select distinct CURRENT_TIME from Certification");
          System.out.println(q.getSingleResult());
      
          // 從DB取得TIMESTAMP
          q = em.createQuery("select distinct CURRENT_TIMESTAMP from Certification");
          System.out.println(q.getSingleResult());
      }
                  
  • 使用select
    1. select的內容可以用逗號分隔開一個以上的identifier variable 或 single-value path expression 或 aggregate functions.
      private void useJPQLSelect() {
          EntityManager em = GeneralEAO.getEntityManager();
          Query q = null;
      
          // simple select clause
          q = em.createQuery("select p from Programmer p, Address a where a.employee.id = p.id");
          List< Programmer > simpleProgrammer = q.getResultList();
          for (Programmer programmer : simpleProgrammer) {
              System.out.println(programmer.getName());
          }
      
          // select distinct
          q = em.createQuery("select distinct(p) from Programmer p, " + 
                      "Address a where a.employee.id = p.id");
          List< Programmer > distinctProgrammer = q.getResultList();
          for (Programmer programmer : distinctProgrammer) {
              System.out.println(programmer.getName());
          }
      
          // select single property
          q = em.createQuery("select p.name from Programmer p where " + 
                      "p.payment = (select max(p.payment) from Programmer p)");
          List< String > oneProp = q.getResultList();
          for (String name : oneProp) {
              System.out.println(name);
          }
      
          // select two property
          q = em.createQuery("select p.name, p.payment from Programmer p " + 
                      "where p.payment = (select max(p.payment) from Programmer p)");
          List< Object[] > twoProp = q.getResultList();
          for (Object[] objects : twoProp) {
              String name = (String) objects[0];
              Double payment = (Double) objects[1];
              System.out.println(name + ":" + payment);
          }
      
          // select association entity
          q = em.createQuery("select n.owner.name, n from NoteBook n where " + 
                      "n.owner.payment = (select MAX(p.payment) from Programmer p)");
          List< Object[] > twoProp2 = q.getResultList();
          for (Object[] objects : twoProp2) {
              String name = (String) objects[0];
              NoteBook noteBook = (NoteBook) objects[1];
              System.out.println(name + ":" + noteBook.getMachineNumber() + 
                          ":" + noteBook.getOwner().getName());
          }
      
          // 書上好像說不能select collection的, 不過測試還正常
          // select collection entities
          q = em.createQuery("select p.addressList from Programmer p");
          List< Address > addressList = q.getResultList();
          for (Address address : addressList) {
              System.out.println(address.getStreet());
          }
      
          // 這樣就會有錯, 因為除了collection以外又加一個single-value
          try {
              q = em.createQuery("select p.name, p.addressList from Programmer p");
              List< Object > nameAddressList = q.getResultList();
              for (Object object : nameAddressList) {
                  System.out.println(object);
              }
          } catch (Exception e) {
              System.out.println("select p.name, p.addressList has " + 
                      "exception because can't select single-value and collections." + e);
          }
      
          // new entity.StringObject傳入的參數試過不能是null, 而且要有對映的建構子. 型態也要一樣 如下:
          // < code >
          //    public class StringObject {
          //    
          //    private String str1;
          //    private String str2;
          //    private Double double3;
          //
          //    public StringObject(String str1, String str2, Double double3) {
          // < /code >
          // 這裡的StringObject不用是entity也不用mapping到DB.
          q = em.createQuery("select new entity.StringObject(p.name, p.lang, p.payment)" + 
                      " from Programmer p");
          List< StringObject > stringObjectList = q.getResultList();
          for (StringObject stringObject : stringObjectList) {
              System.out.println(stringObject.getStr1());
              System.out.println(stringObject.getStr2());
              System.out.println(stringObject.getDouble3());
          }
          
          // Employee是abstract super class, 透過JPQL搜尋可取得subclass
          List< Employee > employeeList = em.createQuery("from Employee").getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee);
          }
      }                
                  
  • 使用aggregate functions
      AVG:   Return Double 
      COUNT: Return Long
      MAX:   Return persistence field type
      MIN:   Return persistence field type
      SUM:   Return Long or Double        
              
    1. 這幾個function中, 除了COUNT可以放任何type以外, 其他都只能放persistence field.
      private void useAggregateFunctions() {
          EntityManager em = EntityManagerHelper.getEntityManager();
      
          /* 若放AVG(p.name)或SUM(p.name) 會
           * javax.persistence.PersistenceException: 
           * org.hibernate.exception.SQLGrammarException: 
           * could not execute query */
          Query q = em.createQuery(
              "select AVG(p.id), MAX(p.id), MIN(p.id), SUM(p.id), COUNT(p), " +
                                "MAX(p.name), MIN(p.name),        COUNT(p.name) " + 
                                  "from Employee p");
          Object[] o = (Object[]) q.getSingleResult();
          Double idAvg = (Double) o[0];
          Long   idMax = (Long) o[1];
          Long   idMin = (Long) o[2];
          Long   idSum = (Long) o[3];
          Long   idCnt = (Long) o[4];
          String nameMax = (String) o[5];
          String nameMin = (String) o[6];
          Long   nameCnt = (Long) o[7];
          System.out.println("avg:" + idAvg);
          System.out.println("idMax:" + idMax);
          System.out.println("idMin:" + idMin);
          System.out.println("idSum:" + idSum);
          System.out.println("idCnt:" + idCnt);
          System.out.println("nameMax:" + nameMax);
          System.out.println("nameMin:" + nameMin);
          System.out.println("nameCnt:" + nameCnt);
      }            
  • 使用GROUP BY與HAVING
    1. 使用group by的時候只允許aggregation functions.
    2. 可用having再篩選
    3. private void useGroupByAndHaving() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("select c.owner.name, COUNT(c.id) from " + 
                      "Certification c group by c.owner.name");
          List< Object[] > resultList = q.getResultList();
          for (Object[] result : resultList) {
              String ownerName = (String) result[0];
              Long count = (Long) result[1];
              System.out.println(ownerName + " has " + count + " certs.");
          }
      
          q = em.createQuery("select c.owner.name, COUNT(c.id) from " + 
                      "Certification c group by c.owner.name having COUNT(c.id) = 4");
          resultList = q.getResultList();
          for (Object[] result : resultList) {
              String ownerName = (String) result[0];
              Long count = (Long) result[1];
              System.out.println(ownerName + " has " + count + " certs.");
          }
      
          q = em.createQuery("select c.owner.name, COUNT(c.id) from " + 
                      "Certification c where c.id < 40 group by c.owner.name " + 
                      "having COUNT(c.id) = 4");
          resultList = q.getResultList();
          for (Object[] result : resultList) {
              String ownerName = (String) result[0];
              Long count = (Long) result[1];
              System.out.println(ownerName + " has " + count + " certs.");
          }
      }          
              
  • 使用order by排序
    1. 有asc, desc兩種
    2. 書上寫個規則就是在order by中若使用path expression而不是只有identifier就必須在select clause中加上order by的欄位.
      比方說order by c.createDate, 這個createDate就要包含在select子句中. 用Hibernate測試的時候沒發現這種限制.
    3. private void useOrderBy() {
          EntityManager em = EntityManagerHelper.getEntityManager();
          Query q = em.createQuery("from Sales s order by s.id desc, s.performance asc");
          List< Sales > list = q.getResultList();
          for (Sales sales : list) {
              System.out.println(sales.getId() + ":" + sales.getPerformance());
          }
      
          /* 書上說這樣是不對的, 因為order by s.id沒包括在select clause中.
           * 不過Hibernate試過還可以 */
          q = em.createQuery("select s.performance from Sales s order by s.id desc");
          List< Double > performanceList = q.getResultList();
          for (Double performance : performanceList) {
              System.out.println(performance);
          }
      }
              
  • 使用subquery
    1. subquery用在where或having中來過遇取得的結果. 注意在from子句不能用subquery
    2. subquery可搭配ANY, SOME, ALL使用, ANY與SOME意思相同
    3. private void useSubQuery() {
          EntityManager em = EntityManagerHelper.getEntityManager();
      
          // normal subquery
          Query q = em.createQuery("select c.owner from Certification c " + 
                      "where c.owner = (from Employee e where e.id = 9)");
          List< Employee > employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee.getName());
          }
      
          // IN : 檢查是否在subquery中
          q = em.createQuery("select c.owner from Certification c " + 
                      "where c.owner in (from Employee e where e.id < 50)");
          employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee.getName());
          }
      
          // EXISTS : 檢查是否有資料存在
          q = em.createQuery("select c.owner from Certification c " + 
                      "where exists (from Employee e where e.id = c.owner.id)");
          employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee.getName());
          }
      
          // ANY : 只要與任一筆相比較符合即可. 可搭配 =, >, >=, <, <=, < >
          q = em.createQuery("select c.owner from Certification c " + 
                      "where c.owner.id > ANY(select e.id from Employee e where e.id < 20)");
          employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee);
          }
      
          // SOME : 只要與任一筆相比較符合即可. 可搭配 =, >, >=, <, <=, < >
          q = em.createQuery("select c.owner from Certification c " + 
                      "where c.owner.id > SOME(select e.id from Employee e where e.id > 700)");
          employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee);
          }
      
          // ALL : 必須與所有的資料比較相符. 可搭配 =, >, >=, <, <=, < >
          q = em.createQuery("select c.owner from Certification c " + 
                  "where c.owner.id > ALL(select e.id from Employee e where e.id < 700)");
          employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee);
          }
      
      }        
              
  • 使用join
    1. theta-join : 雖然是沒有constraint的欄位, 但因為在意義上是有關係的, 所以也拿來作select.
    2. relationship join : 就是用entity間的關連join. 可以把完全吻合的entity都找出來, 若相關聯的entity為空就不算match
    3. inner join的語法是 : [INNER] JOIN join_association_path_expression [AS] identification_variable
    4. outer join和inner join不一樣的就是不一定會match所有條件, 就是即使相關聯的entity為空也算match.
    5. inner [outer] join fetch 可取得原本設為lazy的相關屬性
    6. private void useJoin() {
          EntityManager em = EntityManagerHelper.getEntityManager();
      
          // theta-join
          Query q = em.createQuery("select ae.name from " + 
                  "AnotherEmployee ae, Certification c where ae.id = c.owner.id group by ae.name");
          List< String > nameList = q.getResultList();
          for (String name : nameList) {
              System.out.println(name);
          }
      
          // inner join . "inner" is optional.
          q = em.createQuery("select e from Employee e inner join e.noteBook");
          List< Employee > employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee.getId() + 
                      " noteBook is null: " + (employee.getNoteBook() == null));
          }
      
          // inner join. "inner" and "as" is optional
          q = em.createQuery("select n from Employee e inner join e.noteBook as n");
          List< NoteBook > noteBookList = q.getResultList();
          for (NoteBook noteBook : noteBookList) {
              System.out.println(noteBook.getOwner().getId());
          }
      
          // outer join.
          q = em.createQuery("select e from Employee e left outer join e.noteBook");
          employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee.getId() + 
                      " noteBook is null: " + (employee.getNoteBook() == null) );
          }
      
          /* both join 會有java.lang.IllegalArgumentException: 
           * org.hibernate.hql.ast.QuerySyntaxException: 
           * unexpected token: both near line 1, column 33 
           * [select e from entity.Employee e both join e.noteBook] */
          q = em.createQuery("select e from Employee e both join e.noteBook");
          employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee.getId() + " noteBook is null: " + 
                      (employee.getNoteBook() == null) );
          }
      
          /**
           * right outer join 有 java.lang.UnsupportedOperationException: 
           * join type not supported by OracleJoinFragment (use Oracle9Dialect)
           */
          q = em.createQuery("select e from Employee e right outer join e.noteBook");
          employeeList = q.getResultList();
          for (Employee employee : employeeList) {
              System.out.println(employee.getId() + " noteBook is null: " + 
                      (employee.getNoteBook() == null) );
          }
      
          /** 
           * 用join fetch可直接透過Query取得lazy的相關型態
           * reference: http://edocs.bea.com/kodo/docs41/full/html/ejb3_langref.html
           *                       #ejb3_langref_fetch_joins
           */
          q = em.createQuery("select e from Employee e inner join fetch e.certs");
          employeeList = q.getResultList();
          em.clear(); // 這樣才可以知的detach
          for (Employee employee : employeeList) {
              System.out.println(employee.getCerts());
          }
      
      } 
              
  • 使用update和delete
    1. update, delete一定要在transaction中
    2. update, delete也可以加參數
    3. 建議把update, delete獨立於平常的操作, 因為Query是否與persistence context是由provider決定, JPA沒規定要同步.
    4.     private void useUpdateDelete() {
              EntityManager em = EntityManagerHelper.getEntityManager();
              Query q = em.createQuery("update Programmer p set p.lang = :lang");
              q.setParameter("lang", "Java");
              
              /**
               * 沒transaction會有javax.persistence.TransactionRequiredException:
               * Executing an update/delete query
               */
              em.getTransaction().begin();
              System.out.println("update..." + q.executeUpdate() + " records");
              em.getTransaction().commit();
              
              q = em.createQuery("delete NoteBook n where n.id < :lowID");
              q.setParameter("lowID", Long.valueOf(200));
              em.getTransaction().begin();
              System.out.println("delete..." + q.executeUpdate() + " records");
              em.getTransaction().commit();
          }            
              
  • 使用Native SQL
    1. 盡量不要用Native SQL, 因為不portable.
    2. 和delete, update一樣, 由於沒規定要和persistence context同步, 因此最好把Query操作和一般的entity操作隔開來.
    3. 透過JPQL和Native SQL取得的Query在使用上沒有不同. 在使用named query上也沒有不同, 只是native sql的named query沒有要求provider一定支援
    4. JPA不支援store procedure
    5. private void useNativeSQL() {
          EntityManager em = EntityManagerHelper.getEntityManager();
      
          // simple native sql
          Query q = em.createNativeQuery("select e.EMPLOYEE_ID, e.EMPLOYEE_TYPE, " + 
                      "e.NAME from EMPLOYEE e");
          List< Object[] > objAryList = q.getResultList();
          for (Object[] objAry : objAryList) {
              BigDecimal id = (BigDecimal) objAry[0];
              String type = (String) objAry[1];
              String name = (String) objAry[2];
              System.out.println(id + ":" + type + ":" + name);
          }
      
          // native sql with parameters
          q = em.createNativeQuery("select e.EMPLOYEE_ID, e.EMPLOYEE_TYPE, e.NAME " + 
                      "from EMPLOYEE e where e.EMPLOYEE_ID = :id");
          q.setParameter("id", Long.valueOf(1));
          objAryList = q.getResultList();
          for (Object[] objAry : objAryList) {
              BigDecimal id = (BigDecimal) objAry[0];
              String type = (String) objAry[1];
              String name = (String) objAry[2];
              System.out.println(id + ":" + type + ":" + name);
          }
      
          // 指定所選的class, 注意雖然SQL只說要select EMPLOYEE_ID, 透過Query卻可取出整個owner.
          // 不過這種方式會有一次只能指定一個entity的問題
          q = em.createNativeQuery("select NOTE_BOOK_ID, SERIAL_NO, EMPLOYEE_ID " + 
                      "from NOTE_BOOK where NOTE_BOOK_ID = 200", NoteBook.class);
          List< NoteBook > noteBookList = q.getResultList();
          for (NoteBook noteBook : noteBookList) {
              System.out.println(noteBook.getId() + ":" + noteBook.getSerialNO() + 
                      ":" + noteBook.getOwner());
          }
      
          /** 
           * 透過指定SqlResultSetMapping即可同時用native sql取得兩種entity 
           *  @SqlResultSetMapping(name="allNoteBooksAndCertifications", 
           *      entities={
           *          @EntityResult(entityClass=NoteBook.class), 
           *          @EntityResult(entityClass=Certification.class)})
           */
          q = em.createNativeQuery("select * from NOTE_BOOK n, CERTIFICATION c " + 
                  "where n.EMPLOYEE_ID = c.EMPLOYEE_ID and c.CERTIFICATION_ID = 202"
                  , "allNoteBooksAndCertifications");
          objAryList = q.getResultList();
          for (Object[] objAry : objAryList) {
              NoteBook noteBook = (NoteBook) objAry[0];
              Certification cert = (Certification) objAry[1];
              System.out.println(noteBook);
              System.out.println(cert);
          }
      
          /**
           * 透過在NoteBook上設定named query
           * @NamedNativeQueries({
           *      @NamedNativeQuery(name="all204NoteBook", 
           *          query="select * from NOTE_BOOK where NOTE_BOOK_ID = 204", 
           *          resultClass=NoteBook.class), 
           *      @NamedNativeQuery(name="204NoteBooksAnd202Certifications", 
           *          query="select * from NOTE_BOOK n, CERTIFICATION c " + 
           *              "where n.EMPLOYEE_ID = c.EMPLOYEE_ID" + 
           *              " and c.CERTIFICATION_ID = 202", 
           *          resultSetMapping="allNoteBooksAndCertifications")
           * })
           */
          q = em.createNamedQuery("all204NoteBook");
          noteBookList = q.getResultList();
          for (NoteBook noteBook : noteBookList) {
              System.out.println(noteBook);
          }
          q = em.createNamedQuery("204NoteBooksAnd202Certifications");
          objAryList = q.getResultList();
          for (Object[] objAry : objAryList) {
              NoteBook noteBook = (NoteBook) objAry[0];
              Certification cert = (Certification) objAry[1];
              System.out.println(noteBook);
              System.out.println(cert);
          }
      
      }            
              
  • 星期日 六月 15, 2008

    EJB3 - JPA - CRUD

    Description

    CRUD的方式也很多, 覺得盡量是用官方的建議操作方式, 不然就是檢查執行的sql是否符合需求, 小心會很耗資源或有不可預知的錯誤喔.

    Reference

    EJB3 in Action

    Notes

  • EntityManager的操作可看javax.persistence.EntityManager
  • EJB2叫做Entity Bean是因為可以取得container的服務, 但EJB3叫Entity(沒有Bean)是因為不會使用到container的服務.
  • EntityManager會追蹤的叫attached或managed, 反之叫detached.
  • Managed entity
    1. 所謂managed entity就是EntityManager會確保這個entity和db的資料是同步的.
    2. 為了讓entity managed, 我們會先要求EntityManager開始manage entity, EntityManager會讓entity和db同步
    3. 之後entity要detached時, EntityManager會確保entity的改變會同步到db中. EntityManager透過定期檢查managed entity更新資料與db同步.
    4. EntityManager在eneity removed或離開persistence provider可管理範圍後停止manage.
    5. 一個entity可透過persist, merge, refresh, find變attach狀態
    6. 使用find時不需要transaction, 不過如果在一個無transaction的地方使用find, 則entity在回傳後會立刻detached.
    7. merge, refresh用來attach一個原本attach變detached的entity
  • Detached entity
    1. detached entity就是不被EntityManager管理且不保證資料和db同步.
    2. entity透過離開EntityManager管理範圍或clone或序列化detach, 這是因為EntityManager透過對Java object的reference管理entity, 而clone與序列化就已經不是同一個reference了.
    3. clear會讓Persistence context中所有entity變detached
    4. remove是讓單一的entity從DB刪除並detached
  • Persistence context
    1. 雖然我們透過EntityManager做Persistence operation, 但EntityManager卻不直接追蹤entity, 而是將管理entity的任務delegate給Persistence context.
    2. Persistence context是在指定的persistence scope中由EntityManager管理的一群自我控制的entity.
    3. Persistence scope是一段指定entity維持managed狀態的時間.
    4. Persistence scope分兩種: transaction 和 extended
    5. 用變數來說, transaction-scoped像是local variable, extended-scope像是instance variable.
  • Transaction-scoped EntityManager
    1. transaction-scoped的entity會在transaction開始時attach, 在transaction結束時detached.
  • Extended EntityManager
    1. 就是持續多個transaction的EntityManager.
    2. Extended EntityManager只能用在stateful session bean且與其並存. 所以在stateful session bean中entity會持續被EntityManager管理直到這個session bean結束EntityManager close為止
  • Create - EntityManager.persist
    1. persist前要確保這個identity或PK還不存在於db中. 否則會有PersistenceException
    2. 當persist 後entity會managed, 不過INSERT statement不會立刻執行. transaction-scoped會在transaction結束後commit. extended-scoped可能在EntityManager close之前commit. EntityManager.flush也會commit
    3. 如果id的GeneratedValue的 strategy設為IDENTITY, id就會由DB產生而不會放在INSERT statement, 若strategy是SEQUENCE或TABLE就會先有SELECT statement再有INSERT statement.
    4. 呼叫persist時需要transaction, 若無transaction就會丟TransactionRequiredException給transaction-scoped entity manager. 若想試試看可把persist method宣告為NOT_SUPPORTED
      @Stateless
      public class TestPersistWithoutTransactionBean implements TestPersistWithoutTransactionLocal {
      
        @PersistenceContext(unitName="persistence_oracle", type=PersistenceContextType.TRANSACTION) EntityManager em;
      
        @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
        public String persistWithoutTransaction() {
            String exceptionString = "run persist without transaction";
            try {
                Programmer p = new Programmer();
                p.setName("Joe");
                em.persist(p);
            } catch (TransactionRequiredException tre) {
                Logger.getLogger(TestPersistWithoutTransactionBean.class).error(tre);
                tre.printStackTrace();
            }
            return exceptionString;
        }
      
      }                           
                  
    5. 使用Application managed 或 extended-scoped EntityManager時, EntityManager的操作會在join transaction時commit. 如下所示
      private void test() {
          Programmer p = new Programmer();
          p.setName("Joe");
      
          Certification c = new Certification();
          c.setCertName("SCJP");
          c.setOwner(p);
          p.getCerts().add(c);
      
          GeneralEAO.createWithoutTransaction(p);
          System.out.println("after create without transaction");
          GeneralEAO.hereIsTransactionCommit(); // 在這行會commit
      }        
                  
    6. 同樣的情形適用於flush, merge, refresh
    7. 用find找出已存在的master entity是attach的狀態, 所以對master entity做的改變都會sync到DB. 這種coding方式能套用在兩邊都沒設定cascade的情形
      public static void addCertIntoDetachedProgrammer(Long programmerId) {
          Programmer p = EntityManagerHelper.getEntityManager().find(Programmer.class, programmerId); 
      
          Certification c = new Certification();
          c.setCertName("addCertIntoDetachedProgrammer");
      
          // 由於Programmer已經在DB中, 所以要把PROGRAMMER_ID會是設定到Certification
          // 如果沒設定, CERTIFICATION.PROGRAMMER_ID 會是 null
          c.setOwner(p); 
      
          EntityManagerHelper.getEntityManager().persist(c); 
          GeneralEAO.transactionCommit();
      }                
                  
      我把method改成如下. 這樣看起來沒問題, 但如果兩邊都沒設定cascade, 且Programmer不是一個已經存進db的資料就會出現 org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: entity.Certification.owner -> entity.Programmer的錯誤.
      public static void addCert(Programmer p) {
          Certification c = new Certification();
          c.setCertName("addCert");
          c.setOwner(p); 
      
          //可確保Programmer一定會被存��DB
          EntityManagerHelper.getEntityManager().persist(c); 
          GeneralEAO.transactionCommit();
      }                    
                  
    8. 設定cascade. cascade的類型可看 javax.persistence.CascadeType
      1. 預設上, cascade都是設定成沒有cascade的效果. 所以當master還沒persist, 就直接persist detail會出現exception.
      2. 一但設定了cascade, 就可以確保相關聯的entity可以persist進db中. 這樣一來, 上面的addCert method就可以正常使用.
  • Retrieve - EntityManager.find
    1. 使用find時, 如果找不到PK就回傳null.
    2. find不強制要在transaction context使用, 但若無transaction context則entity一回傳就是detached狀態
    3. find最重要的功能之一就是cache, 就是EntityManager會視情況決定是否要再進DB撈一次資料. 不過caching的功能是optional, 要看provider有沒有實做
    4. 最簡單的就是這樣
      public static Programmer findProgrammer(Long id) {
          return EntityManagerHelper.getEntityManager().find(Programmer.class, id);
      }
                  
    5. 處理@EmbeddedId的情形如下. 這裡剛好看到一個情形就是對同一筆資料查詢時EntityManager會回傳同一個instance給你.
      public static void test() {
        Programmer javaP = ProgrammerEAO.createProgrammer();
      
        ProgrammerPK pk = new ProgrammerPK();
        pk.setName(javaP.getProgrammerPK().getName());
        pk.setProgrammerID(javaP.getProgrammerPK().getProgrammerID());
      
        Programmer p = ProgrammerEAO.findProgrammer( pk );
        System.out.println(p == javaP); // true
      }   
      
      @Entity
      public class Programmer implements Serializable {
        private static final long serialVersionUID = 1L;
      
        private ProgrammerPK programmerPK;
        private String lang;
      
        @EmbeddedId
        public ProgrammerPK getProgrammerPK() {
            return programmerPK;
        }
      
        public void setProgrammerPK(ProgrammerPK programmerPK) {
            this.programmerPK = programmerPK;
        }
      
        @Column(name="LANG")
        public String getLang() {
            return lang;
        }
      
        public void setLang(String lang) {
            this.lang = lang;
        }
      
      }
      
      @Embeddable
      public class ProgrammerPK implements java.io.Serializable {
      
        private UUID programmerID;
        private String name;
      
        @Column(name="NAME")
        public String getName() {
            return name;
        }
      
        public void setName(String name) {
            this.name = name;
        }
      
        @Column(name="PROGRAMMER_ID")
        public UUID getProgrammerID() {
            return programmerID;
        }
      
        public void setProgrammerID(UUID programmerID) {
            this.programmerID = programmerID;
        }
      
      }                
                  
    6. 處理@IdClass的做法如下
      public static void test() {
        Programmer saveP = createProgrammer();
      
        ProgrammerPK pk = new ProgrammerPK();
        pk.setName(saveP.getName());
        pk.setProgrammerID(saveP.getProgrammerID());
      
        Programmer findP = findProgrammer(pk);
        System.out.println(findP == saveP);
      }
      
      @IdClass(ProgrammerPK.class)
      @Entity
      public class Programmer implements Serializable {
      
        private String lang;
      
        private UUID programmerID;
      
        private String name;
      
        @Id
        @Column(name="LANG")
        public String getLang() {
            return lang;
        }
      
        public void setLang(String lang) {
            this.lang = lang;
        }
      
        @Id
        @Column(name="PROGRAMMER_ID")
        public UUID getProgrammerID() {
            return programmerID;
        }
      
        public void setProgrammerID(UUID programmerID) {
            this.programmerID = programmerID;
        }
      
        @Column(name="NAME")
        public String getName() {
            return name;
        }
      
        public void setName(String name) {
            this.name = name;
        }
      
      }
      
      public class ProgrammerPK implements java.io.Serializable {
      
        private UUID programmerID;
        private String name;
      
        @Column(name="NAME")
        public String getName() {
            return name;
        }
      
        public void setName(String name) {
            this.name = name;
        }
      
        @Column(name="PROGRAMMER_ID")
        public UUID getProgrammerID() {
            return programmerID;
        }
      
        public void setProgrammerID(UUID programmerID) {
            this.programmerID = programmerID;
        }
      
        @Override
        public boolean equals(Object obj) {
            if ( obj == null || !(obj instanceof ProgrammerPK) ) {
                return false;
            }
            ProgrammerPK pk = (ProgrammerPK) obj;
            return pk.getName().equals(getName()) && pk.getProgrammerID().equals(getProgrammerID());
        }
      
        @Override
        public int hashCode() {
            return super.hashCode();
        }
      
      }                               
                  
    7. Lazy fetch
      1. JPA 支援的其中一種lazy fetch指定方式是透過@Basic. 以@Lob來說, 在column上指定@Basic(fetch=FetchType.LAZY)即可, 只是lazy fetch是EJB3中optional的功能, 所以不一定有實作. 像我在測的時候感覺@Lob看不出來有lazy fetch的感覺..
    8. @OneToOne和@ManyToOne預設是EAGER, @OneToMany和@ManyToMany預設是LAZY.
    9. 每次的EAGER fetch會有一個SELECT加上JOIN的行為去取得相關的entity, 這樣會比LAZY有效率, 因為一樣要取出collection的資料, EAGER fetch只要一行statement, 而不是一行master table, 一行detail table. 這也是為什麼@OneToOne, @ManyToOne預設是EAGER的原因
    10. 而@OneToMany 和@ManyToMany預設LAZY的原因是由於若用JOIN的方式取得相關的entity, 則會取得很多筆資料, 多筆資料中又有很多重複的資訊(比方說master會重複在每筆資料中). 這樣的效率又比LAZY差, 所以@OneToMany, @ManyToMany用LAZY的原因
    11. 如果每次都要取得@OneToMany與@ManyToMany的Many那方所有entity, 或是@ManyToOne或@OneToOne的One太多又不一定每次都需要, 就不用管預設, 適當的改成EAGER或LAZY.
  • Update
    1. 由於EntityManager會確定attached entity的改變會sync進db, 因此大部分時候我們不用擔心entity update的問題.
      // 改變取得的entity會由EntityManager確認改變sync進db
      public static void changeStudentName(Long id) {
        Student s = EntityManagerHelper.getEntityManager().find(Student.class, id);
        if ( s != null ) { s.setName("KKK"); }
      }                
                  
    2. merge: merge用來把detach的entity重新attach
      // 這個method會因為clear了, EntityManager的entity都detach, 使後來即使有transaction也不會改名為KKK                
      public static void main(String[] args) {
        Long sid = createStudent();
        EntityManager em = EntityManagerHelper.getEntityManager();
        Student s = em.find(Student.class, sid);
        em.clear();
        s.setName("KKK");
        GeneralEAO.transactionCommit();
      }
      
      // 這個method加上merge後使detach的entity重新attach了, 就會把改變後的結果放進DB.
      public static void main(String[] args) {
        Long sid = createStudent();
        EntityManager em = EntityManagerHelper.getEntityManager();
        Student s = em.find(Student.class, sid);
        em.clear();
        s.setName("KKK");
        em.merge(s);
        GeneralEAO.transactionCommit();
      }                
                  
      若merge一個不存在的entity會產生IllegalArgumentException, 不過我用Hibernate去試卻會自己去判斷如果不存在的entity就insert.
      注意使用merge時要在persistence context否則會有TrnsactionRequiredException, 不過測試後發現, Hibernate不會丟exception, 只是沒有確認好transaction context的事情, 操作Hibernate的時候就會有有時work有時不work的情形.
  • delete: remove
    1. remove只能刪除attached entity, 所以要在remove前確認entity是在attached的狀態, 可用merge先把detached entity和EntityManager合併成attached後再remove.
    2. 使用cascade的時候要小心, 因為cascade的刪除可能刪到未知的entity. 通常@OneToOne和@OneToMany才需要cascade
    3. remove需要transaction, 沒有的話會丟TransactionRequiredException. 若是要remove不存在的entity會丟出IllegalStateException.
      public static void main(String[] args) {
        EntityManager em = EntityManagerHelper.getEntityManager();
        Student saveS = new Student();
        saveS.setName("Isaac");
        em.persist(saveS);
        GeneralEAO.transactionCommit();
        em.clear(); // detach
        Student findS = em.find(Student.class, saveS.getId());
        findS.setName("2");
      
        // 有這行就會java.lang.IllegalArgumentException: 
        // Removing a detached instance entity.Student#1
        em.clear(); 
      
        em.remove(findS);
        GeneralEAO.transactionCommit();
      }
      
      public static void main(String[] args) {
        EntityManager em = EntityManagerHelper.getEntityManager();
        Student saveS = new Student();
        saveS.setName("Isaac");
        em.persist(saveS);
        GeneralEAO.transactionCommit();
        em.clear(); // detach
        Student findS = em.find(Student.class, saveS.getId());
        findS.setName("2");
        em.clear();
        // 如果加上merge就算detach了還是可以正常remove
        em.remove(em.merge(findS));
        GeneralEAO.transactionCommit();
      }                
                  
    4. 如果想保留master而刪除detail就要先把entity用EntityManager移除再從master entity中移除
      public static void main(String[] args) {
        Long pid = createProgrammer();
      
        EntityManager em = EntityManagerHelper.getEntityManager();
        Programmer findP = em.find(Programmer.class, pid);
        int certSize = findP.getCerts().size();
        for ( int i = 0; i < certSize; i++ ) {
            Certification cert = findP.getCerts().get(0);
            em.remove( cert );
            findP.getCerts().remove( cert );
        }
        GeneralEAO.transactionCommit();
      }                         
                  
  • flush
    1. 平常對EntityManager的操作都不會立刻改變DB. 而是要等到flush的時候才會. 這是為了效能議題, 把SQL蒐集起來一起執行會比很多次的執行來的有效率.
    2. 預設的flush mode是AUTO, 就是transaction-scoped EntityManager的transaction結束時以及application-managed或extended-scope的 transaction的persistence context close時flush.
    3. 當Query執行時如果有用到某個entity, 則persistence provider會在執行query前flush.
    4. flush mode改為COMMIT就可自己決定如何COMMIT, 一旦變成COMMIT, persistence provider就只會在transaction commit時與DB sync.
    5. 一旦flush mode改為COMMIT就要負責在query執行前flush, 否則就會query到舊資料.
    6. 雖然自己控制可以常常commit, 不過把sql蒐集起來執行還是比較好的方式.
  • refresh
    1. refresh就是從db取資料出來放entity.
    2. refresh只對attached entity有效
    3. 因為refresh是透過ID去DB找資料, 所以要確定refresh的entity存在於db中
    4. 當使用persist把entity存進DB中, 指會產生INSERT statement, 所以若有DB產生的資料並不會放回entity, 這時候就要先flush把EntityManager的東西sync到資料庫, 再透過refresh把資料取回來. 不過這不是好做法, 因此最好盡量別用DB產生的資料.
    5. 有種方便的refresh使用方式就是undo, 如以下範例, 雖然已經remove掉又detachd, 還是可以做出undo效果.
      public static void main(String[] args) {
        Long pid = createProgrammer();
      
        EntityManager em = EntityManagerHelper.getEntityManager();
        Programmer findP = em.find(Programmer.class, pid);
        GeneralEAO.clear();
      
        int certSize = findP.getCerts().size();
        System.out.println("before remove:" + certSize);
        for ( int i = 0; i < certSize; i++ ) {
            Certification cert = findP.getCerts().get(0);
            em.remove( em.merge(cert) );
            findP.getCerts().remove( cert );
        }
        System.out.println("before merge:" + findP.getCerts().size());
        GeneralEAO.clear();
      
        findP = em.merge(findP);
        em.refresh( findP );
        System.out.println("after merge" + findP.getCerts().size());
      
        GeneralEAO.transactionCommit();
      }                        
                  
  • entity lifecycle listener
    1. lifecycle callback可定在entity中, 以
      void < METHOD >()
      的格式
    2. lifecycle callback可定在額外的POJO中, 以
      void < METHID > (Object)
      的格式
    3. lifecycle callback可定在super class中, subclass也會發生作用
    4. lifecycle callback遇到runtime exception就會停止, 可拿來檢查是否為不繼續persistence operation的關卡
    5. 所有lifecycle callback都是stateless的狀態, 而且不應該預設一個listener只對應到一個entity
    6. listener class不支援DI, 因為會在container外使用, 也不支援cross cutting.
    7. 指定listener class的方式是在entity上加@EntityListeners指定listener
    8. 書上說可以支援default listener, 設定方式如下(設定在persistence.xml). 可是目前我試不出來, 連persistence xsd都查不到. 網路上看到的是在orm中設定, 之後再看.
      < persistence-unit name="persistence">
          ...
          < default-entity-listeners>
              aaa.bb.Ccc.class
          < /default-entity-listeners>                
                  
    9. listener class的執行順序為default entity listener -> superclass's listener -> subclass's listener
    10. subclass如果不想執行superclass的listener可在entity上加@ExcludeSuperClassListeners
    11. 如果不想用default entity listener可在entity上加@ExcludeDefaultListeners
    12. 目前無法指定要exclude特定的listener也無法指定listener執行順序, 順序為先assign的listener就先執行.
      @Entity
      @Table(name="EMPLOYEE2")
      @Inheritance(strategy=InheritanceType.JOINED)
      @EntityListeners({EmployeeListener.class})
      @DiscriminatorColumn(name="EMPLOYEE_TYPE", length=1)
      public class Employee implements Serializable {
      
        private static final long serialVersionUID = 1L;
        private Long id;
        private String name;
        private List< Address> addressList = new ArrayList< Address>();
      
        @Id
        @Column(name="EMPLOYEE_ID")
        @GeneratedValue(strategy = GenerationType.AUTO)
        public Long getId() {
            return id;
        }
      
        public void setId(Long id) {
            this.id = id;
        }
      
        @OneToMany(cascade=CascadeType.ALL,mappedBy="employee")
        public List< Address> getAddressList() {
            return addressList;
        }
      
        public void setAddressList(List< Address> addressList) {
            this.addressList = addressList;
        }
      
        @Column(name="NAME")
        public String getName() {
            return name;
        }
      
        public void setName(String name) {
            this.name = name;
        }
      
      }
      
      @Entity
      @Table(name = "PROGRAMMER")
      @DiscriminatorValue(value = "P")
      public class Programmer extends Employee implements Serializable {
      
        private static final long serialVersionUID = 1L;
        private String lang;
      
        @Column(name="LANG")
        public String getLang() {
            return lang;
        }
      
        public void setLang(String lang) {
            this.lang = lang;
        }
      }
      
      @Entity
      @Table(name="SALES")
      @ExcludeDefaultListeners
      @ExcludeSuperclassListeners
      @DiscriminatorValue(value="S")
      public class Sales extends Employee implements Serializable {
      
        private static final long serialVersionUID = 1L;
        private String product;
      
        @Column(name="PRODUCT")
        public String getProduct() {
            return product;
        }
      
        public void setProduct(String product) {
            this.product = product;
        }
      
        @PostPersist
        @PrePersist
        @PostLoad
        @PreUpdate
        @PostUpdate
        @PreRemove
        @PostRemove
        public void monitorSales() {
            System.out.println("Monitor sales product:" + getProduct());
        }
      
      }
      
      public class EmployeeListener {
      
        @PostPersist
        @PrePersist
        @PostLoad
        @PreUpdate
        @PostUpdate
        @PreRemove
        @PostRemove
        public void monitor(Object o) {
            System.out.println("EmployeeListener monitor class: " + o.getClass());
        }
      
      }                
                  
  • 星期四 六月 05, 2008

    EJB3 - JPA - ORM

    description

    Hibernate大家都用很久了, 各種mapping方式也都瞭若指掌.
    不過一方面覺得Hibernate畢竟只是provider, 不太想把Hibernate當成standard用, 雖然他已經這麼強.
    一方面也覺得orm太豐富了, 所以常常怎麼試都work.
    卻不知道有時候work的結果可能是provider cover了, 有時候是瞎貓碰到死耗子.
    記得Hibernate也有說, orm這種東西效能不會差, 但前提是要你用對.
    因此能知道到底正確的使用方式是什麼是重要的.
    正確的使用 + 不想看Hibernate的說明書, 我只要standard. 所以很多餘的紀錄一下自己看書測試的結果.
    orm實在太多了, 已經試很久, 尤其是遇到@SecondaryTable的問題時花更多時間, 平常又要上班.
    希望這個對誰有幫助, 主要是對我自己有幫助啦. :-)

    reference

    EJB3 in Action - CH8 - Object-relational mapping
    javax.persistence (Java EE 5)

    annotations

  • @Table
    1. 用來設定主table的資訊, 如果要設定額外table可用@SecondaryTable或@SecondaryTables.
    2. 如果沒有@Table會mapping到預設值. (此預設值在書上說: If it's omitted, the entity is assumed to be mapped to a table in the default schema with the same name as the entity class)
    3. 如果有@Table沒有設name屬性, 則table name會設成entity name.
    4. uniqueConstraints用來設定唯一特性的欄位, 此欄位的設定在自動ddl建立table的時候才生效.
  • @Column
    1. 指定persistent property 或 field mapping到的column name.
    2. 如果沒設@Column, 會mapping到預設值, 就是mapping到table上符合property name的欄位.
    3. 當一個欄位是透過@SecondaryTable mapping到其他table時要指定table屬性
    4. 如果insertable/updatable設為false, 則insert/update statement執行時就不會包含這個column, 這屬性可用來處理read only column.
    5. columnDefinition用來指定建立此column時的SQL, 比方說byte[]型態的@Lob會建BLOB, 但是透過columnDefinition可建立CLOB, 如下所示.
      @Column(name = "LOB_XML", columnDefinition="clob")
      @Lob
      public byte[] getSecondaryTable() {
          return secondaryTable;
      }                
                  
  • @Enumerated
    1. 指定一個persistent field被存為enumerated type.
    2. value屬性為EnumType, 有ORDINAL(預設值)和STRING. ORDINAL存enum type在該enum存放的index(從0開始算), STRING就是存enum的字串.
  • @Lob
    1. 如果@Lob放 char[]或String, 對應成CLOB. 若是byte[], 就對應BLOB.
    2. 可搭配@Basic用lazy的方式存取, 不過LOB的lazy存取在EJB3 spec中是optional的, 所以vendor不一定支援LOB的lazy存取.
  • @Temporal
    1. 用來指定型態為java.util.Date, java.util.Calendar的mapping.
    2. value屬性值為TemporalType, 有DATE(mapping到java.sql.Date. storing day, month, year), TIME(mapping到java.sql.Time, storing time and not day, month, year), TIMESTAMP(預設值, mapping到java.sql.Timestamp, storing time, day, month and year).
    3. 如果型態本來就是java.sql.Date, java.sql.Time, java.sql.Timestamp, 就不用@Temporal
  • @SecondaryTable
    1. 指定一個entity中屬性mapping到的第二個table.
    2. 如果沒有@SecondaryTable, 則假設所有的entity屬性都屬於同一個table.
    3. 如果pkJoinColumns沒設定則假設join column都reference到主table的PK, 且有相同的型態.
    4. 如果開啟自動產生schema的功能, Hibernate上有個產生錯誤的方式.
      1. 建立主table, 裡面放一個@OneToOne與一個@Column mapping到@SecondaryTable設定的table.
      2. 主entity程式如下
        @Entity
        @Table(name="MY_USER")
        @SecondaryTable(name="USER_PICTURES", pkJoinColumns=@PrimaryKeyJoinColumn(name="USER_ID"))
        public class MyUser implements Serializable {
            private static final long serialVersionUID = 1L;
            private Long id;
            private BillingInfo billingInfo;
            private byte[] picture;
            
            public void setId(Long id) {
                this.id = id;
            }
        
            @Id
            @Column(name="USER_ID")
            @GeneratedValue(strategy = GenerationType.AUTO)
            public Long getId() {
                return id;
            }
        
            @Column(name="PICTURE", table="USER_PICTURES")
            @Lob
            @Basic(fetch=FetchType.LAZY)
            public byte[] getPicture() {
                return picture;
            }
        
            public void setPicture(byte[] picture) {
                this.picture = picture;
            }
        
            @OneToOne(cascade=CascadeType.ALL)
            @JoinColumn(name="USER_BILLING_ID", referencedColumnName="BILLING_ID")
            public BillingInfo getBillingInfo() {
                return billingInfo;
            }
        
            public void setBillingInfo(BillingInfo billingInfo) {
                this.billingInfo = billingInfo;
            }
        
        }                        
                            
      3. 如此在自動建立schema的時候schema在建立table USER_PICTURES時會用以下的SQL
        create table USER_PICTURES (PICTURE blob, USER_ID number(19,0) not null, primary key (USER_BILLING_ID))
                            
        這段SQL錯誤的地方很明顯, 就在指定的PK不是USER_PICTURES的PK, 而是@OneToOne的FK. 去看source, 好像是在準備SQL statement的時候, 準備到SecondaryTable時, 跑去取org.hibernate.mapping.Constraint的columns, 應該存在的是SecondaryTable的PK, 取到的卻是OneToOne的column.
        不過這沒什麼大不了的, 只要自己建一個對的table他就會mapping正確. 只是我在這地方繞了很久, 希望別太多人像我一樣笨還在那試, 書上本來就說不鼓勵使用自動建立schema的功能了.
  • @GeneratedValue
    1. 和@Id搭配, 提供自動建立PK value的功能
    2. strategy值為GenerationType, 有三種值: AUTO, IDENTITY, SEQUENCE, TABLE
      1. AUTO: 由provider幫你決定最合適的strategy
      2. IDENTITY: 適用於MS-Sql, 因為我用的是Oracle所以用這種的會出exception說我的dialet不支援IDENTITY
      3. SEQUENCE: 用database sequence column
        1. 先用@SequenceGenerator建立一個generator, 再設定@GeneratedValue的generator屬性指定要用哪個generator.
          @Id
          @Column(name="USER_ID")
          @SequenceGenerator(
              name="USER_SEQUENCE_GENERATOR", sequenceName="USER_SEQUENCE", 
              initialValue=1, allocationSize=1
          )
          @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="USER_SEQUENCE_GENERATOR")
          public Long getId() {
              return id;
          }                                
                                      
          這樣就會建立一個sequence.
          create sequence USER_SEQUENCE

          initialValue是出始值, allocationSize是每次增加的值.
        2. 如果沒用@SequenceGenerator, Hibernate會下這個SQL當作generator
          create sequence hibernate_sequence
        3. 注意sequence name在persistece module中是共用的, 所以使用上要小心sequence name的唯一性
      4. TABLE: 用database table存放PK value
        1. 先用@TableGenerator建立generator, 再設定generator指定generator名稱.
          @Id
          @Column(name="USER_ID")
          @TableGenerator(
              name="USER_TABLE_GENERATOR", table="SEQUENCE_GENERATOR_TABLE", 
              pkColumnName="SEQUENCE_NAME", valueColumnName="SEQUENCE_VALUE", 
              pkColumnValue="USER_SEQUENCE", initialValue=1, allocationSize=100
          )
          @GeneratedValue(strategy = GenerationType.TABLE, generator="USER_TABLE_GENERATOR")
          public Long getId() {
              return id;
          }                                
                                      
          這樣就會建一個table來放PK value
          create table SEQUENCE_GENERATOR_TABLE ( 
              SEQUENCE_NAME varchar2(255),  SEQUENCE_VALUE number(10,0) 
          ) 
                                      
          insert into SEQUENCE_GENERATOR_TABLE (SEQUENCE_NAME, SEQUENCE_VALUE) 
          VALUES ('USER_SEQUENCE', 1)                                
                                      
        2. 這方式的好處是一個table可以存放很多個entity的PK值
  • @Embedded
    1. 把一個@Embeddable的class放在entity中加上@Embedded, 就能用到@Embeddable class的屬性.
    2. @Embedded預設是mapping到和所屬entity同一個table, 但也可用@SecondaryTable mapping到不同table, 不過會出現前面提到的錯誤.
    3. @Embedded預設的做法如下
      @Embeddable
      public class EmbeddedEntity implements Serializable, RegistedEntity {
      
          private static final long serialVersionUID = 1L;
      
          private Calendar embeddedTime;
      
          @Column(name="EMBEDDED_TIME")
          @Temporal(TemporalType.TIMESTAMP)
          public Calendar getEmbeddedTime() {
              return embeddedTime;
          }
      
          public void setEmbeddedTime(Calendar embeddedTime) {
              this.embeddedTime = embeddedTime;
          }
          
      }
          
      @Entity
      @Table(name = "ONE")
      public class OneTable implements Serializable {
          ...
          @Embedded
          public EmbeddedEntity getEmbeddedEntity() {
              return embeddedEntity;
          }
          ...
      }
                  
    4. 如果想改寫@Embeddable class mapping到的column可用@AttributeOverrides改寫
      @Embedded
      @AttributeOverrides(@AttributeOverride(
          name = "embeddedTime", 
          column = @Column(name = "DIFF_EMBEDDED_TIME"))
      )
      public EmbeddedEntity getDifferentColumnEmbeddedEntity() {
          return differentColumnEmbeddedEntity;
      }                
                  
  • @JoinColumn
    1. 指定用來join table的column
    2. 指定column name的使用方式如下
      @OneToOne(cascade=CascadeType.ALL)
      @JoinColumn(name="ANOTHER_ONE", referencedColumnName="ID")
      public AnotherOneTable getAnotherOne() {
          return anotherOne;
      }                
                  
      這樣在主entity會有個ANOTHER_ONE欄位, mapping到AnotherOneTable這個entity的ID欄位.
    3. 如果沒指定column name, 產生的column name會以一種方式呈現
      < relationship field/property name >_< name of referenced primary key column >
                  
      如以下範例
      @OneToOne(cascade=CascadeType.ALL)
      @JoinColumn
      public AnotherOneTable getAnotherOne() {
          return anotherOne;
      }
                  
      這樣就會在主table建立一個column: anotherOne_id mapping到AnotherOneTable這個entity的id欄位.
    4. 如果是雙向關係的@OneToOne, 在AnotherOneTable那邊就會設定@OneToOne(mappedBy = "anotherOne"),
      mappedBy這個屬性用在owning side的另一端, 所謂的owning side就是擁有關係的那個table.
      例如 ONE table中有個欄位 ANOTHER_ONE mapping到ANOTHER_ONE_TABLE, 則ONE擁有與ANOTHER_ONE_TABLE的關係, 所以就是owning side.
      所以ANOTHER_ONE_TABLE這邊的entity就要設定mappedBy, 而mappedBy的值就是owning side對備擁有的entity設定的變數名稱.
    5. 注意無法在@OneToOne關係的兩邊都用@JoinColumn
  • @PrimaryKeyJoinColumn
    1. 指定一個用來join其他table的column.
    2. 可用在JOIN的繼承strategy
      @Entity
      @Table(name = "PARENT")
      @Inheritance(strategy = InheritanceType.JOINED)
      @DiscriminatorColumn(discriminatorType=DiscriminatorType.STRING, name="CHILD_TYPE", length=1)
      public class Parent implements Serializable {
          ...
      }
      
      @Entity
      @Table(name="GOOD_CHILD")
      @DiscriminatorValue(value="G")
      @PrimaryKeyJoinColumn(name="ID")
      public class GoodChild extends Parent implements java.io.Serializable {
          ...
      }
                  
    3. @SecondaryTable的join PK
      @Entity
      @Table(name="USERS")
      @SecondaryTable(name="USER_PICTURES", pkJoinColumns=@PrimaryKeyJoinColumn(name="USER_ID"))
      public class MyUser implements Serializable {
          private static final long serialVersionUID = 1L;
          private Long id;
          private byte[] picture;
          
          public void setId(Long id) {
              this.id = id;
          }
      
          @Id
          @Column(name="USER_ID")
          @GeneratedValue(strategy = GenerationType.AUTO)
          public Long getId() {
              return id;
          }
      
          @Column(name="PICTURE", table="USER_PICTURES")
          @Lob
          @Basic(fetch=FetchType.LAZY)
          public byte[] getPicture() {
              return picture;
          }
      
          public void setPicture(byte[] picture) {
              this.picture = picture;
          }
      
      }                
                  
    4. @OneToOne的reference key
      @Entity
      @Table(name = "EMPLOYEE")
      public class Employee implements Serializable {
      
          private static final long serialVersionUID = 1L;
          private Long id;
          private String name;
          private Address address;
      
          public void setId(Long id) {
              this.id = id;
          }
      
          @Id
          @Column(name = "EMPLOYEE_ID")
          @GeneratedValue(strategy = GenerationType.AUTO)
          public Long getId() {
              return id;
          }
      
          @OneToOne(cascade=CascadeType.ALL)
          @PrimaryKeyJoinColumn // 如果PK和referencedColumn名稱一樣就不用指定名稱
          public Address getAddress() {
              return address;
          }
      
          public void setAddress(Address address) {
              this.address = address;
          }
      
          @Column(name="NAME")
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      }
      
      @Entity
      @Table(name = "ADDRESS")
      public class Address implements Serializable {
      
          private static final long serialVersionUID = 1L;
          private Long employeeId;
          private String street;
      
          @Id
          @Column(name="EMPLOYEE_ID")
          public Long getEmployeeId() {
              return employeeId;
          }
      
          public void setEmployeeId(Long employeeId) {
              this.employeeId = employeeId;
          }
      
          @Column(name="STREET")
          public String getStreet() {
              return street;
          }
      
          public void setStreet(String street) {
              this.street = street;
          }
      
      }
                  
      這樣用hibernate自動建立的schema中address會少了FK constraint, 所以我又自己加了一個constraint
      alter table address add constraint FK13246123456 foreign key (employee_id) references employee
      這段書上的說明不多, 不曉得我加了FK constraint之後是不是書上說的@PrimaryKeyJoinColumn使用模式
  • @OneToMany
    @Entity
    public class Programmer implements Serializable {
        private static final long serialVersionUID = 1L;
        private Long id;
        private String name;
        private Set< Certification> certs = new HashSet();
        
        public void setId(Long id) {
            this.id = id;
        }
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        public Long getId() {
            return id;
        }
    
        @OneToMany(cascade=CascadeType.ALL, mappedBy="owner")
        public Set< Certification> getCerts() {
            return certs;
        }
    
        public void setCerts(Set< Certification> certs) {
            this.certs = certs;
        }
    
        @Column(name="NAME")
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
    }
    
    @Entity
    public class Certification implements Serializable {
        private static final long serialVersionUID = 1L;
        private Long id;
        private String certName;
        private Programmer owner;
        
        public void setId(Long id) {
            this.id = id;
        }
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        public Long getId() {
            return id;
        }
    
        @Column(name="CERT_NAME")
        public String getCertName() {
            return certName;
        }
    
        public void setCertName(String certName) {
            this.certName = certName;
        }
    
        @ManyToOne
        @JoinColumn(name="PROGRAMMER_ID")
        public Programmer getOwner() {
            return owner;
        }
    
        public void setOwner(Programmer owner) {
            this.owner = owner;
        }
    
    }
        
    1. 不能@OneToMany和@ManyToOne都用@JoinColumn, 會有deployment-time error.
    2. 如果沒設定mappedBy就會變unidirectional, 在這的意義就是Programmer.getCerts()會回傳空值
    3. 建議使用雙向, 就是設定mappedBy, 避免要維護兩邊entity.
    4. @OneToMany和@ManyToOne還可以指到自己
      @Entity
      @Table(name="COMPONENT")
      public class Component implements Serializable {
          private static final long serialVersionUID = 1L;
          private Long id;
          private Component parent;
          private List< Component> children = new ArrayList< Component>();
          
          public void setId(Long id) {
              this.id = id;
          }
      
          @Id
          @Column(name="COMPONENT_ID")
          @GeneratedValue(strategy = GenerationType.AUTO)
          public Long getId() {
              return id;
          }
      
          @ManyToOne
          @JoinColumn(name="PARENT_COMPONENT_ID", referencedColumnName="COMPONENT_ID")
          public Component getParent() {
              return parent;
          }
      
          public void setParent(Component parent) {
              this.parent = parent;
          }
      
          @OneToMany(mappedBy="parent", cascade=CascadeType.ALL)
          public List< Component> getChildren() {
              return children;
          }
      
          public void setChildren(List< Component> children) {
              this.children = children;
          }
      
      }                
                  
  • ManyToMany的關係: 使用@JoinTable. @ManyToMany哪邊是owning side都可以, 所以哪邊設mappedBy都可以
    @Entity
    @Table(name="SALES")
    public class Sales implements Serializable {
    
        private static final long serialVersionUID = 1L;
        private Long id;
        private String name;
        private List< Product> products = new ArrayList< Product>();
    
        public void setId(Long id) {
            this.id = id;
        }
    
        @Id
        @Column(name = "SALES_ID")
        @GeneratedValue(strategy = GenerationType.AUTO)
        public Long getId() {
            return id;
        }
    
        @Column(name = "NAME")
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        // 沒設定mappedBy就會是unidirectional
        @ManyToMany(mappedBy="sales", cascade=CascadeType.ALL)
        public List< Product> getProducts() {
            return products;
        }
    
        public void setProducts(List< Product> products) {
            this.products = products;
        }
    }
    
    @Entity
    @Table(name = "PRODUCT")
    public class Product implements Serializable {
    
        private static final long serialVersionUID = 1L;
        private Long id;
        private String name;
        private List< Sales> sales = new ArrayList< Sales>();
    
        public void setId(Long id) {
            this.id = id;
        }
    
        @Id
        @Column(name = "PRODUCT_ID")
        @GeneratedValue(strategy = GenerationType.AUTO)
        public Long getId() {
            return id;
        }
    
        @Column(name = "NAME")
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @ManyToMany(cascade=CascadeType.ALL)
        @JoinTable(name = "PRODUCT_SALES",  // PRODUCT_SALES是中間的table
            // name是中間table的column, 若和現有column同名就不用referencedColumnName
            joinColumns = @JoinColumn(name = "PRODUCT_ID"), 
            inverseJoinColumns = @JoinColumn(name = "SALES_ID")
        )
        public List< Sales> getSales() {
            return sales;
        }
    
        public void setSales(List< Sales> sales) {
            this.sales = sales;
        }
    }
        
  • 繼承: @Inheritance(strategy=InheritanceType.SINGLE_TABLE)
    1. 注意EMPLOYEE_TYPE的@Column要設定成insertable=false, updatable=false. 因為要persist的時候不用設定type就會JPA就會自己判斷DiscriminatorColumn要存什麼進去. 沒設定insertable, updatable反而�����被告知有重複的column存在.
    2. SINGLE_TABLE是EJB3的預設作法, 優點是最快, 缺點是subclass的column不能nullable, 同時SINGLE_TABLE也會產生很多null值, 而且一旦改屬性就要改整個schema.
    3. public enum EmployeeType {
          P, // Programmer
          S  // Sales
      }
      
      @Entity
      @Table(name = "EMPLOYEE")
      @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
      @DiscriminatorColumn(name="EMPLOYEE_TYPE", length=1, discriminatorType=DiscriminatorType.STRING)
      public abstract class Employee implements Serializable {
      
          private static final long serialVersionUID = 1L;
          private Long id;
          private String name;
          private EmployeeType employeeType;
      
          public void setId(Long id) {
              this.id = id;
          }
      
          @Id
          @Column(name = "EMPLOYEE_ID")
          @GeneratedValue(strategy = GenerationType.AUTO)
          public Long getId() {
              return id;
          }
      
          @Column(name="NAME")
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          @Column(name="EMPLOYEE_TYPE", insertable=false, updatable=false)
          @Enumerated
          public EmployeeType getEmployeeType() {
              return employeeType;
          }
      
          public void setEmployeeType(EmployeeType employeeType) {
              this.employeeType = employeeType;
          }
      }
      
      @Entity
      @DiscriminatorValue(value="P")
      public class Programmer extends Employee implements Serializable {
          private static final long serialVersionUID = 1L;
          private String lang;
      
          @Column(name="LANG")
          public String getLang() {
              return lang;
          }
      
          public void setLang(String lang) {
              this.lang = lang;
          }
      
      }
      
      @Entity
      @DiscriminatorValue(value="S")
      public class Sales extends Employee implements Serializable {
          private static final long serialVersionUID = 1L;
          private String product;
      
          @Column(name="PRODUCT")
          public String getProduct() {
              return product;
          }
      
          public void setProduct(String product) {
              this.product = product;
          }
      
      }
              
  • 繼承: @Inheritance(strategy=InheritanceType.JOINED)
    1. InheritanceType.JOINED表示subclass mapping到分開的table, 與super class透過JOIN的方式關聯
    2. JOINED是最好的設計方式, 透過index的設定也可提升效能.
    3. superclass與subclass的one-to-one關聯透過@PrimaryKeyJoinColumn設定
    4. JOINED的方式和SINGLE_TABLE不一樣的是需要設定EmployeeType
    5. @Entity
      @Table(name = "EMPLOYEE")
      @Inheritance(strategy = InheritanceType.JOINED)
      @DiscriminatorColumn(name="EMPLOYEE_TYPE", length=1, discriminatorType=DiscriminatorType.STRING)
      public abstract class Employee implements Serializable {
      
          private static final long serialVersionUID = 1L;
          private Long id;
          private String name;
          private EmployeeType employeeType;
      
          public void setId(Long id) {
              this.id = id;
          }
      
          @Id
          @Column(name = "EMPLOYEE_ID")
          @GeneratedValue(strategy = GenerationType.AUTO)
          public Long getId() {
              return id;
          }
      
          @Column(name="NAME")
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          @Column(name="EMPLOYEE_TYPE")
          @Enumerated(EnumType.STRING)
          public EmployeeType getEmployeeType() {
              return employeeType;
          }
      
          public void setEmployeeType(EmployeeType employeeType) {
              this.employeeType = employeeType;
          }
      }
      
      @Entity
      @Table(name="PROGRAMMER")
      @DiscriminatorValue(value="P")
      @PrimaryKeyJoinColumn(name="EMPLOYEE_ID")
      public class Programmer extends Employee implements Serializable {
          private static final long serialVersionUID = 1L;
          private String lang;
      
          @Column(name="LANG")
          public String getLang() {
              return lang;
          }
      
          public void setLang(String lang) {
              this.lang = lang;
          }
      
      }
      
      @Entity
      @Table(name="SALES")
      @DiscriminatorValue(value="S")
      @PrimaryKeyJoinColumn(name="EMPLOYEE_ID")
      public class Sales extends Employee implements Serializable {
          private static final long serialVersionUID = 1L;
          private String product;
      
          @Column(name="PRODUCT")
          public String getProduct() {
              return product;
          }
      
          public void setProduct(String product) {
              this.product = product;
          }
      
      }
              
  • @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
    1. 這種方式最糟糕, provider也最難實作, EJB3又沒當成必須實作的部份, 所以不建議使用.
    2. 糟糕的地方在他會重複存值, 比方說Employee有name, Programmer也會在table中存name, 導致重複的資料出線.
  • 星期日 五月 18, 2008

    EJB3 - JPA - 取得EntityManager的方式

    Description

    EntityManager是JPA中很重要的角色, 大部分entity的管理都要透過EntityManager.
    但是取得EntityManager的方式不只一種, 讓我有點混亂, 因此大概整理一下思緒後記錄下來.

    Reference

    EJB3 in Action - CH9 - Manipulating entities with EntityManager

    Focal Points

  • Container Managed EntityManager :
    Container Managed EntityManager只能在Java EE Container內使用.
    Container Managed EntityManager是最方便的EntityManager, 取得的EntityManager不用close, 一切由Java EE Container處理.
    1. 設定Server的連線資訊
    2. 準備JTA的persistence.xml
        < persistence-unit name="persistence_oracle" transaction-type="JTA">
            < provider>org.hibernate.ejb.HibernatePersistence< /provider>
            < jta-data-source>jdbc/oracle< /jta-data-source>
            < properties>
              < property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect"/>
              < property name="hibernate.hbm2ddl.auto" value="create"/>
            < /properties>
          < /persistence-unit>
        < /persistence>
                        
    3. 使用@PersistenceContext
        @Stateless
        public class TestEntityManager implements ITestEntityManager {
            @PersistenceContext(unitName="persistence_oracle" ) EntityManager em;
        }    
                        
  • Application Managed EntityManager :
    Application Managed EntityManager在Container內外都可使用.
    Application Managed EntityManager和Container Managed EntityManager不同的地方在於 Application Managed EntityManager必須處理EntityTransaction的begin與commit.
    Application Managed EntityManager在Java EE Container內與Java EE Container外的差別 在Java EE Container內的EntityManagerFactory不用close而Java EE Container外的要.
    不過Application Managed EntityManager和Container Managed EntityManager不一樣的是, Container Managed EntityManager不用close而EntityManager但Application Managed EntityManager要close.
    此外Java EE Container內的Application Managed EntityManager 可透過EntityManager.joinTransaction join JTA做到和Container Managed EntityManager一樣的效果.
    而Java EE Container外的Application Managed EntityManager不可以joinTransaction且要負責begin與commit transaction.
    步驟如下.
    1. 在Java EE Container內. 在此取得的EntityManager要close, 但EntityManagerFactory不用close, 由server處理.
      1. 設定persistence.xml. transaction type為JTA
          < persistence-unit name="persistence_oracle" transaction-type="JTA">
              < provider>org.hibernate.ejb.HibernatePersistence< /provider>
              < jta-data-source>jdbc/oracle< /jta-data-source>
              < properties>
                < property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect"/>
                < property name="hibernate.hbm2ddl.auto" value="create"/>
              < /properties>
            < /persistence-unit>
          < /persistence>                    
                              
      2. 使用@PersistenceUnit取得EntityManagerFactory再用該factory建立EntityManager
          @Stateless
          public class TestEntityManager implements ITestEntityManager {
              @PersistenceUnit(unitName="persistence_oracle") EntityManagerFactory emf;
              public EntityManager getAppManagedEntityManager() {
                  return emf.createEntityManager();
              }
          }    
                                  
    2. 在Java EE Container外使用Application Managed EntityManager.
      先設定persistence.xml, transaction type為RESOURCE_LOCAL
      在此取得的EntityManagerFactory與EntityManager都要記得close.
      1. 設定Resource Local persistence.xml
          < persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
              http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
              < persistence-unit name="persistence" transaction-type="RESOURCE_LOCAL">
                  < provider>org.hibernate.ejb.HibernatePersistence< /provider>
                  < class>com.isaac.entities.Address< /class>
                  < class>com.isaac.entities.IdentityID< /class>
                  < properties>
                      < property name="hibernate.show_sql" value="true"/>
                      < property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect"/>
                      < property name="hibernate.connection.username" value="sys as sysdba"/>
                      < property name="hibernate.connection.driver_class" value="oracle.jdbc.driver.OracleDriver"/>
                      < property name="hibernate.connection.password" value="shooeugenesea"/>
                      < property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:orcl"/>
                      < property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
                      < property name="hibernate.hbm2ddl.auto" value="update"/>
                  < /properties>
              < /persistence-unit>
          < /persistence>
                                  
      2. 透過Persistence.createEntityManagerFactory取得EntityManagerFactory.
        用這種方式取得的EntityManagerFactory與EntityManager都要close.
          public class TestEntityManager {
              EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence");
              public EntityManager getEntityManager() {
                  return emf.createEntityManager();
              }
          }                    
                                  
  • notation

  • 注意EntityManager不是thread-safe, 因此不要在非thread-safe的地方取得, 例如Servlet. 如果一定要取得, 可透過Context取JNDI資源的方式或用EntityManagerFactory, 因為EntityManagerFactory是thread-safe的.
  • 書上表示Hibernate建議在Web Container上Application Managed EntityManager可用Thread Local pattern取得. ThreadLocal pattern可視provider是否建議而使用.
      public class TestThreadLocal {
      
          private static EntityManagerFactory emf;
          public static final ThreadLocal< EntityManager > threadLocal = new ThreadLocal< EntityManager >();
          
          public static EntityManagerFactory getEntityManagerFactory() {
              if ( emf == null ) {
                  emf = Persistence.createEntityManagerFactory("persistence");
              }
              return emf;
          }
          
          public static EntityManager getEntityManager() {
              EntityManager em = threadLocal.get();
              if ( em == null ) {
                  em = emf.createEntityManager();
                  threadLocal.set(em);
              }
              return em;
          }
          
      }            
              
  • 星期一 五月 12, 2008

    EJB3 - JPA - 一種設定GlassFish CMT的方式

    Reference

    To integrate a JDBC driver
    EJB3 in Action - CH11 - Packaging EJB3 application
    良葛格的 Hibernate 3入門
    GlassFish Admin Console Help Window

    前言

    原本使用MySql部署在GlassFish上用CMT很輕鬆, 可是遇到怪現象就是persist和find都work, 偏偏我MySql Administrator就是找不到我persist的table在哪裡.
    我是很迷信的猜說GlassFish有內建, 因為Sun已經買下MySql, 然後就沒仔細追究詳情如何就決定試試看電腦裡面有裝的Oracle 10g.
    第一件事情就是試試看設定CMT. 其實如果用BMT, 就是用程式控制, 就是像在Tomcat上用Hibernate一樣就沒什麼問題. 不過EntityTransaction是JDBC Transaction, 而不是JTA.
    如果真的要用JTA就要用CMT. CMT在EJB3中是個重要的角色, 所以不管是不是用了CMT就會限制只能在EJB3 container間portable, 都還是看一下怎麼設定.
    結果一設定就發現非常麻煩, 或可能是我以前沒設定過EJB container吧? 反正就是一堆不知道要設什麼的參數要設定才能work.
    試到最後總算有個確定能work的CMT設定方式. 決定開個主題記錄一下.

    這只是個work的紀錄喔!! 很多都還沒用清楚, 比方說TransactionManager是怎樣, 怎麼沒TransactionAttribute之類的問題, 不在本文討論範圍.

    Focal Points

  • Connection Pools
    1. GlassFish Admin Console -> Resources -> JDBC -> Connection Pools -> Click "New" Button
    2. Input
      1. name: OraclePool
      2. DataSource Classname: oracle.jdbc.pool.OracleDataSource
      3. Resource Type: javax.sql.DataSource
    3. Click "Save" Button
    4. 點選 Additional Properties tab
    5. Click Add Property -> Input Name: username Value: myusername
    6. Click Add Property -> Input Name: password Value: mypassword
    7. Click Add Property -> Input Name: url Value: jdbc:oracle:thin:@localhost:1521:orcl
    8. Click "Save" Button
  • JDBC Resources
    1. GlassFish Admin Console -> Resources -> JDBC -> JDBC Resources -> Click "New" Button
    2. Input
      1. JNDI Name: jdbc/oracle
      2. Pool Name: OraclePool
      3. Click "Save" Button
  •     Connection Pool和JDBC Resource如果沒有這樣設定, 
        而是在persistence.xml設定hibernate.XXX的 property的話, 會出現
        RAR5117 : Failed to obtain/create connection from connection pool 
        [ OraclePool ]. Reason : Connection could not be allocated because: 
        Invalid Oracle URL specified: OracleDataSource.makeURL
        RAR5114 : Error allocating connection : [Error in allocating a connection. 
        Cause: Connection could not be allocated because: 
        Invalid Oracle URL specified: OracleDataSource.makeURL]
        這樣的錯誤.
    
  • Integrate JDBC driver
    1. GlassFish Admin Console -> Application Server -> JVM Settings tab -> Path Settings tab
    2. Input Classpath Suffix: C:\XXX\lib\ojdbc14.jar
    3. Click "Save" Button
  •     如果沒有integrate JDBC driver, 
        會出現找不到OracleResultSet的ClassDefNotFound錯誤.
        然後就會很悶, 因為明明連import OracleResultSet都沒問題了啊?
        其實就是因為沒有設定在server上的關係.
    
  • 設定persistence.xml
      注意這裡設定的transaction-type是JTA不是RESOURCE_LOCAL, 而且還要設定jta-data-source
      
      < persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" 
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
          xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
          http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
        < persistence-unit name="persistence_oracle" transaction-type="JTA">
          < provider>org.hibernate.ejb.HibernatePersistence< /provider>
          < jta-data-source>jdbc/oracle< /jta-data-source>
          < properties>
            < property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect"/>
          < /properties>
        < /persistence-unit>
      < /persistence>
              
  • 做entity
      User.java
      @Entity(name="T_USER")
      public class User implements Serializable {
          private static final long serialVersionUID = 1L;
          private Long id;
          private String name;
          private Integer age;
          
          public void setId(Long id) {
              this.id = id;
          }
      
          @Id
          @GeneratedValue(strategy = GenerationType.AUTO)
          public Long getId() {
              return id;
          }
      
          @Override
          public String toString() {
              return "jpa.User[id=" + id + "][name= " + name + " ][age="+ age +"]";
          }
      
          @Column(name="NAME")
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          @Column(name="AGE")
          public Integer getAge() {
              return age;
          }
      
          public void setAge(Integer age) {
              this.age = age;
          }
      
      }            
              
  • 做session bean interface
      @Local
      public interface ITestEntityManager {
          
          EntityManager getEntityManager();
          void createUser();
          
      }            
              
  • 做concrete session bean
      注意PersistenceContext的unitName就是persistence.xml的unitName
      @Stateless
      @TransactionManagement(TransactionManagementType.CONTAINER)
      public class TestEntityManager implements ITestEntityManager {
      
          @PersistenceContext(unitName="persistence_oracle") EntityManager em;
      
          public EntityManager getEntityManager() {
              return em;
          }
      
          public void createUser() {
              User user = new User();
              user.setName("111111111111111");
              user.setAge(new Integer(999));
              em.persist(user);
          }
      
      }