ActiveRecord是Ruby on Rails的ORM層,大體上類似於Java中的Hibernate。ActiveRecord基於約定優於配置的原則,所以它使用起來比Hibernate更容易。在簡化基本的數據操作方面,如創建、讀取、更新和刪除,它確實是非常棒的。
借助於ActiveRecord,你的模型類也會作為數據訪問對象(Data Access Object,DAO)來執行CRUD操作。在初步探究之後,我對ActiveRecord產生了濃厚的興趣,因此開始尋找一種解決方案來簡化基於Java持久化API(Java Persistence API,JPA)的ORM框架的使用。
大多數JPA應用都會有某種類型的數據訪問層(Data Access Layer,DAL)來與數據庫進行交互。通常DAL會包含數據訪問對象或符合Repository設計模式的類。
DAO的實現與實體對象通常是一對一的關系,而Repository則是針對每個聚合根(aggregate root)實現一個。不管是哪種場景,應用最後都會創建多個類與數據庫進行交互。盡管適當的抽象能夠有效限制所創建類的數量,但是它終究還是會在應用中引入一個額外的層,這都是需要維護和測試的。
ActiveJPA基於JPA,提供了Martin Fowler所提出的活動記錄模式(Active Record pattern)的Java實現。借助於ActiveJPA,模型本身會作為DAO並與數據庫交互,這樣就不需要額外的代碼作為數據訪問層了。
ActiveJPA使用到了JPA規范,因此所有JPA的ORM實現(Hibernate、EclipseLink、OpenJPA等)都可以與ActiveJPA協同使用。
要將已有的模型轉換為ActiveJPA,只需讓你的模型實現擴展org.activejpa.entity. Model即可:
@java.persistence.Entity public class YourModel extends org.activejpa.entity.Model { }
你的模型將會從ActiveJPA的模型類中繼承得到很多的CRUD功能。
//根據id獲得訂單 Order order = Order.findById(12345L); // 根據customer獲得其已發貨的訂單 List orders = Order.where("customerEmail", "[email protected]", "status", "shipped"); // 得到匹配過濾條件的第一條訂單記錄 Long count = Order.first("customerEmail", "[email protected]", "status", "shipped"); // 得到匹配過濾條件的唯一一條訂單記錄 Long count = Order.one("customerEmail", "[email protected]", "status", "shipped"); // 得到所有的記錄 List orders = Order.all(); // 檢查指定標識符的訂單是否存在 boolean exists = Order.exists(1234L); // 保存訂單 order.persist(); // 刪除訂單 order.delete(); // 刷新訂單 order.refresh(); // 與持久化上下文中已有的訂單進行合並 order.merge();
對記錄進行過濾時,你並不需要創建JPQL或criteria查詢。ActiveJPA提供了一個復雜的過濾器,用於在不同的操作間進行連接(conjunction):
// 獲取匹配指定Email地址且賬單額大於1000的所有訂單,並且要進行分頁 Filter filter = new Filter(); filter.setPageNo(1); filter.setPerPage(25); filter.addCondition(new Condition("customerEmail", Operator.eq, "[email protected]"); filter.addCondition(new Condition("billingAmount", Operator.gt, 1000.00); List orders = Order.where(filter); // 對滿足過濾條件的訂單進行計數 Long count = Order.count(filter); // 刪除匹配這個過濾器的訂單 Long count = Order.deleteAll(filter);
ActiveJPA允許嵌套過濾參數。這樣的話就能更容易地在運行時創建動態查詢。例如,你獲取的訂單中,至少要有一個訂單項是基於“book”類別的產品創建的。
// 得到至少包含一個book項的所有訂單 Filter filter = new Filter(); filter.setPageNo(1); filter.setPerPage(25); filter.addCondition(new Condition("orderItems.product.category", Operator.eq, "books"); List orders = Order.where(filter);
上面討論的所有CRUD操作同時可以在集合級別執行。將查詢范圍設置到集合類的用法如下所示:
// 在訂單中,根據id查找訂單項 order.collections("orderItems").findById(123L); // 得到第一個已發貨的訂單項 order.collections(“orderItems”).first(“status”, “shipped”); // 得到所有取消的訂單項 order.collections(“orderItems”).where(“status”, “cancelled”); // 得到集合中的所有項 order.collections(“orderItems”).all(); // 往集合中添加一項 order.collections(“orderItems”).add(orderItem); // 移除集合中的一項 order.collections(“orderItems”).remove(orderItem);
過濾和分頁也可以使用到集合之中
// 對一個訂單中的訂單項基於過濾器進行查詢並進行分頁 Filter filter = new Filter(); filter.setPageNo(1); filter.setPerPage(25); filter.addCondition(new Condition(“status”, “shipped”); order.collections("orderItems").where(filter); // 在訂單中,得到匹配過濾器的訂單項的數量 order.collections(“orderItems”).count(filter);
ActiveJPA支持對模型的動態更新。在有些場景下這是很有用的,例如用戶通過浏覽器更新一個表單。你可以傳遞一個包含屬性的map來進行更新,而不是調用每個屬性的setter方法:
// 更新屬性 Map attributes = new HashMap(); attributes.put("billingAmount", 1000.0); order.updateAttributes(attributes);
你也可以更新非原始/非包裝類型的域,只需給這些對象傳遞map即可。以下的樣例展現了更新一個訂單的收獲地址和賬單金額。
// 更新一個訂單的收獲地址和賬單金額 Map attributes = new HashMap(); Map address = new HashMap(); address.put(“city”, “Bangalore”); address.put(“state”, “Karnataka”); attributes.put(“shippingAddress”, address); attributes.put("billingAmount", 1000.0); order.updateAttributes(attributes);
注意:目前尚不支持更新list/set/array域,未來的版本將會提供支持。
默認情況下,如果沒有事務,ActiveJPA將會為所有的更新操作啟動一個事務,不過你也可以將整個工作單元包裝到一個事務之中:
JPAContext context = JPA.instance.getDefaultConfig().getContext(); context.beginTxn(); boolean failed = true; try { // 你的工作單元置於此處 failed = false; } finally { // 提交或回滾事務 context.closeTxn(failed); }
如果已經存在了外部的事務,那麼ActiveJPA將會使用這個事務,但是不會進行提交或回滾。應該由應用來負責關閉該事務。
ActiveJPA為TestNG提供了一個基本的測試類,它會將ActiveJPA以掛鉤(hook)的方式添加到測試運行時之中。只需讓你的測試類擴展自org.activejpa.entity.testng.BaseModelTest類即可。以下為一個樣例代碼:
public class OrderTest extends BaseModelTest { @Test public void testCreateOrder() { Order order = new Order(); order.setCustomerEmail("[email protected]"); ... ... order.persist(); Assert.assertEquals(Order.where("customerEmail", "[email protected]").get(0), order); } } }
ActiveJPA可以以Maven artifact的方式來獲取,能夠非常容易地集成到你的應用之中。只需在你的pom.xml文件中添加如下的maven依賴:
<dependencies> <dependency> <groupId>org.activejpa</groupId> <artifactId>activejpa-core</artifactId> <version>0.1.5</version> </dependency> </dependencies> <repositories> <repository> <id>activejpa-repo</id> <url>https://raw.github.com/ActiveJpa/activejpa/mvn-repo/releases</url> <snapshots> <enabled>true</enabled> <updatePolicy>always</updatePolicy> </snapshots> </repository> </repositories>
ActiveJPA需要在實體類加載前就嵌入到你的應用之中。如果你使用Tomcat的話,那麼ServletContextListener就是做這件事的一個很好的地方。你可以將以下的代碼編寫到上下文監聽器的contextInitialized()方法之中。
// 動態加載Java代理 ActiveJpaAgentLoader.instance().loadAgent(); // 添加定義在persistence.xml中的持久化單元,以“order”名進行標識。 persistence.xml應該位於類路徑下 JPA.addPersistenceUnit("order"); // 如果你已經創建了實體管理工廠的話,可以將其關聯到ActiveJpa上 // JPA.addPersistenceUnit("order", entityManagerFactory);
將ActiveJPA與Spring這樣的框架進行集成是很容易的。大多數的應用都會使用Spring的注解來配置JPA和管理事務。ActiveJPA能夠以兩種方式來進行配置——你可以讓它來創建實體管理工廠也可以傳入一個已存在的對象。在Spring配置JPA的情況下,我們可以使用Spring所創建的實體管理工廠。這樣就能保證ActiveJPA使用Spring所創建的相同的連接和事務,從而提供無縫的集成。
以下的代碼展現了如何將ActiveJPA集成到Spring應用之中,這個應用是部署在servlet容器裡面的。它使用了一個自定義的上下文加載監聽器,從而將ActiveJPA嵌入到應用之中。需要注意的是,這很類似與上面的servlet樣例,區別在於這裡使用了Spring框架的ContextLoaderListener:
public class CustomContextListener extends ContextLoaderListener { @Override public void contextInitialized(ServletContextEvent event) { try { // 在這裡動態加載Java代理 ActiveJpaAgentLoader.instance().loadAgent(); } catch (Exception e) { throw new RuntimeException(e); } super.contextInitialized(event); JPA.instance.addPersistenceUnit("default", getCurrentWebApplicationContext().getBean(EntityManagerFactory. class), true); } }
在GitHub的ActiveJPA工程頁面上有一個示例應用,包含了很多更具體的樣例,展現了Spring-ActiveJPA的集成。