本教程出自于白师傅
Cayenne 是一个 Apache 下的持久层框架项目, 作用与 Hibernate / mybatis 之类的持久层框架类似.
- 本文译自 Cayenne 4.0 版的官方文档 (Cayenne Getting Started Guide)
- 译文的目的是为初学者快速了解 Cayenne 减少一些语言障碍. 若要深入研究和使用 Cayenne 建议至官方网站查阅更多相关文档.

1. 设置环境

本章的目标是安装(或检查你是否已经安装)一个用于创建 Cayenne 应用程序的基本软件环境.

1.1 安装 Java

显然, JDK 必须安装. Cayenne 4.0需要 JDK1.7 或更新版本.

1.2 安装 IntelliJ IDEA

下载并安装IntelliJ IDEA Community Edition. 本教程基于版本2016.3, 仍然适用于任何最新的IntelliJ IDEA版本.

2. 学习映射基础

2.1 开始一个项目

本章的目标是在 IntelliJ IDEA 中创建一个包含基本 Cayenne 映射的新 Java 项目, 介绍 CayenneModeler 图形化工具, 展示了如何创建初始映射对象:DataDomainDataNodeDataMap.

在 IntelliJ IDEA 中创建一个新项目

在 IntelliJ IDEA 中选择File > New > Project…, 然后选择Maven并点击Next. 在如下截图所示的对话框中填写Group IdArtifact Id, 点击Next.

在下一个对话框界面中你可以自定义项目存储路径并点击Finish. 现在你应该有了一个新的空项目.

下载并启动 CayenneModeler

虽然稍后我们将使用 Maven 引入 Cayenne 运行时所需的 jar 文件到项目中, 这里你仍然需要下载 Cayenne 以使用 CayenneModeler 工具.

如果你已在使用 Maven, 你也可以从 Maven 启动 CayenneModeler. 这里我们将使用较传统的方式.

下载 最新版 CayenneModeler. 将分发包解压缩到文件系统中的某个位置, 然后按照特定于平台的说明启动 CayenneModeler. 在大多数平台上, 只需双击 CayenneModeler 图标即可启动. CaynenneModeler 的欢迎屏幕如下所示:

在 CayenneModeler 中创建新的映射项目

单击欢迎屏幕上的 New Project按钮将出现一个包含单个DataDomain的新映射项目. DataDomain的含义将在用户指南的其他地方进行说明. 现在, 明白 DataDomain 是映射项目的根就足够了.

创建 DataNode

下一个你需要创建的项目对象是DataNode. DataNode 是应用程序将连接到的单个数据库的描述. Cayenne映射项目可以连接多个数据库, 但现在, 我们只连接一个.

选择左侧的 “project”, 单击工具栏上的Create DataNode按钮图标 (或从菜单中选择Project > Create DataNode), 一个新的 DataNode 就出现了. 现在你需要设置 JDBC 连接参数. 对于内存型数据库 Derby 你可以输入如下配置:

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

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

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

此外, 你需要改变 “Schema Update Strategy”.

从下拉菜列表中选择org.apache.cayenne.access.dbsync.CreateIfNoSchemaStrategy, 这样 Cayenne 将在应用程序启动时根据 ORM 映射在 Derby 中创建一个新的数据库.

译注: ORM - Object Relational Mapping, 对象关系模型

创建 DataMap

现在你需要创建一个 DataMap. DataMap 是一个包含了所有映射信息的对象. 点击 “Create DataMap” 按钮 (或选择相应的菜单项). 注意, 新创建的 DataMap 将自动关联到你上一步创建的 DataNode. 若有多个 DataNode, 你可能需要手动选择一个合适的 DataNode. 换句话说, DataDomain 中的一个 DataMap 必须通过映射指向一个数据库描述.

除了其中的 “Java Package”, 你可以保留 DataMap 中的默认值.

在 “Java Package” 处输入org.example.cayenne.persistent`. 这个名称将应用于所有的持久层类.

保存项目

在处理实际的映射前, 让我们来保存项目. 点击工具栏上的 “Save” 按钮, 指定存储路径到本节此前创建的tutorial IDEA 项目的src/main/resources子目录, 将项目保存在这里. 现在返回到 IDEA, 你将看到 2 个 Cayenne 的 XML 文件.

注意: XML 文件的存储路径并非随意. Cayenne 运行时将在应用程序的CLASSPATH中寻找cayenne-*.xml文件. 并且src/main/resources文件夹应是我们项目的 “类文件夹” (如果我们通过命令使用 Maven, 这里同样是 Maven 复制 jar 文件的位置).

2.2 开始对象关系映射 (ORM)

本节的目标是学习如何使用 CayenneModeler 创建一个简单的对象关系模型. 我们将为如下的数据库模型创建一个完整的 ORM 模型.

在多数情况下, 你已经有一个数据库了. 此时可通过 “Tool > Reengineer Database Schema” 来快速导入. 相对于手工映射, 这将节省你大量时间. 尽管如此, 明白如何手工创建映射也同样重要. 因此, 我们下面将演示”手工”方法.

译注: 关于如何使用 CayenneModel 逆向工程创建映射, 即上述的 “Reengineer Database Schema”, 可参看本博客的另一篇文章 “CayenneModeler 数据库逆向工程”

映射数据库表和列

让我们回到 CayenneModeler, 在那里打开刚才新创建的项目, 然后添加 ARTIST 表。 数据库表在 Cayenne 映射中被称作 DbEntities(数据库实体, 可以是实际表或数据库视图).

在左侧的项目树中选择 “datamap”, 点击 “Create DbEntity” 按钮 (或使用 “Project > Create DbEntity” 菜单). 一个新的 DbEntity 即创建出来了. 在 “DbEntity Name” 字段中填入 “ARTIST”. 然后点击实体工具栏 (entity toolbar) 上的 “Create Attribute” 按钮, 将切换至 “Properties” 标签页并添加一个名为 “unitiledAttr” 的新属性 (这里属性即为数据库中表的列). 让我们将其重命名为 ID, 设置类型为INTEGER并标记为 PK (主键):

同样地, 添加 NAME VARCHAR(200) 和 DATA_OF_BIRTH DATE 属性. 重复以上步骤, 创建 PAINTING 和 CALLERY 实体, 以和本节开始部分的对象关系模型图一致.

映射数据库关系

现在你需要指定 ARTIST, PAINTING, GALLERY 表之间的关系 (Relationship). 开始创建 ARTIST/PAINTING 表之间的一对多关系:

  • 选择左边的 ARTIST DbEntity, 点击 “Properties” 标签页.

  • 点击实体工具栏上的 “Create Relationship” 按钮, 将创建出一个名为 “untitledRel” 的关系.

  • 选择 “Target” 为 “Painting”

  • 点击 “Database Mapping” 按钮, 出现关系配置对话框. 在这里, 你可以为关系指定名称, 也可以指定其反向关系. 名称可以任意命名 (这其实是数据外键约束的名称), 但建议使用有效的 Java 标识符, 因为这将避免在以后的操作中重复输入. 我们将这个关系命名为 “paintings”, 而反向关系命名为 “artist”

  • 点击右侧的 “Add” 按钮添加一个 Join

  • “Source” 选择 “ID” 列, “target” 选择 “ARTIST_ID” 列.

  • 现在, 关系信息将如下图所示:

  • 点击 “Done” 按钮以确认更改并关闭对话框.

  • 现在已经创建了两个关系: ARTIST -> PAINTING 及其反向关系 ( PAINTING -> ARTIST) . 你可能注意到 “paintings” 关系应该是 “to-many”, 但是 “To Many” 复选框未被选中. 让我们来调整一下: 选中 “paintings” 关系, 然后点击 PAINTING DbEntity, 取消选中 “artist” 关系的 “To Many” 使得反向关系变成 “to-one”.

  • 重复以上步骤创建 PAINTING 到 GALLERY 的 many-to-one 关系, 把这两个关系分别命名为 “gallery” 和 “paintings”.

映射 Java 类

现在数据库模式的映射已经完成. CayenneModeler 可以根据 DbEntities 来生成所有的 Java 类 (也称作 “ObjEntities”, 对象实体). 目前, 无法通过一次点击就生成整个 DataMap 的 Java 类. 因此, 我们将分别对每个表执行此操作.

  • 选择 “ARTIST” DbEnity, 点击实体工具栏或主工具栏上的 “Create ObjEntity” 按钮. 一个名叫 “Artist” 的 ObjEntity 即被创建出来, 其 Java class 字段将设置为 “org.example.cayenne.persistent.Artist”. CayenneModeler 会将数据库中的名称转换为 Java 友好的名称 (例如, 如果单击 “属性” 选项卡, 你将看到 “DATE_OF_BIRTH” 列已转换为 “dateOfBirth” Java 类属性).
  • 选择 “GALLERY” DbEntity 并再次点击 “Create ObjEntity” 按钮, 你将看到 “Gallery” ObjEntity 被创建出来了.
  • 最后, 针对 “PAINTING” 执行相似的操作.

现在, 你需要同步关系. 在还没有相关的 “Painting” 实体之前, Artist 和 Gallery 实体就已经被创建了, 所以他们之间的关系还没有设置.

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

除非你想要自定义 Java 类和属性名称 (你可以轻松搞定), 否则映射操作已经完成了.

2.3 创建 Java 类

这里我们将从上一节中创建的模型生成 Java 类. CayenneModeler 也可用于生成数据库模式, 但由于我们在创建 DataNode 之前指定了 “CreateIfNoSchemaStrategy”,因此我们将跳过创建数据库模式步骤. 若有需要, 可通过 “Tools >Create Database Schema” 来实现.

创建 Java 类

  • 选择 “Tools > Generate Classes” 菜单
  • “Type” 选中 “Standard Persistent Objects” (若未选中)
  • “Output Directory” 选择你 IDEA 项目下的 “src/main/java” 文件夹 ( 这和我们此前选择的 cayenne-*.xml位置相关).
  • 点击 “Classes” 选项卡, 选中 “Check All Classes” 复选框 ( 若已选中, 此复选框提示将变为 “Uncheck all Classes”).
  • 点击 “Generate”.

现在回到 IDEA, 你应该看到每一个被映射的实体生成了一对类文件. 你可能还会看到 IDEA 中新生成的 Java 类旁边有一堆红色的波浪线. 这是因为我们的项目还没有将 Cayenne 加到 Maven 依赖. 让我们来将 “cayenne-server” 和 “cayenne-java8” artifacts 添加到 pom.xml文件的最后. 同样, 我们应该告诉 Maven 编译插件, 我们的项目需要 Java 8. 修改后的 POM 应像下面的样子:

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
<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>
<properties>
<cayenne.version>4.0.1</cayenne.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.apache.cayenne</groupId>
<artifactId>cayenne-server</artifactId>
<version>${cayenne.version}</version>
</dependency>
<!-- For java.time.* types you need to use this dependency-->
<dependency>
<groupId>org.apache.cayenne</groupId>
<artifactId>cayenne-java8</artifactId>
<version>${cayenne.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
</project>

(1) 第 8 行: 指定你实际使用的 Cayenne 版本号

(2) 第 9 行: 告诉 Maven 支持 Java 8

你的计算机必须连接到 Internet. 一旦你编辑了pom.xml, IDEA 将下载所需的 Cayenne jar 文件并将其添加到项目构建路径. 此时, 所有的错误将消失了.

在 “控制台输出” 教程中, 我们使用 slf4j-simple 这个 logger 实现. 由于 Apache Cayenne 使用 SLF4J logger, 你可以通过桥接的方式使用你自己的 logger (例如: log4j 或 commons-logging).

译注: SLF4J 可理解为抽象的日志工具 ( logger ), 相对而言, slf4j-simple, log4j, commons-logging 等是更具体日志系统实现. 因 Cayenne 使用 SLF4J API 来输出日志, 所以在实际项目中你可以通过配置灵活地桥接不同的具体日志工具 (如: log4j) 来输出 Cayenne 日志.

现在让我们来检视一下实体类. 每一个实体生成了一个父类 (superclass, 如: _Artist) 和一个子类 (subclass, 如: Artist).

不应该编辑那些以 “_” (下划线) 开头的父类, 因为它们将在后续重新运行生成命令时被覆盖.

所有的自定义逻辑应被放在org.example.cayenne.persistent包里的子类中 - 它们在生成类文件时不会被覆盖.

类生成提示:

通常你会从 CayenneModeler 开始生成类文件. 但是在后续的项目开发阶段, 通常会由 Ant cgen task 或 Maven cgen mojo 自动完成. 所有的这些方法都是可互相替代的, 但是使用 Ant 和 Maven 的方法可确保你不会忘记在映射发生变化时重新生成类文件, 因为他们被集成在了 build 阶段

3. 学习 Cayenne API

3.1 从 ObjectContext 开始

本节我们将写一个简单的 Main 类, 运行我们的程序, 以简要介绍 Cayenne ObjectContext.

创建 Main 类

  • 在 IDEA 中创建一个名为 “Main” 的类, 将其置于 “org.example.cayenne” 包.
  • 创建一个标准的 “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
14
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 = ServerRuntime.builder()
.addConfig("cayenne-project.xml")
.build();
ObjectContext context = cayenneRuntime.newContext();
}
}

在 Cayenne 中ObjectContext是一个独立的 “会话 (session)”, 它提供了所有和数据打交道的 API.

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

当程序中的第一个 ObjectContext 被创建时, Cayenne 将加载 XML 映射文件并创建一个共享的访问栈, 它将在此后被其他 ObjectContext 重用.

运行程序

让我们看看运行程序时会发生什么. 但在此之前我们需要添加其它的依赖到pom.xml: Apache Derby, 我们的嵌入式数据库引擎. 把下面的 XML 片段添加到<dependencies>…</dependencies>部分 ( 那里已经有我们此前针对 Cayenne 的配置了 ).

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

现在我们已经可以运行程序了. 在 IDEA 中右键点击 “Main” 类, 选择 “Run ‘Main.main()’”.

在控制台中你将看到类似现在这样的输出, 这意味着 Cayenne 已经被启动了.

1
2
3
4
5
6
INFO: Loading XML configuration resource from file:/.../cayenne-project.xml
INFO: Loading XML DataMap resource from file:/.../datamap.map.xml
INFO: loading user name and password.
INFO: Connecting to 'jdbc:derby:memory:testdb;create=true' as 'null'
INFO: +++ Connecting: SUCCESS.
INFO: setting DataNode 'datanode' as default, used by all unlinked DataMaps</screen>

如何配置 Cayenne 日志

按照 “日志” 一章中的说明调整日志的输出的详细程度.

译注: 这里所说的 “日志”一章并不在此教程中.

3.2 开始使用持久层对象

本章我们将学习持久层对象, 如何自定义他们, 以及如何创建并将他们保存到数据库.

检视和自定义持久层对象

Cayenne 中的持久层类实现了 DataObject 接口. 如果你检视本教程前面生成的任何类 (如: org.example.cayenne.persistent.Artist), 你将看到它继承了一个名称以下划线开头的类 (org.example.cayenne.persistent.auto._Artist), 而_Artist 又继承了 org.apache.cayenne.CayenneDataObject.

将每个持久化类拆分为用户可自定义的子类(Xyz)和 自动生成的父类(_Xyz)是一种很有用的技术, 可以避免在从建模工具中刷新类文件时覆盖自定义代码.

例如, 让我们在 Artist 类中添加一个实用的方法, 用于设置 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
25
26
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 {

LocalDate date;
try {
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern(DEFAULT_DATE_FORMAT);
date = LocalDate.parse(yearMonthDay, formatter);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(
"A date argument must be in format '"
+ DEFAULT_DATE_FORMAT + "': " + yearMonthDay);
}
setDateOfBirth(date);
}
}
}

创建新对象

现在我们将创建一些对象并将它们保存到数据库中. 使用 “newObject” 方法创建一个对象并将其注册到ObjectContext.

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

将此代码添加到Main 类的 “main” 方法:

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

请注意,此时 “picasso” 对象仅存储在内存中, 还未保存在数据库中. 让我们继续添加大都会博物馆 (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);

现在, 让我们调用 1 个方法来同时保存 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
...
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”). 然后, 它执行了许多 insert 语句, 即时生成了主键. 仅仅几行代码就完成了这些操作, 还不赖.

3.3 检索对象

本节展示如何使用ObjectSelect从数据库中检索对象.

ObjectSelect 介绍

前面已经展示了如何持久化新对象. Cayenne 查询用来访问已存储的对象. 用于检索对象的主要查询类是ObjectSelect.它可以使用 CayenneModeler 进行映射或使用 API 创建. 本节我们将使用后一种方法. 虽然数据库中没有太多数据, 但我们仍然可以演示主要的方法, 如下:

  • 检索所有的画作 (paintings), 代码及输出日志如下:

    1
    List<Painting> paintings1 = ObjectSelect.query(Painting.class).select(context);
    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
    List<Painting> paintings2 = ObjectSelect.query(Painting.class)
    .where(Painting.NAME.likeIgnoreCase("gi%")).select(context);
    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 年前的画家的作品:

    1
    2
    3
    List<Painting> paintings3 = ObjectSelect.query(Painting.class)
    .where(Painting.ARTIST.dot(Artist.DATE_OF_BIRTH).lt(LocalDate.of(1900,1,1)))
    .select(context);
    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.3 删除对象

本节介绍如何建模关系删除规则, 以及如何删除单个对象和对象集. 还将演示使用 Cayenne 类来执行查询.

设置删除规则

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

这样做是可选 (并非必须) 的, 但可以简化已删除对象相关联的对象的处理.

在 CayenneModeler 中转到 “Artist” ObjEntity, “Relationships” 选项卡, 为 “paintings” 关系选择 “Cascade” 删除规则:

重复这个步骤设置其它关系:

  • 针对 Gallery, 设置 “paintings” 关系为 “Nullify”, 因为可存在未在任何画廊展出的画作.
  • 对于 Painting, 设置其两个关系规则均为 “Nullify”.

现在, 保存映射文件.

删除对象

虽然可限定一个或多个 ID, 通过 SQL 删除对象. 但在 Cayenne (或一般的ORM) 中, 更常见的做法是先获取对象, 然后通过 context 删除它. 让我们使用工具类找到一位艺术家:

1
2
Artist picasso = ObjectSelect.query(Artist.class)
.where(Artist.NAME.eq("Pablo Picasso")).selectOne(context);

现在删除这个艺术家:

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应用程序

本章介绍如何在Web应用程序中使用Cayenne.

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

此部分教程的 Web 部分是在 JSP 中完成的, 而 JSP 是 Java Web 技术中最常见的实现方法. 本教程在 UI 方面尽可能地简单, 主要专注于 Cayenne 集成, 而不是界面.

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

  • 在应用程序上下文启动时, 使用一个特定的 Servlet 过滤器加载 Cayenne 的配置

  • 用户请求被过滤器拦截, 并将 DataContext 绑定到请求线程, 因此应用程序可以从任何地方轻松访问它.

  • 同一个 DataContext 实例在单个用户会话 ( Session ) 中被重用; 不同的会话使用不同的 DataContexts ( 以及不同的对象集).

    根据具体情况, 可以使用不同作用域的上下文 (Context). 本教程中我们将使用会话作用域上下文 ( session-scoped context) .

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

  • 在 IDEA 中的 “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.*" %>

<%
ObjectContext context = BaseContext.getThreadObjectContext();
List<Artist> artists = ObjectSelect.query(Artist.class)
.orderBy(Artist.NAME.asc())
.select(context);
%>

<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
68
69
70
71
<%@ 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="java.util.*" %>
<%@ page import="java.text.*" %>
<%@ page import="java.time.format.DateTimeFormatter" %>

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

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

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();

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
String dob = artist.getDateOfBirth() == null
? "" :artist.getDateOfBirth().format(formatter);
%>
<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 应用程序

我们需要为应用程序提供 javax servlet-api.

pom.xml

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>

为运行这个 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>
  • 转到 “Select Run/Debug Configuration” 菜单, 然后 “Edit Configuration…”

idea edit configurations

  • 点击 + 按钮并选择 “Maven”. 按如下截图输入 “Name” 和 “Command line”:

idea run configuration

  • 点击 “Apply” 和 “Run”. 在首次运行时, 可能需要花费几分钟为 Jetty 插件下载所有的依赖, 最终你将看到如下日志输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    [INFO] ------------------------------------------------------------------------
    [INFO] Building tutorial 0.0.1-SNAPSHOT
    [INFO] ------------------------------------------------------------------------
    ...
    [INFO] Configuring Jetty for project: tutorial
    [INFO] webAppSourceDirectory not set. Trying src/main/webapp
    [INFO] Reload Mechanic: automatic
    [INFO] Classes = /.../tutorial/target/classes
    [INFO] Logging initialized @1617ms
    [INFO] Context path = /
    [INFO] Tmp directory = /.../tutorial/target/tmp
    [INFO] Web defaults = org/eclipse/jetty/webapp/webdefault.xml
    [INFO] Web overrides = none
    [INFO] web.xml file = file:/.../tutorial/src/main/webapp/WEB-INF/web.xml
    [INFO] Webapp directory = /.../tutorial/src/main/webapp
    [INFO] jetty-9.3.0.v20150612
    [INFO] Started o.e.j.m.p.JettyWebAppContext@6872f9c8{/,file:/.../tutorial/src/main/webapp/,AVAILABLE}{file:/.../tutorial/src/main/webapp/}
    [INFO] Started ServerConnector@723875bc{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
    [INFO] Started @2367ms
    [INFO] Started Jetty Server</screen>
  • 这样 Jetty 容器就成功启动了.

  • 现在, 打开 http://localhost:8080/ . 你应该在浏览器中看到 “No artists found message” 页面, 并在 IDEA 的控制台中看到如下输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    INFO: Loading XML configuration resource from file:/.../tutorial/target/classes/cayenne-project.xml
    INFO: loading user name and password.
    INFO: Connecting to 'jdbc:derby:memory:testdb;create=true' as 'null'
    INFO: +++ Connecting: SUCCESS.
    INFO: setting DataNode 'datanode' as default, used by all unlinked DataMaps
    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: SELECT t0.DATE_OF_BIRTH, t0.NAME, t0.ID FROM ARTIST t0 ORDER BY t0.NAME
    INFO: === returned 0 rows. - took 17 ms.
    INFO: +++ transaction committed.</screen>
  • 你可以点击 “Create new artist” 链接以来创建一个艺术家. 点击已存在的艺术家的名字可以编辑他:

chrome webapp

你已完成本教程!