星期五, 8月 04, 2006

JBoss EJB3(Entity Beans)備忘記

第一編介紹如何安裝 JBoss 及建立第一個 Stateless Session Beans HelloWorld:
http://blog.matrix.org.cn/page/joeyta?entry=jboss_ejb3_helloworld_%E5%82%99%E5%BF%98%E8%A8%98


第二編介紹 Stateful Session Beans:
http://blog.matrix.org.cn/page/joeyta?entry=jboss_ejb3_stateful_session_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

如下圖所示:


這要切記不要 ejbjar 及 run.client 一起執行,
因為 JBoss 相應的 Beans 未deploy完, 就執行 run.client,
會出現 javax.naming.NameNotFoundException


[9] 使用 HSQL Database Manager 瀏覽資料庫:
進入網止 http://localhost:8080/jmx-console/
點選 database=localDB,service=Hypersonic 連結
然後在 void startDatabaseManager() 的地方按 Invoke 按鈕.
如下圖所示:


進入 HSQL Database Manager 後, 執行下面 DML.
select * from purchase_order x,item y where x.id = y.order_id;
就會出現如下圖所示:


這次 JBoss EJB3 Entity Beans 教學己到了終點.
正籌備 Message Driven Beans 教學.
不過我相信有了上面這個起點, 學下去也很容易.


如果想更了解 EJB3 , 這裡有免費的 Mastering EJB 3.0. 下載.
http://www.theserverside.com/tt/books/wiley/masteringEJB3/index.tss


Sun EJB Documentation page:
http://java.sun.com/products/ejb/docs.html


JBoss EJB 3.0 reference guide:
http://docs.jboss.org/ejb3/app-server/reference/build/reference/en/html/index.html


JBoss EJB 3.0 Full Documentation Suite:
http://docs.jboss.org/ejb3/


EJB 3.0 TrailBlazer Learning Application:
http://trailblazer.demo.jboss.com/EJB3Trail/


JBoss EJB# Wiki:
http://wiki.jboss.org/wiki/Wiki.jsp?page=EJB3

沒有留言: