Cayenne 是一个 Apache 下的持久层框架项目, 作用与 Hibernate / mybatis 之类的持久层框架类似.
- 本文译自 Cayenne 3.1 版的官方文档 (Cayenne Getting Started Guide)
- 译文的目的是为初学者快速了解 Cayenne 减少一些语言障碍. 若要深入研究和使用 Cayenne 还是建议直接阅读官方原版文档.

- Apache Cayenne 官方已发布了 Cayenne 4.0 版本, 为方便小伙伴们学习, 我同样将 4.0 版的 “Cayenne Getting Started Guide” 译成中文了, 若没有特殊原因, 就别看这个教程了, 去看 “Cayenne 起步 (Version 4.0)“吧

- 另附对Cayenne的相关知识阐述更全面的 Cayenne Guide 链接: https://cayenne.apache.org/docs/3.1/cayenne-guide

1. 配置环境

本章的目标是安装(或检查您是否已安装)构建Cayenne应用程序所需的最低软件环境.

1.1 安装Java

显然,JDK 必须安装. Cayenne 3.1需要 JDK 1.5 或 更高版本.

1.2 安装Eclipse IDE 和 Maven插件

译注: Maven是一个项目管理工具, 本教程主要使用它来进行项目所依赖的 jar 的管理(自动下载)

下载Eclipse. 本教程基于Galileo JEE版本的Eclipse(Eclipse 3.5), 但它同样适用于最新的其它Eclipse通用版本. Eclipse下载完后, 解压并运行之.

对本教程而言, 你唯一需要安装的插件是m2eclipse.

选择Eclipse菜单 “Help > Install New Software”, 然后点击 “Add…” 添加一个新的下载站点(download site), 在 “Name” 输入框中输入 “Maven”, “Location”框中输入http://m2eclipse.sonatype.org/sites/m2e.

你可以选择任何你想要的可选组件, 但是对本教程而言, 你只需要选择最少的基本组件即可, 如下图:

按照Eclipse对话框中的提示, 完成安装.

2. 映射(mapping)基础

2.1. 开始一个项目

本章目标是创建一个包含基本Cayenne映射(Cayenne mapping)的Java项目. 其中展示 CayenneModeler 图形化工具的使用,
演示如何创建初始的映射对象: DataDomain, DataNode, DataMap.

译注: CayenneModeler 是 Cayenne的一个图形化建模工具, 使用此工具可比较直观且自动化地创建 Cayenne 持久层对象模型所需各类文件

在Eclipse中创建一个新项目

在Eclipse中选择 “File > New > Other…​”, “Maven > Maven Project”.
点击 “Next”.

在接下来的界面中选中 “Create a simple project” 复选框, 再次点击 “Next”.
对话框中显示如下图所示内容, 填写 “Group Id” 和 “Artifact Id” 并点击 “Finish”.

现在, 在Eclipse的workspace里应该有一个空的项目. 检查一下这个项目的Java编译设置是否正确.
右键单击 “tutorial” 项目, 选择 “Properties > Java Compiler”, 确保 “Compiler compliance level”
至少为 “1.5” (一些版本的Maven插件似乎会默认将其设置为1.4)

下载并运行CayenneModeler

尽管在本教程中稍后我们将使用Maven来导入Cayenne的运行时所需的jar文件到项目中, 你仍然需要下载Cayenne 以便可以使用 CayenneModeler 工具.

如果你直接使用Maven, 你也可以从Maven直接启动CayenneModeler. 这里我们使用更传统的文件来做.

下载最新的发布版. 解压到任意位置, 根据特定操作系统平台的要求启动 CayenneModeler.

在大多数平台下, 只需要简单地双击 Modeler 的图标即可.
Modeler的欢迎界面如下图:

在 CayenneModeler 中创建一个新的映射项目(Mapping Project)

在欢迎界面中点击 New Project 按钮即会出现一个新的 Mapping 项目, 并包含一个DataDomain.

DataDomain 的含意将会在本教程的其他地方解释.
现在你只需要知道 DataDomain 是你的Mapping项目的根.

创建一个DataNode

你需要创建的下一个项目对象是 DataNode.

DataNode 是你应用程序将要连接的单个数据库的描述.
Cayenne 的 mapping 项目可应用于多于一个数据库的情况, 但是现在我们仅使用一个数据库.

选中左侧的 “project”, 点击工具栏上的 Create DataNode 按钮 ( 或者在菜单中选择Project > Create DateNode ), 一个新的DataNode就出现了.

现在你需要指定JDBC连接参数.
如果使用内存型数据库 Derby, 那你可以输入如下的配置:

  • JDBC Driver: org.apache.derby.jdbc.EmbeddedDriver
  • DB URL: jdbc:derby:memory:testdb;create=true

这里我们创建了一个内存型数据库(in-memory database). 因此, 当你停止你的程序时, 所有的数据将会丢失. 在更多实际的项目中, 你应该会连接一个实际将数据存储于磁盘的数据库, 但是对于这个简单的教程而言, 我们将使用内存数据库.

译注: 与传统的数据库(如mysql)不同, 内存型数据库可直接将数据加载到内存中来运行, 可理解为一个直接在内存中运行的关系型数据库. 本教程使用 Derby, 并在 DB URL 处配置 create=true, 这样可根据 CayenneModeler 建立的模型来自动生成数据库.

同时, 你需要更改 “Schema Update Strategy”.

在下拉列框中选择 org.apache.cayenne.access.dbsync.CreateIfNoSchemaStrategy,
这样当程序启动时, Cayenne 将会根据对象关系模型映射(ORM mapping)信息在Derby中创建一个新的数据库模式(Schema).

创建一个DataMap

现在, 你将要创建一个 DataMap.

DataMap 是一个包含了所有映射信息的对象. 点击工具栏上的 “Create DataMap” 按钮 (或选择相应的菜单项).

注意, 新创建的 DataMap 将自动关联到上一步骤中创建的 DataNode. 如果有多于一个DataNode, 你应该手动关联 DataMap 到正确的 DataNode. 也就是说, 在 DataDomain 中的一个 DataMap 必须通过指定关联来指向一个数据库描述.

在DataMap的配置中, 除了 “Java Package”, 你都可以保留DataMap的默认配置.

在 “Java Package” 框中输入 “org.example.cayenne.persistent”. 这个包名将在随后应用于所有的持久层类.

保存项目

在你进行实际的映射配置之前, 让我们先保存一下这个项目.

点击工具栏上的 “Save” 按钮, 并指定保存路径到本章前面创建的名为 “tutorial” 的 Eclipse 项目的文件夹中的子文件夹 “src/main/resources”, 并将项目保存在这里.

现在, 回到 Eclipse, 右键点击 “tutorial” 项目, 并选择 “Refresh(刷新)”, 你将看到3个 Cayenne 的 XML 文件. ( 译注: 原文为3个, 但我只看到2个)

注意, XML文件的存放位置不是随意的. Cayenne 运行时将在应用程序的 CLASSPATH 寻找 cayenne-*.xml 文件.

src/main/resources 文件夹应成为Eclipse中我们项目的”class folder”.
( 如果我们以命令行方式使用Maven, 那上述位置也是 Maven 复制 jar 文件的标准目标位置 )

译注: 按前面一段的步骤保存项目文件到 “src/main/resources” 就行了, 对于 Eclipse 中创建的 Maven 项目 “src/main/resources” 默认就是在CLASSPATH中的. 如果你没有使用 Maven, 可直接保存到 src 根目录即可.

2.2. 对象关系映射入门(ORM)

本节的目标是学会怎么使用 CayenneModeler 来创建一个简单的对象关系模型 ( Object-Relational model, ORM ).

我们将为以下数据库模式创建一个完整的 ORM 模型:

数据库模式

通常情况下, 你已经有创建好了的数据库, 那你可以通过菜单 “Tools > Reengineer Database Schema” 将其快速导入到 Cayenne 中. 相比手工映射, 这将节省你很多时间. 但是, 懂得如何手工创建映射同样重要,
因此, 我们下面将演示手工操作的方法.

映射数据库表和列

让我们回到 CayenneModeler, 打开新我们新创建的项目, 并开始添加 ARTIST 表.

在 Cayenne 映射中, 数据库表被称作 DbEntities ( 可以是实际的表或视图 ).

在左边项目树中选中 “datamap”, 点击工具栏上的 “Create DbEntity” 按钮 ( 或使用菜单 “Project > Create DbEntity” ), 一个新的 DbEntity 即被创建出来了.

在 “DbEntity Name” 字段输入 “ARTIST”. 然后点击实体工具栏(entity toolbar, 译注:就是下图右侧详情区域上方的二级工具栏 ) 上的 “Create Attribute” 按钮切换到 “Attribute” 标签页, 并新增一个名叫 “untitledAttr” 的属性( Attribute, 这里的 attribute 对应一个表中的列 ).

让我们将其重命名为ID, 并设置为 INTEGER 和 ‘PK’ ( 主键 ):

类似地, 增加 NAME VARCHAR(200) 和 DATE_OF_BIRTH DATE 属性.

然后, 重复上述过程, 创建如前面数据库模式图中所示的 PAINTING 和 GALLERY 实体.

不要忘记定期保存你的项目, 以免丢失你所做的工作.
因为 Eclipse 默认情况下并不会自动感知建模工具中所做的修改, 所以, 每次 CayenneModeler 保存后,
你都应该在 Eclipse 中刷新项目.

映射数据库关系

现在我们需要指定 ARTIST, PAINTING 和 GALLERY 表之间的 ( 外键 ) 关系.

首先创建一对多的 ARTIST/PAINTING 关系:

  • 选择左边的 ARTIST DbEntity, 并点击 “Relationship” 标签页
  • 点击实体工具栏上的 “Create Relationship” 按钮 , 一个名为 “untitledRel” 的关系即被创建出来.
  • 选择 “Target” 为 “PAINTING”
  • 点击右侧工具栏上的 “Database Mapping” 按钮, 即会弹出关系配置对话框.
    在这里你可以给关系取个名字, 同样也可以给反向关系取名. 这个名字可以任取 ( 这实际上是数据库参考约束的一个符号名称 ), 但是, 更推荐使用 Java 标识符, 因为稍后这个名字将被以同样的拼写方式保存下来. 我们将这个关系称作”paintings”, 反向
    关系称作 “artist”.
    译注: 这里的关系即 ARTIST 表和 PAINTING 表之间一对多的外键约束, 关系名称即对于一个 ARTIST 来说 PAINTING 表的数据是它的什么, 而反向关系即: 对于一个 PAINTING 来说, ARTIST 是它的什么. 呵呵, 有点绕~ 简单说, 对于一个艺术家( ARTIST ) 而言PAINTING 表中的数据是它的画作, 而对于 PAINTING 表中的一条数据而言, ARTIST 表的对应数据是这幅画的作者
  • 点击右边的 “Add” 按钮
  • “Source” 选择 “ID” 列, “Target” 选择 “ARTIST_ID” 列
  • 关系信息应如下图所示:

  • 点击 “Done” 以确认所做的修改并关闭对话框.

  • 两个关系已经被创建: 从 ARTIST 到 PAINTING 的关系, 以及反向的关系. 不过你可能注意到有件事
    忘记了: “paintings” 关系应该是 to-many, 但是 “To Many” 复选框并没有选中.
    让我们来改一下: 选中 “paintings” 关系的 “To Many” 复选框, 同时, 点击 PAINTING DBEntity, 取消 “artist” 关系的 “To Many” 复选框以设置反向关系, 因为反向的 PAINTING 指向 ARTIST 的关系应该是多对一(to-one).
  • 重复前面的步骤, 以建立从 PAINTING 到 GALLERY 的多对一关系, 让我们将这对关系命名为 “gallery” 和 “paintings”.

映射 Java 类

现在, 数据库模式已经映射完成, CayenneModeler 可以根据DbEntity中的所有内容来创建Java类的映射(又称作 “ObjEntity”).

目前还不能直接通过一次点击就完所有的 DataMap 映射, 因此我们将逐个表来做.

  • 选择 “ARTIST” DbEntity 并点击实体工具栏或主工具栏上的 “Create ObjEntity” 按钮, 一个名为 “Artist” 的 ObjEntity 即被创建出来, 同时 Java class 输入框的值被设置为 “org.example.cayenne.Artist”. 建模工具会将数据库中的名称转换为 Java 风格的名称 ( 例如: 如果你点击 “Attributes” 标签页, 将看到 “DATA_OF_BIRTH” 列被转换为 Java 类属性 “dateOfBirth” ).
  • 选择 “GALLERY” DbEntity 并再次点击 “Create ObjEntity” 按钮, 你将看到一个 “Gallery” ObjEntity 被创建出来.
  • 最后, 为 “PAINTING” 做同样的操作.

现在, 你需要同步关系. 因为在还没有相关联的 “Painting” 实体 ( objEntity ) 之前, Artist 和 Gallery 实体就已经被创建出来了. 因此, 他们之间的关系并未被自动设置.

  • 点击 “Artist” ObjEntity. 点击工具栏上的 “Sync ObjEntity with DbEntity” 按钮, 你会看到出现了 “paintings” 关系
  • 对 “Gallery” 实体做同样的操作.

除非你想要自定义 Java 类和属性名 ( 你可以很容易地做到 ), 映射已经完成了.

2.3. 创建Java类

这里我们将根据前面章节中创建的模型生成 Java 类.

CayenneModeler 同样可用来生成数据库模式, 因为在我们先前创建 DataNode 的时候指定了 “CreateIfNoSchemaStrategy”, 因此我们将跳过创建数据库模式的步骤. ( 译注: 因为设置了CreateIfNoSchemaStrategy策略, 建模工具会自动创建相应的数据库模式 )

如果你有需要, 可通过 “Tools > Create Database Schema” 做到这一点 ( 生成数据库模式 ).

创建 Java 类

  • 选择 “Tools > Generate Classes” 菜单
  • “Type” 选择 “Standard Persistent Objects” ( 如果没有选中的话 )
  • “Output Directory” 选择你项目下的 “src/main/java” 文件夹 ( 这是与之前我们为 cayenne-*.xml 选择的位置同等的位置 )
  • 点击 “Classess” 标签, 选中 “Check All Classes” 复选框
  • 点击 “Generate”

现在回到 Eclipse, 右键点击 “tutorial” 项目选择 “Refresh” - 你应该看到每个被映射的实体生成了两个类.你可能也注意到, 有一堆红色的波浪线在 Eclipse 中新出现的 Java 类旁边.

译注: 应该是红色的小叉叉吧~

这是因为我们的项目还没有将 Cayenne 作为 Maven 的依赖包含进来.
让我们来修复它, 在 pom.xml 文件的最下面插入 “cayenne-server” artifact.
最终的POM应该像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example.cayenne</groupId>
<artifactId>tutorial</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.apache.cayenne</groupId>
<artifactId>cayenne-server</artifactId>
<!-- 在这里指定你实际想使用的 Cayenne 版本. 译注: 建议使用3.1.1这样的RELEASE版本, 而不是SNAPSHOT版本 -->
<version>3.1.3-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

你的电脑必须连上Internet.
一旦你保存了 pom.xml, Eclipse 会自动下载 Cayenne 所需的 jar 文件, 并将其添加到项目构建路径 ( build path ).

最终, 所有的错误就消失了.

现在, 让我们来看一下实体类. 每个实体都有一个父类 ( 如: _Artist ) 和一个子类 ( 如: Artist ).

你不应该修改名称以”_” ( 下划线 ) 开头的父类, 因为他们将会在后续生成器运行的时候被覆盖. 应该把所有的自定义逻辑放在 “org.example.cayenne.persistent” 包的子类中(Artist), 这些子类不会被类生成器覆盖.

类生成提示

通常你会先从 CayenneModeler 生成类, 但是在项目的后期阶段, 通常通过Ant cgen task 或 Maven cgen mojo 自动生成代码. 这三种方法均可, 但 Ant 和 Maven 方法可以确保您不会忘记在映射更改时重新生成类, 因为它们已集成到构建周期中.

3. 学习Cayenne API

3.1. ObjectContext 入门

本节我们将写一个简单的 main 类来运行我们的应用程序, 并对 Cayenne ObjectContext 作简单的介绍.

创建 Main 类

  • Eclipse中, 在 “org.example.cayenne” 包中创建一个新的类, 命名为 “Main”
  • 创建一个标准的 “main” 方法, 以使其成为一个可运行的类:

    1
    2
    3
    4
    5
    6
    7
    package org.example.cayenne;

    public class Main {

    public static void main(String[] args) {
    }
    }
  • 要访问数据库首先要做的是创建一个 ServerRuntime 对象 ( 这实质上是对Cayenne的一个封装 ), 并使用它获得一个 ObjectContext 的实例.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package org.example.cayenne;

    import org.apache.cayenne.ObjectContext;
    import org.apache.cayenne.configuration.server.ServerRuntime;

    public class Main {

    public static void main(String[] args) {
    ServerRuntime cayenneRuntime = new ServerRuntime(
    "cayenne-project.xml");
    ObjectContext context = cayenneRuntime.getContext();
    }
    }

在 Cayenne 中, ObjectContext 是一个独立的 “Session”, 它提供了处理数据所需的所有API.

ObjectContext 拥有执行查询和管理持久层对象的方法. 我们将在后续的章节中讨论它们.

当应用程序中的第一个 ObjectContext 被创建时, Cayenne 将加载 XML 映射文件, 并创建共享的访问接口, 这将可被后续创建的其它 ObjectContext 重用.

运行应用程序

让我们来看一下运行程序时发生了什么.

但是在此之前, 我们需要添加其它的依赖 ( Apache Derby - 我们的嵌入式数据库引擎 ) 到 pom.xml.

下面这段XML代码需要添加到 <dependencies>…​</dependencies> 部分, 在这里我们之前已经添加过 Cayenne 所需的 jar:

1
2
3
4
5
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.8.1.2</version>
</dependency>

现在我们可以运行了.

在 Eclipse 中右键单击 “Main” 类, 选择”Run As > Java Application”.

在控制台你将看到类似如下的输出. 这表示 Cayenne 已经被启动起来了:

1
2
3
4
5
6
INFO: Loading XML configuration resource from file:cayenne-project.xml
INFO: loading user name and password.
INFO: Created connection pool: jdbc:derby:memory:testdb;create=true
Driver class: org.apache.derby.jdbc.EmbeddedDriver
Min. connections in the pool: 1
Max. connections in the pool: 1

如何配置Cayenne的日志

按照日志一章中的介绍, 调整日志记录输出的详尽程度.

译注: 我没找到所谓”日志一章”

3.2. 开始使用持久层对象

本节我们将学习关于持久层对象的知识, 如何定义它们, 如何创建并将其保存到数据库.

检视和定义持久层对象

在 Cayenne 中持久层类实现了一个数据对象 ( DataObject ) 接口.

如果你查看本教程此前生成的任何一个类 ( 如:org.example.cayenne.Artist ), 你会看到它继承了一个名称以下划线开头的类( 如: org.example.cayenne._Artist), 而这个类又继承了 org.apache.cayenne.CayenneDataObject.

将每一个持久层类分解为一个用户自定义子类 ( Xyz ) 和一个自动生成的父类 ( _Xyz ) 是一个很有用的技术, 它将避免在刷新映射模型时覆盖自定义的代码.

让我们来举个例子, 添加一个工具方法到 Artist 类中, 用于设置出生日期. 此方法接收一个字符型的日期参数. 即使后续模型发生变化, 这个方法亦将被保护 ( 避免被建模工具修改 ) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Artist extends _Artist {

static final String DEFAULT_DATE_FORMAT = "yyyyMMdd";

/**
* Sets date of birth using a string in format yyyyMMdd.
*/
public void setDateOfBirthString(String yearMonthDay) {
if (yearMonthDay == null) {
setDateOfBirth(null);
} else {
Date date;
try {
date = new SimpleDateFormat(DEFAULT_DATE_FORMAT)
.parse(yearMonthDay);
} catch (ParseException e) {
throw new IllegalArgumentException(
"A date argument must be in format '"
+ DEFAULT_DATE_FORMAT + "': " + yearMonthDay);
}
setDateOfBirth(date);
}
}
}

创建新对象

现在我们将创建一组对象, 并将其保存到数据库.

使用 ObjectContext 的”newObject”方法可创建并注册一个对象.

对象必须被注册到 DataContext 才能被持久化, 也才能被允许设置与其它对象的关系.

添加如下代码到 Main 类的 “main” 方法中:

1
2
3
Artist picasso = context.newObject(Artist.class);
picasso.setName("Pablo Picasso");
picasso.setDateOfBirthString("18811025");

注意, 此时对象 picasso 仅被存储于内存中, 还未被保存到数据库.

让我们继续添加一个名为 Metropolitan Museum 的 “Gallery” 对象, 和一些毕加索的画作( Paintings ).

1
2
3
4
5
6
7
8
Gallery metropolitan = context.newObject(Gallery.class);
metropolitan.setName("Metropolitan Museum of Art");

Painting girl = context.newObject(Painting.class);
girl.setName("Girl Reading at a Table");

Painting stein = context.newObject(Painting.class);
stein.setName("Gertrude Stein");

现在我们可以把这些对象关联起来, 建立关系.

注意, 在下面的每一个例子里, 双向的关系均被自动建立起来. ( 例如: picasso.addToPaintings(girl) 完全等效于 girl.setToArtist(picasso) ).

1
2
3
4
5
picasso.addToPaintings(girl);
picasso.addToPaintings(stein);

girl.setGallery(metropolitan);
stein.setGallery(metropolitan);

现在, 让我们使用一个方法来同时保存所有的5个对象:

1
context.commitChanges();

现在, 你可以使用前面章节中所述的方法的来再次运行程序.

新的输出将显示一些实际的数据库操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
...
org.apache.cayenne.configuration.XMLDataChannelDescriptorLoader load
INFO: Loading XML configuration resource from file:cayenne-project.xml
...
INFO: Opening connection: jdbc:derby:memory:testdb;create=true
Login: null
Password: *******
INFO: +++ Connecting: SUCCESS.
INFO: Detected and installed adapter: org.apache.cayenne.dba.derby.DerbyAdapter
INFO: --- transaction started.
INFO: No schema detected, will create mapped tables
INFO: CREATE TABLE GALLERY (ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE ARTIST (DATE_OF_BIRTH DATE, ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE PAINTING (ARTIST_ID INTEGER, GALLERY_ID INTEGER, ID INTEGER NOT NULL,
NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (ARTIST_ID) REFERENCES ARTIST (ID)
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (GALLERY_ID) REFERENCES GALLERY (ID)
INFO: CREATE TABLE AUTO_PK_SUPPORT (
TABLE_NAME CHAR(100) NOT NULL, NEXT_ID BIGINT NOT NULL, PRIMARY KEY(TABLE_NAME))
INFO: DELETE FROM AUTO_PK_SUPPORT WHERE TABLE_NAME IN ('ARTIST', 'GALLERY', 'PAINTING')
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('ARTIST', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('GALLERY', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('PAINTING', 200)
INFO: SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = ? FOR UPDATE [bind: 1:'ARTIST']
INFO: SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = ? FOR UPDATE [bind: 1:'GALLERY']
INFO: SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = ? FOR UPDATE [bind: 1:'PAINTING']
INFO: INSERT INTO GALLERY (ID, NAME) VALUES (?, ?)
INFO: [batch bind: 1->ID:200, 2->NAME:'Metropolitan Museum of Art']
INFO: === updated 1 row.
INFO: INSERT INTO ARTIST (DATE_OF_BIRTH, ID, NAME) VALUES (?, ?, ?)
INFO: [batch bind: 1->DATE_OF_BIRTH:'1881-10-25 00:00:00.0', 2->ID:200, 3->NAME:'Pablo Picasso']
INFO: === updated 1 row.
INFO: INSERT INTO PAINTING (ARTIST_ID, GALLERY_ID, ID, NAME) VALUES (?, ?, ?, ?)
INFO: [batch bind: 1->ARTIST_ID:200, 2->GALLERY_ID:200, 3->ID:200, 4->NAME:'Gertrude Stein']
INFO: [batch bind: 1->ARTIST_ID:200, 2->GALLERY_ID:200, 3->ID:201, 4->NAME:'Girl Reading at a Table']
INFO: === updated 2 rows.
INFO: +++ transaction committed.

Cayenne 创建了必要的表 ( 记住, 我们使用了 “CreateIfNoSchemaStrategy”).

然后它运行一些插入, 即时生成了主键.

就这么几行代码就搞成这样还不赖.

3.3. 检索对象

本节演示如何使用 ObjectSelect 来从数据库中检索 ( 查询 ) 对象.

ObjectSelect 介绍

前面已经展示了如何持久化新的对象.

Cayenne 的 query 被用来访问已经保存的对象.

用于检索对象的主要查询类型是 ObjectSelect. 它可以直接在 CayenneModeler 中进行映射, 也可以通过 API 创建. 本节我们将使用后一种方法.

虽然我们还没有太多的数据在数据库中, 但是我们仍可以演示如下的主要方法.

  • 检索所有的画作 ( 代码 及 产生的日志输出 ):
1
2
SelectQuery select1 = new SelectQuery(Painting.class);
List paintings1 = context.performQuery(select1);
1
2
INFO: SELECT t0.GALLERY_ID, t0.ARTIST_ID, t0.NAME, t0.ID FROM PAINTING t0
INFO: === returned 2 rows. - took 18 ms.
  • 检索以 “gi” 开头的画作, 忽略大小写:
1
2
3
4
5
Expression qualifier2 = ExpressionFactory.likeIgnoreCaseExp(
Painting.NAME_PROPERTY,
"gi%");
SelectQuery select2 = new SelectQuery(Painting.class, qualifier2);
List paintings2 = context.performQuery(select2);
1
2
3
INFO: SELECT t0.GALLERY_ID, t0.NAME, t0.ARTIST_ID, t0.ID FROM PAINTING t0 WHERE UPPER(t0.NAME) LIKE UPPER(?)
[bind: 1->NAME:'gi%'] - prepared in 6 ms.
INFO: === returned 1 row. - took 18 ms.
  • 检索所有100年前出生的艺术家的画作 ( 演示使用 Expression.fromString(..) 而不是 ExpressionFactory ):
1
2
3
4
5
6
7
Calendar c = new GregorianCalendar();
c.set(c.get(Calendar.YEAR) - 100, 0, 1, 0, 0, 0);

Expression qualifier3 = Expression.fromString("artist.dateOfBirth < $date");
qualifier3 = qualifier3.expWithParameters(Collections.singletonMap("date", c.getTime()));
SelectQuery select3 = new SelectQuery(Painting.class, qualifier3);
List paintings3 = context.performQuery(select3);
1
2
3
INFO: SELECT t0.GALLERY_ID, t0.NAME, t0.ARTIST_ID, t0.ID FROM PAINTING t0 JOIN ARTIST t1 ON (t0.ARTIST_ID = t1.ID)
WHERE t1.DATE_OF_BIRTH < ? [bind: 1->DATE_OF_BIRTH:'1911-01-01 00:00:00.493'] - prepared in 7 ms.
INFO: === returned 2 rows. - took 25 ms.

3.4. 删除对象

本节解释如何建立关系的删除约束模型, 如何删除单个对象和一组对象.

同时, 也将演示执行查询时 Cayenne 类的使用.

设置删除约束

在我们讨论删除对象的 API 前, 让我们回到 CayenneModeler, 进行一些删除约束的设置.

这样做是可选的 ( 不是必须的 ) , 但它将使得我们可以以简单的方式正确处理与被删除对象相关联的其它对象.

在建模工具中转到 “Artist” ObjEntity 的 “Relationships” 标签页, 为 “paintings” 关系选择 “Cascade” ( 级联 ) 删除约束:

为其它关系重复上述步骤:

  • 为 Gallery 设置 “paintings” 关系为 “Nullify”, 因为可以存在一副画作未在任何画廊展出的情况.
  • 为 Painting 设置其两个关系的删除约束均为 “Nullify”.

现在, 保存映射.

删除对象

虽然可以通过SQL删除对象. 但在Cayenne( 或一般的ORM )中更常用的方法是首先获取对象, 然后通过 context 删除它.

让我们使用 Cayenne 的工具类找到一个艺术家:

1
2
3
Expression qualifier = ExpressionFactory.matchExp(Artist.NAME_PROPERTY, "Pablo Picasso");
SelectQuery select = new SelectQuery(Artist.class, qualifier);
Artist picasso = (Artist) Cayenne.objectForQuery(context, select);

现在, 让我们删除这个艺术家:

1
2
3
4
if (picasso != null) {
context.deleteObject(picasso);
context.commitChanges();
}

因为我们为 Artist.paintings 关系设置了 “Cascade” 删除约束, Cayenne会自动删除这个艺术家所有的画作.

因此, 当我们运行这个程序时, 你将会看到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
INFO: SELECT t0.DATE_OF_BIRTH, t0.NAME, t0.ID FROM ARTIST t0
WHERE t0.NAME = ? [bind: 1->NAME:'Pablo Picasso'] - prepared in 6 ms.
INFO: === returned 1 row. - took 18 ms.
INFO: +++ transaction committed.
INFO: --- transaction started.
INFO: DELETE FROM PAINTING WHERE ID = ?
INFO: [batch bind: 1->ID:200]
INFO: [batch bind: 1->ID:201]
INFO: === updated 2 rows.
INFO: DELETE FROM ARTIST WHERE ID = ?
INFO: [batch bind: 1->ID:200]
INFO: === updated 1 row.
INFO: +++ transaction committed.

4. 转换为Web应用程序

本章将展示 Cayenne 如何在Web应用程序中工作

4.1. 将 tutorial 项目转换为 Web应用程序

Web应用程序教程的 Web 部分是在 JSP 中完成的, 而 JSP 是 Java Web 技术中最常见的实现方法.

本教程在 UI 方面尽可能地简单, 主要专注于 Cayenne 集成, 而不是界面.

一个典型的 Cayenne Web 应用程序像下面这样工作:

  • 在应用程序上下文启动时, 使用一个特定的 Servlet 过滤器加载 Cayenne 的配置
  • 用户请求被过滤器拦截, 并将 DataContext 绑定到请求线程, 因此应用程序可以从任何地方轻松访问它.
  • 同一个 DataContext 实例在单个用户会话 ( Session ) 中被重用; 不同的会话使用不同的 DataContexts ( 以及不同的对象集). 根据应用的具体情况, 上下文可以有不同的范围. 本教程中我们将使用会话范围的上下文 ( Context ).

让我们将我们此前创建的 tutorial 项目转换为一个 Web 应用程序:

  • 在Eclipse中的 “tutorial” 项目下创建一个新的文件夹 “src/main/webapp/WEB-INF”.
  • WEB-INF 下创建一个新文件 web.xml ( 一个标准的Web应用程序描述文件 ):

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Cayenne Tutorial</display-name>

<!-- This filter bootstraps ServerRuntime and then provides each request thread
with a session-bound DataContext. Note that the name of the filter is important,
as it points it to the right named configuration file.
-->
<filter>
<filter-name>cayenne-project</filter-name>
<filter-class>org.apache.cayenne.configuration.web.CayenneFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cayenne-project</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

  • 创建一个艺术家浏览页面 src/main/webapp/index.jsp, 包含如下内容:

webapp/index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<%@ page language="java" contentType="text/html" %>
<%@ page import="org.example.cayenne.persistent.*" %>
<%@ page import="org.apache.cayenne.*" %>
<%@ page import="org.apache.cayenne.query.*" %>
<%@ page import="org.apache.cayenne.exp.*" %>
<%@ page import="java.util.*" %>

<%
SelectQuery query = new SelectQuery(Artist.class);
query.addOrdering(Artist.NAME_PROPERTY, SortOrder.ASCENDING);

ObjectContext context = BaseContext.getThreadObjectContext();
List<Artist> artists = context.performQuery(query);
%>
<html>
<head>
<title>Main</title>
</head>
<body>
<h2>Artists:</h2>

<% if(artists.isEmpty()) {%>
<p>No artists found</p>
<% } else {
for(Artist a : artists) {
%>
<p><a href="detail.jsp?id=<%=Cayenne.intPKForObject(a)%>"> <%=a.getName()%> </a></p>
<%
}
} %>
<hr>
<p><a href="detail.jsp">Create new artist...</a></p>
</body>
</html>

  • 创建一个艺术家编辑页面 src/main/webapp/detail.jsp, 包含如下内容:

webapp/detail.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<%@ page language="java" contentType="text/html" %>
<%@ page import="org.example.cayenne.persistent.*" %>
<%@ page import="org.apache.cayenne.*" %>
<%@ page import="java.util.*" %>
<%@ page import="java.text.*" %>

<%
ObjectContext context = BaseContext.getThreadObjectContext();
String id = request.getParameter("id");

// find artist for id
Artist artist = null;
if(id != null && id.trim().length() > 0) {
artist = Cayenne.objectForPK(context, Artist.class, Integer.parseInt(id));
}

if("POST".equals(request.getMethod())) {
// if no id is saved in the hidden field, we are dealing with
// create new artist request
if(artist == null) {
artist = context.newObject(Artist.class);
}

// note that in a real application we would so dome validation ...
// here we just hope the input is correct
artist.setName(request.getParameter("name"));
artist.setDateOfBirthString(request.getParameter("dateOfBirth"));

context.commitChanges();

response.sendRedirect("index.jsp");
}

if(artist == null) {
// create transient artist for the form response rendering
artist = new Artist();
}

String name = artist.getName() == null ? "" : artist.getName();
String dob = artist.getDateOfBirth() == null
? "" : new SimpleDateFormat("yyyyMMdd").format(artist.getDateOfBirth());
%>
<html>
<head>
<title>Artist Details</title>
</head>
<body>
<h2>Artists Details</h2>
<form name="EditArtist" action="detail.jsp" method="POST">
<input type="hidden" name="id" value="<%= id != null ? id : "" %>" />
<table border="0">
<tr>
<td>Name:</td>
<td><input type="text" name="name" value="<%= name %>"/></td>
</tr>
<tr>
<td>Date of Birth (yyyyMMdd):</td>
<td><input type="text" name="dateOfBirth" value="<%= dob %>"/></td>
</tr>
<tr>
<td></td>
<td align="right"><input type="submit" value="Save" /></td>
</tr>
</table>
</form>
</body>
</html>

运行Web应用程序

为了运行这个Web应用程序, 我们将使用 “maven-jetty-plugin”.

译注: jetty 是一个 Web 应用程序容器, 作用类似 Tomcat.

为了激活它, 让我们添加如下的代码到 “pom.xml” 中, 跟在 “dependencies” 部分的后面, 保存POM.

pom.xml

1
2
3
4
5
6
7
8
9
<build>
<plugins>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.3.14.v20161028</version>
</plugin>
</plugins>
</build>

  • 打开 “Run > Run Configurations…​” 菜单, 选择 “Maven Build”, 点击右键并选择 “New”
  • 确定你填写了 “Name”, “Base directory” 和 “Goals”, 如下图:

  • 依次点击 “Apply” 和 “Run”.

首次运行时可能会花费几分钟下载Jetty插件所有的依赖,
但是最终你将看到如下的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building tutorial 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
...
[INFO] Configuring Jetty for project: tutorial
[INFO] Webapp source directory = /.../tutorial/src/main/webapp
[INFO] Reload Mechanic: automatic
[INFO] Classes = /.../tutorial/target/classes
[INFO] Context path = /tutorial
[INFO] Tmp directory = determined at runtime
[INFO] Web defaults = org/mortbay/jetty/webapp/webdefault.xml
[INFO] Web overrides = none
[INFO] web.xml file = /.../tutorial/src/main/webapp/WEB-INF/web.xml
[INFO] Webapp directory = /.../tutorial/src/main/webapp
[INFO] Starting jetty 6.1.22 ...
INFO::jetty-6.1.22
INFO::No Transaction manager found - if your webapp requires one, please configure one.
INFO::Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server

  • 至此, Jetty 容器已经启动了.
  • 现在, 在浏览器中打开网址 http://localhost:8080/tutorial/.

    你应该在浏览器中看到 “No artists found message”, 同时在 Eclipse 的控制台中可看到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
INFO: Loading XML configuration resource from file:/.../tutorial/target/classes/cayenne-project.xml
INFO: loading user name and password.
INFO: Created connection pool: jdbc:derby:memory:testdb;create=true
Driver class: org.apache.derby.jdbc.EmbeddedDriver
Min. connections in the pool: 1
Max. connections in the pool: 1
INFO: Opening connection: jdbc:derby:memory:testdb;create=true
Login: null
Password: *******
INFO: +++ Connecting: SUCCESS.
INFO: Detected and installed adapter: org.apache.cayenne.dba.derby.DerbyAdapter
INFO: --- transaction started.
INFO: No schema detected, will create mapped tables
INFO: CREATE TABLE GALLERY (ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE ARTIST (DATE_OF_BIRTH DATE, ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE PAINTING (ARTIST_ID INTEGER, GALLERY_ID INTEGER, ID INTEGER NOT NULL,
NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (ARTIST_ID) REFERENCES ARTIST (ID)
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (GALLERY_ID) REFERENCES GALLERY (ID)
INFO: CREATE TABLE AUTO_PK_SUPPORT (
TABLE_NAME CHAR(100) NOT NULL, NEXT_ID BIGINT NOT NULL, PRIMARY KEY(TABLE_NAME))
INFO: DELETE FROM AUTO_PK_SUPPORT WHERE TABLE_NAME IN ('ARTIST', 'GALLERY', 'PAINTING')
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('ARTIST', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('GALLERY', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('PAINTING', 200)
INFO: SELECT t0.DATE_OF_BIRTH, t0.NAME, t0.ID FROM ARTIST t0 ORDER BY t0.NAME - prepared in 43 ms.
INFO: === returned 0 rows. - took 56 ms.
INFO: +++ transaction committed.
  • 你可以点击 “Create new artist” 链接去新建艺术家. 对于已存在的艺术家的可以通过点击他的名字来进行编辑.

你已完成了本教程!

注: 目前 Apache 已发布了 Cayenne 的 4.0 版本, 相对而言 4.0 版本更好用些, 当然使用方法上也有些变化.

请参阅: Cayenne 起步 (Version 4.0).