Cayenne,开源 ORM 盛宴中的另道佳肴,第 1 部分: 初尝小辣椒 - Apache Cayenne 基本功能
在正式介绍 Apache Cayenne 的功能之前,首先让我们先来看一下 ORM 是什么,我们为什么需要 ORM。 大家知道,持久化(persistence)的目的是为了将内存中的数据或对象保存到存储设备上(如磁盘),其中主要的应用就是保存到关系型数据库,或其他类型的数据库中。而在一些大型的应用程序中,软件设计者都会设计一个持久化层来专门做这样的工作,包括持久化和反持久化(将磁盘上或者数据库中的数据反持久化到内存中)。而 ORM,即对象关系映射,就是数据持久化层的一项重要技术。 有了它,软件设计和开发人员就可以更加关注业务逻辑,它会帮助将业务逻辑与数据库操作逻辑分离,实现系统的松耦合关系,从而使得开发人员从繁杂的与有关数据库操作的工作中解脱出来。可以说,一个健壮、优秀的 ORM 框架能够在为我们节省开发时间的同时,还能够提供高效,可靠的数据持久层代码。
首先,Cayenne 是一个完全开源的基于 Apache License 的数据持久化框架。除了具有一般 ORM 工具所具有的特性外,它还具有很多非常好的特性,比如可以将一个或多个模式数据库和 Java 对象绑定,可以对提交和回滚操作做原子化管理,可以对数据库表做逆向工程并生成 Java 代码,支持远程对象持久化等。 不仅如此,对于 DBA 所关心的性能问题,Cayenne 同样也有很好的支持,如 Cache 等等。而这些特性都可以通过它的一个非常易用的可视化工具 CayenneModeler 来配置完成。可以说,这降低了学习者的学习曲线,节省了开发时间。因此,我们将非常愿意通过本篇 Apache Cayenne 基础功能介绍和另一篇文章 Apache Cayenne 高级特性 向您介绍 Cayenne,如何使用 Cayenne,并希望在你学习过这两篇文章后,Cayenne 能够成为您开发企业应用数据库持久层框架中的一个选择。
可以从 Apache Cayenne 网站上下载稳定版的 Cayenne2.0 安装包。开发工具,我们选择 Eclipse 3.4。因为 Cayenne 是一个数据持久层框架,所以我们还需要一个数据库。这里,我们选择同样开源的且轻量级的 Derby 数据库。
需要下载的产品和技术请参阅参考资源中的 “获得产品和技术”部分。
首先,在 Eclipse 中创建一个 Java 工程。然后,将运行 Cayenne 和 Derby 所需要的库文件放到 build path 中。
图 1. 导入 Cayenne 和 Derby 所需的 lib 文件
图 2. 将 lib 文件放到 build path 中
完成后,我们再回到 Cayenne 的解压目录下,在 bin 目录中打开 CayenneModeler。
图 3. 打开 CayenneModeler
我们对于 Cayenne 的了解将从这里开始。打开后,我们新建一个工程,
图 4. CayenneModeler 主界面
进入到 Cayenne Modeler 的主界面。在导航条上有三个主要的按钮,分别是 Create DataDomain, Create DataNode, Create DataMap。其中,DataDomain, DataNode 和 DataMap 是一个 Cayenne 工程中三个重要的概念。
图 5. 导航条上的 DataDomain, DataNode 和 DataMap
图 6 说明了这三个概念间的关系
图 6. DataDomain, DataNode 和 DataMap 的关系
在一个 Cayenne 应用中可以定义多个 Data Domain,同时,作为虚拟数据源的 Domains 也可以包含多个实体数据源(Data Node)。而不同的数据源又可以对应不同的数据库 Schema 和 Java 对象的映射关系,即 Data Map。因此,Cayenne 可以方便灵活的实现应用程序调用不同的数据库操作,而具体细节 Cayenne 都帮我们进行了封装和处理。本文中,为了便于读者理解,我们仅先考虑一个 Data Domain 包含一个 Data Node,以及处理一个 Data Map 的情况。
在 DataDomain Configuration 视图中输入一个名字,如 cayenne-store。接下来,再创建一个 DataNode. 选中根节点 cayenne-store,点击创建 DataNode 按钮。所谓 DataNode Configuration 就是对你所要用的一个实体数据库信息的配置或数据源的配置。这里,我们选择 Embedded Derby。
图 7. 配置 DataDomain 和 DataNode
在配置完 JDBC Driver 信息之后,然后先将我们所做的配置信息保存到我们开始建立的一个 Eclipse 工程 Cayenne-store 下面。这里,需要注意的是,Cayenne 的映射文件 cayenne.xml 必须放到程序运行时的 CLASSPATH 中,所以,我们可以将它保存放到所建立的 Java 工程 src 目录下面。
图 8. 保存 Cayenne 主配置文件
这时,回到 Eclipse 工程里面,就会看到这个应用的主配置文件 cayenne.xml 以及相应的 JDBC Driver 配置文件了。
图 9. 生成 Cayenne.xml
到此,开发环境的搭建就算完成了。下面,我们将通过一个简单的例子带您逐步的了解 Cayenne,看看它如何帮助我们产生数据持久层的 Java 代码以及数据库表。
这个例子很简单,我们以一个购物网站为背景进行数据建模。可以想象,这里我们需要如下四张表,客户信息表 ClientTB,商品信息表 CommodityTB,客户购买商品的订单信息表 OrderTB,还有一个保存订单和订单所包含商品的订单商品表 OrderCommodityTB。它们之间的关系可用如下 E-R 图表示,
图 9. E-R 图表
为了在 Cayenne Modeler 中创建 DBEntity 模型,需要先建立一个 DataMap。在 cayenne-storeNode DataNode 下创建 DataMap。如图 10。在 Java Package 输入框中,你可以定义将要存储持久化代码的包名。其余的,可以暂时留空。
图 10. 建立 DataMap
接下来,基于这张 E-R 图,我们就可以在 Cayenne Modeler 中建立 DBEntity 模型了。建立 DBEntity 模型的过程与在一个数据库管理客户端建立表的过程类似。即,需要指定表的名字、字段及字段大小等信息。对表 ClientDB 和 CommodityTB 等表的建模结果如下图所示,
图 11. 建立 ClientTB 等 DBEntity
这里,需要注意的一点是表间关系的建立。 以 clientTB 和 orderTB 为例,因为 clientTB 和 orderTB 是一对多的关系,所以在 Cayenne Modeler 中需要在 Relationships 面板上建立表间联合关系。
图 12. 建立 ClientTB 和 OrderTB 的表间关系
同时,还需要建立从 orderTB 到 clientTB 的反转关系,即多对一的关系。
图 13. 建立 OrderTB 到 ClientTB 的反转关系
Cayenne modeler 会自动在 OrderTB 的 Relationships 面板中产生一个表间关系
图 14. OrderTB 的 Relationships 面板
|
这里,由于篇幅关系,就不将建立其他表和表间关系的步骤列出来了。读者可以依照上述例子将其他 DBEntity 及关系建立起来或者参考附录中的源代码示例。然后,需要我们做的就是产生 ObjEntity,即数据库表到 Java 对象之间的映射。 也很简单,在每个数据库表的 Entity 面板上点击“同步 DBEntity 到 ObjEntity”按钮即可。只是,要注意在 Relationships 中的 Delete Rule。例如,一旦某个 Client 从表中被删除后,他的相关 Order 记录也需要被级联删除。
图 15. Modeler 产生的 ObjectEnties
到这,建立模型的过程就告一段落了。接下来,我们让 Modeler 来产生 Database Schema 和 Java Classes. 这可以通过在 Tools Menu 下的 Generate Classes 和 Generate Database Schema 两个按钮来完成。
图 16. 生成 Java 代码和 Database Schema
完成之后,你就会发现在所建的 cayenne-store 工程下面生成了 Java 代码。
图 17. Eclipse 中生成的 Java 代码
以及 cayenneStore 数据库文件目录。
图 18. 文件系统中生成的 Derby 数据库
到此,您可能已经品尝到了 Cayenne 所带来的快捷,比如功能丰富的建模工具 Cayenne Modeler,自动生成 Java 代码等。当然,Cayenne Modeler 还有许多功能和特性。由于篇幅关系,我们就不一一介绍了。读者可以查阅本文所附的 “参考资料”继续深入了解和学习。
那么,在拥有了 Cayenne 为我们自动产生的代码之后,我们还需要做哪些工作呢?如何应用这些产生的代码呢?接下来,让我们看看 Cayenne 所提供的主要的 API。
DataContext 类是一个取得 Cayenne 所提供功能的一个用户入口 , 它搭建了一个用户与数据库之间的会话通道,用户通过它来实现与数据库相关的各种操作(如 CRUD)。不同的用户可以建立自己的 DataContext。这里,值得注意的是,DataContext 会主动隔离处在不同 context 中的对象(除非是共享的)。所以,在一个会话中要尽量保持一个 DataContext 实例来操作 DataObjects。
清单 1. 创建一个 DataContext 示例
import org.apache.cayenne.access.DataContext; ... DataContext context = DataContext.createDataContext(); |
用户对数据库表的不同操作都可以看成是查询。这里,查询可以分为选择性和非选择性。前者可以看成是对数据库表的读操作,而后者则可看出是对数据库表的插入、删除、更新操作。在 Cayenne 中,有多种 Query 对象可被用户调用。如最常用的 SelectQuery, SQLTemplate Query, ProcedureQuery。 而构建一个 Query 也比较简单。
清单 2. 创建 Query 示例
import org.apache.cayenne.query.SelectQuery; ... // this is a valid Cayenne query that would allow to fetch // all records from the ClientTB table as ClientTB objects SelectQuery query = new SelectQuery(ClientTB.class); // create a qualifier with one parameter: "clientID" Expression qual = Expression.fromString("clientID = $clientID); // build a query prototype of a query - simply another select query SelectQuery proto = new SelectQuery(ClientTB.class, qual); Context.performQuery(proto); |
当然,也可以在 Cayenne Modeler 中构建 Query。
图 19. 在 Modeler 中创建 Query
然后在代码中调用这个 Query
清单 3. 调用并执行在 Modeler 中创建的 Query
Map param = new HashMap(); param.put("manuName","Nokia Beijing"); // 执行“getCommodities” Query 并不刷新 Cache List context.performQuery(“getCommodities”, param, false); |
一个 ORM 框架 , 它的一端连着 Database, 一端连着 Java 对象。因此,这里的 DataObject 可以理解成一个 DB Record 在内存中的对象的映射。它由属性和对象关系组成。在 ORM 中,对数据库表中一行记录的修改转变为对一个 DataObject 属性或关系的修改。其余的操作,包括数据检查,生成 SQL 语句、事务控制、回滚等交由 ORM 框架来完成。
DataObject 可以通过 Query 执行产生,也可以通过用户自己创建产生。
清单 4. 执行 Query 返回 DataObjects 并修改其属性
List <ClientTB> clients = context.performQuery(“allClients”,true); // 修改 DataObject 的属性 ClientTB clientTB = clients.get(0); clientTB.setClientMail(“cayennedemo@hotmail.com”); // 提交修改 context.commitChanges(); |
|
为了让读者更好的体会 ORM 以及 Cayenne API, 我们来看一个 Demo。在这个 Demo 中,首先会装载一个 SQL 脚本并初始化数据库表中的基础数据。然后查询所有 Client 信息。最后,模拟一个 client 购买两个商品并生成 Order 的过程。另外,考虑到代码的实用性和可读性,我们创建了一些 DAO 类将 ObjEntity 以及数据库操作做进一步的封装。代码的主要文件结构如图 19 所示。读者也可以在源代码中仔细查看它们的类间关系。
图 19. 工程文件结构
由于在生成 ObjEntity 时设置了对象间的 Delete Rule, 所以我们可以直接删除 ClientTB 和 CommodityTB 中的数据即可。OrderTB 和 OrderCommodityTB 中的数据将会被级联删除。
清单 4. 清空数据库表的代码
ClientDao clientDao = (ClientDao)DaoManager.getInstance().getDao(ClientDao.class); CommodityDao comDao = (CommodityDao) DaoManager.getInstance().getDao(CommodityDao.class); CayenneStoreDao csd = (CayenneStoreDao)DaoManager.getInstance() .getDao(CayenneStoreDao.class); List<ClientTB> clients = clientDao.getClients(); for (ClientTB c : clients){ csd.getDataContext().deleteObject(c); } SelectQuery queryCom = new SelectQuery(CommodityTB.class); List<CommodityTB> coms = csd.getDataContext().performQuery(queryCom); for (CommodityTB cd : coms){ csd.getDataContext().deleteObject(cd); } csd.commitChanges(); |
装载原始数据时,我们会通过读取一个 SQL 脚本并生成一个 QueryChain。然后调用 DataContext 执行。
清单 5. 装载数据
QueryChain qc = new QueryChain(); BufferedReader in = new BufferedReader(new InputStreamReaderDBDataHelper.class.getResourceAsStream("/loadData.sql"))); String line ; try { while ((line = in.readLine())!=null){ if (line.endsWith(";")){ line = line.substring(0,line.length()-1); } qc.addQuery(new SQLTemplate(getDataDomain().getMap("cayenne-storeMap"), line)); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } CayenneStoreDao csd = (CayenneStoreDao)DaoManager.getInstance() .getDao(CayenneStoreDao.class); csd.getDataContext().performNonSelectingQuery(qc); |
查询客户信息的例子中,我们首先查询出所有 Client 的信息,包括 ClientID,ClientName,ClientMail。然后查询指定 ClientName 的 Client 其所拥有的订单信息。
清单 6. 查询客户信息
ClientDao clientDao = (ClientDao)DaoManager.getInstance().getDao(ClientDao.class); List<ClientTB> clients = clientDao.getClients(); for (ClientTB ct: clients){ System.out.println("client : "+ct.getObjectId()+ " "+ct.getClientName()+" "+ct.getClientPWD()+" "+ct.getClientMail()); } String clientName = "YingChu"; ClientTB ctt = clientDao.getClient(clientName); System.out.println("client YingChu 's id , pwd, mail "+ctt.getObjectId()+ " "+ctt.getClientPWD()+" "+ctt.getClientMail()); List<OrderTB> orders = ctt.getOrdersOwned(); if (orders.size()==0){ System.out.println("The client "+clientName+" doesn't sign any order"); }else{ for (OrderTB order: orders){ List<OrderCommodityTB> orderedComs = order.getCommoditiesContained(); StringBuffer sb = new StringBuffer(); for (OrderCommodityTB com: orderedComs){ sb.append(com.getCommoditiesBought().getComName()); sb.append(" , "); } if (sb.length()>0) sb.delete(sb.length()-3, sb.length()); System.out.println("The client "+clientName+ " has ordered the following commodities : "+ sb.toString()+" on "+order.getOrderDate().toString()); } } |
在这个例子中,首先指定一个 Client。然后指定购买的商品。之后,就是在 OrderTB 和 OrderCommodityTB 中创建记录的过程。
清单 7. 模拟客户购买商品生成订单的代码
//Assume current client is YingChu ClientDao clientDao = (ClientDao)DaoManager.getInstance().getDao(ClientDao.class); String clientName = "YingChu"; ClientTB ctt = clientDao.getClient(clientName); //Assume two commodities are chosen, two is Nokia Phone n95, one is Thinkpad t61p CommodityDao comDao = (CommodityDao)DaoManager.getInstance().getDao(CommodityDao.class); CommodityTB nokiacom = comDao.getCommodity("Nokia Phone n95"); CommodityTB thinkPad = comDao.getCommodity("ThinkPad T61p"); //Generate an order record CayenneStoreDao csd=(CayenneStoreDao)DaoManager.getInstance() .getDao(CayenneStoreDao.class); OrderTB order = (OrderTB)csd.getDataContext().createAndRegisterNewObject(OrderTB.class); Date dd = Calendar.getInstance().getTime(); order.setOrderDate(dd); //Add the generated order into the buying list of current client order.setClientBelonged(ctt); //Generate two records in OrderCommodityTB OrderCommodityTB OCNokia = (OrderCommodityTB)csd.getDataContext() .createAndRegisterNewObject(OrderCommodityTB.class); OCNokia.setComNumber(1); OrderCommodityTB OCThinkpad = (OrderCommodityTB)csd.getDataContext() .createAndRegisterNewObject(OrderCommodityTB.class); OCThinkpad.setComNumber(1); //Create the relationship between OrderTB and OrderCommodityTB OCNokia.setBelongedToOrders(order); OCThinkpad.setBelongedToOrders(order); //Create the relationship between CommodityTB and OrderCommodityTB OCNokia.setCommoditiesBought(nokiacom); OCThinkpad.setCommoditiesBought(thinkPad); //Update the quantity number in CommodityTB nokiacom.updateCommodityNumber(1); thinkPad.updateCommodityNumber(1); //Commit changes csd.commitChanges(); //Check the result of purchase searchClients (); |
本文主要介绍了 Apache Cayenne 的一些基础特性与应用,包括如何搭建环境、建立模型以及生成 Database Schema 和 Java 代码。最后,本文还通过一个 Demo 介绍了如何使用 Cayenne API 来完成一个简单的数据库操作应用。在第二部分中,我们将主要介绍与 Cayenne 有关的高级特性。包括 Remote Object Persistence 远程访问技术和数据库查询优化等主题。