EJB 3.0 (JSR 220) 以「配置由註解取代 XML」為核心,徹底簡化了實體 Bean 開發。本文基於 JBoss EJB 3.0 Preview 5 官方教學範例,完整呈現 Order-LineItem 雙向關聯、ShoppingCart 狀態型 Session Bean、EntityManager 操作的端到端實作。
package org.jboss.tutorial.entity.bean;
import javax.persistence.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
@Entity
@Table(name = "PURCHASE_ORDER") // 指定資料表名,預設為類別名
public class Order implements Serializable {
private int id;
private double total;
private Collection lineItems;
@Id(generate = GeneratorType.AUTO) // 自動遞增主鍵,對應資料庫 IDENTITY/SEQUENCE
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 (lineItems == null) lineItems = new ArrayList();
LineItem item = new LineItem();
item.setOrder(this); // 設定反向關聯 (關鍵!)
item.setProduct(product);
item.setQuantity(quantity);
item.setSubtotal(quantity * price);
lineItems.add(item);
total += quantity * price;
}
// 一對多雙向關聯:Order 為擁有方,LineItem 為被擁有方
@OneToMany(
cascade = CascadeType.ALL, // 全級聯:PERSIST/MERGE/REMOVE/REFRESH
fetch = FetchType.EAGER, // 急切載入 (預設 LAZY)
mappedBy = "order" // 關聯由 LineItem.order 欄位維護
)
public Collection getLineItems() { return lineItems; }
public void setLineItems(Collection lineItems) { this.lineItems = lineItems; }
}
package org.jboss.tutorial.entity.bean;
import javax.persistence.*;
@Entity // 無 @Table,資料表名預設為 LINEITEM
public class LineItem implements Serializable {
private int id;
private double subtotal;
private int quantity;
private String product;
private Order order; // 反向導航屬性
@Id(generate = GeneratorType.AUTO)
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public double getSubtotal() { return subtotal; }
public void setSubtotal(double subtotal) { this.subtotal = subtotal; }
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
@JoinColumn(name = "order_id") // LINEITEM 表的外鍵欄位
public Order getOrder() { return order; }
public void setOrder(Order order) { this.order = order; }
}
package org.jboss.tutorial.entity.bean;
import javax.ejb.Remote;
import javax.ejb.Remove;
@Remote
public interface ShoppingCart {
void buy(String product, int quantity, double price);
Order getOrder();
@Remove void checkout(); // 標記移除:方法完成後 Bean 實例銷毀
}
package org.jboss.tutorial.entity.bean;
import javax.ejb.*;
import javax.persistence.EntityManager;
@Stateful // 狀態型:維護用戶會話狀態 (購物車內容)
public class ShoppingCartBean implements ShoppingCart {
@Inject // EJB 3.0 早期註解,後標準化為 @PersistenceContext
private EntityManager manager; // 核心:查詢、建立、尋找、移除實體
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
public void checkout() {
manager.persist(order); // 交易邊界結束時同步寫入資料庫
}
}
package org.jboss.tutorial.entity.client;
import org.jboss.tutorial.entity.bean.*;
import javax.naming.InitialContext;
public class Client {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
ShoppingCart cart = (ShoppingCart) ctx.lookup(ShoppingCart.class.getName());
System.out.println("Buying 2 memory sticks");
cart.buy("Memory stick", 2, 500.00);
System.out.println("Buying a laptop");
cart.buy("Laptop", 1, 2000.00);
System.out.println("Print cart:");
Order order = cart.getOrder();
System.out.println("Total: $" + order.getTotal());
for (LineItem item : order.getLineItems()) {
System.out.println(item.getQuantity() + " " + item.getProduct() + " " + item.getSubtotal());
}
System.out.println("Checkout");
cart.checkout(); // 觸發 @Remove,Bean 銷毀,持久化完成
}
}
| 註解 | 位置 | 語義 | 關鍵屬性 |
|---|---|---|---|
| @Entity | 類別 | 標記為 JPA 實體 | name (實體名稱,JPQL 用) |
| @Table | 類別 | 指定資料表映射 | name, schema, catalog, uniqueConstraints |
| @Id | Getter | 主鍵欄位 | generate: AUTO/IDENTITY/SEQUENCE/TABLE |
| @OneToMany | Getter | 一對多關聯 | cascade, fetch, mappedBy, targetEntity |
| @ManyToOne | Getter | 多對一關聯 | cascade, fetch, optional |
| @JoinColumn | Getter | 指定外鍵欄位 | name, referencedColumnName, nullable |
| @Stateful | 類別 | 狀態型 Session Bean | name, mappedName, description |
| @Inject / @PersistenceContext | 欄位 | 注入 EntityManager | unitName, type (TRANSACTION/EXTENDED) |
| @Remove | 方法 | 標記 Bean 移除 | retainIfException (預設 false) |
// 持久化新實體
em.persist(entity);
// 根據主鍵查詢
Order order = em.find(Order.class, orderId);
// JPQL 查詢
Query q = em.createQuery("SELECT o FROM Order o WHERE o.total > :min");
q.setParameter("min", 1000.0);
List orders = q.getResultList();
// 合併離脫實體
Order merged = em.merge(detachedOrder);
// 移除實體
em.remove(managedOrder);
// 刷新同步資料庫狀態
em.refresh(entity);
// 取得交易同步狀態
em.flush(); // 強制同步到資料庫
# 1. 啟動 JBoss (含 HSQLDB)
cd jboss_home/bin
run.bat -c all
# 2. 建置與部署 (Ant)
ant build
ant deploy
# 3. 執行客戶端
ant run-client
# 4. 驗證資料庫
# HSQL Database Manager: 啟動腳本中找到 startDatabaseManager
# 執行查詢:SELECT * FROM PURCHASE_ORDER; SELECT * FROM LINEITEM;
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=record.log
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%p %d{hh:mm:ss} %t %c{1} -%m%n
log4j.appender.R.MaxBackupIndex=1
log4j.appender.R.MaxFileSize=100KB
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) -%m%n
log4j.rootLogger=stdout,R