JBoss EJB3(Entity Beans)備忘記

EJB3 不只是標準的 J2EE 編程接口, 它還提供新的企業應用程式開發方案.
EJB3 使用 POJOs (Plain Old Java Object), Java annotation 及 依賴注入設計模式.
使代碼分離及便於測試.

在 multi tier 的企業應用系統中, 典型地分為兩種 objects.
Application logic components 及 Persistent data objects.
前者主要是處理 business processes. 如計算結帳價格.
後者則是處理生命週期較長的 business data. 如將結帳價格存入資料庫.

Entity Beans 為 ORM (Object Relational Mapping) 的實作.
是一種模組化並對應關係式資料庫. 它是一個完全 POJO 為基楚的儲存框架,

當開發資料庫相關的應用程式時, 開發員典型地需要管理兩種不同的 model 結構.

第一種 model 結構為 java objects ,
java objects model 資料存於內存中, 整個應用程式建構於這些資料 objects,
Java 編程語言及其 APIs 就是設計成有效率地控制這些 objects.
開發員則利用這些 APIs 來開發應用程式.

第二種則為關聯式 model 結構.
資料存於關聯式資料庫裡, 有利於分佈式環境及有效率地提高大量資料的搜尋.

處理這些 java objects 及 關聯式資料表 需要不同的語法及 APIs,
因此開發員需要同時模組化相同的資料兩次, 及處理兩種資料所在的系統及中間的轉換,
這是非常沒有效率的事情.

Entity beans 則是設計成處理這兩種資料的橋樑, 並且它是簡單的 java objects,
可以在其上利用標記簡單地定義如何存入資料庫,
而這些 objects 及 relational 資料映射關係自動地交由 EJB3 容器處理,
開發員則不用關心資料庫的結構, 管理及定義訪問的APIs.
而 EntityManager 提供的訪問接口及使用 EJB QL 來處理這些 objects instance.

Entity beans 及其 callback methods 的生命週期:
可以在 entity beans 定義下面的標記, 容器會自動管理及喚起這些被標記的 methods.

@PrePersist: 此標記為當 entity 存入在資料庫 前 被喚起.
@PostPersist: 此標記為當 entity 存入在資料庫 後 被喚起.
@PreRemove: 此標記為當 entity 在資料庫裡刪除 前 被喚起.
@PostRemove: 此標記為當 entity 在資料庫裡刪除 後 被喚起.
@PreUpdate: 此標記為當 entity 在資料庫裡更新 前 被喚起.
@PostUpdate: 此標記為當 entity 在資料庫裡更新 後 被喚起.
@PostLoad: 此標記為當 entity 從資料庫取得 後 被喚起.

@Remove: 這標記並不是 callback methods, 當應用程式呼叫此 method 時,
                    bean instance 會從 EntityManager managed context 裡移除.
                   這時侯 bean 變成分離狀況及不能使用.

亦可選擇建立新 class 並標記這些 callback methods,
然後在 entity bean 裡使用 @EntityListener 標記這新 class 為 callback listener.
如下例所示:
@EntityListener(CostomizeEntityListener.class)

為使這備忘記更簡化, 故這裡並不實作這些喚回函數.
本編主要介紹如何實作簡單的 Entity Beans.
要熟識 Entity Beans, 需要了解的知識實在不少,
本編結尾提供連結, 可免費下載 Mastering EJB 3.0 PDF 版本.

開始備忘記:
這次備忘記以 Shopping Cart 作為例子, 是一種 One-To-Many 的映射關係實作.

------------                        -----------
|                  |  1            *     |              |
|  Order      | <---------> |   Item   |
|                  |                       |              |
------------                        -----------

[1] Eclipse 啟動 JBoss Server
[2] Eclipse 建立 Shopping Cart Project
[3] 建立 Shopping cart Entity Beans
[4] 建立 D:/eclipse_wtp/workspace/ShoppingCartEJB3/src/META-INF/persistence.xml
[5] 建立 Shopping cart Stateless Session Beans
[6] 建立 Shopping cart Stateful Session Beans
[7] 建立客戶端測試程式
[8] 使用 ANT 建立 EJB-JAR 並執行 ShoppingCartClient 程式
[9] 使用 HSQL Database Manager 瀏覽資料庫

[1] Eclipse 啟動 JBoss Server:
Eclipse: Windows -> Show View -> Other
  -->> JBoss-IDE -> Server Configuration 就會顯示 JBoss Server Configuration console
  然後 right client 它按 start , JBoss 就會啟動, 如下圖所示:


[2] Eclipse 建立 Shopping Cart Project:
Eclipse: File -> New -> Other -> EJB 3.0 -> EJB 3.0 Project
Project Name: ShoppingCartEJB3 -> Next
選擇上一編已建立的 JBoss 4.0.x: jboss_configuration [default](not running)
打開後右鍵點選 JBoss 4.0.x -> new
然後按 Finish. ShoppingCartEJB3 project 就建立了

[3] 建立 Shopping cart Entity Beans:
/*----------------------- Order.java ---------------------*/
package ejb3.joeyta.domain;
import java.util.ArrayList;
import java.util.Collection;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@SuppressWarnings("serial")
@Entity  // 表示此為 Entity Beans
@Table(name = "PURCHASE_ORDER")   // 表示將建立的 Table 名為 PURCHASE_ORDER
public class Order implements java.io.Serializable
{
   private int id;
   private double total;
   private Collection<Item> items;

   @Id @GeneratedValue(strategy=GenerationType.AUTO) 
           // AUTO 定義由 persistence provider 決定怎樣產生 ID
   public int getId()
   {
      return id;
   }

   public void setId(int id)
   {
      this.id = id;
   }

   public double getTotal()
   {
      return total;
   }

   public void setTotal(double total)
   {
      this.total = total;
   }

   public void addPurchase(String product, int quantity, double price)
   {
      if (items == null) items = new ArrayList<Item>();
      Item item = new Item();
      item.setOrder(this);
      item.setProduct(product);
      item.setQuantity(quantity);
      item.setPrice(quantity * price);
      items.add(item);
      total += quantity * price;
   }

   @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy="order")
   public Collection<Item> getItems()
   {
      return items;
   }

   public void setItems(Collection<Item> items)
   {
      this.items = items;
   }
}
/*----------------------- Order.java ---------------------*/

/*----------------------- Item.java ---------------------*/
package ejb3.joeyta.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

@SuppressWarnings("serial")
@Entity  // 表示此為 Entity Beans
public class Item implements java.io.Serializable
{
   private int id;
   private double price;
   private int quantity;
   private String product;
   private Order order;


   @Id @GeneratedValue(strategy=GenerationType.AUTO) 
          // AUTO 定義由 persistence provider 決定怎樣產生 ID
   public int getId()
   {
      return id;
   }

   public void setId(int id)
   {
      this.id = id;
   }

   public double getPrice()
   {
      return price;
   }

   public void setPrice(double price)
   {
      this.price = price;
   }

   public int getQuantity()
   {
      return quantity;
   }

   public void setQuantity(int quantity)
   {
      this.quantity = quantity;
   }

   public String getProduct()
   {
      return product;
   }

   public void setProduct(String product)
   {
      this.product = product;
   }

   @ManyToOne    // 定義 Many-To-One 關係
   @JoinColumn(name = "order_id")   // 定義使用 foreign key, 即 order_id 對應到 Order 裡的 id
   public Order getOrder()
   {
      return order;
   }

   public void setOrder(Order order)
   {
      this.order = order;
   }
}
/*----------------------- Item.java ---------------------*/

以上 Order 與 Item 為 One-To-Many 的關係.
使用 EJB3 Annotation 比 設定 Hibernate mapping file 還要輕鬆.
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy="order")
類似 Hibernate,
OneToMany 表示為 One-To-Many mapping.
cascade 定義 Order 的每個 DML 是否關聯至 Item.
cascade = CascadeType.ALL 表示所有DML均關聯, 例如 Order delete 時, 所有相關的 Item 亦會 delete.
fetch 可定義 lazy initialization
fetch = FetchType.EAGER 表示不使用 lazy initialization,
例如當 load Order 時, 所有相關 Item 也一起 load 出來. 如使用 LAZY 剛 Item 使用時才 load 出來.
mappedBy 定義 Order 與 Item 相向關係, Persistence manager 根據此值瞭解相向對應關係.
mappedBy="order" 表示 Item 裡的 order property 與此對應, 即 Hibernate 裡的 inverse. 防止 duplication DML.

這裡如果沒明確設定 @Table , Persistence manager 就會根據 Class 名來建立 Table.
這裡如果沒明確設定 @Column , Persistence manager 就會根據 Class 裡的 Property 名來建立 column.

這裡如果不使用 EJB3 Annotation,
Persistence provider 預設會搜尋 META-INF 下的 orm.xml
亦可以在 META-INF/persistence.xml 裡設定 mapping file orm.xml. 然後在 orm.xml 裡設定 OR Mapping.
jboss.xml 則可設定 JNDI XML Binding.

[4] 建立 D:/eclipse_wtp/workspace/ShoppingCartEJB3/src/META-INF/persistence.xml:
<!--------------------- persistence.xml -------------------->
<?xml version="1.0" encoding="UTF-8"?>
<persistence>
 <persistence-unit name="joeyta">
  <jta-data-source>java:/DefaultDS</jta-data-source>
  <properties>
   <property name="hibernate.hbm2ddl.auto" value="create-drop" />
  </properties>
 </persistence-unit>
</persistence>
<!--------------------- persistence.xml -------------------->
這個檔案定義資料庫的相關部署.
joeyta 為 unit name, EntityManager 需要指定相應的 unit name.
java:/DefaultDS 表示使用 D:/jboss/server/default/deploy 下 xxxxx-ds.xml 結尾資料庫設定檔對應的 jndi name.
這實作裡使用 JBoss 預設的 HSQLDB, 即 D:/jboss/server/default/deploy/hsqldb-ds.xml
<property name="hibernate.hbm2ddl.auto" value="create-drop" /> 定義:
當project deploy 時就會自動產生 database schema. 當project undeploy 時就會自動清除 database schema.


[5] 建立 Shopping cart Stateless Session Beans:
/*----------------------- ShoppingCartDAO.java ---------------------*/
package ejb3.joeyta.sessions;
import java.util.List;
public interface ShoppingCartDAO {
 public List findByProduct(String product);
 public List loadAllOrders();
}
/*----------------------- ShoppingCartDAO.java ---------------------*/

/*----------------------- ShoppingCartDAOBean.java ---------------------*/
package ejb3.joeyta.sessions;
import java.util.List;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Stateless  // 定義 Stateless Session Beans
@Remote(ShoppingCartDAO.class)   // 定義 ShoppingCartDAO.class 為 Remote interface
public class ShoppingCartDAOBean {
 @PersistenceContext(unitName = "joeyta")
 private EntityManager manager;
 // 這裡定義將 EntityManager inject 進來. unitName="joeyta" 為上面定義的 persistence.xml 
 public List findByProduct(String product) {
  return manager.createQuery("from Item o where o.product = :product")
    .setParameter("product", product).getResultList();
 }

 public List loadAllOrders(){
  return manager.createQuery("from Order").getResultList(); 
 }
}
/*----------------------- ShoppingCartDAOBean.java ---------------------*/


[6] 建立 Shopping cart Stateful Session Beans:
/*----------------------- ShoppingCart.java ---------------------*/
package ejb3.joeyta.sessions;
import ejb3.joeyta.domain.Order;
public interface ShoppingCart {
 public void buy(String product, int quantity, double price);
 public Order getOrder();
 public void checkout();
}
/*----------------------- ShoppingCart.java ---------------------*/

/*----------------------- ShoppingCartBean.java ---------------------*/
package ejb3.joeyta.sessions;

import javax.ejb.Remote;
import javax.ejb.Remove;
import javax.ejb.Stateful;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import ejb3.joeyta.domain.Item;
import ejb3.joeyta.domain.Order;

@SuppressWarnings("serial")
@Stateful  // 定義此為 Stateful Session Beans
@Remote(ShoppingCart.class)  // 定義 ShoppingCart.class 為 Remote interface
public class ShoppingCartBean implements ShoppingCart, java.io.Serializable {
 @PersistenceContext(unitName = "joeyta")
 private EntityManager manager;
 // 這裡定義將 EntityManager inject 進來. unitName="joeyta" 為上面定義的 persistence.xml

 private Order order;

 public void buy(String product, int quantity, double price) {
  if(order == null) order = new Order();
  order.addPurchase(product, quantity, price);
 }
 
 public Order getOrder() {
  return order;
 }

 @Remove   // @Remove 表示當呼叫 checkout(), 就會清除這個 Instance
 public void checkout() {
  manager.persist(order);
  System.out.println("checkout"); 
  System.out.println("*******************************"); 
  System.out.println("Print total order list on server:");
  for(Item item : order.getItems()){
   System.out.println("Item:" + item.getProduct() + "  Price:" + item.getPrice());   
  } 
  System.out.println("Order id:" + order.getId() + "  Total Price: " + order.getTotal()); 
 }
}
/*----------------------- ShoppingCartBean.java ---------------------*/


[7] 建立客戶端測試程式:
/*----------------------- ShoppingCartClient.java ---------------------*/
package ejb3.joeyta.clients;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import javax.naming.InitialContext;
import ejb3.joeyta.domain.Item;
import ejb3.joeyta.domain.Order;
import ejb3.joeyta.sessions.ShoppingCart;
import ejb3.joeyta.sessions.ShoppingCartDAO;

public class ShoppingCartClient {
  public static InitialContext getInitialContext() throws javax.naming.NamingException {
     // 這裡亦可在 CLASSPATH 下建立 jndi.properties
  Properties p = new Properties();
  p.put(InitialContext.INITIAL_CONTEXT_FACTORY,
    "org.jnp.interfaces.NamingContextFactory");
  p.put(InitialContext.URL_PKG_PREFIXES, " org.jboss.naming:org.jnp.interfaces");
  p.put(InitialContext.PROVIDER_URL, "jnp://localhost:1099");
  return new javax.naming.InitialContext(p);
 }
 
 public static void main(String[] args) throws Exception {
  InitialContext ctx = getInitialContext();
  ShoppingCart cart = (ShoppingCart) ctx.lookup("ShoppingCartBean/remote");

  System.out.println("Buying Cake");
  cart.buy("Cake", 1, 3.9);    // 買cake

  System.out.println("Buying Bread");
  cart.buy("Bread", 1, 2.9);   // 買bread

  System.out.println("Checkout");
  cart.checkout();  // 結帳

  ShoppingCartDAO cartDAO = (ShoppingCartDAO) ctx.lookup("ShoppingCartDAOBean/remote"); 

  System.out.println("*******************************"); 
  System.out.println("Print total order list on client:");  // 列印所有 order list
  List orders = cartDAO.loadAllOrders();
  for (Iterator iter = orders.iterator(); iter.hasNext();) {
   Order order = (Order) iter.next();
   for(Item item : order.getItems()){
    System.out.println("Item:" + item.getProduct() + "  Price:" + item.getPrice());   
   }
   System.out.println("Order id:" + order.getId() + "  Total Price: " + order.getTotal());
  }

  System.out.println("*******************************"); 
  System.out.println("Print cake order list on client:"); // 列印所有 cake order list
  List orderList = cartDAO.findByProduct("Cake");
  for (Iterator iter = orderList.iterator(); iter.hasNext();) {
   Item element = (Item) iter.next();
   System.out.println(element.getProduct() + "   "
     + element.getQuantity() + "   " + element.getPrice());
  }

 }
}
/*----------------------- ShoppingCartClient.java ---------------------*/
這裡當 選購項目時使用 Shopping Cart 的 Statefull Session Beans.
列印己選購項目使用 DAO (Data Access Model) 的 Stateless Session Beans.

項目結構如下所示:

 [8] 使用 ANT 建立 EJB-JAR 並執行 ShoppingCartClient 程式:
在 ShoppingCartEJB3 project 下建立 build.xml [ ANT build File ]
內容如下:
<!------------------------- build.xml ------------------------------->
<?xml version="1.0"?>
<project name="JBoss" default="run.client" basedir=".">
 <property environment="env" />
 <property name="src.dir" value="${basedir}/src" />
 <property name="resources" value="${basedir}/META-INF" />
 <property name="jboss.home" value="${env.JBOSS_HOME}" />
 <property name="classes.dir" value="bin" />

 <path id="classpath">
  <fileset dir="${jboss.home}/client">
   <include name="**/*.jar" />
  </fileset>
  <pathelement location="${classes.dir}" />
  <pathelement location="${basedir}/client-config" />
 </path>

 <target name="clean">
  <delete file="${basedir}/ShoppingCart.jar" />
  <delete file="${jboss.home}/server/default/deploy/ShoppingCart.jar" />
 </target>

 <target name="ejbjar" depends="clean">
  <jar jarfile="ShoppingCart.jar">
   <fileset dir="${classes.dir}">
    <include name="ejb3/joeyta/domain/*.class" />
    <include name="ejb3/joeyta/sessions/*.class" />
    <include name="META-INF/*.xml" />
   </fileset>
  </jar>
  <copy file="ShoppingCart.jar" todir="${jboss.home}/server/default/deploy" />
 </target>

 <target name="run.client">
  <java classname="ejb3.joeyta.clients.ShoppingCartClient" fork="yes" dir=".">
   <classpath refid="classpath" />
  </java>
 </target>

</project>
<!------------------------- build.xml ------------------------------->

build.xml 項目:
classpath 描述所需的 libraries 及 classes 的位置.
clean 清除項目所產生的 ShoppingCart.jar 及 JBoss deploy 裡的 ShoppingCart.jar
ejbjar 產生 ShoppingCart.jar
run.ShoppingCartClient 執行 Client 測試程式 ShoppingCartClient

點選 build -> Run As -> 3. Ant Build ->> ejbjar
JBoss 自動 deploy 後就會產生相應的 Database Schema.
Eclipse Console 輸出為
Buildfile: D:/eclipse_wtp/workspace/ShoppingCartEJB3/build.xml
clean:
   [delete] Deleting: D:/eclipse_wtp/workspace/ShoppingCartEJB3/ShoppingCart.jar
   [delete] Deleting: D:/jboss/server/default/deploy/ShoppingCart.jar
ejbjar:
      [jar] Building jar: D:/eclipse_wtp/workspace/ShoppingCartEJB3/ShoppingCart.jar
     [copy] Copying 1 file to D:/jboss/server/default/deploy
BUILD SUCCESSFUL
Total time: 4 seconds

JBoss Console  輸出為
15:48:14,835 INFO  [SchemaExport] Running hbm2ddl schema export
15:48:14,835 INFO  [SchemaExport] exporting generated schema to database
15:48:14,850 INFO  [SchemaExport] schema export complete
15:48:14,850 INFO  [NamingHelper] JNDI InitialContext properties:{java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory, java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces}
15:48:14,866 INFO  [JmxKernelAbstraction] installing MBean: jboss.j2ee:jar=ShoppingCart.jar,name=ShoppingCartBean,service=EJB3 with dependencies:
15:48:14,866 INFO  [JmxKernelAbstraction]  persistence.units:jar=ShoppingCart.jar,unitName=joeyta
15:48:14,975 INFO  [EJBContainer] STARTED EJB: ejb3.joeyta.sessions.ShoppingCartBean ejbName: ShoppingCartBean
15:48:14,991 INFO  [JmxKernelAbstraction] installing MBean: jboss.j2ee:jar=ShoppingCart.jar,name=ShoppingCartDAOBean,service=EJB3 with dependencies:
15:48:14,991 INFO  [JmxKernelAbstraction]  persistence.units:jar=ShoppingCart.jar,unitName=joeyta
15:48:15,022 INFO  [EJBContainer] STARTED EJB: ejb3.joeyta.sessions.ShoppingCartDAOBean ejbName: ShoppingCartDAOBean
15:48:15,053 INFO  [EJB3Deployer] Deployed: file:/D:/jboss/server/default/deploy/ShoppingCart.jar

點選 build -> Run As -> 3. Ant Build ->> run.client
Eclipse Console 輸出為
Buildfile: D:/eclipse_wtp/workspace/ShoppingCartEJB3/build.xml
run.client:
     [java] log4j:WARN No appenders could be found for logger (org.jboss.remoting.Client).
     [java] log4j:WARN Please initialize the log4j system properly.
     [java] Buying Cake
     [java] Buying Bread
     [java] Checkout
     [java] *******************************
     [java] Print total order list on client:
     [java] Item:Cake  Price:3.9
     [java] Item:Bread  Price:2.9
     [java] Order id:1  Total Price: 6.8
     [java] *******************************
     [java] Print cake order list on client:
     [java] Cake   1   3.9
BUILD SUCCESSFUL
Total time: 8 seconds
如下圖所示:

JBoss Console  輸出為
15:49:58,379 INFO  [STDOUT] checkout
15:49:58,379 INFO  [STDOUT] *******************************
15:49:58,379 INFO  [STDOUT] Print total order list on server:
15:49:58,379 INFO  [STDOUT] Item:Cake  Price:3.9
15:49:58,379 INFO  [STDOUT] Item:Bread  Price:2.9
15:49:58,379 INFO  [STDOUT] Order id:1  Total Price: 6.8
如下圖所示:

進入 HSQL Database Manager 後, 執行下面 DML.
select * from purchase_order x,item y where x.id = y.order_id;
就會出現如下圖所示:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章