Hibernate-和-MongoDB-高级教程-全-

Hibernate 和 MongoDB 高级教程(全)

原文:Pro Hibernate and MongoDB

协议:CC BY-NC-SA 4.0

零、简介

这本书涵盖了开发 Hibernate OGM-MongoDB 应用的所有重要方面。它为充分利用 Hibernate OGM-MongoDB duo 提供了清晰的说明,并提供了许多通过 Hibernate Native API 和 Java Persistence API 集成 Hibernate OGM 的例子。您将学习如何为最流行的 web 和企业服务器开发桌面、web 和企业应用,例如 Tomcat、JBoss AS 和 Glassfish AS。您将看到如何利用 Hibernate OGM-MongoDB 以及许多常见技术,如 JSF、Spring、Seam、EJB 等等。最后,您将了解如何迁移到云—MongoHQ、MongoLab 和 OpenShift。

这本书是给谁的

这本书是为有经验的 Java 开发人员编写的,他们对探索 NoSQL 数据库的 Hibernate 解决方案感兴趣。对于开篇章节(第一章–第三章),熟悉 ORM 范式、Hibernate 原生 API 和 JPA 的主要方面就足够了。这本书提供了这些概念的简要概述。从第四章的开始,你应该对开发部署在 Tomcat、JBoss AS 或 GlassFish AS 服务器下的 web 应用(使用 NetBeans 或 Eclipse)有所了解。此外,您需要熟悉 web 应用中常用的 Java 技术和框架,比如 servlets、EJB、JSF、JSP、Seam、Spring 等等。

这本书的结构

以下是每章的主要重点:

第一章【Hibernate OGM 入门

本章简要介绍了 Hibernate OGM 世界。在这一章的第一部分,我讨论了 Hibernate OGM 架构,它当前的特性,以及我们对未来支持的期望。然后,我提供了几种下载、安装和配置 Hibernate OGM 和 MongoDB 的方法。

第二章 : Hibernate OGM 和 MongoDB1

在这一章中,我通过关注 Hibernate OGM 如何与 MongoDB 一起工作来更清楚地定义 Hibernate OGM 和 MongoDB 之间的关系。您将学习如何存储数据,如何映射主键和关联,以及如何处理事务和查询。

第三章:引导 Hibernate OGM

本章展示了如何通过 Hibernate 本地 API 和 JPA 来引导 Hibernate OGM。

第四章:Hibernate 工作中的 OGM

这是最重要的章节之一。您将学习如何在部署在不同服务器上的最常见的 web 和企业 Java 应用中集成 Hibernate OGM 和 MongoDB。以下是应用的完整列表:

  • Java SE 和 Mongo DB——一个“Hello world”示例
  • 在非 JTA 环境(JDBC 事务,Tomcat 7)中 Hibernate OGM(通过 Hibernate Native API)
  • 在独立的 JTA 环境(JBoss JTA,Tomcat 7)中 Hibernate OGM(通过 Hibernate Native API)
  • 在内置的 JTA 环境(没有 EJB,GlassFish 3)中 Hibernate OGM(通过 Hibernate Native API)
  • 在内置的 JTA 环境(EJB/BMT,GlassFish 3)中 Hibernate OGM(通过 Hibernate Native API)
  • 在内置的 JTA 环境(EJB/CMT,GlassFish 3)中 Hibernate OGM(通过 Hibernate Native API)
  • 在内置的 JTA 环境(GlassFish AS 3)中 Hibernate OGM(通过 JPA)
  • 在内置的 JTA 环境(JBoss AS 7)中 Hibernate OGM(通过 JPA)
  • 在内置的 JTA 环境(JBoss AS 7 和 Seam 应用)中 Hibernate OGM(通过 JPA)
  • 在内置的 JTA 环境(GlassFish 和 Spring 应用)中 Hibernate OGM(通过 JPA)
  • 在独立的 JTA 环境中 Hibernate OGM(通过 JPA)JPA/JTA(Tomcat)
  • 在非 JTA 环境中 Hibernate OGM(RESOURCE _ LOCAL,Apache Tomcat 7)

第五章 : Hibernate OGM 和 JPA 2.0 注解

在 Hibernate OGM 中映射 Java 实体可以分为支持的和不支持的注释。在这一章中,我展示了支持的注释,以及每个注释的支持程度。

第六章 : Hibernate OGM 查询 MongoDB

本章探索 Hibernate OGM 的查询功能。我从 MongoDB 原生查询开始,然后学习用 Hibernate Search 和 Apache Lucene 编写的复杂查询。

第七章 : MongoDB 电子商务数据库模型

至此,您已经掌握了足够的专业知识来开发一个包含 Hibernate OGM 和 MongoDB 的真正的应用。电子商务网站是一个良好的开端,也是一个有趣的研究案例,因此在本章中,我将一个经典的 SQL 数据库模型改编为 Hibernate OGM 和 MongoDB 风格。我还研究了电子商务数据库架构的各个方面。

第八章 : MongoDB 电子商务数据库查询

在开发了 MongoDB 电子商务数据库模型之后,是时候绘制和实现主要的特定于电子商务的查询了。在这一章中,我使用 Hibernate Search 和 Apache Lucene 来编写这样的查询。结果是一个名为 RafaEShop 的完整电子商务应用。

第九章:将 MongoDB 数据库迁移到云端

在本章中,您将学习如何将在第七章中开发的 MongoDB 电子商务数据库迁移到两个云中:MongoHQ 和 MongoLab。

第十章:在 OpenShift 上迁移 RafaEShop 应用

这最后一章是将电子商务 RafaEShop 应用迁移到两台企业服务器上的 OpenShift cloud 的详细指南:JBoss AS 和 GlassFish AS。

下载代码

本书中所示示例的代码可在 Apress 网站www.apress.com上获得。您可以在该书的信息页面上的源代码/下载选项卡下找到链接。该选项卡位于页面相关标题部分的下方。

联系作者

如果您有任何问题或意见——或者发现您认为我应该知道的错误——您可以通过leoprivacy@yahoo.com联系我。

一、Hibernate OGM 入门

您可能熟悉 Hibernate ORM,这是一个强大、健壮的工具,用于在关系数据库(RDBMS)和面向对象编程语言之间转换数据。作为一个对象关系映射(ORM)框架,Hibernate ORM 使用 SQL 存储。然而,近年来,开发人员对 NoSQL 数据库产生了兴趣,这种数据库为存储和检索大量数据进行了优化。NoSQL 数据库往往是非关系的、开源的、可水平伸缩的、分布式的和无模式的。

描述 NoSQL 商店有多种方式,但它们通常按数据模型分类,尤其是以下方式:

  • 文档存储(Mongo DB、RavenDB、CouchDB 等等)
  • 宽列商店(Hypertable、Cassandra、HBase 等)
  • key value/元组 stores (DynamoDB、LevelDB、Redis、Ryak 等)
  • 图形数据库(Neo4J、GraphBase、InfoGrid 等等)

这些也很常见:

  • 多模式数据库(OrientDB、ArangoDB 等)
  • 对象数据库(db4o、Versant 等等)
  • 网格和云数据库(GigaSpaces、Infinispan 等等)
  • XML 数据库(eXist、Sedna 等等)

显然,NoSQL 的商店非常复杂多样。一些网站拥有庞大的用户群,而另一些则鲜为人知。每一种都有自己的优点和缺点。你甚至可以说,NoSQL 是一个非常有争议的话题,程序员谈论它的时间比他们实际使用它的时间还要多。

然而,随着最近发布的 Hibernate OGM(对象网格映射器)项目,这种情况可能会改变,该项目提供了一个完整的 Java 持久性 API (JPA)引擎,用于在 NoSQL 商店中存储数据。这个项目给了寻求利用 NoSQL 商店的 Java 开发人员一个真正的推动,因为它提供了一个公共接口——众所周知的 JPA 编程模型——作为各种 NoSQL 方法的前端。Hibernate OGM 基于 Hibernate ORM 核心引擎,重用 Java 持久性查询语言(JP-QL)作为查询存储数据的接口,并且已经提供了对三个 NoSQL 商店的支持:MongoDB、Ehcache 和 Infinispan,Apache Cassandra 将来应该会得到支持。尽管这个项目还很年轻,Hibernate OGM 团队的目标保证了它在未来有巨大的潜力——以及大量的工作要完成。

特点和期望

在撰写本书时,最新的 Hibernate OGM 发行版是 4.0.0 Beta2,它已经成功地为不同的 NoSQL 方法提供了一个通用的接口;快速扩大或缩小数据存储;独立于底层存储技术;和 Hibernate 搜索。以下是 Hibernate OGM 目前支持的内容:

  • 在文档存储中存储数据(MongoDB)
  • 将数据存储在键/值存储中(Infinispan 的数据网格和 Ehcache)
  • JPA 实体的创建、读取、更新和删除(CRUD)操作
  • 多态实体(支持超类、子类等等)
  • 可嵌入的对象(例如,可嵌入的类,在 JPA 中用@Embeddable注释;可嵌入类的实例集合,在 JPA 中用@ElementCollection注释
  • 基本类型(如数字、StringURLDate、枚举)
  • 联想(@ManyToOne@OneToOne@OneToMany@ManyToMany)
  • 双向关联
  • 集合(SetListMap等)
  • Hibernate 搜索的全文查询
  • JPA 和原生 Hibernate ORM API (Hibernate OGM 可以通过 JPA 或 Hibernate Session 引导,我将在第三章向您展示。)

将来,Hibernate OGM 将支持:

  • 其他键/值对系统
  • 其他 NoSQL 发动机
  • 声明性反规格化
  • 复杂的 JP-QL 查询,包括一对多连接和聚合
  • 前置现有的 JPA 应用

image 注意反规格化是一种加速读取过程的数据库技术。其思想是尽可能减少查询中的连接数量;连接降低了读取性能,因为必须从多个表中提取数据,而不能破坏它们之间的关联。虽然规范化促进了将相关数据拆分到多个关联的表中,但是反规范化鼓励添加少量冗余来限制连接。即使一些数据被复制,性能通常也会提高。

Hibernate OGM 体系结构〔??〕

因为 Hibernate OGM 尽可能地使用现有的 Hibernate ORM 模块,所以 OGM 架构本质上是通过插入和拔出不同的组件来扩展 ORM 架构。Hibernate ORM 使用一组接口和类在关系数据库和面向对象编程语言之间转换和保存数据。其中包括 JDBC 层,用于连接数据库和发送查询, Persister s 和 Loader s 接口,负责持久化和加载实体和集合,如图图 1-1 所示。

9781430257943_Fig01-01.jpg

图 1-1 。Hibernate ORM 架构

Hibernate OGM 旨在实现相同的目标,但是使用 NoSQL 商店。因此,Hibernate OGM 不再需要 JDBC 层,取而代之的是两个新元素:一个数据存储提供者和一个数据存储方言,如图图 1-2 所示。这两者都充当 Hibernate OGM Core 和 NoSQL 商店之间的适配器。(一个数据存储库是一个适配器,它将核心映射引擎与特定的 NoSQL 技术连接起来。)

9781430257943_Fig01-02.jpg

图 1-2 。Hibernate OGM 数据存储提供者和数据存储方言

数据存储提供者负责管理到 NoSQL 存储的连接,而数据存储方言管理与 NoSQL 存储引擎的通信。实际上,这些概念体现在两个接口中,org.hibernate.ogm.datastore.spi.DatastoreProviderorg.hibernate.ogm.dialect.GridDialectDatastoreProvider接口负责启动、维护和停止商店连接,而GridDialect接口处理 NoSQL 商店中的数据持久性。此外,PersistersLoaders界面被重新编写以支持 NoSQL 商店的功能。

目前有四个DatastoreProvider的实现:

  • EhcacheDatastoreProvider(对于 NoSQL Encache)
  • InfinispanDatastoreProvider(适用于 NoSQL Infinispan)
  • MongoDBDatastoreProvider(适用于 NoSQL MongoDB)
  • MapDatastoreProvider(用于测试目的)

从特定的网格实现中抽象 Hibernate OGM 有五种实现方式:

  • EhcacheDialect(用于 EhCache)
  • InfinispanDialect(对于 Infinispan)
  • MongoDBDialect(用于 MongoDB)
  • HashMapDialect(用于测试)
  • GridDialectLogger(用于记录在真实方言上执行的呼叫)

image 注意如果你决定写一个新的数据存储,你必须实现一个DatastoreProvider和一个GridDialect。在https://community.jboss.org/wiki/HowToWriteADatastoreInHibernateOGM可以找到更多细节。

持久数据

通过修改后的Loader s 和 s 接口,Hibernate OGM 能够将数据保存到 NoSQL 商店。但是,在这样做之前,OGM 需要在内部表示和存储数据。为此,Hibernate OGM 尽可能多地保留了关系数据库的概念,并根据自己的需要修改这些概念。一些概念,比如存储实体,完全遵循关系模型,而另一些概念,比如存储关联,部分遵循关系模型。因此,数据存储为基本类型(实体存储为元组);仍然使用主键外键 的概念;应用数据模型和商店数据模型的关系是通过像这样的概念来抽象维护的。

OGM 使用元组 来表示数据的基本单位。元组意味着在概念上将实体存储为一个Map<String, Object>。关键字是列名(实体属性/字段或@Column注释值),值是作为原始类型的列值(见图 1-3 )。

9781430257943_Fig01-03.jpg

图 1-3 。Hibernate OGM 元组

每个元组代表一个实体实例,存储在一个特定的键中。实体实例通过由表名、主键列名和主键列值组成的特定键查找来标识。参见图 1-4 。

9781430257943_Fig01-04.jpg

图 1-4 。存储实体实例的 Hibernate OGM

image 注意 Java 集合被表示为一个元组列表。特定键由包含集合的表名、代表外键的列名和列值组成。

图 1-5 显示了多对多关联的关系数据库模型。

9781430257943_Fig01-05.jpg

图 1-5 。多对多关联的关系数据库模型

相比之下,Hibernate OGM 中的关联存储为类型为Map<String, Object>的元组集。例如,对于多对多关联,每个元组存储一对外键。Hibernate OGM 将从一个实体导航到其关联所必需的信息存储在一个特定的键中,该键由表名、列名和值组成,这些值表示我们所来自的实体的外键。这个@ManyToMany关联由 Hibernate OGM 内部存储,如图 图 1-6 所示。(您可以看到从第 8 行开始的关联元组。)这种方法通过键查找来促进可达数据,但是它有缺点:数据可能是冗余的,因为必须为关联双方存储信息。

9781430257943_Fig01-06.jpg

图 1-6 。多对多关系的 Hibernate OGM 数据网格

Hibernate OGM 将 JPA 实体存储为元组,而不是可序列化的 blobs。这更接近于关系模型。序列化实体有几个缺点:

  • 与其他实体相关联的实体也必须被存储,这很可能导致一个大图。
  • 很难保证重复对象之间的对象同一性甚至一致性。
  • 很难添加或删除一个属性或包含一个超类并处理反序列化问题。

image 注意 Hibernate OGM 将种子(当标识符需要种子时)存储在值中,该值的键由表示段的表名、列名和列值组成。

显然,这种表现并不是所有 NoSQL 商场都有的。例如,对于面向文档的商店 MongoDB 来说,情况就不同了。在这种情况下,使用GridDialect,它的主要任务是将这个表示转换成 NoSQL 商店的预期表示。对于 MongoDB,MongoDBDialect将其转换成 MongoDB 文档。

image 注意由于 NoSQL 商店不知道模式的概念,Hibernate OGM 元组不绑定到模式。

查询数据

当然,Hibernate OGM 需要提供一个强大的查询数据引擎,在撰写本文时,根据查询的性质和 NoSQL 查询支持,这可以通过多种不同的方式实现。

CRUD 操作是 Hibernate ORM 引擎的责任,它们遵循一个简单的过程。独立于 JPA 或 Hibernate 原生 API,Hibernate ORM 将持久性和加载查询委托给 OGM 引擎,OGM 引擎将 CRUD 操作委托给DatastoreProvider / GridDialect,后者与 NoSQL 存储交互。图 1-7 描述了这个过程。

9781430257943_Fig01-07.jpg

图 1-7 。Hibernate OGM 和 CRUD 操作

因为 Hibernate OGM 想要提供整个 JPA,所以它需要支持 JP-QL 查询。这意味着复杂的查询引擎(QE)应该对特定的 NoSQL 商店查询能力和 JP-QL 查询的复杂性敏感。最乐观的例子是具有查询功能和简单的 JP-QL 查询的 NoSQL。在这种情况下,查询被委托给特定于 NoSQL 的查询翻译器,结果由 Hibernate OGM 管理,以组成特定的对象(参见图 1-8 )。

9781430257943_Fig01-08.jpg

图 1-8 。Hibernate OGM 和 JP-QL 简单查询(支持查询的 NoSQL)

当 NoSQL 商店不支持当前查询时,情况就不那么乐观了。在这种情况下,JBoss Teiid 数据虚拟化系统介入,将 JP-QL 查询拆分成可以由数据存储执行的简单查询。(更多信息见www.jboss.org/teiid)。Teiid 也对结果进行处理,得到最终的查询结果,如图图 1-9 所示。

9781430257943_Fig01-09.jpg

图 1-9 。Hibernate OGM 和 JP-QL 复杂查询

最糟糕的情况是 NoSQL 商店很少或没有查询支持。由于这是一个棘手的案例,它需要重炮,就像 Hibernate Search,一个基于 Hibernate Core 和 Apache Lucene 的企业全文搜索工具。基本上,Hibernate 搜索索引引擎从 Hibernate ORM 核心接收事件,并保持实体索引过程最新,而 JP-QL 查询解析器将查询翻译委托给 Hibernate 搜索查询引擎(对于简单查询)或 Teiid(对于中级到复杂查询),并使用 Lucene 索引执行它们(参见图 1-10 )。此外,Hibernate Search 还提供了集群支持和面向对象的抽象,其中包括查询领域特定语言(DSL)。

9781430257943_Fig01-10.jpg

图 1-10 。Hibernate OGM 和 JP-QL 查询(很少或没有 NoSQL 支持)

获取 Hibernate OGM 发行版

在撰写本文时,Hibernate OGM 发行版是 4.0.0.Beta2。获得完整文档、源代码和依赖项的最佳方式是访问www.hibernate.org/subprojects/ogm.html并下载相应的 ZIP/TGZ 归档文件。

不幸的是,这并不像看起来那么简单。由于本书的重点是 Hibernate OGM 和 MongoDB,所以您需要找到专门用于“连接”OGM 和 MongoDB 的 jar:hibernate-ogm-mongodb-``x``.jarmongo-java-driver- x .jar。(MongoDB 对大多数编程语言都有客户端支持;这是 MongoDB 团队开发的 MongoDB Java 驱动,Hibernate OGM 用来和 MongoDB 交互)。在 Hibernate OGM 版本 4.0.0.Beta1 中,你会在\hibernate-ogm-4.0.0.Beta1\dist\lib\mongodb文件夹中找到这些 jar:hibernate-ogm-mongodb-4.0.0.Beta1.jarmongo-java-driver-2.8.0.jar。在 Hibernate OGM 版本 4.0.0.Beta2 中,缺少了\mongodb文件夹,所以新的 jar 没有被打包。

这意味着您仍然可以在 Hibernate OGM 4.0.0.Beta2 中使用hibernate-ogm-mongodb-4.0.0.Beta1.jarmongo-java-driver-2.8.0.jar,或者您可以编译 Hibernate OGM 4.0.0.Beta2 的源代码来获得最新的快照。要编译代码,请访问www.sourceforge.net/projects/hibernate/files/hibernate-ogm/4.0.0.Beta2/。我已经编译好代码,获得了 MongoDB JAR,命名为hibernate-ogm-mongodb-4.0.0-SNAPSHOT

如果你看一下图 1-11 中显示的 Hibernate OGM 变更日志,你会看到 Hibernate OGM 4.0.0.Beta2 已经升级到支持 MongoDB Java Driver 2.9。x。这意味着如果您决定编译代码并使用 MongoDB 概要文件的结果快照,您也可以添加一个 2.9。x MongoDB Java 驱动,而不是 2.8。x

9781430257943_Fig01-11.jpg

图 1-11 。Hibernate OGM 更改日志

对于这本书,我选择使用 Hibernate OGM 4.0.0.Beta2 和 Hibernate OGM for MongoDB 4 . 0 . 0 . beta 1。

从 Maven 中央存储库获取 Hibernate OGM

也可以从 Maven 中央存储库 ( www.search.maven.org/)下载 Hibernate OGM。搜索“hibernate ogm”,将会返回您在图 1-12 中看到的内容。

9781430257943_Fig01-12.jpg

图 1-12 。Maven 中央存储库中列出的 Hibernate OGM 发行版

如您所见,下载 Hibernate OGM 核心和概要文件非常容易,包括 MongoDB 概要文件。您可以下载 jar 或 POMs(项目对象模型)文件。

从 Maven 命令行获取 Hibernate OGM

Hibernate OGM 也可以从 Apache Maven 命令行 获得。显然,Maven 必须在您的计算机上安装和配置。首先,您必须修改您的settings.xml文档,它存储在 Maven 本地存储库.m2文件夹中(默认位置)。对于 Unix/Mac OS X 用户,这个文件夹应该是∼/.m2;对于 Windows 用户来说是C:\Documents and Settings\{your username}\.m2 or C:\Users\{your username}\.m2。如果settings.xml文件不存在,你应该在这个文件夹中创建它,如清单 1-1 所示。(如果您已经有了这个文件,只需相应地修改它的内容。)

image 注意如果创建或修改settings.xml因为太冗长而显得太复杂,你可以简单地在你的pom.xml中使用<repository><dependency>标签。

清单 1-1。settings . XML

<?xml version="1.0" encoding="UTF-8"?>

<settings FontName2">http://maven.apache.org/SETTINGS/1.0.0 " xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
          xsi:schemaLocation=" http://maven.apache.org/SETTINGS/1.0.0
                                             http://maven.apache.org/xsd/settings-1.0.0.xsd ">
 <!-- jboss.org config start -->
 <profiles>
    <profile>
      <id>jboss-public-repository</id>
      <repositories>
        <repository>
          <id>jboss-public-repository-group</id>
          <name>JBoss Public Maven Repository Group</name>
          <url> https://repository.jboss.org/nexus/content/groups/public-jboss/</url >
          <layout>default</layout>
          <releases>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
          </releases>
          <snapshots>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
          </snapshots>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>jboss-public-repository-group</id>
          <name>JBoss Public Maven Repository Group</name>
          <url> https://repository.jboss.org/nexus/content/groups/public-jboss/</url >
          <layout>default</layout>
          <releases>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
          </releases>
          <snapshots>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
          </snapshots>
        </pluginRepository>
      </pluginRepositories>
    </profile>
    <profile>
      <id>jboss-deprecated-repository</id>
      <repositories>
        <repository>
          <id>jboss-deprecated-repository</id>
          <name>JBoss Deprecated Maven Repository</name>
          <url> https://repository.jboss.org/nexus/content/repositories/deprecated/</url >
          <layout>default</layout>
          <releases>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
          </releases>
          <snapshots>
            <enabled>false</enabled>
            <updatePolicy>never</updatePolicy>
          </snapshots>
        </repository>
      </repositories>
    </profile>
    <!-- jboss.org config end -->
  </profiles>

  <!-- jboss.org config start -->
  <activeProfiles>
    <activeProfile>jboss-public-repository</activeProfile>
  </activeProfiles>
  <!-- jboss.org config end -->
</settings>

image 注意你可以通过在settings.xml中添加标签localRepository来修改 Maven 本地库的默认位置,比如:<localRepository> new_repository_path </localRepository>

接下来,你需要创建一个pom.xml文件。显然,这个文件的内容取决于您想从 Hibernate OGM 存储库中获得什么。例如,清单 1-2 中的pom.xml将下载 Hibernate OGM 核心发行版(包括依赖项)并将其本地存储在D:/Hibernate_OGM中(您也可以使用默认的./m2文件夹,但这样会更清晰、更容易导航)。

清单 1-2。POM . XML

<project FontName2">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/xsd/maven-4.0.0.xsd ">
    <modelVersion>4.0.0</modelVersion>
    <groupId>maven.hibernate.ogm</groupId>
    <artifactId>Maven_HOGM</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>Maven_HOGM</name>
    <dependencies>
        <dependency>
            <groupId>org.hibernate.ogm</groupId>
            <artifactId>hibernate-ogm-core</artifactId>
            <version>4.0.0.Beta2</version>
        </dependency>
    </dependencies>
    <build>
       <directory>D:/Hibernate_OGM</directory>
       <defaultGoal>dependency:copy-dependencies</defaultGoal>
    </build>
</project>

最后一步是执行 Maven mvn命令。为此,打开命令提示符,导航到包含pom.xml文件的文件夹,运行mvn命令(参见图 1-13 )。几秒钟后,您应该在pom.xml文件指定的路径中找到 Hibernate OGM 二进制文件(包括依赖项)。

9781430257943_Fig01-13.jpg

图 1-13 。运行 mvn 命令

添加 MongoDB 工件

现在您知道了如何获得 Hibernate OGM 4.0.0.Beta2 核心(和依赖项),但是没有任何 NoSQL 数据存储构件。目前,您可以为以下 NoSQL 商店添加工件: Ehcache、Infinispan 和 MongoDB。因为我们的重点是 Hibernate OGM 和 MongoDB,所以您需要通过将以下依赖项放入pom.xml文件来添加 MongoDB 工件:

...
<dependency>
   <groupId>org.hibernate.ogm</groupId>
   <artifactId>hibernate-ogm-mongodb</artifactId>
   <version>4.0.0.Beta1</version>
 </dependency>
...

image 注意对于 Infinispan,只需用hibernate-ogm-infinispan替换工件 id,对于 Ehcache 用hibernate-ogm-ehcache

现在,再次运行mvn命令将增加两个罐子,hibernate-ogm-mongodb-4.0.0.Beta1.jarmongo-java-driver-2.8.0.jar,如图图 1-14 所示。MongoDB 驱动程序也可以作为 jar 在www.mongodb.org/display/DOCS/Drivers地址下载。

9781430257943_Fig01-14.jpg

图 1-14 。添加 MongoDB 工件后运行 mvn 命令

使用 NetBeans IDE 获得 Hibernate OGM 发行版

如果您是 NetBeans 的粉丝,从 NetBeans Maven 项目中使用 Maven 会简单得多。本节描述了创建这样一个项目的主要步骤,以获得作为 NetBeans 库的 Hibernate OGM 发行版,以便在其他项目中使用。启动 NetBeans(我在 NetBeans 7.2.1 上进行了测试),并遵循以下步骤:

  1. From the File menu, select the New Project option. In the New Project wizard, select Maven in the Categories list and POM Project in the Projects list, as shown in Figure 1-15.

    9781430257943_Fig01-15.jpg

    图 1-15 。使用 NetBeans 7 创建 POM 项目

    image 注意如果您的 NetBeans 发行版中没有 Maven,您可以按照http://wiki.netbeans.org/InstallingAPlugin中关于第三方插件安装的教程来安装它。

  2. Type the project name (Maven_HOGM), select the project location (D:\Apress\apps\NetBeans), type the group id (maven.hibernate.ogm) and the version (1.0-SNAPSHOT) and click Finish as shown in Figure 1-16. (Note that I’ve used example names and locations here. Feel free to choose your own.) The empty project will be created and listed under the Projects panel.

    9781430257943_Fig01-16.jpg

    图 1-16 。设置项目名称和位置

  3. Expand the Maven_HOGM | Project Files node and locate pom.xml and settings.xml. If settings.xml isn’t listed, right-click on the Project Files node, select Create settings.xml (as shown in Figure 1-17), and fill the file with the appropriate content.

    9781430257943_Fig01-17.jpg

    图 1-17 。从 NetBeans 7 创建 settings.xml 文件

  4. 根据需要编辑pom.xml。此时,两个文件都应该准备好被 Maven 处理了。

  5. 右键单击Maven-HOGM节点并选择Clean and Build。等到任务成功结束,然后展开Maven_OGM | Dependencies节点查看下载的 jar。

  6. Now you can create a NetBeans library. (I recommend that you create this library because the applications developed with NetBeans, in later chapters, refer to it.) From the NetBeans main menu, select Tools | Ant Libraries. In the Ant Library Manager, click the New Library button, provide a name for the library, such as Hibernate OGM Core and MongoDB, and click OK. Next, click on the Add JAR/Folderbutton and navigate to the JARs (if you followed my example path, you’ll find them inD:\Hibernate_OGM`dependency, as shown in Figure 1-18). Select all of the JARs and add them to this library. Click OK to finish creating the library.

    9781430257943_Fig01-18.jpg

    图 1-18 。为 Hibernate OGM 和 MongoDB 创建用户库`

`现在,通过将 Hibernate OGM Core/Hibernate OGM Core 和 MongoDB 库添加到您的项目库中,您可以轻松地将 Hibernate OGM/MongoDB 发行版集成到您的任何 NetBeans 项目中。

完整的应用可以在 Apress 库中找到。这是一个名为 Maven_HOGM 的 NetBeans 项目。

使用 Eclipse IDE 获得 Hibernate OGM 发行版

如果你是一个 Eclipse 粉丝,从 Eclipse Maven 项目中使用 Maven 要简单得多。本节描述了创建这样一个项目的主要步骤,以获得作为 Eclipse 库的 Hibernate OGM 发行版,以便在其他项目中使用。因此,启动 Eclipse(我们在 Eclipse JUNO 上进行了测试),并遵循以下步骤:

  1. From the File menu, select New | Other. In the New wizard, expand the Maven node and select Maven Projectas shown in Figure 1-19. Click Next.

    9781430257943_Fig01-19.jpg

    图 1-19 。用 Eclipse JUNO 创建一个新的 Maven 项目

    如果您的 Eclipse 发行版中没有 Maven,您可以下载一个独立的 Maven 发行版并从Window | Preferences | Maven | Installations安装它,或者您可以从Eclipse Marketplace安装 Maven for Eclipse,您可以在Help菜单中找到它。一旦你在市场中找到 Maven,按照向导完成安装(参见图 1-20 )。

    9781430257943_Fig01-20.jpg

    图 1-20 。用 Eclipse JUNO 创建一个新的 Maven 项目

  2. 勾选标有Create a simple project (skip archetype selection)的方框。可以选择默认工作区,点击Next

  3. 键入组 id ( maven.hibernate.ogm)和工件 id ( Maven_HOGM)。点击Finish按钮,等待项目成功创建并在Package Explorer面板中列出。

  4. 在 maven 本地存储库中手动更新或创建settings.xml文件。

  5. Maven_HOGM项目中找到pom.xml并双击它。

  6. Next, in the editor, switch to the pom.xml tab where you’ll see a pom.xml skeleton. Add to it the missing parts from your pom.xml and save the project (see Figure 1-21).

    9781430257943_Fig01-21.jpg

    图 1-21 。在 Eclipse JUNO 中编辑 pom.xml 文件

  7. Package Explorer面板中,右击项目名称并选择Run As | Maven build。当流程成功结束时,您应该会在由pom.xml中的<directory>标签定义的路径下看到 Hibernate OGM 发行版(包括依赖项)。

  8. Window菜单中选择Preferences。在左边的树中,展开Java | Build Path节点,选择User Libraries

  9. 点击New按钮创建一个新的库。为新库键入一个名称,比如 Hibernate OGM Core 和 MongoDB,然后单击OK

  10. 单击Add External JARs按钮,导航到下载 Hibernate OGM 发行版的文件夹。选择所有的 jar 并将它们添加到库中。点击OK

现在,通过将 Hibernate OGM Core/Hibernate OGM Core 和 MongoDB 库添加到您的项目构建路径中,您可以轻松地将 Hibernate OGM/MongoDB 发行版集成到您的任何 Eclipse 项目中。

image 注意如果您更喜欢用 Maven 创建整个项目,只需相应地添加 Hibernate OGM 依赖项。你所要做的就是添加相应的<repository><dependency>标签。

完整的应用可以在 Apress 库中找到。这是一个名为 Maven_HOGM 的 Eclipse 项目。

获取 MongoDB 发行版

在写这本书的时候,推荐的 MongoDB 发行版是版本 2.2.2(我选择这个版本是因为它是 Hibernate OGM 和 OpenShift 的“首选”)。您可以在官方网站http://www.mongodb.org/轻松下载。您将在http://docs.mongodb.org/manual/installation/找到安装步骤。本书中的示例是在 64 位版本的 Windows 7 和 8 下开发和测试的,安装非常简单。

下载并安装 MongoDB 发行版后,您就可以看到 MongoDB 服务器是否启动并响应命令了。打开命令提示符,导航到{MONGODB_HOME}/bin文件夹并键入mongod --dbpath ../命令来启动服务器(--dbpath选项指示您按照安装指南在{MONGODB_HOME}文件夹中手动创建的/data/db文件夹的位置)。如果没有错误,打开另一个命令提示符,导航到同一个文件夹,然后键入mongo。如果你看到类似于图 1-22 所示的东西,那么 MongoDB 已经成功安装了。

9781430257943_Fig01-22.jpg

图 1-22 。检查 MongoDB 服务器可用性

要进行更彻底的测试,请尝试http://docs.mongodb.org/manual/tutorial/getting-started/的入门教程中的命令。按下CTRL-C就可以轻松关闭 MongoDB 服务器。

摘要

在这一介绍性章节中,我们迈出了理解和使用 Hibernate OGM 的第一步。我们研究了 Hibernate OGM 的概念、特性和目标,并简要概述了 Hibernate OGM 的架构。(如果你想理解下一章,知道事情是如何在内部管理是很重要的)。

然后,您看到了如何获得作为 ZIP/TGZ、命令行 Maven 项目和基于 NetBeans/Eclipse Maven 的项目的 Hibernate OGM 发行版。最后,您学习了如何安装 MongoDB 发行版,以及如何将相应的 jar 添加到 Hibernate OGM 发行版中。`

二、Hibernate OGM 和 MongoDB

到目前为止,您应该对 Hibernate OGM 的一般范围和架构有了一些了解。在第一章的中,我讨论了 Hibernate OGM 如何与一般的 NoSQL 存储一起工作,我谈到了它的一般焦点以及如何表示、持久化和查询数据。此外,您学习了如何获得 Hibernate OGM 发行版,并且安装了一个 MongoDB NoSQL 存储,执行了一个简单的命令行测试来验证 MongoDB 服务器是否正确响应。

在这一章中,我将更清楚地定义 Hibernate OGM 和 MongoDB 之间的关系。我将重点介绍 Hibernate OGM 如何与 MongoDB store 一起工作,而不是一般的可能性,您将看到 Hibernate OGM 可以“吞噬”多少 MongoDB,以及迫使 Hibernate OGM 加班管理它们的一些 MongoDB 缺点。

配置 MongoDB-Hibernate OGM 属性

当您提供一组配置属性时,Hibernate OGM 会意识到 MongoDB。如果您以前使用过 Hibernate ORM,那么您应该已经熟悉了这些类型的属性。具体来说,有三种设置这些属性的方法,您将在下一章中看到:

  • 声明性的,通过hibernate.cfg.xml配置文件
  • 以编程方式,通过 Hibernate 本机 API
  • 声明性的,通过 JPA 上下文中的persistence.xml配置文件

image 注意记住,我们使用的是 Hibernate OGM 4 . 0 . 0 beta 2 和用于 MongoDB 4 . 0 . 0 . 0 . beta 1 的 Hibernate OGM 以及用于 MongoDB 2.8.0 的 Java 驱动程序。

让我们看看使 Hibernate OGM 能够与 MongoDB 一起工作的属性。

hibernate.ogm.datastore.provider

正如你从第一章中了解到的,Hibernate OGM 目前支持几个 NoSQL 商店,包括 MongoDB。这个属性值是让 Hibernate OGM 知道您想要使用哪个 NoSQL 存储的方式。对于 MongoDB,该属性的值必须设置为mongodb

hibernate.ogm.mongodb.host

接下来,Hibernate OGM 需要定位 MongoDB 服务器实例。首先,它必须定位主机名,主机名由托管 MongoDB 实例的机器的 IP 地址表示。默认情况下,该属性的值为127.0.0.1,相当于 localhost,也可以通过 MongoDB 驱动程序设置:

Mongo mongo = new Mongo("127.0.0.1");
Mongo mongo = new Mongo(new ServerAddress( "127.0.0.1"));

hibernate.ogm.mongodb.port

没有端口的主机名是什么?默认情况下,MongoDB 实例运行在端口号27017,上,但是您可以使用任何其他 MongoDB 端口,只要您将它指定为该属性的值。如果您直接使用 MongoDB 驱动程序,端口通常是这样设置的:

Mongo mongo = new Mongo("127.0.0.1", 27017);
Mongo mongo = new Mongo( new ServerAddress("127.0.0.1", 27017));

hibernate.ogm.mongodb.database

现在 Hibernate OGM 可以通过它的主机和端口定位 MongoDB。您还必须指定要连接的数据库。如果您指定了一个不存在的数据库名称,将自动创建一个具有该名称的新数据库(该属性没有默认值)。您还可以使用 MongoDB 驱动程序进行连接,如下所示:

DB db = mongo.getDB(" *database_name*");
Mongo db = new Mongo( new DBAddress( "127.0.0.1", 27017, " *database_name*" ));

hibernate.ogm.mongodb.username
hibernate.ogm.mongodb.password

这两个属性代表认证凭证。它们没有缺省值,通常一起出现在 MongoDB 服务器上对用户进行身份验证(尽管如果设置了密码而没有设置用户名,Hibernate OGM 会忽略the hibernate.ogm.mongodb.password属性)。您还可以使用 MongoDB 驱动程序来设置身份验证凭证,如下所示:

boolean auth = db.authenticate(" *username* ", " *password* ".toCharArray());

hibernate.ogm.mongodb.safe

注意,这个属性有点棘手。MongoDB 不擅长事务;它不执行回滚,也不能保证插入的数据确实在数据库中,因为驱动程序在返回之前不会等待应用写操作。在巨大的速度优势背后——由驱动程序执行对 MongoDB 服务器的写操作这一事实导致——隐藏着一个可能丢失数据的危险陷阱。

MongoDB 团队知道这个缺点,所以开发了一个名为写关注点的新特性来告诉 MongoDB 一段数据有多重要。这也用来表示数据的初始状态,默认写,(WriteConcern.NORMAL )。

MongoDB 定义了几个级别的数据重要性,但是 Hibernate OGM 允许您在默认的写入和安全写入之间切换。

使用写安全,驱动程序不会立即返回;它等待写操作成功,然后返回。显然,这可能会对性能产生严重影响。您可以使用hibernate.ogm.mongodb.safe属性设置该值。默认情况下,这个属性的值是true,这意味着写安全是活动的,但是如果写丢失不是您的主要问题,您可以将它设置为false

下面是如何直接使用 MongoDB 驱动程序来设置写安全:

DB db = mongo.getDB(" *database_name* ");
DBCollection dbCollection = db.getCollection(" *collection_name* ");
dbCollection.setWriteConcern(WriteConcern.SAFE);
dbCollection.insert( *piece_of_data* );
//or, shortly
dbCollection.insert( *piece_of_data* , WriteConcern.SAFE);

image 注意目前 Hibernate OGM 只让你启用写安全 MongoDB 写关注(WriteConcern.SAFE)。因此,像写 FSYNC_SAFE ( WriteConcern.FSYNC_SAFE)、写 JOURNAL_SAFE ( WriteConcern.JOURNAL_SAFE)和写多数(WriteConcern.MAJORITY)这样的策略只能通过 MongoDB 驱动程序来控制。

hibernate.ogm.mongodb.connection_timeout

MongoDB 为不同种类的耗时操作支持一些超时选项。目前,Hibernate OGM 通过这个属性公开 MongoDB 选项connectTimeout(参见com.mongodb.MongoOptions)。这用毫秒表示,表示启动与 MongoDB 实例的连接时驱动程序使用的超时。默认情况下,Hibernate OGM 将其设置为 5000 毫秒,以覆盖驱动程序默认值 0(这意味着没有超时)。您可以按如下方式设置该属性:

mongo.getMongoOptions().connectTimeout= *n_miliseconds* ;

hibernate.ogm.mongodb.associations.store

该属性定义 Hibernate OGM 存储关联相关信息的方式。可接受的值有:IN_ENTITY, COLLECTION, and GLOBAL_COLLECTION。我将在本章稍后讨论这三种策略。

hibernate.ogm.datastore.grid_dialect

这是一个可选属性,通常会被忽略,因为数据存储提供者会自动选择最佳的网格方言。但是如果您想覆盖推荐值,您必须指定GridDialect实现的完全限定类名。对于 MongoDB,正确的值是org.hibernate.ogm.dialect.mongodb.MongoDBDialect

这是 Hibernate OGM 用来配置到 MongoDB 服务器的连接的一组属性。至此,您已经获得了创建与 MongoDB 服务器良好通信的基本设置。在未来的 OGM 版本中,我们希望能够访问 MongoDB 驱动程序的更多设置。

数据存储表示

如你所知,关系数据模型在 MongoDB 方面是没用的,MongoDB 是基于文档的数据库系统; MongoDB 中的所有记录(数据)都是文档。但是,即使如此,MongoDB 也必须在关系术语和它自己的概念之间保持概念上的一致。因此,MongoDB 不使用,而是使用集合,不使用记录,而是使用文档(集合包含文档)。MongoDB 文档是 BSON (二进制 JSON——类 JSON 文档的二进制编码序列化)对象,具有以下结构:

{
   field1: value1,
   field2: value2,
   field3: value3,
   ...
   fieldN: valueN
}

存储实体

好的,但是我们仍然在存储和检索 Java 实体,对吗?是的,答案肯定是肯定的!如果说 Hibernate ORM 提供了将 Java 实体转换成关系表的完整支持,那么 Hibernate OGM 提供了将 Java 实体转换成 MongoDB 集合的完整支持。每个实体代表一个 MongoDB 集合;每个实体实例代表一个 MongoDB 文档;并且每个实体属性将被翻译成一个文档字段(见图 2-1 )。

9781430257943_Fig02-01.jpg

图 2-1 。在 MongoDB 文档中存储 Java 对象

Hibernate OGM 团队努力为 MongoDB 尽可能自然地存储数据,以便第三方应用可以在没有 Hibernate OGM 帮助的情况下利用这些数据。例如,假设我们有一个 POJO 类,类似于清单 2-1 中的那个。(我确信您已经在关系数据库中存储了大量这样的 Java 对象,所以我不提供这个简单类的细节。)

清单 2-1。 一波乔类

import java.util.Date;

public class Players {

    private int id;
    private String name;
    private String surname;
    private int age;
    private Date birth;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }
}

现在,假设使用 Hibernate OGM 将这个 POJO 的实例存储到 MongoDB players集合中,如下所示:

{
        "_id": 1,
        "age": 26,
        "birth": ISODate("1986-06-03T15:43:37.763Z"),
        "name": "Nadal",
        "surname": "Rafael"
}

如果您使用以下命令通过 MongoDB shell 手动存储,这正是您所获得的结果:

>db.players.insert(
                  {
                        _id: 1,
                        age: 26,
                        birth: new ISODate("1986-06-03T15:43:37.763Z"),
                        name: "Nadal",
                        surname: "Rafael"
                  }
                       )

实际上,结果没有什么不同。您无法判断文档是由 Hibernate OGM 生成的还是通过 MongoDB shell 插入的。太好了!此外,Hibernate OGM 知道如何将这个结果转换回 POJO 的实例。那就更厉害了!而且您不会感到任何编程上的不适,因为 Hibernate OGM 不需要您编写任何底层 MongoDB 代码。那是最棒的!

存储主键 s

MongoDB 文档或集合具有非常灵活的结构。它支持简单的对象:在其他对象和数组中嵌入对象和数组;同一馆藏中不同种类的文献;而且它还包含一个专门为存储主键而保留的文档字段。这个字段被命名为_id,它的值可以是任何信息,只要它是唯一的。如果不将_id设置为任何值,该值将自动设置为“MongoDB Id Object”。

Hibernate OGM 在将标识符存储到 MongoDB 数据库时会识别这些规范;它允许您使用任何 Java 类型的标识符,甚至是复合标识符,并且它总是将它们存储在保留的_id字段中。

图 2-2 显示了不同 Java 类型的一些标识符,以及它们在 MongoDB 中的样子。

9781430257943_Fig02-02.jpg

图 2-2 。Java 风格的主键和 MongoDB 标识符之间的对应

存储关联

可能关系数据库最强大的特性依赖于关联。任何有意义功能的数据库都可以利用关联:一对一、一对多、多对一和多对多。在关系模型中,关联需要存储附加信息,称为关联的导航信息。

例如,在双向多对多关联中,关系模型通常使用三个表,两个数据表和一个附加表,称为连接表。连接表包含一个组合键,该组合键由引用两个数据表主键的两个外键字段组成(参见图 2-3 )。请注意,同一对外键只能出现一次。

9781430257943_Fig02-03.jpg

图 2-3 。双向多对多关联,以关系模型表示形式显示

在 MongoDB 多对多关联中,您将连接表存储为文档。Hibernate OGM 提供了三种解决方案来完成这个任务:IN_ENTITY, COLLECTIONGLOBAL_COLLECTION。为了更好地理解这些策略,让我们临时准备一个简单的场景——两个关系表(PlayersTournaments)分别填充了三个玩家、两个锦标赛和一个多对多关联,如图图 2-4 所示。(第一个和第二个玩家P1P2参加了两个锦标赛T1T2,第三个玩家(P3)只参加第二个锦标赛T2。或者,从关联的另一方来看,第一个锦标赛T1包括第一和第二个玩家P1P2,第二个锦标赛T2包括第一、第二和第三个玩家P1P2P3。)

9781430257943_Fig02-04.jpg

图 2-4 。关系模型表示中的双向多对多关联—测试用例

现在,让我们用这个测试用例来看看 Hibernate OGM 存储关联的策略。我们希望观察连接表是如何基于所选策略存储在 MongoDB 中的。我们将从默认策略IN_ENTITY,开始,接着是GLOBAL_COLLECTION,,最后是COLLECTION

在 JPA 术语中,表示这个关系模型的主要方式有:Players实体定义了一个名为idPlayers的主键字段,是关联的所有者;Tournaments实体定义了一个名为idTournaments的主键,并且是关联的非所有者方——它包含了mappedBy元素。而且,Players实体定义了一个Tournaments的 Java 集合,命名为tournaments,Tournaments实体定义了一个Players的 Java 集合,命名为players

身份

存储关联导航信息的默认策略名为IN_ENTITY 。在这种情况下,Hibernate OGM 将关联另一端的主键(外键)存储到:

  • 如果映射涉及单个对象,则为字段。
  • 如果映射涉及集合,则为嵌入式集合。

使用IN_ENTITY策略运行 MongoDB 的关系场景,结果如图 2-5 和图 2-6 所示。

9781430257943_Fig02-05.jpg

图 2-5 。Hibernate OGM-IN_ENTITY 策略结果(玩家集合)

9781430257943_Fig02-06.jpg

图 2-6 。Hibernate OGM-IN_ENTITY 策略结果(锦标赛集合)

图 2-5 显示了Players关系表对应的 MongoDB Players集合;如您所见,每个集合的文档都包含作为嵌入集合的关联部分。(Players集合包含连接表中引用Tournaments集合的部分。)

image 注意从 shell 中探索 MongoDB 集合的最简单方法是调用find方法,该方法返回指定集合中的所有文档。此外,调用pretty方法会导致输出被很好地格式化。当一个集合包含的文档多于一个 shell 窗口所能容纳的文档时,您需要键入it命令,该命令支持文档分页。

Players集合显示了三个主文档,其中_id设置为 1、2 和 3,每个文档都在一个字段中封装了相应的外键,该字段的命名类似于所有者端声明的 Java 集合(tournaments)。嵌入集合中的每个文档都包含一个存储在字段中的外键值,该字段的名称由所有者端声明的 Java 集合名称(tournaments)和下划线以及非所有者端主键字段名(idTournaments)连接而成。

对应于Tournaments关系表的Tournaments集合就像是Players集合的反映——Players主键变成了Tournaments外键(Tournaments集合包含了引用Players集合的连接表部分)。图 2-6 显示了Tournaments集合的内容。

Tournaments集合包括两个主文档,其中_id被设置为 1 和 2。每一个都将相应的外键封装在一个字段中,该字段的名称类似于非所有者端声明的 Java 集合(players)。嵌入集合的每个文档都包含一个存储在字段中的外键值,该字段的名称由非所有者端声明的 Java 集合名称(players)与下划线和所有者端主键字段名(idPlayers)连接而成。

在单向情况下,只有代表所有者端的集合将包含关联的导航信息。

您可以通过将hibernate.ogm.mongodb.associations.store配置属性设置为值IN_ENTITY来使用这种存储关联导航信息的策略。实际上,这是该属性的默认值。

全局 _ 集合

当您不想将关联的导航信息存储到实体的集合中时,您可以选择GLOBAL_COLLECTION策略(或者COLLECTION,您将在下一节中看到)。在这种情况下,Hibernate OGM 创建了一个名为Associations的额外集合,专门用于存储所有导航信息。该集合的文档具有由两部分组成的特殊结构。第一部分包含一个复合标识符_id,由两个字段组成,它们的值代表关联所有者的主键和关联表的名称;第二部分包含一个名为rows的字段,它在一个嵌入式集合中存储外键。对于双向关联,创建另一个文档,其中 id 是相反的。

为 MongoDB 和GLOBAL_COLLECTION策略运行我们的关系场景揭示了如图 2-7 和图 2-8 所示的结果。

9781430257943_Fig02-07.jpg

图 2-7 。Hibernate OGM-GLOBAL_COLLECTION 策略结果(玩家和锦标赛集合)

9781430257943_Fig02-08.jpg

图 2-8 。Hibernate OGM-GLOBAL_COLLECTION 策略结果(关联集合)

在图 2-7 中,可以看到PlayersTournaments集合只包含纯信息,没有导航信息。

包含导航关联的额外的、唯一的集合被命名为Associations,并在图 2-8 中列出。

这是一个双向的关联。车主侧(Players)映射在图 2-8 的左侧,非车主侧(Tournaments)映射在图 2-8 的右侧。在单向关联中,只存在所有者一方。

现在,关注第一个_id字段下的嵌套文档(图 2-8 ,左侧)。第一个字段名players_idPlayers,由非所有者端定义的相应 Java 集合名(players)组成,或者,对于单向关联,由表示所有者端的集合名(Players)和表示所有者端主键的字段名(idPlayers)组成。第二个字段名是table;它的值由表示所有者端的集合名和下划线以及表示非所有者端的集合名组成(Players_Tournaments)。rows嵌套集合包含每个外键一个文档。每个外键都存储在一个字段中,该字段的名称由所有者端(tournaments)中定义的相应 Java 集合名和下划线以及非所有者端(idTournaments)的主键字段名组成。双向性的结果是,事情发生了逆转,如图 2-8 右侧所示。

通过将hibernate.ogm.mongodb.associations.store配置属性设置为值GLOBAL_COLLECTION,可以使用这种策略来存储关联的导航信息。

收藏

如果GLOBAL_COLLECTION将所有导航信息存储在一个全局集合中,那么COLLECTION策略就不那么全局了,并且为每个关联创建一个 MongoDB 集合。例如,在我们的场景中,将有一个名为associations_Players_Tournaments的额外集合。在这个策略中,每个集合都以单词associations为前缀,后跟关联表的名称。使用这个约定可以很容易地将associations集合与其他集合区分开来。

该集合的文档具有由两部分组成的特殊结构。第一部分包含关联所有者的主键,第二部分包含一个名为rows的字段,它将所有外键存储在一个嵌入式集合中。对于每个外键,嵌入式集合中都有一个文档。对于双向情况,创建另一个文档,其中 id 是相反的。

如果您熟悉关系模型,这种策略应该更接近您的体验。在图 2-9 中,可以看到associations_Players_Tournaments集合的部分内容——车主端的导航信息(Players)。

9781430257943_Fig02-09.jpg

图 2-9 。hibernate OGM-收集策略结果(协会 _ 玩家 _ 锦标赛收集)

您可以很容易地看到集合结构与GLOBAL_COLLECTION案例中的相同。惟一的区别是 _id 字段不再包含名为table的字段中的关联表名称,这是合乎逻辑的,因为关联表名称是集合名称的一部分(associations_Players_Tournaments)。

您可以通过将hibernate.ogm.mongodb.associations.store配置属性设置为值COLLECTION来使用这种存储关联导航信息的策略。

image 注意基于这个例子,你可以很容易地直觉到一对一、一对多和多对一情况下的关联是如何表示的。请记住,集合和字段名可以被 JPA 注释修改,比如@Column, @Table, @JoinTable等等。我给出的例子没有使用这样的注释。

从 JPA 的角度来看,当双向关联没有定义拥有方(使用mappedBy元素)时,Hibernate OGM 认为每一方都是一个单独的关联。换句话说,在这种情况下,你将获得两个关联,而不是一个。例如,COLLECTION策略将产生两个集合来存储两个关联。

现在,由您来决定哪种策略更符合您的需求。

管理交易

在从关系模型系统切换到像 Mongo DB 这样的 NoSQL 平台之前,理解它们之间的差异以及它们在您的应用需求环境中的优缺点是很重要的。只知道 MongoDB 不支持 SQL,而关系模型不支持集合和文档,会导致应用实现出现严重问题。这实际上是两者之间的根本区别,但还有许多其他的区别,包括消耗的空间量和执行语句、缓存、索引以及可能是最痛苦的事务管理所需的时间。

当开发人员意识到数据事务完整性是必须的时,许多 MongoDB 先锋项目都悲惨地失败了,因为 MongoDB 不支持事务。MongoDB 遵循这个指令:"写操作在单个文档的层次上是原子的:没有一个写操作可以原子地影响多个文档或多个集合。“它还提供了两阶段提交机制,用于模拟多个文档上的事务。你会在www.docs.mongodb.org/manual/tutorial/perform-two-phase-commits/找到更多细节。但是这两种机制都忽略了事务系统最强大的特性——回滚操作。

因此,如果您需要事务,使用 MongoDB 可能是一个微妙甚至不合适的选择。MongoDB 不是 SQL 的“时尚”选择,只有当它比 RDBMS 更能满足您的应用需求时,才应该使用它。当您的数据库模型不隐含事务或者当您可以使您的数据库模型不需要事务时,您应该选择 MongoDB。

Hibernate OGM 不能提供回滚功能,但是通过在刷新期间应用更改之前查询所有更改,它确实减少了事务问题。为此,OGM 建议使用事务分界来触发提交时的刷新操作。

管理查询

Hibernate OGM 提供了三种针对 MongoDB 数据库执行查询的解决方案:

  • 部分 JP-QL 支持
  • Hibernate 搜索
  • 原生 MongoDB 查询

这些都将在第六章中讨论和演示。

摘要

虽然这是一个简短的章节,但它包含了大量的信息。我介绍了管理 Hibernate OGM 和 MongoDB 之间关系的规则。您看到了如何从 Hibernate OGM 配置 MongoDB,以及如何根据 OGM 实现将数据持久化到 MongoDB 中。此外,我描述了事务的 MongoDB 视图,并以 Hibernate OGM 支持的查询机制的快速枚举作为结束。

三、引导 Hibernate OGM

由于 Hibernate OGM 充当 NoSQL 数据存储的 JPA 实现,很明显我们可以通过 JPA 引导它。此外,它也可以通过 Hibernate 本地 API 进行引导。无论您选择哪种方式来引导 Hibernate OGM,强烈建议您在 Java 事务 API (JTA)环境中使用它,即使您没有使用 Java EE。

在进入实际的引导过程之前,让我们先简要地看一下这些规范。在接下来的章节中,您会希望记住这些技术的主要特性。当然,如果你已经是一个大师,你可以直接跳过。

JPA 简要概述

Java 持久性 API 的目标是为存储、更新和映射关系数据库和 Java 对象之间的数据的操作提供支持,反之亦然。你可以说 JPA 是决定直接使用对象而不是 SQL 语句(ORM 范式)的开发人员的完美工具。

image 对象关系映射是一种编程技术,它在关系数据库和面向对象编程语言之间提供了一个虚拟的对象层。编程语言通过这一层读写关系数据库。您不用编写 SQL 语句来与数据库交互,而是使用对象。此外,代码更加清晰易读,因为它没有使用 SQL 语句进行“检测”。在写这本书的时候,JPA 规范有几个实现或持久性提供者。有些是受欢迎的、经过测试的、稳定的(EclipseLink、Hibernate 和 Apache OpenJPA),而有些可能不太常见,但具有非常高的基准性能(BatooJPA)。EclipseLink 是 JPA 的参考实现,它可以在 Java EE 环境和独立的 Java 应用中工作,每个 JPA 实现都应该这样。

JPA 易于使用,这要归功于持久性元数据 定义了 Java 对象和数据库表之间的关系。您可能熟悉语言级的持久性元数据,如 JDK 5.0 注释或 XDoclet 样式的注释,它们是类型安全的,并在编译时进行检查。可以说 JPA 注释实际上是普通的 JDK 5.0 注释。有些隐藏复杂的任务。一个这样的注释是javax.persistence.Entity ( @Entity注释),它用于标记应该在数据库中持久化的 POJO Java 类——用@Entity注释的每个类都存储在一个表中,每个表行都是一个实体类实例。实体必须定义主键(一个简单或复杂的主键,如果存在@GeneratedValue注释,则显式指定或自动生成)。实体不能是最终的,并且必须定义不带参数的构造函数。表名可以反映类名,也可以通过@Table注释显式提供,如@Table(name=" my_table_name ")

一个实体类定义了一组字段,每个字段默认为一个表中与该字段同名的列;您可以使用@Column注释对此进行更改,例如@Column(name=" my_column_name ")。JPA 可以通过 gettersetter 方法访问字段。默认情况下,用@Transient标注的字段不会被持久化,而其他字段会被持久化。

实体类是定义类(表)之间关系的地方。类与其他类可以有一对一(@OneToOne)、一对多(@OneToMany)、多对一(@ManyToOne)和多对多(@ManyToMany)的关系。当两个类存储彼此的引用时,这种关系是双向的,你必须用元素mappedBy在另一个类中指定关系的拥有方。当引用只是从一个类到另一个类而不是相反时,关系是单向的并且mappedBy元素是不必要的。

一旦有了反映数据库表的实体,就需要一个实体管理器(应用和持久性上下文之间的接口,),Hibernate 文档将其描述为“一组实体实例,其中对于任何持久性实体标识都有一个唯一的实体实例”,或者更简洁地说,一个实体管理器的所有实体能够提供存储、检索、合并和查找数据库中对象的方法。实际上,这是在 Java EE 环境中自动提供的javax.persistence.EntityManager,比如 GlassFish 或 JBoss。如果您在非 Java EE 环境中,比如 Tomcat 或 Java SE,您必须自己管理EntityManager生命周期。

可以由给定的EntityManager实例管理的一组实体(通常是逻辑相关的)被定义为一个持久性单元 ,其中的每一个都有一个惟一的名称,并驻留在一个名为 persistence.xml 的 XML 文档中。 Persistence.xml 是 JPA 的标准配置文件。它包含 JPA 提供者、JTA 或非 JTA 数据源、数据库连接信息,比如驱动程序、用户、密码、DDL 生成等等。(在 Java SE 应用中,该文件通常保存在源目录中名为 META-INF 的文件夹中,而在 web 应用中,该文件通常保存在 /src/conf 文件夹中,但是,根据应用的架构,它也可以位于其他位置)。一个 persistence.xml 文件可以包含多个持久性单元;根据您的应用使用的数据库,服务器将知道对哪个数据库执行查询。换句话说,通过一个持久性单元,应用用来获取应用管理的实体管理器的EntityManagerFactory被配置用于一组实体。您可以将此视为在 JPA 中实例化EntityManagerFactory的一种可移植的方式。

图 3-1 显示了 JPA 架构的主要组件之间的关系。

9781430257943_Fig03-01.jpg

图 3-1 。JPA 体系结构的主要组件之间的关系

嗯,真够快的。现在让我们来看看 JTA。

JTA 概述

Java 事务 API (JTA) 支持分布式事务。基本上,一个事务由一组任务(例如,SQL 语句)组成,它们必须作为一个不可分割的单元来处理。这是一个原子操作,事实上,“一个任务对所有人,所有任务对一个人”的规则是事务的首要原则。交易以 ACID 属性为特征,如下所示:

  • 原子性要求如果任何一个任务失败,那么事务失败,并且它被回滚。如果所有任务都成功执行,则事务被提交。换句话说,交易是一个要么全有要么全无的命题。

  • 一致性确保任何提交的事务将以有效状态离开数据库(根据所有定义的规则,写入的数据必须有效)。

  • 隔离意味着你的交易是你的,是你一个人的;没有其他事务可以接触它,因为数据库使用锁定机制来保护事务,直到它成功或失败地结束。有四种隔离级别:

  • Read Uncommitted: 您的事务可以读取其他事务的未提交数据(在多线程环境中从不推荐)。

  • Read Committed: 你的事务永远不能读取其他事务的未提交数据。

  • Repeatable: 您的事务将在多次读取相同的行时获得相同的数据,直到它结束。

  • Serializable: 这种隔离级别保证了您所接触的一切(所有表)在事务处理期间保持不变。这是最严格的隔离级别,开销最大,导致的性能瓶颈也最多。

  • 耐久性保证系统崩溃后,任何提交的交易都是安全的。

这些概念非常重要,因为事务通常会修改共享资源。

一般来说,有两种管理交易的方式:

  • Container Managed Transactions (CMT) use deployment descriptors or annotations (transaction attributes). In this case, the container is responsible for starting, committing, and rolling back a transaction. This is the declarative technique of demarcating transactions. In EJB containers, you can explicitly indicate a container-managed transaction using the annotation @TransactionManagement, like this:

    @TransactionManagement(TransactionManagementType.CONTAINER)

  • Moreover, you can tell the EJB container how to handle the transaction via the @TransactionAttribute annotation, which supports six values: REQUIRED (default), REQUIRES_NEW, SUPPORTS, MANDATORY, NOT_SUPPORTED, NEVER. For example, you can set MANDATORY like this:

    @TransactionAttribute(TransactionAttributeType.MANDATORY)

  • Bean Managed Transactions (BMT) require you to explicitly (programmatically) start, commit, and roll back transactions. This is the programmatic technique of demarcating transactions. In EJB containers, you can explicitly indicate a bean-managed transaction via the annotation @TransactionManagement, like this:

    @TransactionManagement(TransactionManagementType.BEAN)

有两种类型的交易:

  • 本地事务访问并更新单个网络资源(一个数据库)上的数据。
  • 分布式事务访问和更新两个或多个网络资源(多个数据库)上的数据。

从编程角度来说,JTA 是一个基于三个主要接口访问事务的高级 API :

  • UserTransaction:``javax.transaction.UserTransaction接口允许开发者以编程方式控制事务边界。为了划分 JTA 事务,您调用该接口的begincommitrollback方法。
  • TransactionManager:``javax.transaction.TransactionManager允许应用服务器控制事务边界。
  • XAResource:``javax.transaction.xa.XAResource是基于 X/Open CAE 规范的标准 XA 接口的 Java 映射。你可以在www.en.wikipedia.org/wiki/X/Open_XAwww.docs.oracle.com/javaee/6/api/javax/transaction/xa/XAResource.html找到更多关于 XA 的细节。

这是对 JTA 的一瞥。

MongoDB 和事务

MongoDB 不支持事务,这似乎是一种限制,取消了任何潜在的好处。MongoDB 仅在更改影响单个文档或单个文档的多个子文档时支持原子性。当更改(比如写操作)影响多个文档时,它们是而不是自动应用的,这可能导致数据不一致、其他操作交错等等。显然,由于对多个文档的更改不是原子性的,回滚是不适用的。

MongoDB 在一致性和持久性方面做得更好。MongoDB 写操作可以跨连接保持一致。此外,MongoDB 支持接近实时的复制,因此可以确保操作在返回之前已经被复制。

Hibernate OGM 减轻了 MongoDB 对事务支持的不足,它在刷新时应用更改之前对所有更改进行排队。即使 MongoDB 不支持事务,Hibernate OGM 也建议使用事务分界来透明地触发刷新操作(在提交时)。但是,正如官方文档所指出的,回滚不是一个选项。因此,本书中开发的应用将使用 Hibernate OGM 推荐的 JTA。

image 注意根据我提到的限制,很容易得出结论,MongoDB 不能满足我们应用的需求。但是,让我们考虑一下为什么我们会得出这样的结论。我们是否过于沉迷于复杂的数据库模式设计,有许多需要事务的连接和表,以及难以编写和管理的查询?在这里讨论这些问题远不是我的目的,但是也许你会花一点时间去思考它们,并为你的应用找到正确的答案。

Hibernate 原生 API 简介

直接使用 Hibernate API 的应用被称为本地 Hibernate 应用。开发一个本地 Hibernate 应用由几个简单的步骤组成,在这些步骤中,您可以:

  • 定义持久性类
  • 指定属性和映射文档
  • 将这些加载到应用的配置中
  • 基于这个配置,创建一个会话工厂
  • 从会话工厂获取(打开)会话
  • 执行查询和事务

本地 API 的起点和核心是org.hibernate.cfg.Configuration类,它使用属性和映射文档()。properties.cfg.xmlhbm.xml 文件)来创建org.hibernate.SessionFactory,一个线程安全的对象,它被实例化一次,并提供一个工厂来获取会话(org.hibernate.Session)。Session实例用于执行交易(JTA)和/或查询。

图 3-2 表示 Hibernate 原生 API 架构。

9781430257943_Fig03-02.jpg

图 3-2 。Hibernate 本地 API 架构

使用 JPA 引导 Hibernate OGM

使用 JPA 引导 Hibernate OGM 是最简单的情况,因为 Hibernate OGM 充当持久性提供者。如前所述,持久性提供者是在持久性单元内的 persistence.xml 文件中指定的。 persistence.xml JTA 或非 JTA;特定于数据库的要求;服务器配置;诸如此类。我试图为 Hibernate OGM 编写一个包含最低强制设置的 persistence.xml 文件。

  1. The first step is to write a persistence.xml skeleton, which (in a Java SE/EE application) generally looks like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence "                                              xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
    xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">
    ...
    </persistence>
    

    这个文件通常保存在源目录中一个名为 META-INF 的文件夹中,尽管在 web 应用中它通常保存在 /src/conf 文件夹中。

    接下来,您添加一个持久性单元;你想叫它什么都可以。JPA 实现可以通过 RESOURCE_LOCAL 自己管理事务,或者让应用服务器的 JTA 实现管理事务。您使用transaction-type属性来指定实体管理器工厂为持久性单元提供的实体管理器应该是 JTA 的还是资源本地的。这里我将把事务类型表示为JTA,因为我们想要使用 JTA 实体管理器。(无论服务器环境如何,Hibernate OGM 都推荐使用 JTA)。

    <persistence-unit name=" *{PU_NAME}*" transaction-type="JTA">
    </persistence-unit>
    ...
    

    记住不要使用RESOURCE_LOCAL(一个资源本地实体管理器),因为它使用基本的 JDBC 级事务,并且更特定于 Java SE 应用,而 JTA 是 Java EE 环境中的缺省值。

  2. 现在您需要指定持久性提供者。您可能熟悉用于 GlassFish v3 的 EclipseLink 2.0、用于 JBoss AS 7 的 Hibernate 4、用于 WebSphere 6 和 7 的 OpenJPA 以及用于 WebLogic 的 OpenJPA/KODO。对于 Hibernate OGM,提供者被命名为org.hibernate.ogm.jpa.HibernateOgmPersistence,它可以被显式地添加到 persistence.xml 中,比如:

    ...
    <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
    ...
    
  3. 现在我们来到了 persistence.xml 的属性部分。使用hibernate.transaction.jta.platform设置的第一个属性是 JTA 平台。该属性可以有以下值(这些类属于 Hibernate 核心;它们是部署在不同应用服务器上的事务管理器):

  • JBoss 应用服务器 7 ( www.jboss.org/as7 ) 1 org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform
  • Bitronix JTA 交易管理器(www.docs.codehaus.org/display/BTM/Home ) org.hibernate.service.jta.platform.internal.BitronixJtaPlatform
  • Borland 企业服务器 6.0 ( www.techpubs.borland.com/am/bes/v6/ ) org.hibernate.service.jta.platform.internal.BorlandEnterpriseServerJtaPlatform
  • JBoss 事务(已知与org.jboss.jbossts:jbossjta:4.9.0.GA一起工作的独立 JTA 事务管理器);不适用于 Jboss AS 7) ( www.jboss.org/jbosstm ) org.hibernate.service.jta.platform.internal.JBossStandAloneJtaPlatform
  • 乔纳斯·OSGi 企业服务器(OW2) ( www.jonas.ow2.org/xwiki/bin/view/Main/ ) org.hibernate.service.jta.platform.internal.JOnASJtaPlatform
  • Java 开放事务管理器(JOTM),一个独立的事务管理器(www.jotm.objectweb.org/ ) org.hibernate.service.jta.platform.internal.JOTMJtaPlatform
  • JRun 4 应用服务器(www.adobe.com/products/jrun/ ) org.hibernate.service.jta.platform.internal.JRun4JtaPlatform
  • NoJtaPlatform类,未配置 JTA 时使用的无操作版本(www.docs.jboss.org/hibernate/orm/4.0/javadocs/org/hibernate/service/jta/platform/internal/NoJtaPlatform.html ) org.hibernate.service.jta.platform.internal.NoJtaPlatform
  • Oracle 应用服务器 10 g (OC4J) ( www.oracle.com/technetwork/middleware/ias/index-099846.html ) org.hibernate.service.jta.platform.internal.OC4JJtaPlatform
  • Caucho 树脂应用服务器(www.caucho.com/ ) org.hibernate.service.jta.platform.internal.ResinJtaPlatform
  • Sun ONE Application Server 7(该事务管理器也与 GlassFish v3 应用服务器一起工作)(www.docs.oracle.com/cd/E19957-01/817-2180-10/pt_chap1.html ) org.hibernate.service.jta.platform.internal.SunOneJtaPlatform
  • Weblogic 应用服务器(www.oracle.com/us/products/middleware/cloud-app-foundation/weblogic/overview/index.html ) org.hibernate.service.jta.platform.internal.WeblogicJtaPlatform
  • WebSphere 应用服务器版本 6 ( www-01.ibm.com/software/webservers/appserv/was/ ) org.hibernate.service.jta.platform.internal.WebSphereExtendedJtaPlatform
  • WebSphere 应用服务器版本 4、5.0 和 5.1 ( www-01.ibm.com/software/webservers/appserv/was/ ) org.hibernate.service.jta.platform.internal.WebSphereJtaPlatform
  • 事务管理器查找桥,一个到遗留(和废弃的)org.hibernate.transaction.TransactionManagerLookup实现(www.docs.jboss.org/hibernate/orm/4.0/javadocs/org/hibernate/service/jta/platform/internal/TransactionManagerLookupBridge.html ) org.hibernate.service.jta.platform.internal.TransactionManagerLookupBridge的桥
  • 猎户座应用服务器——这个服务器好像已经不存在了org.hibernate.service.jta.platform.internal.OrionJtaPlatform

image 注意请记住,这些值在本书写作时是有效的。它们在 Hibernate 4.1 中是可用的,但是很有可能在将来会改变。你可以在www.docs.jboss.org/hibernate/orm/4.1/devguide/en-US/html_single/Hibernate 开发者指南中查看这个列表。

这里有一个为柯乔树脂设置 JTA 平台的例子:

...
<property name="hibernate.transaction.jta.platform"
         value="org.hibernate.service.jta.platform.internal.ResinJtaPlatform"/>
...

接下来的五个属性配置使用哪个 NoSQL 数据存储以及如何连接到它。例如,您可以通过设置数据存储提供者、网格方言(可选)、数据库、主机和端口来连接到现成的 MongoDB 发行版,如下所示:

...
<property name="hibernate.ogm.datastore.provider" value="mongodb"/>
<property name="hibernate.ogm.datastore.grid_dialect"
value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
<property name="hibernate.ogm.mongodb.database" value="test"/>
<property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
<property name="hibernate.ogm.mongodb.port" value="27017"/>
...

就这样!现在我们可以把这些片段粘在一起,为开箱即用的 MongoDB 提供一个通用的 persistence.xml ,如清单 3-1 所示。在下一章中,我们将修改这个文件以适应不同的环境。

清单 3-1。 一个通用的 persistence.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence "
                                           xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">
  <persistence-unit name=" *{PU_NAME}*" transaction-type="JTA">
    <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
    <properties>
      <property name="hibernate.transaction.jta.platform"
                value=" *{JTA_PLATFORM}*"/>
      <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
      <property name="hibernate.ogm.datastore.grid_dialect"
                value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
      <property name="hibernate.ogm.mongodb.database" value="test"/>
      <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
      <property name="hibernate.ogm.mongodb.port" value="27017"/>
    </properties>
  </persistence-unit>
</persistence>

使用 Hibernate Native API 引导 Hibernate OGM

前面,您看到了通过简单的几个步骤就可以开发一个本地 API 应用。其中三个步骤——加载属性并将文件映射到应用中;为当前配置创建全局线程安全SessionFactory;以及通过SessionFactory获得Session s(单线程工作单元)——通常在众所周知的HibernateUtil类中实现。(你可以写这个类,但是你也可以在网上找到不同的“形状”)不变的是,在这个类中,你会有一些类似这样的代码行(对于 Hibernate 3):

private static final SessionFactory sessionFactory;
...
sessionFactory = new Configuration().configure().buildSessionFactory();
...

看第二行,它通过一个org.hibernate.cfg.Configuration类的实例构建了SessionFactory。实际上,这是设置 Hibernate OGM 与原生 API 一起工作的入口点,因为您需要使用org.hibernate.ogm.cfg.OgmConfiguration类,而不是使用特定于 Hibernate ORM 的org.hibernate.cfg.Configuration类。因此,第二行将变成:

...
sessionFactory = new OgmConfiguration().configure().buildSessionFactory();
...

从 Hibernate 4 开始,这段代码将给出一个关于不推荐使用的方法buildSessionFactory()的警告。在这种情况下,javadoc 建议使用形式buildSessionFactory(ServiceRegistry serviceRegistry)。因此,如果您使用 Hibernate 4(推荐),请用下面的代码替换前面的代码:

private static final SessionFactory sessionFactory;
private static final ServiceRegistry serviceRegistry;
...
OgmConfiguration cfgogm = new OgmConfiguration();
cfgogm.configure();
serviceRegistry = new ServiceRegistryBuilder().
applySettings(cfgogm.getProperties()).buildServiceRegistry();
sessionFactory = cfgogm.buildSessionFactory(serviceRegistry);
...

这种方法(使用 Hibernate 3 或 4)需要一个包含特定配置的 hibernate.cfg.xml 文件。对于 Hibernate OGM,该文件需要包含正确的事务策略和正确的事务管理器查找策略。您必须通过设置 Hibernate 配置属性 hibernate.transaction.factory_class来为Transaction实例指定一个工厂类。可接受的值为:

  • org.hibernate.transaction.JDBCTransactionFactory—这是默认值,它代表数据库(JDBC)事务。
  • org.hibernate.transaction.JTATransactionFactory—使用 bean 管理的事务,这意味着您必须手动划分事务边界。
  • org.hibernate.transaction.CMTTransactionFactory—该值委托给容器管理的 JTA 事务。

通过编程,您可以像这样实现此设置:

...
OgmConfiguration cfgogm = new OgmConfiguration();
...
cfgogm.setProperty(Environment.TRANSACTION_STRATEGY,
" *{TRANSACTION_STRATEGY}*");
...

接下来,您必须通过设置名为hibernate.transaction.jta.platform 的属性来指定 JTA 平台。该属性的值必须由查找实现的完全限定类名组成。前面的“使用 JPA 引导 Hibernate OGM”一节中列出了可接受的值。

通过编程,您可以像这样实现此设置:

...
OgmConfiguration cfgogm = new OgmConfiguration();
...
cfgogm.setProperty(Environment.JTA_PLATFORM," *{JTA_PLATFORM}*");
...

最后,您需要配置您想要使用哪个 NoSQL 数据存储库以及如何连接到它。

对于开箱即用的 MongoDB 发行版,您需要设置数据存储提供者、网格方言(可选)、数据库、主机和端口,如下所示:

...
<property name="hibernate.ogm.datastore.provider">mongodb</property>
<property name="hibernate.ogm.mongodb.database">test</property>
<property name="hibernate.ogm.datastore.grid_dialect">
                org.hibernate.ogm.dialect.mongodb.MongoDBDialect</property>
<property name="hibernate.ogm.mongodb.host">127.0.0.1</property>
<property name="hibernate.ogm.mongodb.port">27017</property>
...

通过编程,您可以用清单 3-2 中的代码实现这些设置。

清单 3-2。 将 MongoDB 配置为数据存储

...
OgmConfiguration cfgogm = new OgmConfiguration();
...
cfgogm.setProperty("hibernate.ogm.datastore.provider","mongodb");
cfgogm.setProperty("hibernate.ogm.mongodb.database","test");
cfgogm.setProperty("hibernate.ogm.datastore.grid_dialect ","
                    org.hibernate.ogm.dialect.mongodb.MongoDBDialect");
cfgogm.setProperty("hibernate.ogm.mongodb.host","127.0.0.1");
cfgogm.setProperty("hibernate.ogm.mongodb.port","27017");
...  

因此,如果您使用非编程设置,那么 hibernate.cfg.xml 可能如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" " http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd ">
<hibernate-configuration>
  <session-factory>
    <property name="hibernate.transaction.factory_class">
       { *TRANSACTION_STRATEGY* }
    </property>
    <property name="hibernate.transaction.jta.platform">
       *{JTA_PLATFORM}*
    </property>
    <property name="hibernate.ogm.datastore.provider">mongodb</property>
    <property name="hibernate.ogm.mongodb.database">test</property>
    <property name="hibernate.ogm.datastore.grid_dialect">
       org.hibernate.ogm.dialect.mongodb.MongoDBDialect</property>
    <property name="hibernate.ogm.mongodb.host">127.0.0.1</property>
    <property name="hibernate.ogm.mongodb.port">27017</property>
    <mapping resource="..."/>
    ...
  </session-factory>
</hibernate-configuration>

清单 3-3 显示了使用这个配置文件的 HibernateUtil 类。

清单 3-3。 ibernateUtil

import java.util.logging.Level;
import java.util.logging.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.ogm.cfg.OgmConfiguration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

/**
 * HibernateUtil class (based on hibernate.cfg.xml)
 *
 */
public class HibernateUtil {

    private static final Logger log = Logger.getLogger(HibernateUtil.class.getName());
    private static final SessionFactory sessionFactory;
    private static final ServiceRegistry serviceRegistry;

    static {
        try {
            // create a new instance of OmgConfiguration
            OgmConfiguration cfgogm = new OgmConfiguration();

            //process configuration and mapping files
            cfgogm.configure();
            // create the SessionFactory
            serviceRegistry = new ServiceRegistryBuilder().
                 applySettings(cfgogm.getProperties()).buildServiceRegistry();
            sessionFactory = cfgogm.buildSessionFactory(serviceRegistry);
        } catch (Throwable ex) {
            log.log(Level.SEVERE,
                    "Initial SessionFactory creation failed !", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}

如果你使用的是编程设置,你不需要一个 hibernate.cfg.xml 文件,你的 HibernateUtil 将会如清单 3-4 所示。

清单 3-4。?? 一个不需要 Hibernate.cfg.xml 的 HibernateUtil 类

import java.util.logging.Level;
import java.util.logging.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.hibernate.ogm.cfg.OgmConfiguration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

/**
 * HibernateUtil class (no need of hibernate.cfg.xml)
 *
 */
public class HibernateUtil {

    private static final Logger log = Logger.getLogger(HibernateUtil.class.getName());
    private static final SessionFactory sessionFactory;
    private static final ServiceRegistry serviceRegistry;

    static {
        try {
            // create a new instance of OmgConfiguration
            OgmConfiguration cfgogm = new OgmConfiguration();

            // enable transaction strategy
            cfgogm.setProperty(Environment.TRANSACTION_STRATEGY,
                                                "{ *TRANSACTION_STRATEGY*}");
            // specify JTA platform
            cfgogm.setProperty(Environment.JTA_PLATFORM, "{ *JTA_PLATFORM*}");

            //configure MongoDB connection
            cfgogm.setProperty("hibernate.ogm.datastore.provider", "mongodb");
            cfgogm.setProperty("hibernate.ogm.datastore.grid_dialect",
              "org.hibernate.ogm.dialect.mongodb.MongoDBDialect");
            cfgogm.setProperty("hibernate.ogm.mongodb.database", "test");
            cfgogm.setProperty("hibernate.ogm.mongodb.host", "127.0.0.1");
            cfgogm.setProperty("hibernate.ogm.mongodb.port", "27017");

            //add our annotated class
            cfgogm.addAnnotatedClass(*.class);

            // create the SessionFactory
            serviceRegistry = new ServiceRegistryBuilder().
               applySettings(cfgogm.getProperties()).buildServiceRegistry();
            sessionFactory = cfgogm.buildSessionFactory(serviceRegistry);
        } catch (Throwable ex) {
            log.log(Level.SEVERE,
                "Initial SessionFactory creation failed !", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}

现在,图 3-2 中呈现的 Hibernate Native API 可以重绘成图 3-3 中的样子。

9781430257943_Fig03-03.jpg

图 3-3 。Hibernate OGM 中的 Hibernate Native API 架构

image 注意使用默认配置(org/hibernate/ogm/datastore/Infinispan/default-config . XML)设置 infini span 可以通过将hibernate.ogm.datastore.provider属性的值指定为infinispan来完成。通过将相同的属性设置为Ehcache,您可以使用默认配置(org/hibernate/ogm/datastore/Ehcache/default-Ehcache . XML)设置 Ehcache。对于这两个 NoSQL 产品,Hibernate OGM 还支持一个特定的属性来指示 XML 配置文件。对于 Infinispan,该属性称为hibernate . ogm . Infinispan . configuration _ resourcename,对于 Ehcache,该属性称为hibernate . ogm . Ehcache . configuration _ resourcename。因此,对于 Infinispan 和 Ehcache,不需要设置方言、数据库、端口和主机。

Hibernate OGM 过时配置选项

随着 Hibernate OGM 的出现,Hibernate ORM 中的一组选项不再可用。因此,根据 Hibernate OGM 规范,以下选项不应在 OGM 环境中使用:

  • hibernate.dialect
  • hibernate.connection.*特别是hibernate.connection.provider_class
  • hibernate.show_sql and hibernate.format_sql
  • hibernate.default_schema and hibernate.default_catalog
  • hibernate.use_sql_comments
  • hibernate.jdbc.*
  • hibernate.hbm2ddl.autohibernate.hbm2ddl.import_file

摘要

在简要了解了 Java 持久性 API (JPA) 、Java 事务 API (JTA) 和 Hibernate 本机 API 之后,您看到了如何使用 JPA 和 Hibernate 本机 API 引导 Hibernate OGM。您了解了如何编写一个通用的 persistence.xml,以及如何为 Hibernate OGM 实现一个 HibernateUtil 类。最后,您看到了 Hibernate OGM 中不再提供的 Hibernate ORM 配置属性列表。

12013 年 4 月 Red Hat,Inc .宣布下一代 JBoss 应用服务器将被称为 Wildfly。参见http://gb.redhat.com/about/news/press-archive/2013/4/red-hat-reveals-plans-for-its-next-generation-java-application-server-project

四、实际场景中的 Hibernate OGM

到目前为止,您已经了解了 Hibernate OGM 可以通过 Java 持久性 API 或 Hibernate 本机 API 来使用。此外,您了解了实现 Hibernate OGM 引导的原理,并且已经查看了一些相关的代码片段。显然,从这些代码片段跳到真正的应用需要的不仅仅是复制和粘贴,因为您必须处理集成过程以及每个环境的特定特性和设置。

试图给出一个完全符合每个程序员需求的例子是不可救药的,但是我能做的是提供一系列使用 Hibernate OGM 的例子。在本章中,我将向您展示一些现成的 Hibernate OGM 应用,它们可以部署在 Java EE 6 服务器(如 JBoss 和 GlassFish)和 Web 服务器(如 Tomcat)上,使用 Seam 和 Spring 等框架以及 EJB 等规范。除了直接与 Hibernate OGM 交互的内核技术之外,我们还将使用一些开发工具,如 NetBeans 和 Eclipse 等 ide,以及 Maven、JBoss Forge、Ant 等,它们帮助我们以最少的工作量构建应用。把这些工具看作是我的选择,而不是必须的。您可以使用产生相同结果的任何其他工具。

整套应用共享一些简单的业务逻辑,在 MongoDB 集合中存储一个随机整数——我们称这个整数为幸运数字。正如您将看到的,存储的整数甚至不是用户请求的;它是在用户按下按钮时随机生成的(每次按下按钮都会生成并存储一个新的整数)。这个琐碎的业务逻辑的要点是尽可能保持应用代码简单,并关注 Hibernate OGM 与上下文的集成。我们真正关心的是成功地将 Hibernate OGM 绑定到应用上下文,并设置与 MongoDB 的交互。在后面的章节中,我们将有足够的时间来讨论 MongoDB 的高级设置、存储原则、JP-QL、Hibernate 搜索等等。

一般先决条件

在我们开始之前,确保您已经正确安装了 MongoDB(正如您在第一章中看到的),并且您有可用的 Hibernate OGM JARs,包括 MongoDB 支持所需的 jar(本地或通过 Maven 工件)。其余的工具,比如应用服务器、框架、ide 等等,可以根据你的需求单独安装;你可能不会对下面所有的例子感兴趣。在任何情况下,为了测试本章中完整的应用套件,您将需要以下内容:

  • Java EE 6
  • JDK 1.7
  • GlassFish AS 3(与 NetBeans 7.2.1 或 7.3 捆绑在一起)
  • JBoss AS 7(应单独安装)
  • Apache Tomcat 7(与 NetBeans 7.2.1 或 7.3 捆绑在一起)
  • NetBeans 7.2.1 或 7.3(建议与 GlassFish AS 3 和 Tomcat 7 一起使用)
  • Eclipse JUNO (JBoss AS 7 可以通过 JBoss AS 工具在这个 Eclipse 发行版下进行配置)
  • MongoDB 2.2.2(你应该从第一章的开始安装)
  • Hibernate OGM 4.0.0.Beta2(来自第一章,你应该有一个名为 Hibernate OGM Core 和 MongoDB 的 NetBeans 和 Eclipse 库)
  • MongoDB Java 驱动程序 2.8.0(这存在于 Hibernate OGM 核心和 MongoDB 库中)
  • JBoss JTA 4.16.4 最终版本
  • Forge 1.0.5 或 1.3.1(独立或作为 Eclipse 插件运行)
  • Spring 3.3.1(与 NetBeans 7.2.1 或 7.3 捆绑在一起)

此外,在您开始之前,您可能会发现了解以下内容会有所帮助:

  • 本章介绍的每个应用都可以从 Apress 资源库下载。每个应用都有一小段描述应用名称和测试的技术条件。换句话说,你没有必要在阅读这本书的时候重新构建每一个应用,除非你想这么做。
  • 这些示例向您展示了如何在涉及多种技术的不同类型的应用中集成 MongoDB 和 Hibernate OGM。如您所知,这样的应用需要许多附加文件——XML 配置文件、XHTML 页面、servlets、托管 beans、控制器等等。我试图保持代码尽可能的干净,以便更容易理解如何集成 MongoDB 和 Hibernate OGM,所以我跳过了不相关的“意大利面条”代码。此外,我不会试图教您如何创建 servlet、会话 bean 或 XHTML 页面,或者如何编写 web.xml 文件。我假设您已经知道如何使用 NetBeans、Eclipse 或其他 IDE。不要期望看到循序渐进的 NetBeans 或 Eclipse 教程。

image 注意对于作为 Apache Maven 项目开发的应用,不要忘记编辑settings.xml,就像你在第一章中看到的那样。或者,如果你认为settings.xml太冗长,你可以简单地在你的pom.xml中使用<repository>标签。但是请记住,缺少存储库会导致错误。

Java SE 和 MongoDB——Hello World 示例

我们将以一个例外开始我们的应用系列:第一个应用不涉及 Hibernate OGM。这个应用实际上只是一个快速测试,以确保 MongoDB 服务器正在运行并对连接尝试做出响应。考虑一下我们面向 Java-MongoDB 新手的 Hello World 应用。如果你认为这是浪费你的时间,你可以跳过它。要不,我们走吧!

这是有史以来最简单的 Java SE/MongoDB 示例——它只是将一个随机数存储到 MongoDB 集合中。

先决条件

  • MongoDB 2.2.2
  • MongoDB Java 驱动程序 2 . 8 . 0(mongo-Java-driver-2 . 8 . 0 . jar)
  • JDK 1.7
  • NetBeans 7.2.1(或 Eclipse JUNO)

发展中的

启动 NetBeans 后,创建一个由简单的 Maven Java 应用组成的新项目,并将其命名为HelloWorld。在New Java Application向导中,为Artifact Id键入HelloWorld,为Group IdPackage键入hello.world.mongodb。一旦您看到项目列在Projects窗口中,编辑pom.xml文件(它必须在Project Files节点下)。在pom.xml文件中,通过粘贴以下代码添加 MongoDB Java 驱动程序版本 2.8.0:

<dependencies>
   <dependency>
      <groupId>org.mongodb</groupId>
      <artifactId>mongo-java-driver</artifactId>
      <version>2.8.0</version>
</dependency>
..
<dependencies>

现在保存项目,驱动程序 JAR 将列在Dependencies节点下。

image 注意如果你不是 Maven 迷,创建一个简单的 Java 应用,从 GitHub 下载 MongoDB Java 驱动 2.8.0,https://github.com/mongodb/mongo-java-driver/downloads。显然,在这种情况下,你必须手动将其添加到Libraries节点。

现在必要的库已经可用了。接下来,编辑应用的主类。如果你没有重命名它,它会被列为hello.world.mongodb.helloworld包的Source Packages节点中的App.java。在main方法下,逐步插入以下代码:

  1. 在默认端口 270127

    Mongo mongo = new Mongo("127.0.0.1", 27017);
    

    上连接到本地主机(127.0.0.1)上的 MongoDB 存储

  2. 创建一个名为helloworld_db的 MongoDB 数据库。这个数据库很可能是由 MongoDB 自动创建的,因为它并不存在。

    DB db = mongo.getDB("helloworld_db");
    
  3. 创建一个名为helloworld的 MongoDB 集合。这个集合可能会由 MongoDB 在helloworld_db数据库中自动创建,因为它并不存在。

    DBCollection dbCollection = db.getCollection("helloworld");
    
  4. 创建用于存储键/值对的文档。密钥只是文本,值是生成的数字。

    BasicDBObject basicDBObject = new BasicDBObject();
     basicDBObject.put("Lucky number", new Random().nextInt(1000));
    
  5. 将该对保存到helloworld集合:

    dbCollection.insert(basicDBObject);
    

搞定了。

现在,把这五个步骤放在一起。清单 4-1 显示了结果。

清单 4-1。Hello World 举例

package hello.world.mongodb.helloworld;

import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import java.net.UnknownHostException;
import java.util.Random;

/**
 * Hello world!
 *
 */
public class App {

    public static void main(String[] args) {
        try {
            // connect to the MongoDB store
            Mongo mongo = new Mongo("127.0.0.1", 27017);

            // get the MongoDB database, helloworld_db
            DB db = mongo.getDB("helloworld_db");

            //get the MongoDB collection named helloworld
            DBCollection dbCollection = db.getCollection("helloworld");

            // create a document for storing a key/value pair
            BasicDBObject basicDBObject = new BasicDBObject();
            basicDBObject.put("Lucky number", new Random().nextInt(1000));

            // save the pair into helloworld collection
            dbCollection.insert(basicDBObject);

            System.out.println("MongoDB has stored the lucky number!");

        } catch (UnknownHostException e) {
            System.err.println("ERROR: " + e.getMessage());
        } catch (MongoException e) {
            System.err.println("ERROR: " + e.getMessage());
        }
    }
}

测试

按照第一章中的方法启动 MongoDB 服务器。接下来,由于您在 NetBeans(或 Eclipse)中,有一个Run按钮可以实现这一功能。Run应用,如果你得到消息“ MongoDB 已经存储了幸运数字!”,一切都完美地进行着。

打开命令提示符,键入图 4-1 所示的命令,查看您的工作结果。

9781430257943_Fig04-01.jpg

图 4-1 。查看“helloworld”收藏内容

如果您没有获得类似的结果,那么在创建下一个应用之前必须解决一个问题。

完整的 Hello World 应用可以在 Apress 存储库中获得,当然,它被命名为HelloWorld。它是一个 NetBeans 项目,在 JDK 1.7 和 MongoDB 2.2.2 下进行了测试。

通过 Hibernate Native API 使 OGM Hibernate

一旦您确认 MongoDB 已经准备好为您的应用提供服务,就该转向 Hibernate OGM 了。在这一节中,我们将使用 Hibernate 本机 API 开发一系列涉及 Hibernate OGM 的应用。以下是我们将开发的应用:

  • 在非 JTA 环境中 Hibernate OGM(JDBC 事务,Apache Tomcat 7)
  • 在独立的 JTA 环境中 Hibernate OGM(JBoss JTA,Apache Tomcat 7)
  • 在内置的 JTA 环境中 Hibernate OGM(没有 EJB,GlassFish AS 3)
  • 在内置的 JTA 环境中 Hibernate OGM(EJB 3/BMT,GlassFish AS 3)
  • 在内置的 JTA 环境中 Hibernate OGM(EJB 3/CMT,GlassFish AS 3)

非 JTA 环境中的 hibernate OGM(JDBC 事务,Apache Tomcat 7)

这个应用将通过 Hibernate Native API 在非 JTA 环境中引导 Hibernate OGM。我们将使用旧式的 JDBC 交易,而不是 JTA。实际上,我们将使用 Hibernate 的事务 API 和内置的每请求会话功能,而不是直接调用 JDBC API。该应用将部署在 Apache Tomcat 7 web 容器下。

image 注意当 Hibernate OGM 在非 JTA 环境中使用时,回滚特性不能得到保证。这就是为什么 Hibernate OGM 团队不推荐在 Hibernate OGM 4.0.0.Beta2 发行版中使用这种环境,但是这种情况有望在下一个发行版中变得更加有利。因为我们使用的是不支持事务的 MongoDB,所以这不是我们所关心的。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta2
  • JDK 1.7
  • NetBeans 7.2.1(或 Eclipse JUNO)
  • 阿帕奇雄猫 7

发展中的

启动 NetBeans 后,创建一个包含一个空 Maven web 应用的新项目,并将其命名为HOGMviaHNAPI_JDBC_Tomcat7。在New Web Application向导中,为Group IdPackage字段键入hogm.hnapi。不要忘记选择 Apache Tomcat 7 web 服务器来部署这个应用。一旦你看到在Projects窗口中列出的项目,编辑pom.xml文件(它必须在Project Files节点下)。在pom.xml文件中,通过粘贴以下依赖项来添加 Hibernate OGM 发行版(包括 MongoDB 支持):

<dependencies>
   <dependency>
      <groupId>org.hibernate.ogm</groupId>
      <artifactId>hibernate-ogm-core</artifactId>
      <version>4.0.0.Beta2</version>
   </dependency>
   <dependency>
      <groupId>org.hibernate.ogm</groupId>
      <artifactId>hibernate-ogm-mongodb</artifactId>
      <version>4.0.0.Beta1</version>
   </dependency>
...
<dependencies>

现在保存项目,MongoDB Java 驱动程序 JAR 将列在 Dependencies 节点下。

编写应用代码

现在我们准备添加一些代码。我们从一个简单的 POJO 类开始,它能够表示数据库中的对象。正如你在清单 4-2 中看到的,该类包含一个名为luckynumber的字段(除了主键字段)和众所周知的 getter 和 setter 方法。

清单 4-2。??【幸运数字】Pojo 类

package hogm.hnapi.pojo;

public class LuckyNumberPojo {

    private String id;
    private int luckynumber;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public int getLuckynumber() {
        return luckynumber;
    }

    public void setLuckynumber(int luckynumber) {
        this.luckynumber = luckynumber;
    }
}

大多数使用 Hibernate 的应用需要一个名为HibernateUtil,的特殊类,它是一个助手类,提供对代码中任何地方的SessionFactory的访问。互联网上有很多 Hibernate ORM 的版本,比如“买者自负”演示中的那个。对于 Hibernate OGM,我们可以基于 Hibernate ORM 的最简单版本开发一个HibernateUtil,它通常看起来像清单 4-3 中的所示。您可能对它很熟悉,并且在 Hibernate 3 中使用过多次。

清单 4-3。 一个基本的冬眠类用于冬眠 ORM

import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {

    private static final SessionFactory sessionFactory;

    static {
        try {
            sessionFactory = new
                             Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}

现在,为 Hibernate OGM 开发一个HibernateUtil是基于这个源代码的两个主要修改的任务。首先,我们需要实例化用于配置 Hibernate OGM 环境的OgmConfiguration类,而不是创建一个Configuration类的新实例。其次,从 Hibernate 4 开始,会话工厂必须通过传递给buildSessionFactory方法的服务注册中心获得。记住这些,代码可以很容易地转换成 Hibernate OGM 的HibernateUtil,如清单 4-4 中的所示。

清单 4-4。 一个 HibernateUtil 类为 Hibernate OGM

package hogm.hnapi.util.with.hibernate.cfg;

import java.util.logging.Level;
import java.util.logging.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.ogm.cfg.OgmConfiguration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

public class HibernateUtil {

    private static final Logger log =
            Logger.getLogger(HibernateUtil.class.getName());
    private static final SessionFactory sessionFactory;
    private static final ServiceRegistry serviceRegistry;

    static {
        try {
            // create a new instance of OmgConfiguration
            OgmConfiguration cfgogm = new OgmConfiguration();

            // process configuration and mapping files
            cfgogm.configure();
            // create the SessionFactory
            serviceRegistry = new ServiceRegistryBuilder().
             applySettings(cfgogm.getProperties()).buildServiceRegistry();
            sessionFactory = cfgogm.buildSessionFactory(serviceRegistry);
        } catch (Throwable ex) {
            log.log(Level.SEVERE,
                    "Initial SessionFactory creation failed !", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}

为了从这个HibernateUtil,获得有效的会话工厂,我们需要构建 Hibernate 配置文件(hibernate.cfg.xml)和相应的映射文件(*.hbm.xml)。如您所知,hibernate.cfg.xml文件包含了用于调整 Hibernate 环境和数据库连接的主要信息。因为我们处于非 JTA 环境中,并且遵循众所周知的 Hibernate 线程绑定策略(Hibernate 将当前会话绑定到当前 Java 线程),所以我们首先设置访问该策略所必需的两个属性:

<property name="hibernate.transaction.factory_class">
          org.hibernate.transaction.JDBCTransactionFactory
</property>
<property name="hibernate.current_session_context_class">
          thread
</property>

接下来的五个属性是我们的重中之重,因为它们代表了 MongoDB 配置,所以我们指定了数据存储提供者、方言、要连接的数据库的名称、MongoDB 服务器主机和端口(我们将使用 localhost 和 MongoDB 服务器的默认端口 27017):

<property name="hibernate.ogm.datastore.provider">mongodb</property>
<property name="hibernate.ogm.datastore.grid_dialect">
          org.hibernate.ogm.dialect.mongodb.MongoDBDialect</property>
<property name="hibernate.ogm.mongodb.database">tomcat_db</property>
<property name="hibernate.ogm.mongodb.host">127.0.0.1</property>
<property name="hibernate.ogm.mongodb.port">27017</property>

最后,我们添加映射资源,在本例中,它由单个类LuckyNumberPojo表示。添加最后一行:

<mapping resource="/LuckyNumberPojo.hbm.xml"/>

hibernate.cfg.xml的末尾,得到中所示的代码,清单 4-5 。

清单 4-5。 一个 Hibernate 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd ">
<hibernate-configuration>
  <session-factory>
    <property name="hibernate.transaction.factory_class">
              org.hibernate.transaction.JDBCTransactionFactory</property>
    <property name="hibernate.current_session_context_class">thread</property>
    <property name="hibernate.ogm.datastore.provider">mongodb</property>
    <property name="hibernate.ogm.datastore.grid_dialect">
              org.hibernate.ogm.dialect.mongodb.MongoDBDialect</property>
    <property name="hibernate.ogm.mongodb.database">tomcat_db</property>
    <property name="hibernate.ogm.mongodb.host">127.0.0.1</property>
    <property name="hibernate.ogm.mongodb.port">27017</property>
    <mapping resource="/LuckyNumberPojo.hbm.xml"/>
  </session-factory>
</hibernate-configuration>

当 web 应用启动时,文件hibernate.cfg.xml必须位于类路径的根目录中。在一个 Maven 项目中,就像这样,它应该保存在src/main/resources目录中(在 NetBeans 中,这个目录可以在Other Sources节点中找到)。在非 Maven 应用中,将文件保存在WEB-INF/classes目录中。

写作是我们的下一个目标。因为我们有一个普通的 POJO,所以任务很简单。首先,我们将主键字段和生成器描述为 UUID2。(这将生成符合 IETF RFC 4122(变体 2)的 128 位 UUID。更多详情请见www.ietf.org/rfc/rfc4122.txt。)然后我们描述luckynumber场。结果如清单 4-6 所示。

清单 4-6。luckynumberpojo . hbm . XML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" " http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd ">
<hibernate-mapping>
    <class name="hogm.hnapi.pojo.LuckyNumberPojo" table="jdbc">
        <id name="id" type="string">
            <column name="id" />
            <generator class="uuid2" />
        </id>
        <property name="luckynumber" type="int">
            <column name="luckynumber"/>
        </property>
    </class>
</hibernate-mapping>

该文件应该放在与hibernate.cfg.xml.相同的文件夹中

赋值table="jdbc"在 MongoDB 中创建了一个名为jdbc的集合。如果你想创建一个名为 XXX .jdbc,的收藏,你可以像这样添加catalog=" XXX ",:

<class name="hogm.hnapi.pojo.LuckyNumberPojo" table="jdbc" catalog=" *XXX* ">

最后,我们已经到了可以添加一些业务逻辑的时候了。我们将编写一个 DAO 类,将幸运数字保存到数据库中。这样的类通常至少包含所有 CRUD 操作的方法。然而,我们需要的只是一个持久化操作的方法。实际上,persist 有两个实现,每个打开会话策略一个。如您所知,Hibernate 提供了用于获取当前会话的getCurrentSessionopenSession方法。调用getCurrentSession将 Hibernate 在后台绑定的“当前”会话返回到事务范围,或者在第一次调用getCurrentSession时打开一个新的会话。只要事务运行,该会话在代码中的任何地方都是可用的,并且当事务结束时,它会自动关闭并刷新。如果你想显式刷新和关闭会话,你必须使用openSession方法。清单 4-7 展示了我们的带有两个persist方法的 DAO 类,一个用于getCurrentSession,一个用于openSession。两者都使用声明性的事务边界划分,使用org.hibernate.Session方法,如beginTransactioncommit

清单 4-7。 刀法类用两种persist

package hogm.hnapi.dao;

import hogm.hnapi.pojo.LuckyNumberPojo;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.hibernate.Session;

public class LuckyNumberDAO {

    private static final Logger log = Logger.getLogger(LuckyNumberDAO.class.getName());

     /**
     * Insert data (use getCurrentSession and POJO)
     *
     * @param transientInstance
     * @throws Exception
     */
    public void persist_cs_with_cfg(LuckyNumberPojo transientInstance) throws java.lang.Exception {

        log.log(Level.INFO, "Persisting LuckyNumberPojo instance ...");
        Session session = hogm.hnapi.util.with.hibernate.cfg.HibernateUtil.
                                      getSessionFactory().getCurrentSession();
        try {
             session.beginTransaction();
             session.persist(transientInstance);
             session.getTransaction().commit();

             log.log(Level.INFO, "Persist successful ...");
        } catch (RuntimeException re) {
            session.getTransaction().rollback();
            log.log(Level.SEVERE, "Persist failed ...", re);
            throw re;
        }
    }

    /**
     * Insert data (use openSession and POJO)
     *
     * @param transientInstance
     * @throws Exception
     */
    public void persist_os_with_cfg(LuckyNumberPojo transientInstance) throws java.lang.Exception {

        log.log(Level.INFO, "Persisting LuckyNumberPojo instance ...");
        Session session = hogm.hnapi.util.with.hibernate.cfg.HibernateUtil.
                                     getSessionFactory().openSession();

        try {
             session.beginTransaction();
             session.persist(transientInstance);
             session.flush(); // flush happens automatically anyway
             session.getTransaction().commit();

             log.log(Level.INFO, "Persist successful...");
        } catch (RuntimeException re) {
            session.getTransaction().rollback();
            log.log(Level.SEVERE, "Persist failed...", re);
            throw re;
        } finally {
            session.close();
        }
    }
}

image 注意虽然这里没有列出,但是这个应用的源代码(可以在 Apress 资源库中找到)也演示了使用实体而不是 POJO,并用HibernateUtil类中的编程配置替换hibernate.cfg.xml

我们快完成了!我们剩下要实现的就是一个简单的用户界面和一个 servlet。当用户按下界面中的一个按钮时,一个空表单被提交给 servlet,servlet 调用我们的 DAO 类(persist_cs_with_cfgpersist_os_with_cfg)将生成的幸运数字存储到数据库中。servlet 的主要代码片段如清单 4-8 所示。

清单 4-8。 幸运数字 Servlet

package hogm.hnapi.servlet;
...
@WebServlet(name = "LuckyNumberServlet", urlPatterns = {"/LuckyNumberServlet"})
public class LuckyNumberServlet extends HttpServlet {
 ...
 protected void processRequest(HttpServletRequest request, HttpServletResponse
 response) throws ServletException, IOException, Exception {
   ...
   LuckyNumberDAO luckyNumberDAO = new LuckyNumberDAO();
   LuckyNumberPojo luckyNumberPojo = new LuckyNumberPojo();
   luckyNumberPojo.setLuckynumber(new Random().nextInt(1000000));

   luckyNumberDAO.persist_cs_with_cfg(luckyNumberPojo);
   // luckyNumberDAO.persist_os_with_cfg(luckyNumberPojo);
   ...
  }
}

提交给这个 servlet 的 HTML 表单也非常简单。代码放在index.jsp页面上,在 NetBeans 项目中,该页面列在项目的Web Pages节点下)。

...
<form action="./LuckyNumberServlet" method="POST">
    <input type="submit" value="Generate Lucky Number">
</form>
...
Done!

测试

启动 MongoDB 服务器,如你在第一章中所见。接下来,因为您处于 NetBeans/Tomcat(或 Eclipse/Tomcat)环境中,所以只需保存项目并单击Run(或 Eclipse 中的Run on Server)按钮来启动 Tomcat 并部署和运行应用。如果应用成功启动,你会在浏览器中看到类似于图 4-2 所示的东西。

9781430257943_Fig04-02.jpg

图 4-2 。运行 HOGMviaHNAPI_JDBC_Tomcat7 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(tomcat_db)集合(jdbc))中。打开命令提示符,输入如图 4-3 所示的命令,查看您的工作结果。这让您可以监控 Tomcat 日志消息,以防发生任何不必要的事情。

9781430257943_Fig04-03.jpg

图 4-3 。正在检查“jdbc”收集内容

这个名为HOGMviaHNAPI_JDBC_Tomcat7的应用的完整源代码可以在 Apress 资源库中找到。它是一个 NetBeans 项目,在 Tomcat 7 下测试过。(我用的是 NetBeans 7.2.1 捆绑的 Tomcat。)

在独立的 JTA 环境中 Hibernate OGM(JBoss JTA,Apache Tomcat 7)

我们的下一个应用将在独立的 JTA 环境中通过 Hibernate Native API 引导 Hibernate OGM。正如您将看到的,Hibernate 可以在任何使用 JTA 的环境中工作,事实上,它可以自动将当前会话绑定到当前 JTA 事务。由于 Tomcat 不是 J2EE 环境,它不提供自动 JTA 事务管理器,所以我们必须选择 JTA 的独立实现。有几个开源的实现,如 JOTM,Bitronix JTA 和 Atomikos,但我更喜欢 JBoss JTA。它是著名的 JBoss TS(阿尔诸那事务服务)的一部分,附带了 JTA 和 JTS API 的一个非常健壮的实现。

现在让我们看看这个应用的先决条件是什么。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta2
  • JBoss JTA 4.16.4 最终版本
  • JDK 1.7
  • NetBeans 7.2.1(或 Eclipse JUNO)
  • 阿帕奇雄猫 7

发展中的

启动 NetBeans 并创建一个包含一个空 Maven web 应用的新项目,并将其命名为HOGMviaHNAPI_JTA_Tomcat7。在New Web Application向导中,为Group IdPackage字段键入hogm.hnapi。不要忘记选择 Apache Tomcat 7 web 服务器来部署这个应用。一旦您看到项目列在Projects窗口中,编辑pom.xml文件(它必须在Project Files节点下)。在pom.xml中,通过粘贴以下依赖项来添加 Hibernate OGM 发行版(包括 MongoDB 支持)。

<dependencies>
   <dependency>
      <groupId>org.hibernate.ogm</groupId>
      <artifactId>hibernate-ogm-core</artifactId>
      <version>4.0.0.Beta2</version>
   </dependency>
   <dependency>
      <groupId>org.hibernate.ogm</groupId>
      <artifactId>hibernate-ogm-mongodb</artifactId>
      <version>4.0.0.Beta1</version>
   </dependency>
...
<dependencies>

现在保存项目,驱动程序 JAR 将列在Dependencies节点下。

我们仍然需要在应用类路径中为 JBoss JTA 添加 jar,所以现在添加这个依赖项:

<dependencies>
   <dependency>
      <groupId>org.jboss.jbossts</groupId>
      <artifactId>jbossjta</artifactId>
      <version>4.16.4.Final</version>
   </dependency>
...
<dependencies>

编写应用代码

我们现在已经有了所有必要的工件,所以是时候开始开发应用了。首先,我们将创建一个基本的实体类,它可以表示数据库中的对象。该类将只包含一个名为luckynumber的字段(除了主键)。您应该熟悉这类实体,从技术上讲,它们只是带注释的 POJOs。(更多细节,请参考第二章。)清单 4-9 展示了LuckyNumberEntity类。

清单 4-9。lucky number entity 类

package hogm.hnapi.pojo;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name="jta")
public class LuckyNumberEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name="uuid", strategy="uuid2")
    private String id;

    @Column(name="luckynumber", nullable=false)
    private int luckynumber;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public int getLuckynumber() {
        return luckynumber;
    }

    public void setLuckynumber(int luckynumber) {
        this.luckynumber = luckynumber;
    }        
}

在前面的应用中,我们使用了一个简单的 POJO,并且我们开发了一个专门设计的HibernateUtil类,用于在基于hibernate.cfg.xml和映射文件的代码中的任何地方获得会话工厂。在这个应用中,我们将采用另一种方法——我们将使用一个实体(一个用 JDK 5 注解扩展的 POJO)和一个提供以编程方式配置的会话工厂的HibernateUtil。换句话说,没有hibernate.cfg.xml也没有映射文件。

有几个特定于我们的应用的配置属性。首先,我们通过将hibernate.transaction.factory_class设置为org.hibernate.transaction.JTATransactionFactory并将hibernate.current_session_context_class设置为jta,告诉 Hibernate 我们想要使用手动事务划分。从编程角度来说,这些属性被映射为org.hibernate.cfg.Environment类中的常量值:

...
// create a new instance of OmgConfiguration
OgmConfiguration cfgogm = new OgmConfiguration();

cfgogm.setProperty(Environment.TRANSACTION_STRATEGY,
                                 "org.hibernate.transaction.JTATransactionFactory");
cfgogm.setProperty(Environment.CURRENT_SESSION_CONTEXT_CLASS, "jta");
...

接下来,我们指定 JTA 平台,JBoss JTA。为此,我们添加以下内容:

cfgogm.setProperty(Environment.JTA_PLATFORM,
"org.hibernate.service.jta.platform.internal.JBossStandAloneJtaPlatform");

注意,我们设置了 JBoss JTA 独立发行版,而不是 JBoss AS 使用的发行版。

根据 JBoss TS 文档,为了选择本地 JBoss JTA 实现,您需要指定两个属性,com.arjuna.ats.jta.jtaTMImplementationcom.arjuna.ats.jta.jtaUTImplementation。因为这些属性不是 Hibernate 环境的一部分,所以它们在Environment类中没有关联。您可以像这样指定它们:

cfgogm.setProperty("com.arjuna.ats.jta.jtaTMImplementation",
"com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple");
cfgogm.setProperty("com.arjuna.ats.jta.jtaUTImplementation",
"com.arjuna.ats.internal.jta.transaction.arjunacore.UserTransactionImple");

接下来,我们配置 MongoDB 连接:数据存储提供者、方言、要连接的数据库的名称、主机和端口(我们将使用本地主机和默认的 MongoDB 服务器端口 27017):

cfgogm.setProperty("hibernate.ogm.datastore.provider", "mongodb");
cfgogm.setProperty("hibernate.ogm.datastore.grid_dialect",
                   "org.hibernate.ogm.dialect.mongodb.MongoDBDialect"); cfgogm.setProperty("hibernate.ogm.mongodb.database", "tomcat_db");
cfgogm.setProperty("hibernate.ogm.mongodb.host", "127.0.0.1");
cfgogm.setProperty("hibernate.ogm.mongodb.port", "27017");

最后,我们将实体添加到等式中,删除LuckyNumberEntity.hbm.xml映射文件:

cfgogm.addAnnotatedClass(hogm.hnapi.pojo.LuckyNumberEntity.class);

现在将所有这些配置属性添加到特定于 OGM 发行版的HibernateUtil类中,以获得清单 4-10 中所示的代码。请注意,我在前面的应用中更详细地讨论了这个类。

清单 4-10。 另一类冬眠动物

package hogm.hnapi.util.without.hibernate.cfg;

import java.util.logging.Level;
import java.util.logging.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.hibernate.ogm.cfg.OgmConfiguration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

public class HibernateUtil {

    private static final Logger log = Logger.getLogger(HibernateUtil.class.getName());
    private static final SessionFactory sessionFactory;
    private static final ServiceRegistry serviceRegistry;

    static {
        try {
            // create a new instance of OmgConfiguration
            OgmConfiguration cfgogm = new OgmConfiguration();

            // enable JTA strategy
            cfgogm.setProperty(Environment.TRANSACTION_STRATEGY,
                                            "org.hibernate.transaction.JTATransactionFactory");
            cfgogm.setProperty(Environment.CURRENT_SESSION_CONTEXT_CLASS, "jta");

            // specify JTA platform
            cfgogm.setProperty(Environment.JTA_PLATFORM,
                      "org.hibernate.service.jta.platform.internal.JBossStandAloneJtaPlatform");

            // in order to select the local JBoss JTA implementation it is necessary to specify these properties
            cfgogm.setProperty("com.arjuna.ats.jta.jtaTMImplementation",
                      "com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple");
            cfgogm.setProperty("com.arjuna.ats.jta.jtaUTImplementation",
                      "com.arjuna.ats.internal.jta.transaction.arjunacore.UserTransactionImple");

            //configure MongoDB connection
            cfgogm.setProperty("hibernate.ogm.datastore.provider", "mongodb");
            cfgogm.setProperty("hibernate.ogm.datastore.grid_dialect",
                      "org.hibernate.ogm.dialect.mongodb.MongoDBDialect");
//you can ignore this setting
            cfgogm.setProperty("hibernate.ogm.mongodb.database", "tomcat_db");
            cfgogm.setProperty("hibernate.ogm.mongodb.host", "127.0.0.1");
            cfgogm.setProperty("hibernate.ogm.mongodb.port", "27017");

            //add our annotated class
            cfgogm.addAnnotatedClass(hogm.hnapi.pojo.LuckyNumberEntity.class);

            // create the SessionFactory
            serviceRegistry = new ServiceRegistryBuilder().applySettings(cfgogm.getProperties()).
                                                buildServiceRegistry();
            sessionFactory = cfgogm.buildSessionFactory(serviceRegistry);
        } catch (Throwable ex) {
            log.log(Level.SEVERE, "Initial SessionFactory creation failed !", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}

到目前为止,我们已经有了实体和会话工厂提供者。下一部分非常有趣,因为我们开始开发 DAO 类。这意味着使用 JBoss JTA 来划分事务,为此我们关注两个 JBoss JTA 类:

  • com.arjuna.ats.jta.UserTransaction—该类自动将新创建的事务与调用线程相关联,并公开了用于控制事务边界的begin, commit,rollback等方法。它还提供了一个名为userTransaction的静态方法,该方法返回一个代表用户事务的javax.transaction.UserTransaction:

    javax.transaction.UserTransaction tx = com.arjuna.ats.jta.UserTransaction.userTransaction();
    
  • com.arjuna.ats.jta.TransactionManager—这是一个允许应用服务器控制事务边界的接口。它还提供了像begin, commit,rollback,这样的方法,但是它是专门为应用服务器设计的,应用服务器可以初始化事务管理器并调用它来为您划分事务。可以通过transactionManager的方法获得javax.transaction.TransactionManager,比如:

javax.transaction.TransactionManager tx = com.arjuna.ats.jta.TransactionManager.transactionManager();

如果你更喜欢用 Hibernate getCurrentSession的方法来获取当前的Session,,你可以使用 JBoss JTA 实现一个将幸运数字保存到数据库中的 DAO 方法,如清单 4-11 所示。

清单 4-11。 幸运数字道类 - getCurrentSession方法

package hogm.hnapi.dao;
...
public class LuckyNumberDAO {
 ...
 private static final Logger log =
   Logger.getLogger(LuckyNumberDAO.class.getName());
 ...
 public void persist_cs_without_cfg(LuckyNumberEntity transientInstance) throws
 java.lang.Exception {

        log.log(Level.INFO, "Persisting LuckyNumberEntity instance ...");

        // javax.transaction.TransactionManager tx =
                                        com.arjuna.ats.jta.TransactionManager.transactionManager();
        javax.transaction.UserTransaction tx = com.arjuna.ats.jta.UserTransaction.userTransaction();

        try {
            tx.begin();
            hogm.hnapi.util.without.hibernate.cfg.HibernateUtil.getSessionFactory().
                                                    getCurrentSession().persist(transientInstance);
            tx.commit();

            log.log(Level.INFO, "Persist successful...");
        } catch (RuntimeException re) {
            tx.rollback();
            log.log(Level.SEVERE, "Persist failed...", re);
            throw re;
        }
    }
}

但是,如果您想自己控制会话刷新和关闭,请选择 Hibernate openSession方法,它可以以几乎相同的方式与 JBoss JTA 交织在一起,如清单 4-12 中的所示。

清单 4-12。 幸运数字道类- openSession方法

package hogm.hnapi.dao;
...
public class LuckyNumberDAO {
 ...
 private static final Logger log =
   Logger.getLogger(LuckyNumberDAO.class.getName());
 ...
 public void persist_os_without_cfg(LuckyNumberEntity transientInstance) throws
 java.lang.Exception {

        log.log(Level.INFO, "Persisting LuckyNumberEntity instance ...");

        // javax.transaction.TransactionManager tx =
                                        com.arjuna.ats.jta.TransactionManager.transactionManager();
        javax.transaction.UserTransaction tx = com.arjuna.ats.jta.UserTransaction.userTransaction();
        Session session = hogm.hnapi.util.without.hibernate.cfg.HibernateUtil.
                                       getSessionFactory().openSession();

        try {
            tx.begin();
            session.persist(transientInstance);
            session.flush();
            tx.commit();

            log.log(Level.INFO, "Persist successful...");
        } catch (RuntimeException re) {
            tx.rollback();
            log.log(Level.SEVERE, "Persist failed...", re);
            throw re;
        } finally {
            session.close();
        }
    }
}

申请快完成了。它的主要部分是可用的,我们只需要添加一个 servlet 来调用 DAO 方法,以及一个简单的用户界面来向这个 servlet 提交一个空表单。来自LuckyNumberServlet的主要代码片段显示在清单 4-13 中。

清单 4-13。LuckyNumberServlet 的一个片段

package hogm.hnapi.servlet;
...
@WebServlet(name = "LuckyNumberServlet", urlPatterns = {"/LuckyNumberServlet"})
public class LuckyNumberServlet extends HttpServlet {
...

 protected void processRequest(HttpServletRequest request, HttpServletResponse
 response) throws ServletException, IOException, Exception {
  ...
  LuckyNumberDAO luckyNumberDAO = new LuckyNumberDAO();
  LuckyNumberEntity luckyNumberEntity = new LuckyNumberEntity();
  luckyNumberEntity.setLuckynumber(new Random().nextInt(1000000));

  luckyNumberDAO.persist_cs_without_cfg(luckyNumberEntity);
  // luckyNumberDAO.persist_os_without_cfg(luckyNumberEntity);
  ...
 }
}

下面是与这个 servlet 交互的表单(在 index.jsp 中):

...
<form action="./LuckyNumberServlet" method="POST">
   <input type="submit" value="Generate Lucky Number">
</form>
...

搞定了。

测试

首先启动 MongoDB 服务器,如第一章所述。接下来,因为您处于 NetBeans/Tomcat(或 Eclipse/Tomcat)环境中,所以只需保存项目并单击Run(或 Eclipse 中的Run on Server)按钮来启动 Tomcat 并部署和运行应用。如果应用成功启动,你会在浏览器中看到类似于图 4-4 所示的东西。

9781430257943_Fig04-04.jpg

图 4-4 。运行 HOGMviaHNAPI_JTA_Tomcat7 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(tomcat_db)集合(jta))中。打开命令提示符,输入如图 4-5 所示的命令,查看您的工作结果。和以前一样,您可以监控 Tomcat 日志消息,查看是否发生了任何不需要的事情。

9781430257943_Fig04-05.jpg

图 4-5 。检查 jta 集合内容

这个名为HOGMviaHNAPI_JTA_Tomcat7的应用的完整源代码可以在 Apress 资源库中找到。它是 NetBeans 项目,在 Tomcat 7 下测试过(我使用了 NetBeans 7.2.1 捆绑的 Tomcat)。

在内置的 JTA 环境中 Hibernate OGM(没有 EJB,GlassFish AS 3)

在前面的例子中,我们开发了一个基于独立 JTA 环境的应用。我们可以重用大部分代码来编写相同类型的应用,但是这次是基于内置的 JTA 环境提供者,比如 GlassFish v3 AS。您可能知道,这是一个完全兼容的 J2EE 应用服务器,它自动处理(通过 JTA TransactionManager)每个数据源的事务生命周期。换句话说,我们将开发与上一节相同的应用,但是我们将使用容器提供的 JTA 事务管理器,而不是使用和配置 JBoss JTA。请注意,我们仍然手动划分事务边界;这不是容器管理事务(CMT)策略。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta2
  • JDK 1.7
  • NetBeans 7.2.1(或 Eclipse JUNO)
  • 玻璃鱼 3.1.2.2

正在开发

启动 NetBeans 后,创建一个包含一个空 Maven web 应用的新项目,并将其命名为HOGMviaHNAPI_JTA_GlassFish3。在New Web Application向导中,为Group IdPackage字段键入hogm.hnapi,并选择 GlassFish web server 来部署该应用。现在,只需遵循上一节中的场景。我们将做一些小而重要的修改。

编写应用代码

在添加 Hibernate OGM/Mongo DB jar 之后(像前面的例子一样使用 Maven),创建相同的LuckyNumberEntity实体。继续编写清单 4-14 中所示的HibernateUtil类。

清单 4-14。 一个修改过的 HibernateUtil 类

package hogm.hnapi.util.without.hibernate.cfg;

import java.util.logging.Level;
import java.util.logging.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.hibernate.ogm.cfg.OgmConfiguration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

public class HibernateUtil {

    private static final Logger log = Logger.getLogger(HibernateUtil.class.getName());
    private static final SessionFactory sessionFactory;
    private static final ServiceRegistry serviceRegistry;

    static {
        try {
            // create a new instance of OmgConfiguration
            OgmConfiguration cfgogm = new OgmConfiguration();

            // enable JTA strategy
            cfgogm.setProperty(Environment.TRANSACTION_STRATEGY,
                         "org.hibernate.transaction.JTATransactionFactory");
            cfgogm.setProperty(Environment.CURRENT_SESSION_CONTEXT_CLASS, "jta");

            // specify JTA platform
            cfgogm.setProperty(Environment.JTA_PLATFORM,
                         "org.hibernate.service.jta.platform.internal.SunOneJtaPlatform");

            //configure MongoDB connection
            cfgogm.setProperty("hibernate.ogm.datastore.provider", "mongodb");
            cfgogm.setProperty("hibernate.ogm.datastore.grid_dialect",
                         "org.hibernate.ogm.dialect.mongodb.MongoDBDialect");
//you can ignore this setting
            cfgogm.setProperty("hibernate.ogm.mongodb.database", "glassfish_db");
            cfgogm.setProperty("hibernate.ogm.mongodb.host", "127.0.0.1");
            cfgogm.setProperty("hibernate.ogm.mongodb.port", "27017");

            //add our annotated class
            cfgogm.addAnnotatedClass(hogm.hnapi.pojo.LuckyNumberEntity.class);

            // create the SessionFactory
            serviceRegistry = new ServiceRegistryBuilder().applySettings(cfgogm.getProperties()).
                                         buildServiceRegistry();
            sessionFactory = cfgogm.buildSessionFactory(serviceRegistry);
        } catch (Throwable ex) {
            log.log(Level.SEVERE, "Initial SessionFactory creation failed !", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}

如您所见,相关代码以粗体显示:

cfgogm.setProperty(Environment.JTA_PLATFORM,
             "org.hibernate.service.jta.platform.internal.SunOneJtaPlatform");

这段代码告诉 Hibernate 要使用的 JTA 平台。显然,我们希望使用内置的 JTA 平台,对于 GlassFish v3 来说,不需要任何库或 JAR 一切都由容器提供。通过查看第二章中的可用 JTA 平台列表,您可以很容易地为其他受支持的容器(JTA 内置平台)修改该属性(hibernate.transaction.jta.platform)。例如,如果您在 JBoss 7 AS 下部署这个应用,内置的 JTA 平台是org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform;不要将这个 JTA 与独立的 JBoss JTA 平台混淆。

如果你决定使用hibernate.cfg.xml,添加 JTA 平台,就像这样:

<property name="hibernate.transaction.jta.platform">
                org.hibernate.service.jta.platform.internal.SunOneJtaPlatform</property>

现在让我们开发 DAO 类。如果您跟踪了早期的应用,您会知道我们只关注在使用getCurrentSessionopenSession方法获得的 Hibernate 会话中将对象持久化到数据库中。如您所知,Hibernate 可以自动将当前会话绑定到当前 JTA 事务,但为此我们需要控制事务本身并添加相应的分界。为了在 J2EE 环境中完成这个任务,我们可以简单地利用标准的 JNDI 子上下文java:comp/UserTransaction.javax.transaction.UserTransaction应该在遵循 J2EE 规范的java:comp/UserTransaction,中可用:

UserTransaction tx = (UserTransaction) new InitialContext().lookup("java:comp/UserTransaction");

现在,对于getCurrentSession方法,我们可以调用清单 4-15 中的begin, commit,rollback方法。

清单 4-15。getCurrentSession接近

package hogm.hnapi.dao;
...
public class LuckyNumberDAO {
...
private static final Logger log =
   Logger.getLogger(LuckyNumberDAO.class.getName());
 ...
 public void persist_cs_without_cfg(LuckyNumberEntity transientInstance) throws
 java.lang.Exception {

        log.log(Level.INFO, "Persisting LuckyNumberEntity instance ...");

        UserTransaction tx = (UserTransaction) new
InitialContext().lookup("java:comp/UserTransaction");

        try {
            tx.begin();
            hogm.hnapi.util.without.hibernate.cfg.HibernateUtil.getSessionFactory().
                                                     getCurrentSession().persist(transientInstance);
            tx.commit();

            log.log(Level.INFO, "Persist successful...");
        } catch (RuntimeException re) {
            tx.rollback();
            log.log(Level.SEVERE, "Persist failed...", re);
            throw re;
        }
    }
}

或者,如果你喜欢使用清单 4-16 中的方法。

清单 4-16。openSession接近

package hogm.hnapi.dao;
...
public class LuckyNumberDAO {
...
private static final Logger log =
   Logger.getLogger(LuckyNumberDAO.class.getName());
 ...
 public void persist_os_without_cfg(LuckyNumberEntity transientInstance) throws
 java.lang.Exception {

        log.log(Level.INFO, "Persisting LuckyNumberEntity instance ...");

        UserTransaction tx = (UserTransaction) new InitialContext().lookup("java:comp/UserTransaction");
        Session session = hogm.hnapi.util.without.hibernate.cfg.HibernateUtil.
                                                                  getSessionFactory().openSession();

        try {
            tx.begin();
            session.persist(transientInstance);
            session.flush();
            tx.commit();

            log.log(Level.INFO, "Persist successful...");
        } catch (RuntimeException re) {
            tx.rollback();
            log.log(Level.SEVERE, "Persist failed...", re);
            throw re;
        } finally {
            session.close();
        }
    }
}

现在,整个 Hibernate OGM 机制已经设置好了。剩下的工作就是添加一个简单的用户界面,将一个“空”表单提交给一个与 DAO 类通信的基本 JSF bean(如果您不是 JSF 迷,可以用 servlet 代替它)。清单 4-17 显示了与 DAO 类交互的代码。

清单 4-17。TestManagedBean 类

package hogm.hnapi.jsf;
 ...
 public class TestManagedBean {
 ...
 public void persistAction() throws Exception {
  ...
  LuckyNumberDAO luckyNumberDAO = new LuckyNumberDAO();
  LuckyNumberEntity luckyNumberEntity = new LuckyNumberEntity();
  luckyNumberEntity.setLuckynumber(new Random().nextInt(1000000));

  luckyNumberDAO.persist_cs_without_cfg(luckyNumberEntity);
  // luckyNumberDAO.persist_os_without_cfg(luckyNumberEntity);
 ...
 }
}

这里是用户表单的代码,它在index.xhtml页面上:

...
<h:form>
   <h:commandButton action="#{bean.persistAction()}" value="Generate Lucky Number"/>
</h:form>
...

就这样!

测试

现在启动 MongoDB 服务器,就像你在第一章中看到的那样。接下来,因为您处于 NetBeans/GlassFish(或 Eclipse/GlassFish)环境中,所以只需保存项目并单击Run(或 Eclipse 中的Run on Server)按钮来启动 GlassFish 并部署和运行应用。如果应用成功启动,你会在浏览器中看到类似于图 4-6 所示的东西。

9781430257943_Fig04-06.jpg

图 4-6 。运行 HOGMviaHNAPI_JTA_GlassFish3 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(glassfish_db)集合(jta))中。打开命令提示符,键入图 4-7 中的命令,查看您的工作结果。您还可以监控 GlassFish 日志消息,以防发生任何不必要的事情。

9781430257943_Fig04-07.jpg

图 4-7 。检查 jta 集合内容

这个应用的完整源代码被命名为HOGMviaHNAPI_JTA_GlassFish3,可以在 Apress 存储库中找到。它是一个 NetBeans 项目,并在 GlassFish 3 下进行了测试(我使用了与 NetBeans 7.2.1 捆绑在一起的 GlassFish)。

内置 JTA 环境中的 hibernate OGM(EJB 3/BMT,GlassFish AS 3)

在前面的示例中,我们开发了一个基于 GlassFish 3 内置 JTA 环境的应用。您看到了如何通过在 JNDI 子上下文java:comp/UserTransaction中查找来获得当前事务,以及如何在一个普通的 DAO 类中手工划分事务边界。现在我们将开发相同类型的应用,但是这次我们将使用一个被注释为 bean 管理事务(BMT)的 EJB 组件。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta1
  • JDK 1.7
  • NetBeans 7.2.1(或 Eclipse JUNO)
  • 玻璃鱼 3.1.2.2

正在开发

启动 NetBeans 后,创建一个包含一个空 Maven web 应用的新项目,并将其命名为HOGMviaHNAPI_JTA_EJB_BMT_GlassFish3。在New Web Application向导中,为Group IdPackage字段键入hogm.hnapi。不要忘记选择 GlassFish web 服务器来部署这个应用。请注意,即使我们要添加一个 EJB 组件,我们也不会创建一个企业应用来将 web 模块与 EJB 模块分开。我们更喜欢 web 应用,因为我们希望能够从 EJB 组件调用 web 组件。

编写应用代码

在添加 Hibernate OGM/Mongo DB jar 之后(像前面的例子一样使用 Maven)创建众所周知的LuckyNumberEntity实体(这次使用@Table(name="bmt"),或 POJO 版本LuckyNumberPojo,如果您想使用hibernate.cfg.xml)。继续编写HibernateUtil类,启用 JTA 策略并添加 GlassFish 3 内置的 JTA 平台:

OgmConfiguration cfgogm = new OgmConfiguration();
...
cfgogm.setProperty(Environment.TRANSACTION_STRATEGY,
                                 "org.hibernate.transaction.JTATransactionFactory");
cfgogm.setProperty(Environment.CURRENT_SESSION_CONTEXT_CLASS, "jta");
cfgogm.setProperty(Environment.JTA_PLATFORM,
                                "org.hibernate.service.jta.platform.internal.SunOneJtaPlatform");

或者,如果您喜欢使用hibernate.cfg.xml文件,将它添加到那里(在这种情况下,不要忘记写入LuckyNumberPojo.hbm.xml并指定table="bmt" ) :

<property name="hibernate.transaction.factory_class">
                            org.hibernate.transaction.JTATransactionFactory</property>
<property name="hibernate.current_session_context_class">jta</property>
<property name="hibernate.transaction.jta.platform">
                            org.hibernate.service.jta.platform.internal.SunOneJtaPlatform</property>

接下来,添加一个名为BMTBean的无状态 bean(一个 EJB 组件);没有必要为它创建一个接口。因为默认情况下,EJB 方法中的代码是在事务中执行的,所以我们必须通过添加@TransactionManagement语句来修改它,如下所示:

package hogm.hnapi.ejb;
...
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class BMTBean {
...

你可以在第二章中找到关于这个注释的更多细节。

现在我们控制了事务边界。我们所需要的是可以使用@Resource注释获得的UserTransaction,就像这样:

@Resource
private UserTransaction userTransaction;

image 你也可以通过EJBContext、通过 JNDI 查找甚至通过 CDI 注入机制(@Inject UserTransaction)获得UserTransaction 。在选择您的方法之前,咨询 J2EE 实现的官方文档总是一个好主意。

现在,我们可以很容易地调用UserTransaction.begin, commitsetRollbackOnly方法,通过从getCurrentSessionopenSession.获得的 Hibernate OGM 会话来控制与 MongoDB 数据库的事务(如果听起来 MongoDB 支持事务,其实并不支持。请记住,我们使用这种方法是因为 OGM 文档推荐使用事务分界,即使对于 MongoDB 也是如此。)例如,我们可以存储一个幸运数字,如清单 4-18 所示。注意,代码包含两种情况——使用实体和 POJO。

清单 4-18。 存储幸运数字的两种方法——BMT 法

package hogm.hnapi.ejb;

import hogm.hnapi.pojo.LuckyNumberEntity;
import hogm.hnapi.pojo.LuckyNumberPojo;
import java.util.Random;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.inject.Named;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import org.jboss.logging.Logger;

@Stateless
@Named("bean")
@TransactionManagement(TransactionManagementType.BEAN)
public class BMTBean {

    @Resource
    private UserTransaction userTransaction;
    private static final Logger log = Logger.getLogger(BMTBean.class.getName());

    public void persistAction() {

        log.info("Persisting LuckyNumberEntity instance ...");

        LuckyNumberEntity luckyNumberEntity = new LuckyNumberEntity();
        luckyNumberEntity.setLuckynumber(new Random().nextInt(1000000));
        LuckyNumberPojo luckyNumberPojo = new LuckyNumberPojo();
        luckyNumberPojo.setLuckynumber(new Random().nextInt(1000000));

        try {
            // Start the transaction
            userTransaction.begin();

            hogm.hnapi.util.without.hibernate.cfg.HibernateUtil.getSessionFactory().
                                                   getCurrentSession().persist(luckyNumberEntity);
            hogm.hnapi.util.with.hibernate.cfg.HibernateUtil.getSessionFactory().
                                                   getCurrentSession().persist(luckyNumberPojo);

            //persist here through openSession method

            // Commit the transaction
            userTransaction.commit();
        } catch (Exception ex) {
            try {
                //Rollback the transaction
                userTransaction.setRollbackOnly();
            } catch (IllegalStateException ex1) {
                log.log(Logger.Level.ERROR, ex1, ex1);
            } catch (SystemException ex1) {
                log.log(Logger.Level.ERROR, ex1, ex1);
            }
        }
        log.info("Persist successful ...");
    }
}

为了运行这个应用,我们选择激活 JSF 框架和 CDI 支持(通过在/WEB-INF文件夹中添加相应的beans.xml)。我们已经用@Named("bean")注释了 EJB 组件——如代码所示——并且我们使用简单的 JSF 表单从应用起始页调用它,就像这样(index.xhtml):

...
<h:form>
   <h:commandButton action="#{bean.persistAction()}"
                    value="Generate Lucky Number"/>            
</h:form>
...

测试

启动 MongoDB 服务器,如你在第一章中所见。接下来,由于您处于 NetBeans/GlassFish(或 Eclipse/GlassFish)环境中,保存项目并单击Run(或 Eclipse 中的Run on Server)按钮启动 GlassFish 并部署和运行应用。如果应用成功启动,你会在浏览器中看到类似于图 4-8 所示的内容。

9781430257943_Fig04-08.jpg

图 4-8 。运行 HOGMviaHNAPI _ JTA _ EJB _ BMT _ glassfish 3 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(glassfish_db)集合,(bmt).)对于每一次按,插入两个新文档,一个用于 enitity,一个用于 POJO。打开命令提示符,键入图 4-9 中的命令,查看您的工作结果。您还可以监控 GlassFish 日志消息,以防发生任何不必要的事情。

9781430257943_Fig04-09.jpg

图 4-9 。查看 bmt 收藏内容

这个应用的完整源代码可以在 Apress 存储库中找到,命名为HOGMviaHNAPI_JTA_EJB_BMT_GlassFish3。它是一个 NetBeans 项目,并在 GlassFish 3 下进行了测试(我使用了捆绑到 NetBeans 7.2.1 的 GlassFish)。

在内置的 JTA 环境中 Hibernate OGM(EJB 3/CMT,GlassFish AS 3)

在前面的示例中,我们开发了一个基于 GlassFish 3 内置 JTA 环境和 bean 管理事务(BMT)的应用。通过应用一些必要的更改,我们可以很容易地将这个应用转换成容器管理的事务(CMT)。我可以告诉你“检查前面的例子,修改这个,修改那个。。. ",但如果你对之前的申请不感兴趣,你可能不会觉得那太有吸引力。所以我会尽量在这里提供更多的信息,并要求你只从以前的应用中复制本章中重复几次的部分。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta1
  • JDK 1.7
  • NetBeans 7.2.1(或 Eclipse JUNO)
  • 玻璃鱼 3.1.2.2

正在开发

启动 NetBeans 后,创建一个包含一个空 Maven web 应用的新项目,并将其命名为HOGMviaHNAPI_JTA_EJB_CMT_GlassFish3。在New Web Application向导中,为Group IdPackage字段键入hogm.hnapi。不要忘记选择 GlassFish web 服务器来部署这个应用。注意,即使我们添加了一个 EJB 组件,我们也不会创建一个企业应用来将 web 模块与 EJB 模块分开。我们更喜欢 web 应用,因为我们希望能够从 EJB 组件调用 web 组件。

编写应用代码

在添加 Hibernate OGM/MongoDB jar(像前面的例子一样使用 Maven)之后,创建众所周知的LuckyNumberEntity实体(这次使用@Table(name="cmt"),或 POJO 版本LuckyNumberPojo,如果您想使用hibernate.cfg.xml)。继续编写HibernateUtil类,启用 CMT 策略并添加 GlassFish 3 内置的 JTA 平台:

OgmConfiguration cfgogm = new OgmConfiguration();
 ...
cfgogm.setProperty(Environment.TRANSACTION_STRATEGY,
                                 "org.hibernate.transaction.CMTTransactionFactory");
cfgogm.setProperty(Environment.JTA_PLATFORM,
                                 "org.hibernate.service.jta.platform.internal.SunOneJtaPlatform");

或者,如果您喜欢使用hibernate.cfg.xml文件,将它添加到那里(在这种情况下,不要忘记写入LuckyNumberPojo.hbm.xml and specify table="cmt"):

<property name="hibernate.transaction.factory_class">
                              org.hibernate.transaction.CMTTransactionFactory</property>
<property name="hibernate.transaction.jta.platform">
                              org.hibernate.service.jta.platform.internal.SunOneJtaPlatform</property>

添加一个名为CMTBean的无状态 bean(一个 EJB 组件)(不需要为它创建接口)。因为默认情况下,EJB 方法中的代码是在事务中执行的,所以我们不需要干预。然而,只是为了好玩,我们可以手动提供已经默认的注释— @TransactionManagement@TransactionAttribute.关于这个注释的更多细节可以在第二章中找到。

现在我们可以很容易地利用 CMT 策略,并使用从getCurrentSessionopenSession方法获得的 Hibernate OGM 会话在 MongoDB 数据库中存储幸运数字,如清单 4-19 所示。注意,代码包含两种情况——使用实体和 POJO。

清单 4-19。 存储幸运数字的两种方法——CMT 法

package hogm.hnapi.ejb;

import hogm.hnapi.pojo.LuckyNumberEntity;  //entity case
import hogm.hnapi.pojo.LuckyNumberPojo;    //POJO case
import java.util.Random;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.inject.Named;
import org.jboss.logging.Logger;

@Stateless
@Named("bean")
@TransactionManagement(TransactionManagementType.CONTAINER) //this is the default
public class CMTBean {

    private static final Logger log = Logger.getLogger(CMTBean.class.getName());

    @TransactionAttribute(TransactionAttributeType.REQUIRED) //this is the default
    public void persistAction() {

        log.info("Persisting LuckyNumberEntity instance ...");

        LuckyNumberEntity luckyNumberEntity = new LuckyNumberEntity();
        luckyNumberEntity.setLuckynumber(new Random().nextInt(1000000));
        LuckyNumberPojo luckyNumberPojo = new LuckyNumberPojo();
        luckyNumberPojo.setLuckynumber(new Random().nextInt(1000000));

        hogm.hnapi.util.without.hibernate.cfg.HibernateUtil.getSessionFactory().
                                              getCurrentSession().persist(luckyNumberEntity);
        hogm.hnapi.util.with.hibernate.cfg.HibernateUtil.getSessionFactory().
                                              getCurrentSession().persist(luckyNumberPojo);
        //persist here through openSession method
        log.info("Persist successful ...");
    }
}

为了运行这个应用,我们将激活 JSF 框架和 CDI 支持(通过在/ WEB-INF文件夹中添加相应的beans.xml)。我们已经用@Named("bean")注释了 EJB 组件——如代码所示——并且我们使用简单的 JSF 表单从应用起始页调用它,就像这样(index.xhtml):

...
<h:form>
   <h:commandButton action="#{bean.persistAction()}"
                    value="Generate Lucky Number"/>
</h:form>
...

测试

按照第一章中的方法启动 MongoDB 服务器。接下来,因为您处于 NetBeans/GlassFish(或 Eclipse/GlassFish)环境中,所以只需保存项目并单击Run(或 Eclipse 中的Run on Server)按钮来启动 GlassFish 并部署和运行应用。如果应用成功启动,你会在浏览器中看到类似于图 4-10 所示的东西。

9781430257943_Fig04-10.jpg

图 4-10 。运行 HOGMviaHNAPI _ JTA _ EJB _ CMT _ glassfish 3 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(glassfish_db)集合(cmt).)中。每次按下按钮,都会插入两个新文档,一个用于 enitity,一个用于 POJO。打开命令提示符,键入图 4-11 中的命令,查看您的工作结果。您可以监控 GlassFish 日志消息,以防发生任何不必要的事情。

9781430257943_Fig04-11.jpg

图 4-11 。检查“cmt”集合内容

这个应用的完整源代码被命名为HOGMviaHNAPI_JTA_EJB_CMT_GlassFish3,可以在 Apress 存储库中找到。它是一个 NetBeans 项目,并在 GlassFish 3 下进行了测试(我使用了捆绑到 NetBeans 7.2.1 的 GlassFish)。

通过 Java 持久性 API (JPA 2.0)Hibernate OGM

Hibernate OGM 也可以通过 JPA 引导。这非常有用,因为它不涉及任何 Hibernate ORM 知识,也不需要任何与 Hibernate 相关的代码。实际上,如果您以前使用过 JPA(不管是哪种实现),将 Hibernate OGM 配置为您的 JPA 提供者应该是小菜一碟。

在本节中,您将看到一组应用,它们将 Hibernate OGM 作为不同架构和技术下的 JPA 提供者。您将看到它是如何工作的:

  • 内置 JTA 环境(EJB 3,GlassFish AS 3)
  • 内置 JTA 环境(EJB 3,JBoss AS 7)
  • 独立的 JTA 环境(Apache Tomcat 7)
  • 内置 JTA 环境(JBoss AS 7 和 Seam 3 应用)
  • 内置 JTA 环境(GlassFish 3 和 Spring 3 应用)
  • 非 JTA 环境(RESOURCE_LOCAL,Apache Tomcat 7)

在内置的 JTA 环境中 Hibernate OGM(EJB 3,GlassFish AS 3)

我们从部署在 GlassFish 上的企业应用(称为 EAR—企业归档)开始。这是 Java 世界中经常使用的经典重型应用之一,通常涉及几种技术,如 JPA、JSF、Struts、EJB、Hibernate、Spring 等等。网络技术进入一个模块(战争模块),EJB 组件进入另一个模块(EJB 模块)。WAR 模块可以访问 EJB 模块,但反之则不行。从程序员的角度来看,JPA 的核心由一个名为persistence.xml的 XML 文件组成,它作为配置文件放在 EJB 模块中。因此,让我们来看看 Hibernate OGM 作为 JPA 提供者时这个文件是什么样子的。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta2
  • JDK 1.7
  • NetBeans 7.2.1(或 Eclipse JUNO)
  • 玻璃鱼 3.1.2.2

发展中的

启动 NetBeans 后,创建一个包含一个空 Maven enterprise 应用的新项目,并将其命名为HOGMviaJPA_EE_GlassFish。在New Enterprise Application向导中,为Group IdPackage字段键入hogm,并选择用于部署该应用的 GlassFish 应用服务器。在Projects窗口中看到项目后,您可以在HOGMviaJPA_EE_GlassFish-ear项目模块中编辑pom.xml文件(它必须在Project Files节点下)。在pom.xml,中,通过粘贴以下依赖项来添加 Hibernate OGM 发行版(包括 MongoDB 支持):

<dependencies>
   <dependency>
      <groupId>org.hibernate.ogm</groupId>
      <artifactId>hibernate-ogm-core</artifactId>
      <version>4.0.0.Beta2</version>
   </dependency>
   <dependency>
      <groupId>org.hibernate.ogm</groupId>
      <artifactId>hibernate-ogm-mongodb</artifactId>
      <version>4.0.0.Beta1</version>
   </dependency>
...
<dependencies>

现在保存项目,MongoDB Java 驱动程序 JAR 将列在Dependencies节点下。

编写应用代码

现在,我们有了所有需要的工件,所以我们准备添加一些代码。首先,在HOGMviaJPA_EE_GlassFish -ejb 模块中,我们开发了一个基本的实体类,它能够表示数据库中的对象。它包含一个名为luckynumber的字段(除了主键字段)。(您应该熟悉这类实体,从技术上讲,它们只是带注释的 POJOs。你可以在第二章中找到更多细节。)清单 4-20 显示了 LuckyNumberEntity 类的代码。

清单 4-20。lucky number entity 类

package hogm.jpa.entities;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "jpa")
public class LuckyNumberEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(name = "luckynumber", nullable = false)
    private int luckynumber;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public int getLuckynumber() {
        return luckynumber;
    }

    public void setLuckynumber(int luckynumber) {
        this.luckynumber = luckynumber;
    }
}

让我们继续我们感兴趣的要点,将 Hibernate OGM 集成为 JPA 提供者。您可以从使用 NetBeans 向导创建一个persistence.xml框架开始。这将为 GlassFish 默认数据源提供一个“空”的持久性单元(这是最方便的,因为我们实际上并不需要它)或者没有数据源。从 Hibernate OGM 的角度来看,这个数据源是不需要的,也从来没有使用过,但是根据具体情况,您可能需要指定一个现有的数据源,因为这是 JPA 的要求。(根据 JPA 1.0/2.0 规范,"" JTA 的事务类型假设将提供 JTA 数据源—要么由 jta-data-source 元素指定,要么由容器提供。))为了确定,你得自己测试一下。据我所知,没有必要指定数据源;在 NetBeans 向导中将该字段留空,您将获得一个没有数据源的persistence.xml框架——没有<jta-data-source>标签。如果出现相关错误,那么在 GlassFish 中添加默认数据源,如下所示:

...
<!-- out of the box data source for GlassFish v3-->
<jta-data-source>jdbc/sample</jta-data-source>
...

我们还将持久性单元重命名为HOGM_JPA_GLASSFISH_PU,并将事务类型表示为JTA。这是推荐的。请记住,我们有两个可能的值:RESOURCE_LOCAL表示事务将由 JPA provider 实现管理,JTA表示事务将由应用服务器(在本例中是 GlassFish)管理。最后,我们指定由这个持久性单元管理的实体列表。

此外,我们正在添加 Hibernate OGM 作为 JPA 提供者。这非常简单快捷,因为它只需要添加<provider>标记,就像这样:

...
<provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
...

默认情况下,NetBeans 将自动检测实体,并将标签<exclude-unlisted-classes>,添加到persistence.xml中,该标签默认为false—由该持久性单元管理的归档中的所有实体 beans。您可以保持不变,或者删除该标记并显式添加实体类:

...
<class>hogm.jpa.entites.LuckyNumberEntity</class>
...

由于我们处于 JTA 环境中,JTA 平台应该会被自动检测到并被使用,而无需我们的干预。但是,可以肯定的是,您可以相应地设置hibernate.transaction.jta.platform属性:

...
<property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform"/>
...

我们快完成了。我们只需要配置 MongoDB 连接(提供者、方言(可选)、数据库名称、主机和端口)。一旦我们完成了这些,我们就有了完整的persistence.xml文件,如清单 4-21 所示。

清单 4-21。 persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">

<persistence-unit name="HOGM_JPA_GLASSFISH_PU" transaction-type="JTA">
   <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
    <class>hogm.jpa.entities.LuckyNumberEntity</class>
    <properties>
      <property name="hibernate.transaction.jta.platform"
                     value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform"/>
      <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
      <property name="hibernate.ogm.datastore.grid_dialect"
                      value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
      <property name="hibernate.ogm.mongodb.database" value="glassfish_db"/>
      <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
      <property name="hibernate.ogm.mongodb.port" value="27017"/>
    </properties>
  </persistence-unit>
</persistence>

Hibernate OGM 现在可以作为 JPA 提供者为我们的应用提供服务了。

这是一个企业应用,所以 EJB 组件(默认情况下是事务性的)非常适合利用 OGM 提供的全新实体管理器。CMTBean实现了将幸运数字存储到 MongoDB 数据库的业务逻辑(不需要本地或远程接口),如清单 4-22 所示。

清单 4-22。CMT bean 类

package hogm.jpa.ejb;

import hogm.jpa.entities.LuckyNumberEntity;
import java.util.Random;
import javax.ejb.Stateless;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Stateless
@Named("bean")
public class CMTBean {

    @PersistenceContext(unitName = "HOGM_JPA_GLASSFISH_PU")
    private EntityManager em;

    public void persistAction() {
        LuckyNumberEntity luckyNumberEntity = new LuckyNumberEntity();
        luckyNumberEntity.setLuckynumber(new Random().nextInt(1000000));

        em.persist(luckyNumberEntity);
    }
}

最后,我们需要一些粘合代码来获得一个功能应用。如您所见,EJB 组件用@Named进行了注释,这意味着您需要通过添加beans.xml文件来激活 CDI 支持。如果您按下正确的按钮,NetBeans 会为您完成这项工作,但您也可以手动添加。在一个 Maven 项目中,在*-ejb模块中,beans.xml应该放在src/main/resources文件夹中(在Other Resource节点下)。并且在*-war模块中,beans.xml要放在/WEB-INF文件夹中(在Web Pages节点下)。两处都加beans.xml

通过 CDI 调用 EJB 可以从 JSF 表单中完成—您需要激活 JSF 框架:

...
<h:form>
   <h:commandButton action="#{bean.persistAction()}" value="Generate Lucky Number"/>
</h:form>
...

完成了!

测试

启动 MongoDB 服务器,就像你在第一章中看到的那样。接下来,因为您处于 NetBeans/GlassFish(或 Eclipse/GlassFish)环境中,所以只需保存项目并选择HOGMviaJPA_EE_GlassFish-ear节点。单击Run(或 Eclipse 中的Run on Server)按钮启动 Glassfish 并部署和运行应用。如果应用成功启动,您将在浏览器中看到类似于图 4-12 所示的内容。

9781430257943_Fig04-12.jpg

图 4-12 。运行 HOGMviaJPA_EE_GlassFish 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(glassfish_db)集合(jpa))中。打开命令提示符,键入图 4-13 中的命令,查看您的工作结果。您可以监控 GlassFish 日志消息,以防发生任何不必要的事情。

9781430257943_Fig04-13.jpg

图 4-13 。正在检查 jpa 收藏内容

image 注意忽略hibernate_sequences收藏,因为暂时不相关。你将在第五章中了解它是如何出现的以及为什么会出现。

这个应用的完整源代码被命名为HOGMviaJPA_EE_GlassFish,可以在 Apress 存储库中找到。它是一个 NetBeans 项目,并在 GlassFish 3 下进行了测试(我使用了捆绑到 NetBeans 7.2.1 的 GlassFish)。

在内置的 JTA 环境中 Hibernate OGM(EJB 3,JBoss AS 7)

在这一节中,您将看到如何运行上一节中开发的应用,但是使用 JBoss AS 而不是 GlassFish AS。不幸的是,它不能像在 JBoss 应用服务器下那样工作,所以您需要在应用服务器级别调整一些东西,并在persistence.xml文件中添加一些修改。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta2
  • JDK 1.7
  • 日蚀朱诺
  • JBoss AS 7.1

发展中的

有一个不成文的规则,GlassFish 粉丝更喜欢 NetBeans IDE 和 JBoss,因为粉丝喜欢使用 Eclipse IDE。显然,这不是强制性的。毕竟,我们讨论的是独立于 ide 的企业应用,应该可以在任何经过认证的 EE 应用服务器下工作。不过,您很可能同意这种关联,这就是为什么我们将使用 Eclipse IDE 将 JBoss 开发为应用。因此,在启动 Eclipse 之后,创建一个新项目,它由一个名为HOGMviaJPA_EE_JbossAS的空Enterprise Application Project组成。选择 EAR 版本 6.0,JBoss AS 7.1,默认配置为目标运行时。添加 Web 和 EJB 模块,命名为HOGMviaJPA_EE_JBossAS-webHOGMviaJPA_EE_JBossAS-ejb

image 注意我使用了 Eclipse JUNO 发行版,并通过 JBoss AS Tools 插件添加了 JBoss AS 7.1,因为这个应用服务器在 JUNO 中默认是不可用的(我使用的链接是www.download.jboss.org/jbosstools/updates/development/indigo/)。可以随意使用任何其他 Eclipse 发行版,只要它绑定到 JBoss AS 7.1。

现在,我们将让这个应用保持原样,并将注意力转移到 JBoss AS 7 模块上,因为我们需要将 Hibernate OGM JARs 配置为应用服务器内部的一个模块。没有这个模块,我们将无法成功部署包含 OGM 的 Hibernate 应用。

首先,定位三个罐子:hibernate-ogm-core-4.0.0.Beta2.jar, hibernate-ogm-mongodb-4.0.0.Beta1.jarmongo-java-driver-2.8.0.jar。接下来,浏览{JBOSSAS_HOME?? 路径并创建一个名为ogm的新文件夹。将这三个 jar 复制到这个新文件夹中,并将清单 4-23 中的module.xml文件添加到这个文件夹中。

清单 4-23。 module.xml

<module name="org.hibernate" slot="ogm">
    <resources>
        <resource-root path="hibernate-ogm-mongodb-4.0.0.Beta1.jar"/>
        <resource-root path="hibernate-ogm-core-4.0.0.Beta2.jar"/>
         <resource-root path="mongo-java-driver-2.8.0.jar"/>
    </resources>

    <dependencies>
        <module name="org.jboss.as.jpa.hibernate" slot="4"/>
        <module name="org.hibernate" slot="main" export="true" />
        <module name="javax.api"/>
        <module name="javax.persistence.api"/>
        <module name="javax.transaction.api"/>
        <module name="javax.validation.api"/>
        <module name="org.infinispan"/>
        <module name="org.javassist"/>
        <module name="org.jboss.logging"/>
    </dependencies>
</module>

保存文件。这里我们还要做一件事——在模块中添加 Hibernate 4.1.9 来代替 4.0.1。首先,定位以下 jar:hibernate-core-4.1.9.Final.jarhibernate-entitymanager-4.1.9.Final.jar,然后浏览{ JBOSSAS_HOME }/modules/org/hibernate/main路径。现在,把旧的罐子换成这些,或者只加这些。编辑同一文件夹中的module.xml文件,并相应替换旧的引用:

<module name="org.hibernate">
    <resources>
        <resource-root path="hibernate-core-4.1.9.Final.jar"/>
        <resource-root path="hibernate-entitymanager-4.1.9.Final.jar"/>
        <resource-root path="hibernate-commons-annotations-4.0.1.Final.jar"/>
        <resource-root path="hibernate-infinispan-4.0.1.Final.jar"/>
        <!-- Insert resources here -->
    </resources>
...

搞定了。我们完成了为 Hibernate OGM 应用准备 JBoss AS 7.1 的所有必要工作。

编写应用代码

现在,我们可以切换回应用开发,更具体地说,切换到persistence.xml文件,该文件必须进行一些重要的修改,您将在接下来的段落中看到。要添加这个文件,您可以使用 Eclipse IDE 向导,如下所示:

  • Project Explorer中,找到HOGMviaJPA_EE_JBossAS-ejb模块。右击它并从上下文菜单中选择Properties。导航到Properties窗口中的Project Facets,找到 JPA 刻面。选择它,你应该会看到类似于图 4-14 所示的东西。

9781430257943_Fig04-14.jpg

图 4-14 。添加 JPA 方面

  • 我们对底部的文本"Further configuration required ..."(或者它可能会说"Further configuration available ...")特别感兴趣。点击文本打开Modify Faceted Project窗口。我们必须选择 JPA 实现,它是 Hibernate OGM。在JPA Implementation部分选择Generic 2.0Platform,选择User LibraryType
  • 接下来,我们必须指定 Hibernate OGM 和 MongoDB 库。如果您遵循了第一章中的章节“使用 Eclipse IDE 获得 Hibernate OGM 发行版”,那么您应该拥有 Hibernate OGM 核心和 MongoDB 库选中后点击OK,,如图图 4-15 所示。如果没有这个库,现在就创建它。点击ApplyOK返回应用主界面。

9781430257943_Fig04-15.jpg

图 4-15 。选择 JPA 实现

现在,您应该在HOGMviaJPA_EE_JBossAS-ejb | JPA Content节点下看到一个空的persistence.xml叶子。在编辑器中打开这个文件,让我们添加我们需要的内容:

  • 将持久化单元重命名为HOGM_JPA_JBOSSAS_PU,并将交易类型设置为JTA :

    <persistence-unit name="HOGM_JPA_JBOSSAS_PU" transaction-type="JTA">
    
  • 使用<provider>标签:

    <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
    

    将 JPA 提供者指定为 Hibernate OGM

  • 添加可以由持久性单元定义的EntityManager实例管理的实体(在我们的例子中,是一个名为LuckyNumberEntity :

    <class>hogm.jpa.entities.LuckyNumberEntity</class>
    

    的实体)

  • Optionally, indicate the JTA platform. Normally, this is auto-detected in an EE environment. Notice that for JBoss AS 7, the correct value is:

    org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform
    

    这不是用于 JBoss JTA 单机版的值。

    <property name="hibernate.transaction.jta.platform"
                    value="org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform"/>
    
  • 默认情况下,JPA 应用将使用 JBoss AS 7 集成适配器模块中配置的 Hibernate 集成类,除非您将设置为另一个值的属性jboss.as.jpa.adapterModule添加到您的persistence.xml属性列表中。该属性的值表示 Hibernate 集成类的名称,这些集成类帮助应用服务器与持久性提供者一起工作。在我们的例子中,我们需要 Hibernate 集成类 4,所以我们使用下面的设置:

    <property name="jboss.as.jpa.adapterModule" value="org.jboss.as.jpa.hibernate:4"/>
    
  • 我们还需要添加属性jboss.as.jpa.providerModule来表明我们希望使用 Hibernate OGM。这是我们在本节前面手动添加的模块:

    <property name="jboss.as.jpa.providerModule" value="org.hibernate:ogm"/>
    
  • 此外,我们需要禁用持久性单元的类转换器(默认情况下,类增强或重写是允许的)。为此,将jboss.as.jpa.classtransformer设置为false :

    <property name="jboss.as.jpa.classtransformer" value="false"/>        
    
  • 接下来,通过将hibernate.listeners.envers.autoRegister属性设置为false :

    <property name="hibernate.listeners.envers.autoRegister" value="false"/>
    

    来关闭自动 Envers 事件监听器注册

  • 最后,配置 MongoDB 连接(提供者、方言(可选)、数据库名称、主机和端口)。一旦你完成了,整个persistence.xml文件就可用了,如清单 4-24 所示。

清单 4-24。 persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">

 <persistence-unit name="HOGM_JPA_JBOSSAS_PU" transaction-type="JTA">
   <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
   <class>hogm.jpa.entities.LuckyNumberEntity</class>
   <properties>
      <property name="hibernate.transaction.jta.platform"
                      value="org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform"/>
      <property name="jboss.as.jpa.adapterModule" value="org.jboss.as.jpa.hibernate:4"/>
      <property name="jboss.as.jpa.providerModule" value="org.hibernate:ogm"/>
      <property name="jboss.as.jpa.classtransformer" value="false"/>
      <property name="hibernate.listeners.envers.autoRegister" value="false"/>
      <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
      <property name="hibernate.ogm.datastore.grid_dialect"
                      value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
      <property name="hibernate.ogm.mongodb.database" value="jbossas_db"/>
      <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
      <property name="hibernate.ogm.mongodb.port" value="27017"/>
    </properties>
 </persistence-unit>
</persistence>

关于没有指定数据源的事实,请记住,正如我前面指出的,Hibernate OGM 不需要数据源。然而,在某些情况下,数据源必须被指定为符合 JPA 规范。对于 JBoss AS 7.1,提供数据源的最简单方法(以防出现相关错误,但我没有)是添加现成的数据源,如下所示:

...
<!-- out of the box data source for GlassFish v3-->
<jta-data-source> java:jboss/datasources/ExampleDS</jta-data-source>
...

在这一点上,我可以说我们尊重运行 Hibernate OGM 应用的每一个 JBoss AS 7 需求。

接下来,您必须添加前面示例中讨论的应用代码(LuckyNumberEntity实体、CMTBean EJB 组件(不要忘记将单元名称改为HOGM_JPA_JBOSSAS_PU)和index.xhtml网页),并添加 CDI 和 JSF 设置(可以从Project Facets向导中选择)。完成后,您应该能够部署和运行应用,而不会出现任何不愉快的事件。为了做到这一点,我使用 JBoss 作为 Eclipse JUNO 的工具,但是您可以按照自己喜欢的方式来做。

测试

按照第一章中的方法启动 MongoDB 服务器。接下来,因为您处于 Eclipse/JBoss AS(或 NetBeans/JBoss AS)环境中,所以只需保存项目并选择Run on Server(或 NetBeans 中的Run)来部署和运行应用。如果应用成功启动,你会在浏览器中看到类似于图 4-16 所示的东西。

9781430257943_Fig04-16.jpg

图 4-16 。运行 HOGMviaJPA_EE_JBossAS 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(jbossas_db)集合(jpa))中。打开命令提示符,键入图 4-17 中的命令,查看您的工作结果。您可以将 JBoss 作为日志消息来监控,以防发生任何不必要的事情。

9781430257943_Fig04-17.jpg

图 4-17 。检查 jpa 收藏内容

image 你将会看到hibernate_sequences系列如何以及为什么会出现在第五章中。

这个应用的完整源代码被命名为HOGMviaJPA_EE_JBossAS,可以在 Apress 存储库中找到。它是一个 Eclipse 项目,在 JBoss AS 7.1 下测试过。

在独立的 JTA 环境中 Hibernate OGM(Apache Tomcat 7)

在本章的前面,我们通过 Hibernate Native API 创建了一个 Hibernate OGM,它被部署在一个带有 Tomcat 7 web 服务器的独立 JTA 环境中。在这一节中,我们将用 Java 持久性 API 替换 Hibernate 本机 API 部分。我们将使用一个EntityManager来代替 Hibernate Session,

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta1
  • JDK 1.7
  • NetBeans 7.2.1(或 Eclipse JUNO)
  • 阿帕奇雄猫 7

正在开发

启动 NetBeans 后,创建一个包含一个空 Maven web 应用的新项目,并将其命名为HOGMviaJPAJTA_Tomcat7。在New Web Application向导中,为Group IdPackage字段键入hogm.hnapi,并选择 Apache Tomcat 7 web server 来部署这个应用。当您在Projects窗口中看到项目时,编辑pom.xml文件(它必须在Project Files节点下)。在pom.xml文件中,通过粘贴以下依赖项来添加 Hibernate OGM(包括 MongoDB 支持)和 JBoss JTA(独立于 JBoss 的 JTA)发行版:

<dependencies>
   <dependency>
      <groupId>org.hibernate.ogm</groupId>
      <artifactId>hibernate-ogm-core</artifactId>
      <version>4.0.0.Beta2</version>
   </dependency>
   <dependency>
      <groupId>org.hibernate.ogm</groupId>
      <artifactId>hibernate-ogm-mongodb</artifactId>
      <version>4.0.0.Beta1</version>
   </dependency>
   <dependency>
      <groupId>org.jboss.jbossts</groupId>
      <artifactId>jbossjta</artifactId>
      <version>4.16.4.Final</version>
   </dependency>
...
<dependencies>

现在保存项目,驱动程序 JAR 将列在Dependencies节点下。

编写应用代码

现在添加名为LuckyNumberEntity.的著名实体,您可以在前面的示例中找到它;这是一个简单的 POJO,用@Entity, @Table(name="jpa")进行了注释,有一个名为id的主键字段,类型为String,使用 UUID2 生成器生成,还有一个名为luckynumberint字段。

接下来,我们将编写persistence.xml文件。在一个 Maven 项目中,将这个文件放在Other Sources/src/main/resources/META-INF文件夹中,开始时将持久性单元命名为HOGM_JPA_JTA_TOMCAT_PU,将事务类型命名为JTA:

<persistence-unit name="HOGM_JPA_JTA_TOMCAT_PU" transaction-type="JTA">
...

通过添加<provider>标签,将 Hibernate OGM 设置为 JPA 提供者:

<provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>

使用<class>属性在这个持久性单元中添加实体类:

<class>hogm.hnapi.entities.LuckyNumberEntity</class>

接下来,我们需要指定 JTA 平台——JBoss JTA。为此,请添加以下内容:

<property name="hibernate.transaction.jta.platform"
                 value="org.hibernate.service.jta.platform.internal.JBossStandAloneJtaPlatform"/>

注意,我们指定了 JBoss JTA 独立发行版,而不是 JBoss AS 使用的发行版。

JBoss TS 文档指出,为了选择本地 JBoss JTA 实现,您必须指定两个属性:com.arjuna.ats.jta.jtaTMImplementationcom.arjuna.ats.jta.jtaUTImplementation。我们可以这样指定它们:

<property name="com.arjuna.ats.jta.jtaTMImplementation"
                value="com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple"/>
<property name="com.arjuna.ats.jta.jtaUTImplementation"
                value="com.arjuna.ats.internal.jta.transaction.arjunacore.UserTransactionImple"/>

现在,我们将使用数据存储提供者、方言、要连接的数据库的名称以及主机和端口来配置 MongoDB 连接(我们将使用本地主机和默认的 MongoDB 服务器端口 27017)。把所有东西放在一起,我们得到了清单 4-25 中所示的persistence.xml文件。

清单 4-25。 Persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">
    <persistence-unit name="HOGM_JPA_JTA_TOMCAT_PU" transaction-type="JTA">
        <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
        <class>hogm.hnapi.entities.LuckyNumberEntity</class>
        <properties>
            <property name="hibernate.transaction.jta.platform"
                      value="org.hibernate.service.jta.platform.internal.JBossStandAloneJtaPlatform"/>
            <property name="com.arjuna.ats.jta.jtaTMImplementation"
                      value="com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple"/>
            <property name="com.arjuna.ats.jta.jtaUTImplementation"
                      value="com.arjuna.ats.internal.jta.transaction.arjunacore.UserTransactionImple"/>
            <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
            <property name="hibernate.ogm.datastore.grid_dialect"
                      value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
            <property name="hibernate.ogm.mongodb.database" value="tomcat_db"/>
            <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
            <property name="hibernate.ogm.mongodb.port" value="27017"/>
        </properties>
    </persistence-unit>
</persistence>

至此,我们有了一个实体和相应的持久性单元,所以是时候添加一个 DAO 类来将幸运数字存储到 MongoDB 数据库中了。首先,基于这个持久性单元(HOGM_JPA_JTA_TOMCAT_PU),我们需要获得一个实体管理器工厂,并从这个工厂获得一个实体管理器,如下所示:

private static final EntityManagerFactory emf =
                             Persistence.createEntityManagerFactory("HOGM_JPA_JTA_TOMCAT_PU");
private final EntityManager em = emf.createEntityManager();

现在,实体管理器已经准备好加入一个事务并对 MongoDB 数据库执行语句(在我们的例子中,是持久化语句),但是为此我们需要获取用户事务来设置事务边界。我们在以前的应用中已经这样做了,但如果您不记得了,至少有两种方法可以做到:

  • 使用静态方法transactionManager:
javax.transaction.TransactionManager tx = com.arjuna.ats.jta.TransactionManager.transactionManager();
  • 使用静态方法userTransaction:
javax.transaction.UserTransaction tx = com.arjuna.ats.jta.UserTransaction.userTransaction();

image 注意TransactionManager接口允许应用服务器代表被管理的应用控制事务边界,而UserTransaction接口允许应用控制事务边界。显然,当应用控制事务边界时,您可以使用这两种方法,但是当您允许应用服务器控制事务边界时,您必须使用TransactionManager

现在,您可以用控制事务流的begin, commit,rollback方法来区分持久化语句。在事务开始后(当调用begin方法时),实体管理器必须通过调用joinTransaction方法加入它,如下所示:

...
tx.begin();
em.joinTransaction();
em.persist(transientInstance);
tx.commit();
...

提供用于清除和关闭实体管理器的代码,一些用于监控应用流的消息,您将得到如清单 4-26 所示的 DAO 类。

清单 4-26。 幸运数字道类

package hogm.hnapi.dao;

import hogm.hnapi.entities.LuckyNumberEntity;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class LuckyNumberDAO {

    private static final Logger log = Logger.getLogger(LuckyNumberDAO.class.getName());
    private static final EntityManagerFactory emf =
                                 Persistence.createEntityManagerFactory("HOGM_JPA_JTA_TOMCAT_PU");
    private final EntityManager em = emf.createEntityManager();

    public void persistAction(LuckyNumberEntity transientInstance) throws java.lang.Exception {

        log.log(Level.INFO, "Persisting LuckyNumberEntity instance ...");

        javax.transaction.TransactionManager tx =
                                      com.arjuna.ats.jta.TransactionManager.transactionManager();
        // javax.transaction.UserTransaction tx = com.arjuna.ats.jta.UserTransaction.userTransaction();

        try {
            tx.begin();
            em.joinTransaction();
            em.persist(transientInstance);
            tx.commit();

            log.log(Level.INFO, "Persist successful ...");
        } catch (Exception re) {
            tx.rollback();

            log.log(Level.SEVERE, "Persist failed ...", re);
            throw re;
        }  finally {
            if (em != null) {
                em.clear();
                em.close();
            }
        }
    }
}

重要的部分完成了!我们只需添加一个简单的 servlet 来处理 DAO 类,如下所示:

package hogm.hnapi.servlet;
...
@WebServlet(name = "LuckyNumberServlet", urlPatterns = {"/LuckyNumberServlet"})

public class LuckyNumberServlet extends HttpServlet {
...
protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, Exception {
  ...
  LuckyNumberDAO luckyNumberDAO = new LuckyNumberDAO();
  LuckyNumberEntity luckyNumberEntity = new LuckyNumberEntity();
  luckyNumberEntity.setLuckynumber(new Random().nextInt(1000000));

  luckyNumberDAO.persistAction(luckyNumberEntity);
  ...
  }
}

一个普通的 JSP 页面(index.jsp)向我们的 servlet 发送空请求:

...
<form action="./LuckyNumberServlet" method="POST">
   <input type="submit" value="Generate Lucky Number">
</form>
...

搞定了。

测试

按照第一章中的方法启动 MongoDB 服务器。接下来,因为您处于 NetBeans/Tomcat(或 Eclipse/Tomcat)环境中,所以只需保存项目并单击Run(或 Eclipse 中的Run on Server)按钮来启动 Tomcat 并部署和运行应用。如果应用成功启动,您将在浏览器中看到类似于图 4-18 所示的内容。

9781430257943_Fig04-18.jpg

图 4-18 。运行 HOGMviaJPAJTA_Tomcat7 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(tomcat_db)集合(jpa))中。打开命令提示符,键入图 4-19 中的命令,查看您的工作结果。您可以监控 Tomcat 日志消息,以防发生任何不必要的事情。

9781430257943_Fig04-19.jpg

图 4-19 。检查 jpa 集合内容

这个应用的完整源代码被命名为HOGMviaJPAJTA_Tomcat7,可以在 Apress 存储库中找到。它是一个 NetBeans 项目,并在 GlassFish AS 3 下进行了测试。

内置 JTA 环境中的 hibernate OGM(JBoss AS 7 和 Seam 3 应用)

我在本章的最后保留了两个应用,它们涉及到 Seam 和 Spring,这两个强大而流行的 J2EE 框架。作为一个 Seam 迷,我已经看到 Seam 成为一个成熟和健壮的框架,在版本 3 中转变为"一个为 Java EE 6 应用开发量身定制的模块和开发人员工具的集合,以 CDI 为核心。

由于模块化框架结构和 CDI 注入机制,您可以创建仅涉及您需要的模块的 Seam 3 应用。在下一个应用中,我们使用一个名为 Seam Persistence 的 Seam 3 模块(这是最接近我们主题的模块),它“将事务和持久性引入托管 beans,提供一个简化的事务 API,并将事务传播事件与 CDI 事件总线挂钩。“在 Seam 持久性的众多特性中,有两个非常突出:

  • Seam 管理的持久化上下文——这是一个内置的 Seam 组件,能够管理实体管理器(对于 JPA 它甚至可以在 SE 环境中工作,因为 Seam 持久性扩展将引导EntityManagerFactory和会话(Session代表 Hibernate)。此外,它在 EE 容器内外都提供了稳定性和健壮性。
  • 声明性事务——Seam 已经升级了 EJB 3 众所周知的@TransactionAttribute,为普通 beans 提供声明性事务,更酷的是,这可以在 EJB 完全未知的 EE 容器之外工作。

如果你在这两个特性上再加上简单的配置和集成,你会发现 Seam 的持久性真的很棒!

因此,让我们编写一个使用 Seam 3(Seam 持久性模块)和 Hibernate OGM 作为 JPA 的应用。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta2
  • JDK 1.7
  • 日蚀朱诺
  • 伪造 1.0.5 或 1.1.3
  • JBoss AS 7

发展中的

我们首先关心的是如何开始一个 Seam 持久性项目,因为有几种可能性。例如,您可以通过 Maven 工件添加 Seam 持久性分布:

<dependencies>
 <dependency>
<groupId>org.jboss.seam.persistence</groupId>
<artifactId>seam-persistence-api</artifactId>
<version>${seam.persistence.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.seam.persistence</groupId>
<artifactId>seam-persistence-impl</artifactId>
<version>${seam.persistence.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.seam.solder</groupId>
<artifactId>seam-solder</artifactId>
<version>${seam.solder.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.seam.xml</groupId>
<artifactId>seam-xml-config</artifactId>
<version>${seam.xml.version}</version>
</dependency>
 ...
</dependencies>

或者,更好的是,您可以使用 JBoss Tools 用于 Eclipse,或者使用 Seam Forge Tools 用于 Eclipse(实际上 Seam Forge Tools 现在是 JBoss Tools 的子工具)。然而,对于我们的需求,决定是明确的:我们将为 Eclipse JUNO 使用 Seam Forge 工具插件(www.forge.jboss.org/)。你可能已经在你的 Eclipse 发行版中或者 Eclipse 之外安装了它,并且已经使用过很多次了,但是如果你是 Forge 的新手并且你想快速安装它,那么进入Help|Install New Software窗口,添加JBoss Tools( http://download.jboss.org/jbosstools/updates/development/indigo/ )或者从列表中选择它),展开Abridged JBoss Tools 3.3节点,然后选择Forge Tools(参见图 4-20 )。

9781430257943_Fig04-20.jpg

图 4-20 。为 Eclipse 安装锻造工具

按照向导中的步骤进行安装,然后重新启动 IDE。现在,从Window | Show View窗口,你可以激活Forge | Forge Console.最初锻造没有运行,但是可以通过按下锻造条上的绿色小三角来启动(图 4-21 )。

9781430257943_Fig04-21.jpg

图 4-21 。Eclipse 中的伪造控制台

使用 Forge 的最大好处是你不需要阅读大量的教程,因为它只是命令行工具和自动化的外壳。没有复杂的向导、设置、XMLs 配置或任何东西,只有一束命令,可以在几秒钟内生成整个项目,包括 Seam 和 EE。

编写应用代码

我假设你现在正在看一个 Seam Forge 控制台。(在 Eclipse 下建议这样做,因为它让您在每次键入命令后都能看到项目的创建进度。)让我们插入使用 Seam 持久性模块生成新的 Seam 3 项目所需的命令。

首先,我们需要在项目上下文之外为 Forge 安装 Seam 持久性插件(如果它不存在的话)。这可以通过以下命令轻松完成:

forge install-plugin seam-persistence --version 3.1.0.Final

现在我们可以插入创建新项目的命令:

  • 创建一个名为HOGMviaJPA_SEAM3 :

    new-project --named HOGMviaJPA_SEAM3
    

    的新项目

  • 将 JavaServer Faces scaffold 添加到新项目中(对所有问题都回答是):

    scaffold setup
    
  • 选择要安装的 JBoss Java EE 版本。在版本列表中,找到org.jboss.spec:jboss-javaee-6.0:pom::3.0.1.Final并在其前面键入数字(如果没有,则选择最新的最终版本)。

  • 在一堆成功消息之后,你会看到问题“在 web-root 的哪个子目录下创建 scaffold?”。类型main

  • 安装 Seam 持久模块:

    seam-persistence setup
    
  • 您将被要求指出要安装哪个版本。找到org.jboss.seam.persistence:seam-persistence:::3.1.0.Final版本,并在其前面键入编号(如果没有,选择最新的最终版本)。

  • 安装 Seam 托管持久性上下文:

    seam-persistence install-managed-persistence-context
    
  • 系统将提示您为持久性上下文生成器指定包和类名。只需按下每个问题的Enter键,接受默认建议。

  • 通过键入:

    seam-persistence enable-declarative-tx
    

    激活声明性事务支持

  • 生成一个实体类——LuckyNumberEntity类(通过按Enter键接受建议的包名):

    entity --named LuckyNumberEntity
    
  • 通过键入:

    field int --named luckynumber
    

    将字段luckynumber添加到实体中

  • 搞定了。我们已经拥有了所有需要的组件,所以我们准备构建我们的项目。类型:

    build
    

如果构建成功结束,那么您已经做了很好的工作,项目应该在 Eclipse IDE 的Project Explorer选项卡下可见。不要担心标记项目有错误的红色“x”——这是因为persistence.xml文件是空的。(即使你没有那个红色的“x”,你仍然需要用正确的设置填充persistence.xml。)

让我们使用 Eclipse IDE 向导来消除这个恼人的错误。在Project Explorer中,找到HOGMviaJPA_SEAM3项目节点,右键单击并从上下文菜单中选择Properties。现在,按照本章中“在内置 JTA 环境中 Hibernate OGM(EJB 3,JBoss AS 7 ) ”一节的“编写应用”部分的说明,获取清单 4-27 中显示的persistence.xml内容。

清单 4-27。 persistence.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<persistence FontName2">http://java.sun.com/xml/ns/persistence "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance " version="2.0"
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">
 <persistence-unit name="HOGM_JPA_SEAM3_PU" transaction-type="JTA">

   <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
   <class>com.example.HOGMviaJPA_SEAM3.model.LuckyNumberEntity</class>
   <properties>
      <property name="hibernate.transaction.jta.platform"
                      value="org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform"/>
      <property name="jboss.as.jpa.adapterModule" value="org.jboss.as.jpa.hibernate:4"/>
      <property name="jboss.as.jpa.providerModule" value="org.hibernate:ogm"/>
      <property name="jboss.as.jpa.classtransformer" value="false"/>
      <property name="hibernate.listeners.envers.autoRegister" value="false"/>
      <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
      <property name="hibernate.ogm.datastore.grid_dialect"
                      value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
      <property name="hibernate.ogm.mongodb.database" value="jbossas_db"/>
      <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
      <property name="hibernate.ogm.mongodb.port" value="27017"/>
   </properties>
 </persistence-unit>
</persistence>

image 注意在保存和构建之前,通过添加一行@Table(name="seam")来编辑LuckyNumberEntity

用 Forge build 命令保存并构建项目,错误就会消失。

现在我们需要添加将幸运数字持久化到 MongoDB 数据库中的业务逻辑,我认为 EJB 组件正是我们所需要的,因为我们可以很好地利用它的 CDI 特性。首先,在 Java sources ( src/main/java)下创建一个名为com.example.HOGMviaJPA_SEAM3.view的新包,里面有一个名为CMTBean的空无状态 bean。如果您从 Eclipse 向导创建无状态 bean,请选择 EJB 节点下的会话 Bean (EJB 3.x)叶。

现在我们将使用 Seam 管理的持久性上下文。如果您不熟悉它,您可能会认为将它粘贴到我们的 EJB 组件中会导致大量的代码。但是请记住,我们需要做的只是使用 CDI @Inject注释来获得 Seam managed EntityManager:

@Inject @Forge EntityManager em;

@Forge表示一个 CDI 限定符(Seam 管理的持久性上下文工厂类和限定符类都是由 Seam Forge 生成的,并放在 Java sources src/main/java 下的包 com.example.HOGMviaJPA_SEAM3 中)。

image 注意我们没有使用声明性事务特性(尽管我们安装了它),因为我们在 EE 环境中,EJB 默认是事务性的。

这一行代码完成了注入和管理实体管理器的所有工作。接下来,我们将使用最常见的方法来赋予持久化过程生命:

...
public void persistAction() {
   LuckyNumberEntity luckyNumberEntity = new LuckyNumberEntity();
   luckyNumberEntity.setLuckynumber(new Random().nextInt(1000000));
   em.persist(luckyNumberEntity);
  }
...

最后,我们用@Named注释我们的 EJB 组件,使它在一个简单的 JSF 表单中可见。清单 4-28 显示了完整的 EJB 代码。

清单 4-28。EJB 全集

package com.example.HOGMviaJPA_SEAM3.view;

import java.io.Serializable;
import java.util.Random;
import javax.ejb.Stateful;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.EntityManager;

import com.example.HOGMviaJPA_SEAM3.Forge;
import com.example.HOGMviaJPA_SEAM3.model.LuckyNumberEntity;

@Named("bean")
@Stateful
@RequestScoped
public class CMTBean implements Serializable
{
    private static final long serialVersionUID = 1L;

    @Inject @Forge EntityManager em;

  public void persistAction() {
      LuckyNumberEntity luckyNumberEntity = new LuckyNumberEntity();
      luckyNumberEntity.setLuckynumber(new Random().nextInt(1000000));
      em.persist(luckyNumberEntity);
  }
}

通过对 Seam Forge 在src/main/webapp/main/index.xhtml下生成的index.xhtml文件做一些修改,就可以很容易地用LuckyNumberEntity实例调用persistAction方法。第一个修改涉及使用 Taglib 指令导入 JSF 标记库;为此使用 XML 语法(参见粗体代码):

<ui:composition FontName2">http://www.w3.org/1999/xhtml "
        xmlns:ui=" http://java.sun.com/jsf/facelets "
        template="/resources/scaffold/pageTemplate.xhtml"
       xmlns:h=" [`java.sun.com/jsf/html`](http://java.sun.com/jsf/html) " >
...

其次,将下一个表单放入代码中的某个地方——我将它粘贴到<ui:define>标签中。因为这只是一个例子,所以我保留了生成的设计:

...
<ui:define name="subheader">
<h:form>
   <h:commandButton action="#{bean.persistAction()}" value="Generate Lucky Number"/>
</h:form>
</ui:define>
...

最后,指定应用起始页。编辑web.xml文件(在src/main/webapp/WEB-INF文件夹下),并在末尾添加以下代码:

...
<welcome-file-list>
    <welcome-file>faces/main/index.xhtml</welcome-file>
</welcome-file-list>
...

保存并再次构建项目(使用 Forge 控制台)就这样!

测试

按照第一章中的方法启动 MongoDB 服务器。接下来,因为您处于 Eclipse/JBoss AS(或 NetBeans/JBoss AS)环境中,所以只需保存项目并单击Run on Server(或 NetBeans 中的Run)按钮来启动 JBoss AS 并部署和运行应用。如果应用成功启动,你会在浏览器中看到类似于图 4-22 所示的内容。

9781430257943_Fig04-22.jpg

图 4-22 。运行 HOGMviaJPA_SEAM3 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(jbossas_db)集合(seam))中。打开命令提示符并键入图 4-23 中的命令来查看您的工作结果。您可以将 JBoss 作为日志消息来监控,以防发生任何不必要的事情。

9781430257943_Fig04-23.jpg

图 4-23 。查缝收藏内容

这个应用的完整源代码被命名为HOGMviaJPA_SEAM3,可以在 Apress 资源库中找到。它是一个 Eclipse 项目,在 JBoss AS 7 下测试过。

内置 JTA 环境中的 hibernate OGM(GlassFish 3 和 Spring 3 应用)

市面上最好的开源 Java 企业框架之一,拥有数百万粉丝,就是 Spring,尤其是 distribution 3。在本节中,我们将开发一个通过 JPA 集成 Spring 3 和 Hibernate OGM 的应用。既然您正在阅读本节,那么您可能是 Spring 的粉丝,这个应用对您来说可能看起来非常简单。请记住,这里的重点是向您展示如何将 Hibernate OGM 添加到这个等式中。所以,让我们用 Spring 和 Hibernate OGM 来持久化一些幸运数字。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta2
  • JDK 1.7
  • NetBeans IDE 7.2.1(或 Eclipse JUNO)
  • Spring 3.1.1
  • 玻璃鱼 3.1.2.2

发展中的

启动 NetBeans 后,创建一个由空的Web Application组成的新项目(注意,我们不会在这个应用中使用 Maven),并将其命名为HOGMviaJPA_Spring3。选择 GlassFish AS 来部署该应用,并从 NetBeans 向导添加 Spring Web MVC 框架。

一旦在Projects窗口下有了项目,除了 NetBeans 自动添加的 Spring 3 . 1 . 1 jar 之外,您还需要提供几个 jar。从 Hibernate OGM/MongoDB JARs 开始,它应该在第一章中创建的 Hibernate OGM 核心和 MongoDB 用户库中可用。继续从网上下载两个罐子:asm-3.1.jar ( http://asm.ow2.org/)和aopalliance.jar ( http://aopalliance.sourceforge.net/)。

现在您已经有了所有必需的 jar,我们可以开始编码了。

编写应用代码

我们将从开发实体类和persistence.xml开始。向我们的 MongoDB 数据库提供幸运数字代码的实体类,如清单 4-29 所示,非常简单。

清单 4-29。 实体类

package hogm.spring;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "spring")
public class LuckyNumberEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(name = "luckynumber", nullable = false)
    private int luckynumber;

    public LuckyNumberEntity() {
    }

    public int getLuckynumber() {
        return luckynumber;
    }

    public void setLuckynumber(int luckynumber) {
        this.luckynumber = luckynumber;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

添加一个空的persistence.xmlpersistence.xml包含一个持久化单元HOGMviaJPA_SPRING3_PU和一个定义为JTA的事务类型:

<persistence-unit name="HOGMviaJPA_SPRING3_PU" transaction-type="JTA">
...

接下来,通过添加<provider>标记将 Hibernate OGM 指定为 JPA 提供者:

<provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>

使用<class>属性将清单 4-28 中的entity类添加到这个持久性单元中:

<class>hogm.spring.LuckyNumberEntity</class>

使用hibernate.transaction.jta.platform属性指定 JTA 平台。该属性的值可以在第二章的列表中找到。对于 GlassFish AS,请使用:

...
<property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform"/>
...

我们差不多完成了;我们只需要配置 MongoDB 连接(提供者、方言(可选)、数据库名称、主机和端口)。一旦我们完成了这些,我们就有了如清单 4-30 所示的整个persistence.xml文件。

清单 4-30。 p ersistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence
" xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance " xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">

 <persistence-unit name="HOGMviaJPA_SPRING3_PU" transaction-type="JTA">
    <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
    <class>hogm.spring.LuckyNumberEntity</class>
    <properties>
      <property name="hibernate.transaction.jta.platform"
                      value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform"/>
      <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
      <property name="hibernate.ogm.datastore.grid_dialect"
                      value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
      <property name="hibernate.ogm.mongodb.database" value="glassfish_db"/>
      <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
      <property name="hibernate.ogm.mongodb.port" value="27017"/>
    </properties>
  </persistence-unit>
</persistence>

现在,我们准备添加一些 DAO 业务逻辑来利用 JPA 设置。为此,我们可以编写一个简单的 Spring 组件(用@Component注释该类),它注入一个EntityManager并实现一个事务性的persist方法,如下所示:

package hogm.spring;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class LuckyNumberDAO {

    @PersistenceContext
    private EntityManager em;

    @Transactional
    public void persist(LuckyNumberEntity luckyNumberEntity) {
        em.persist(luckyNumberEntity);
    }
}

注意,我们使用了@Transactional注释,因为我们希望 Spring 在事务中包装该方法。

为了创建一个经典的 Spring 应用,我们需要一个 Spring 控制器(用@Controller注释这个类),它能够接收来自多个用户的 HTTP 请求,并且能够参与 MVC 工作流。我们的控制器将接收用户的 HTTP GET 请求,并为每个请求生成一个新的幸运数字,该数字将成为传递给 DAO persist方法的参数。为此,我们使用了让容器自动连接 bean 的@Autowired注释——在我们的例子中,是在清单 4-31 中显示的LuckyNumberDAO bean。

清单 4-31。??【幸运数字】刀豆

package hogm.spring;

import java.util.Random;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class LuckyNumberController {

    @Autowired
    private LuckyNumberDAO luckyNumberDao;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(ModelMap map) {
        LuckyNumberEntity luckyNumberEntity = new LuckyNumberEntity();
        luckyNumberEntity.setLuckynumber(new Random().nextInt(1000000));

        luckyNumberDao.persist(luckyNumberEntity);

        return "index";
    }
}

用户可以使用我们添加到WEB-INF/jsp/index.jsp页面的 Spring 表单发出 HTTP GET 请求。我们使用 Taglib 指令来导入 Spring 标签库:

<%@ taglib prefix="form" uri=" http://www.springframework.org/tags/form " %>
...
<form:form method="GET" commandName="/">
   <input type="submit" value="Generate Lucky Number" />
</form:form>
...

快好了。再有两个 XML 配置文件,我们就可以运行应用了。清单 4-32 中的所示的众所周知的dispatcher-servlet.xml,,需要包含几个设置,例如启用 Spring MVC @Controller编程模型,定义实体管理器工厂(注意我们指明了我们的 Hibernate OGM 持久性单元名称)和 Spring JTA 事务管理器(它应该放在WEB-INF文件夹中)。

清单 4-32。 d ispatcher-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans FontName2">http://www.springframework.org/schema/beans "
       xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
       xmlns:p=" http://www.springframework.org/schema/p "
       xmlns:mvc=" http://www.springframework.org/schema/mvc "
       xmlns:tx=" http://www.springframework.org/schema/tx "
       xmlns:context=" http://www.springframework.org/schema/context "
       xsi:schemaLocation=" http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd ">

    <context:component-scan base-package="hogm.spring" />
    <context:annotation-config/>
    <mvc:annotation-driven />
    <tx:annotation-driven transaction-manager="txManager" />

    <bean id="jspViewResolver"
            class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="viewClass"
                    value="org.springframework.web.servlet.view.JstlView" />
            <property name="prefix" value="/WEB-INF/jsp/" />
            <property name="suffix" value=".jsp" />
    </bean>

    <bean id="entityManagerFactory"
               class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
            <property name="persistenceUnitName" value="HOGMviaJPA_SPRING3_PU"/>
    </bean>

    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager">
    </bean>
</beans>

最后,生成的web.xml要做相应的调整,如清单 4-33 所示。应该放在WEB-INF文件夹里。

清单 4-33。.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app    version="3.0"
     FontName2">http://java.sun.com/xml/ns/javaee "
     xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
     xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee
     http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd ">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>/</welcome-file>
    </welcome-file-list>
</web-app>

搞定了。

image 注意 Spring 也支持 NoSQL 数据存储,像 MongoDB,没有 Hibernate OGM。更多详情,请访问www.springsource.org/spring-data/mongodb

测试

启动 MongoDB 服务器,如你在第一章中所见。接下来,因为您处于 NetBeans/GlassFish(或 Eclipse/GlassFish)环境中,所以只需保存项目并单击Run(或 Eclipse 中的Run on Server)按钮来启动 GlassFish 并部署和运行应用。如果应用成功启动,你会在浏览器中看到类似于图 4-24 所示的内容。

9781430257943_Fig04-24.jpg

图 4-24 。运行 HOGMviaJPA_SPRING3 应用

按几次Generate Lucky Number按钮,将一些幸运数字保存到 MongoDB 数据库(glassfish_db)集合(spring))中。打开命令提示符并键入图 4-25 中的命令来查看您的工作结果。您可以监控 GlassFish 日志消息,以防发生任何不必要的事情。

9781430257943_Fig04-25.jpg

图 4-25 。查看 Spring 收藏内容

这个应用的完整源代码被命名为HOGMviaJPA_SPRING3,可以在 Apress 存储库中找到。它是一个 NetBeans 项目,并且是在 GlassFish AS 3 下测试的。

在非 JTA 环境中 Hibernate OGM(RESOURCE _ LOCAL,Apache Tomcat 7)

在这一节中,我们将开发一个 Hibernate OGM 应用,它将在不推荐的条件和环境中运行——这就是为什么我们把它放在最后。基本思想是,我们将在非 EE 环境中(在 Tomcat web 容器中)使用类型为RESOURCE_LOCAL的事务。换句话说,我们将让 JPA provider 实现管理非 JTA 容器中的事务(它不提供 JTA 实现,因此它显然不提供自动事务管理)。

Hibernate OGM 文档不建议在 JTA 环境之外使用 OGM(内置或独立)。但是,不推荐并不意味着它不起作用(特别是对于不支持事务的 MongoDB)。因此,我们可以尝试一下,并得出一些结论。

先决条件

  • MongoDB 2.2.2
  • Hibernate OGM 4.0.0.Beta2
  • JDK 1.7
  • NetBeans 7.2.1(或 Eclipse JUNO)
  • 阿帕奇雄猫 7

发展中的

启动 NetBeans 后,创建一个包含一个空 Maven web 应用的新项目,并将其命名为HOGMviaJPA_RESOURCELOCAL_Tomcat7。在New Web Application向导中,为Group IdPackage字段键入hogm.hnapi,并选择 Tomcat 应用服务器来部署这个应用。一旦你看到项目列在Projects窗口中,你需要编辑pom.xml文件(它必须在Project Files节点下)。在pom.xml文件中,通过粘贴众所周知的依赖项来添加 Hibernate OGM 发行版(包括 MongoDB 支持)。

编写应用代码

我们从开发实体类和persistence.xml开始。为我们的 MongoDB 数据库提供幸运数字代码的实体类(LuckyNumberEntity)非常简单,我们几乎在前面所有的例子中都使用过它。因此,我们可以跳过这里的列表(只要记住使用@Table(name="jpa_rl")。接下来,我们关注放在Other Sources/src/main/resources/META-INF)文件夹中的persistence.xml,。正如你在清单 4-34 中看到的,它没有指定 JTA 平台,没有特殊设置,只有设置为RESOURCE_LOCALtransaction-type和 MongoDB 连接设置。

清单 4-34。 persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">
   <persistence-unit name="HOGM_JPA_RESOURCE_LOCAL_PU" transaction-
                                type="RESOURCE_LOCAL">
        <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
        <class>hogm.hnapi.entities.LuckyNumberEntity</class>
        <properties>
            <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
            <property name="hibernate.ogm.datastore.grid_dialect"
                            value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
            <property name="hibernate.ogm.mongodb.database" value="tomcat_db"/>
            <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
            <property name="hibernate.ogm.mongodb.port" value="27017"/>
        </properties>
    </persistence-unit>
</persistence>

现在我们开发负责在 MongoDB 数据库中保存幸运数字的 DAO 类,如清单 4-35 所示。如您所见,我们需要一些管道代码,因为我们使用的是 JPA 提供者提供的事务机制,在我们的例子中是 Hibernate OGM。交易方式begin, commit,rollback通过EntityManager提供。

清单 4-35。??【幸运数字】道类

package hogm.hnapi.dao;

import hogm.hnapi.entities.LuckyNumberEntity;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class LuckyNumberDAO {

    private static final Logger log = Logger.getLogger(LuckyNumberDAO.class.getName());
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("HOGM_JPA_RESOURCE_LOCAL_PU");
    private EntityManager em = emf.createEntityManager();

    public void persistAction(LuckyNumberEntity transientInstance) throws java.lang.Exception {

        log.log(Level.INFO, "Persisting LuckyNumberEntity instance ...");

        try {
            em.getTransaction().begin();
            em.persist(transientInstance);
            em.getTransaction().commit();

            log.log(Level.INFO, "Persist successful...");
        } catch (Exception re) {
            em.getTransaction().rollback();

            log.log(Level.SEVERE, "Persist failed...", re);
            throw re;
        } finally {
            if (em != null) {
                em.clear();
                em.close();
            }
        }
    }
}

现在假设我们有一段代码将用户与 DAO 类(一个 servlet 和一个简单的 XHTML 页面)连接起来,我们运行应用时看到一个错误,如下所示:

Caused by: java.lang.ClassNotFoundException: Could not load requested class : com.arjuna.ats.jta.TransactionManager

这个错误包含两个提示:第一,JPA 提供者没有找到任何 JTA 实现(正常,因为我们在一个非 JTA 环境中),第二,默认情况下,JPA 提供者正在寻找 JBoss JTA 实现。因此,我们需要添加 JBoss JTA JARs,并且我们必须在pom.xml文件中添加相应的 Maven 工件:

<dependency>
    <groupId>org.jboss.jbossts</groupId>
    <artifactId>jbossjta</artifactId>
    <version>4.16.4.Final</version>
</dependency>

现在,再次运行应用,一切都将正常工作(不要忘记 web 页面和 servlet——您可以从以前的项目中复制它们,或者直接从 Apress 存储库中下载应用)。

测试

启动 MongoDB 服务器,如你在第一章中所见。接下来,因为您处于 NetBeans/Tomcat(或 Eclipse/Tomcat)环境中,所以只需保存项目并单击Run(或 Eclipse 中的Run on Server)按钮来启动 Tomcat 并部署和运行应用。如果应用成功启动,你会在浏览器中看到类似于图 4-26 所示的内容。

9781430257943_Fig04-26.jpg

图 4-26 。运行 HOGMviaJPA _ resource local _ Tomcat 7 应用

按几次生成幸运数字按钮,将一些幸运数字保存到 MongoDB 数据库(tomcat_db)集合(jpa_rl).)打开命令提示符,键入来自图 4-27 的命令,查看您的工作结果。您可以监控 Tomcat 日志消息,以防发生任何不必要的事情。

9781430257943_Fig04-27.jpg

图 4-27 。检查 jpa_rl 收藏内容

这个应用的完整源代码被命名为HOGMviaJPA_RESOURCELOCAL_Tomcat7,可以在 Apress 资源库中找到。它是一个 NetBeans 项目,在 Tomcat 7 下进行了测试。

在这个例子中,我们通过 Hibernate Native API 和 JPA 完成了基于 bootstrapping Hibernate OGM 的应用集。

如果您不是 Maven 爱好者,但仍然想测试这些应用,您可以在Libraries节点下手动添加所需的 jar(在 NetBeans/Eclipse 中)并使用 NetBeans/Eclipse 接口工具编译和运行应用(正如在第一章中关于在本地获取 Hibernate OGM 和 MongoDB JARs 的章节中所讨论的)。

如果您也不喜欢 ide,您可以在您最喜欢的编辑器中编辑源代码,甚至是记事本,并从命令行使用 Ant 手动编译应用。例如,清单 4-36 中的 Ant 脚本(build.xml)可以用来编译部署在 Tomcat 下的应用。只需安装 Ant ( http://ant.apache.org/)并将其放在您的类路径中。将 Ant 脚本放在应用根文件夹中,打开命令提示符,导航到该文件夹并键入build.。这将编译应用并构建应用 WAR:

清单 4-36。 build.xml

<project name="hibernate" default="war">

<property name="sourcedir" value="${basedir}/WEB-INF/src"/>
<property name="targetdir" value="${basedir}/WEB-INF/classes"/>
<property name="librarydir" value="${basedir}/WEB-INF/lib"/>
<property name="builddir" value="${basedir}/build"/>

<path id="libraries">
      <fileset dir="${librarydir}">
          <include name="*.jar"/>
      </fileset>
</path>

<target name="clean">
     <delete dir="${targetdir}"/>
     <mkdir dir="${targetdir}"/>
     <delete dir="${builddir}"/>
     <mkdir dir="${builddir}"/>
</target>

<target name="compile" depends="clean, copy-resources">
    <javac srcdir="${sourcedir}"
           destdir="${targetdir}"
           classpathref="libraries"/>
</target>

<target name="copy-resources">
    <copy todir="${targetdir}">
        <fileset dir="${sourcedir}">
              <exclude name="**/*.java"/>
        </fileset>
    </copy>
</target>

<target name="war" depends="compile">
  <jar jarfile="${builddir}/{ *app_name* }.war" basedir="${basedir}"/>
 </target>

</project>

显然,您必须处理应用服务器和浏览器的启动/停止操作。

合成

开发和测试这些应用产生了这个部分。在分析了这些应用之后,我们可以得出一些关于 Hibernate OGM 和 MongoDB 在不同应用环境中集成的一般性结论。显然,Hibernate OGM 能够在许多不同的环境和架构中运行,并且可以与许多框架和工具一起使用。

此外,根据环境(尤其是 EE 和 JTA 单机版)和引导(通过 Hibernate Native API 或 JPA),我们可以提取 Hibernate OGM 正确服务 Java 应用所需的一系列强制和/或推荐设置。

在 EE 容器中通过 JPA Hibernate OGM

当您在 EE 容器中通过 JPA 使用 Hibernate OGM 时,您会希望在persistence.xml文件中包含以下设置:

  • 使用persistence-unit标签的transaction-type JTA 属性将交易类型设置为 JTA。
  • 使用hibernate.transaction.jta.platform属性将 JTA 平台设置为正确的 EE 容器。
  • 指定 JTA 数据源。这应该进行测试,在某些情况下可以跳过。对于 GlassFish,您可以使用内置数据源jdbc/sample(这是相关的 JNDI 名称),对于 JBoss,您可以使用java:/DefaultDS(7 版之前)或java:jboss/datasources/ExampleDS(7 版及更高版本)。使用jta-data-source标签指定数据源。

通过 EE 容器中的 Hibernate Native API 来 Hibernate OGM

当您通过 EE 容器中的 Hibernate Native API 使用 Hibernate OGM 时,您应该在hibernate.cfg.xml文件(或其编程版本)中包含以下设置:

  • 如果手动划分事务边界,则将属性hibernate.transaction.factory_class设置为org.hibernate.transaction.JTATransactionFactory,如果使用声明性事务划分,则设置为org.hibernate.transaction.CMTTransactionFactory
  • 将属性hibernate.current_session_context_class设置为jpa,以指示用于确定“当前”Session实例范围的策略。
  • 使用hibernate.transaction.jta.platform属性将 JTA 平台设置为正确的 EE 容器

在独立的 JTA 中通过 JPA Hibernate OGM

当您在非 EE 容器(独立的 JTA,如 Tomcat)中通过 JPA 使用 Hibernate OGM 时,您应该在persistence.xml文件中包含以下设置:

  • 使用persistence-unit标签的transaction-type JTA 属性将交易类型设置为 JTA。
  • 使用hibernate.transaction.jta.platform属性设置 JTA 平台(这是独立的 JTA——JOTM、JBoss JTA、Bitronix 等等——不是 JTA 内置的容器)。
  • 请查看特定于所选独立 JTA 的文档,因为它可能需要设置一些特定的属性。

通过独立 JTA 中的 Hibernate Native API 来 Hibernate OGM

当您在非 EE 容器(独立的 JTA,如 Tomcat)中通过 Hibernate Native API 使用 Hibernate OGM 时,您应该在hibernate.cfg.xml文件(或其编程版本)中包含以下设置:

  • 如果手动划分事务边界,则将属性hibernate.transaction.factory_class设置为org.hibernate.transaction.JTATransactionFactory,如果使用声明性事务划分,则设置为org.hibernate.transaction.CMTTransactionFactory
  • 将属性hibernate.current_session_context_class设置为jpa,以指示确定当前Session实例范围的策略。
  • 使用hibernate.transaction.jta.platform property设置 JTA 平台(这是独立的 JTA——JOTM、JBoss JTA、Bitronix 等等——不是 JTA 内置的容器)。
  • 请查看特定于所选独立 JTA 的文档,因为它可能需要设置一些特定的属性。

非 JTA 中 JPA 的 OGM 冬眠

当您在非 JTA 环境(如 Tomcat)中通过 JPA 使用 Hibernate OGM 时,您应该在persistence.xml文件中包含以下设置:

  • 使用persistence-unit标签的transaction-type JTA 属性将交易类型设置为RESOURCE_LOCAL
  • 不要指定任何 JTA 平台,而是向应用提供 JBoss JTA JARs。
  • 自己管理EntityManager及其 JTA 交易。

通过非 JTA 的 Hibernate Native API 来 Hibernate OGM

当您在非 JTA 环境(如 Tomcat)中通过 Hibernate Native API 使用 Hibernate OGM 时,您应该在hibernate.cfg.xml文件(或其编程版本)中包含以下设置:

  • 将属性hibernate.transaction.factory_class设置为org.hibernate.transaction.JDBCTransactionFactory.
  • 将属性hibernate.current_session_context_class设置为thread.
  • 使用 Hibernate 的事务和内置的每请求会话功能,而不是调用 JDBC API。

image 注意:hibernate.transaction.jta.platform属性(表示 JTA 平台)接受的值可在第二章的“Bootstrap Hibernate OGM Using JPA”一节中找到。

摘要

在本章中,您看到了如何通过改变容器环境、引导过程以及相关的框架和工具,将 Hibernate OGM 与不同种类的应用集成在一起。本章介绍的应用列表包括:

  • Java SE 和 Mongo DB—hello world 示例
  • 在非 JTA 环境(JDBC 事务,Tomcat 7)中 Hibernate OGM(通过 Hibernate Native API)
  • 在独立的 JTA 环境(JBoss JTA,Tomcat 7)中 Hibernate OGM(通过 Hibernate Native API)
  • 在内置的 JTA 环境(没有 EJB,GlassFish 3)中 Hibernate OGM(通过 Hibernate Native API)
  • 在内置的 JTA 环境(EJB/BMT,GlassFish 3)中 Hibernate OGM(通过 Hibernate Native API)
  • 在内置的 JTA 环境(EJB/CMT,GlassFish 3)中 Hibernate OGM(通过 Hibernate Native API)
  • 在内置的 JTA 环境(GlassFish AS 3)中 Hibernate OGM(通过 JPA)
  • 在内置的 JTA 环境(JBoss AS 7)中 Hibernate OGM(通过 JPA)
  • 在内置的 JTA 环境(JBoss AS 7 和 Seam 应用)中 Hibernate OGM(通过 JPA)
  • 在内置的 JTA 环境(GlassFish 和 Spring 应用)中 Hibernate OGM(通过 JPA)
  • 在独立的 JTA 环境中 Hibernate OGM(通过 JPA)JPA/JTA(Tomcat)
  • 在非 JTA 环境中 Hibernate OGM(RESOURCE _ LOCAL,Apache Tomcat 7)

五、Hibernate OGM 和 JPA 2.0 注解

在 Hibernate OGM 中映射 Java 实体可以分为支持的和不支持的注解。实际上,Hibernate OGM 支持像@Entity@Id,这样的强制注解,以及像@Table@Column这样的所有常用注解。然而,在 4.0.0.Beta2 版本中,它不支持一些“自命不凡”的注解,如@Inheritance@DiscriminatorColumn。不支持的注解可能会导致错误或工作不正常,或者可能被完全忽略。

Hibernate OGM 按照官方规范翻译每个实体,但是适应 MongoDB 功能。这意味着一些注解将完全按照预期工作,而另一些则有一些限制,还有一些可能根本不工作。由于 Hibernate OGM 负责创建 JPA 注解和 MongoDB 存储之间的共生关系,所以毫不奇怪,要使这种共生关系在实践中顺利运行,需要更多的时间和版本。

我将从 OGM 中 Java 支持的类型的简短讨论开始,然后继续讨论急切/延迟加载机制和级联设施。然后,我们将按照一个简单的场景来探索注解:一个简短的概述,对 OGM 支持的一个观察,一些案例研究,最后,在通过 Hibernate OGM 之后,在 MongoDB 中注解的结果。在前面的章节中,尤其是在第四章中,你看到了一些 Java 实体和一些支持的注解。在这一章中,我们将仔细研究这些以及更多的注解,如@Id@Column@Table@Embedded@Enumerated@Temporal。最后,我们将深入研究关联注解。

Java 支持的类型

Java 实体与 Java 类型密切相关,因为它们封装了所有类型的数据:数字、字符串、URL、对象、自定义类型等等。实际上,一个实体的每个持久化字段都由一个 Java 类型来表征,并且必须在 MongoDB 文档字段中表示。因此,Hibernate OGM 的主要关注点之一是(现在也是)为 Java 类型提供尽可能多的支持。

根据官方文档,Hibernate OGM 4 . 0 . 0 . beta 2 支持以下 Java 类型(尽管该列表在未来的版本中可能会有所变化):

  • 布尔代数学体系的
  • 字节
  • 日历(可能会改变)
  • 类别(可能会改变)
  • 日期(可能会改变)
  • 两倍
  • 整数
  • 长的
  • 字节数组
  • 线

这些类型是本机支持的。其他支持的类型,如BigDecimalBigIntegerURL,UUID,都以字符串的形式存储在 MongoDB 中。

急切和懒惰加载注意事项

您可能知道,JPA 可以急切地(立即获取)或懒洋洋地(在需要时获取)从数据库加载数据。当两个(或多个)实体参与一个协会时,这些概念通常会发挥作用。例如,如果一个实体是父实体,而另一个是子实体(这意味着父实体定义了一个子实体集合),可能的情况是:

  • 急切加载-在提取父代时提取子代。
  • 惰性加载——只有当你试图访问一个子节点时,才会获取它。

急切加载在所有 JPA 实现中都是本地支持的,而延迟加载以不同的方式实现或者不被支持。Hibernate(包括 Hibernate OGM)使用代理对象代替实体类的实例来支持延迟加载。

Hibernate 使用代理作为一种解决方案,将从数据库接收的互连数据“分解”成更小的片段,以便于存储在内存中。了解 Hibernate 为延迟加载的对象动态生成代理可能是有用的。很有可能,您并不知道代理对象,直到您得到一些类型为LazyInitializationException,的异常,或者直到您尝试在调试器中测试延迟加载并注意到一些具有空属性的非空对象的存在。不知道何时“工作”在代理对象而不是实体对象上会导致奇怪的结果或异常。我们将在本章后面详细讨论这一点。

可级联操作注意事项

从 1.0 版本开始,JPA 支持可级联操作。简而言之,如果您将一些操作应用到一个实体,并且这些操作可以传播到一个关联的实体,那么这些操作就是可级联的。JPA 有五个可级联的操作:persistmergeremoverefreshdetach(最后一个是在 JPA 2.0 中添加的)。

通过编程,您可以使用 Java enum CascadeType ( http://docs.oracle.com/javaee/6/api/javax/persistence/CascadeType.html来指示哪些操作应该被持久化。例如,您可以指示persistmerge操作应该在一对多关联中持久化:

...
@OneToMany(cascade = { CascadeType.PERSIST,CascadeType.MERGE },
           mappedBy = "...")
    public Set<...> get...() {
        return this...;
    }
...

当所有五个操作都应该传播时,使用CascadeType.ALL:

...
@OneToMany(cascade = { CascadeType.ALL },
           mappedBy = "...")
    public Set<...> get...() {
        return this...;
    }
...

Hibernate OGM 支持所有可级联的操作,一切都按预期运行。在这一章中,你会看到几个例子,你可能会受到启发,自己去探索这些例子中的层叠技术。

实体映射

现在让我们看看 Hibernate OGM 中的实体映射。更具体地说,让我们看看 Hibernate OGM 如何映射 JPA 2.0 注解,包括持久类的注解和字段及关系的注解。我不会遵循严格的 JPA 2.0 注解分类,而是一种允许我逐个引入注解的方法,这样我就可以只根据我们已经看到的注解在每个步骤测试实体。

image 注意为了测试,我使用了一个名为mapping_entities_db的 MongoDB 数据库。在执行每个测试之前,您应该从这个数据库中删除所有现有的集合(您可以使用db.dropDatabase命令)。否则,根据测试的不同,您可能会得到各种错误。

开始吧!

@实体标注

javax.persistence.Entity注解映射。

官方文件:http://docs.oracle.com/javaee/6/api/javax/persistence/Entity.html

简要概述

将一个类标记为一个实体。默认情况下,实体名与带注解的非限定类名相同,但是可以用 name 元素替换(例如,@Entity(name="MyEntityName")))。

OGM 支持

Hibernate OGM 和任何其他实体消费者一样,只是将这个注解作为识别实体类的标志,所以它对持久层(在我们的例子中是 MongoDB)没有直接影响。

例子

import javax.persistence.Entity;
...

@Entity
public class PlayerEntity implements Serializable {
...

在这种情况下,实体名称是PlayerEntity

@Id 标注

javax.persistence.Id注解映射。

官方文档:http://docs.oracle.com/javaee/6/api/javax/persistence/Id.html .

简要概述

@Id注解应用于实体字段(或属性),以将其标记为该实体的主键。主键值是显式设置的,或者使用生成器(专用算法)自动设置,以保证唯一性、一致性和可伸缩性。通常,主键类型表示为数字或字符串,但也可以是日期。

MongoDB 知道主键,并为它们保留了一个字段,_id (正如你从第二章中所知道的。如果没有指定_id值,MongoDB 会自动用“MongoDB Id Object”填充。但是您可以在这个字段中输入任何唯一的信息(数字、时间戳、字符串等等)。

OGM 支持〔??〕

Hibernate OGM 支持@Id注解和一组一致的生成器,包括四个标准的 JPA 生成器。一些 Hibernate 生成器也是可用的,通过一个通用的生成器;它们将在后面列出。为了获得最大的可伸缩性,Hibernate OGM 推荐基于 UUID 的生成器(或者是uuid或者是uuid2)。您还将在 MongoDB 中看到一些受支持的 id 生成器及其效果,但是,显然,不可能涵盖所有类型的生成器。记得测试您自己的生成器(例如,定制的生成器)。我在这里省略了一个生成器,这并不意味着它受支持或不受支持。

简单@Id 的例子

我说的“简单的@Id”是指没有显式生成器的主键。在这种情况下,您必须为需要持久化的每个实体实例手动设置一个惟一的 id 值,否则持久化操作将导致"org . hibernate . hibernate exception:试图插入一个已经存在的实体"类型的错误。

只要您正确设置了主键,一切都可以完美地工作,并且可以在 MongoDB 中找到数据。例如,下面的Players实体使用了一个简单的@Id类型的int:

import javax.persistence.Id;
...

@Entity
public class Players implements Serializable {

    @Id
    private int id;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
...
}

接下来,我创建三个Players并使用setId方法手动指定 ids 1、2 和 3。将这些Players持久化到一个 MongoDB 集合中,你将获得三个文档,如图图 5-1 所示。

9781430257943_Fig05-01.jpg

图 5-1 。将三个玩家实例持久化到一个 MongoDB 集合中

@Id 和自动策略的示例

JPA 提供了四种可以应用于主键生成的策略:AUTOIDENTITYSEQUENCETABLEAUTO让持久性提供者选择关于数据库(表、序列或身份)的正确策略。通常,这是数据库默认的主键生成策略。因此,如果您使用了AUTO,Hibernate OGM 应该根据底层数据库—MongoDB(在本例中是 sequence)选择合适的策略。这种策略的优点是使代码非常具有可移植性,尽管数据库迁移可能会成为一个问题。

您可以使用@GeneratedValue注解设置AUTO策略,如下所示:

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
...

@Entity
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
...
}

我现在将使用 Hibernate OGM 持久化这个实体的一些实例,在 MongoDB 中的结果如图 5-2 所示。

9781430257943_Fig05-02.jpg

图 5-2 。将几个播放器实例持久化到 MongoDB 集合中

注意,当文档被持久化时,Hibernate OGM 告诉数据库使用一个名为hibernate_sequences的幕后集合插入一个顺序生成的数字。插入五个文档(记录)后,hibernate_sequences的内容类似于您在图 5-3 中看到的内容。如您所见,它存储了下一次插入的 id 值。

9781430257943_Fig05-03.jpg

图 5-3 。hibernate_sequences 集合内容

@Id 和身份策略示例

IDENTITY策略要求持久性提供者使用数据库标识列为实体分配主键(类型为short (Short)int (Integer)long (Long))。在关系数据库(MySQL、Microsoft SQL Server、IBM DB2、HypersonicSQL 和 Sybase)中,表通常包含一个自动递增的列,告诉数据库在插入记录时插入一个顺序生成的数字。将IDENTITY策略附加到 auto-increment 列使实体能够在插入数据库时自动生成一个序列号作为主键。在 MongoDB 世界中,您实际上是利用 MongoDB 生成的_id作为持久化对象的主键。

Hibernate OGM 支持这种策略,但是由于它的行为与AUTO策略完全一样,OGM 不使用 MongoDB 生成的_id作为持久化对象的主键。无论如何,众所周知,这种策略存在一些问题,尤其是在可移植性和性能方面。

可以使用@GeneratedValue注解来设置IDENTITY策略,如下所示:

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
...

@Entity
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
...
}

如果你使用 Hibernate OGM 持久化了Players实体的几个实例,MongoDB 将显示Players集合,如图图 5-4 所示。

9781430257943_Fig05-04.jpg

图 5-4 。使用身份策略将几个玩家实例持久化到 MongoDB 集合中

事实上,我期待看到更多类似的东西(没有hibernate_sequences系列):

{ "_id" : ObjectId("4eaafff900694710bfb8fa5b"),
  "id" : NumberLong(1),
   ...
}

或者,更好的是:

{ "_id" : ObjectId("4eaafff900694710bfb8fa5b"),
   ...
}

image 注意关于 ObjectId 及其生成方式的更多细节可以在 MongoDB 官方文档中找到:http://docs.mongodb.org/manual/reference/object-id/

@Id 和序列策略的示例

SEQUENCE策略(在 Hibernate 中称为seqhilo)要求持久性提供者使用数据库序列为实体分配主键(类型为shortint,long)。该策略不是在提交期间生成主键值,而是在提交之前生成多组主键值,这在较早需要主键值时非常有用。(给定分配中的一些 id 可能不会被使用,这可能会导致序列值中的间隙。)

Hibernate OGM 通过将序列信息保存在名为hibernate_sequences的集合中来支持这种策略。为了展示这种策略是如何工作的,我使用@SequenceGenerator注解配置了一个序列生成器,初始值为 5,大小分配(一个组中主键的数量)为 2,如下所示:

@SequenceGenerator(name="mongodb_sequence", initialValue=5, allocationSize=2)

接下来,我定义了一个int主键并指明了SEQUENCE策略:

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
...

@Entity
@SequenceGenerator(name="mongodb_sequence", initialValue=5, allocationSize=2)
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="mongodb_sequence")
    private int id;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
...
}

在持久化第一个对象之后,hibernate_sequencesPlayers集合看起来就像图 5-5 中的所示。

9781430257943_Fig05-05.jpg

图 5-5 。使用序列策略将一个玩家实例持久化到 MongoDB 集合中

注意,第一个对象(文档)的 id 是生成序列的初始值,而生成序列分配大小的计算方法是(分配大小* 2) +初始值,即(2*2) + 5 = 9 (sequence_value 字段)。

然后我又保存了三个对象,结果如图 5-6 所示。

9781430257943_Fig05-06.jpg

图 5-6 。使用序列策略将另外三个玩家实例持久化到 MongoDB 集合中

因此,当我持久化一个 id 等于 7 的对象时,序列会随着分配大小值—2 而自动增加。这里的过程是多余的。

请注意,您可以将可选的catalog元素添加到序列生成器中:

@SequenceGenerator(name="mongodb_sequence",catalog="MONGO",
                                      initialValue=5, allocationSize=2)

现在,hibernate_sequences收藏名称变成了MONGO.hibernate_sequences

此外,如果添加一个schema元素,就像这样:

@SequenceGenerator(name="mongodb_sequence", catalog="MONGO",
                                      schema="MONGOSEQ", initialValue=5, allocationSize=2)

然后,hibernate_sequences收藏名称变为MONGO.MONGOSEQ.hibernate_sequences

一切似乎都在按预期运行!

@Id 和表策略的示例

TABLE策略(在 Hibernate 中称为MultipleHiLoPerTableGenerator)要求持久性提供者使用底层数据库表为实体分配主键(类型为shortintlong)。由于出色的性能、可移植性和集群性,这种策略被广泛使用。JPA 提供者可以自由决定使用哪种方法来完成这项任务。可以使用标准的@TableGenerator注解来配置生成器。

Hibernate OGM 通过为 MongoDB 创建一个名为hibernate_sequences;的集合来支持这种策略,底层表就是一个集合。为了展示这种策略是如何工作的,我使用@TableGenerator注解配置了一个表生成器,初始值为 5,大小分配(一个组中主键的数量)为 2,如下所示:

@TableGenerator(name="mongodb_table", initialValue=5, allocationSize=2)

接下来,我定义了一个int主键并指明了TABLE策略,如清单 5-1 所示。

清单 5-1。 运用表策略

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.TableGenerator;
...

@Entity
@TableGenerator(name="mongodb_table", initialValue=5, allocationSize=2)
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.TABLE, generator="mongodb_table")
    private int id;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
...
}

持久化第一个对象后,hibernate_sequencesPlayers集合的内容如图图 5-7 所示。

9781430257943_Fig05-07.jpg

图 5-7 。使用表策略将一个玩家实例持久化到 MongoDB 集合中

请注意,第一个对象(文档)的 id 是初始值+ 1,而序列分配大小的计算方法是(分配大小* 2) +初始值+ 1,即(2*2) + 5 + 1= 10 (sequence_value 字段)。

接下来,我保存了另外三个对象,得到了如图 5-8 所示的结果:

9781430257943_Fig05-08.jpg

图 5-8 。使用表策略将另外三个玩家实例持久化到 MongoDB 集合中

因此,当我持久化 id 等于 8 的对象时,序列自动增加 1 +分配大小值,增加 3。为此,该过程是多余的。

注意,您可以通过在表格生成器中添加table元素来更改hibernate_sequences的名称:

@TableGenerator(name="mongodb_table", table="pk_table" , initialValue=5, allocationSize=2)

@Id 和泛型生成器的示例—UUID 和 UUID2

除了四个标准的 JPA 生成器之外,UUID 和 UUID2 是 Hibernate 提供的许多生成器中的两个。UUID 基于自定义算法生成 128 位 UUID,而 UUID2 生成符合 IETF RFC 4122(变体 2)的 128 位 UUID。对于 MongoDB,这些种类的主键被表示为字符串。

Hibernate OGM 支持这两种生成器,但是在某些环境中,UUID 会生成一些警告。例如,在 GlassFish 中,使用 UUID 生成器会抛出以下警告:" WARN: HHH000409:使用 org . hibernate . id . uuidhexgenerator,它不会生成符合 IETF RFC 4122 的 UUID 值;考虑使用 org.hibernate.id.UUIDGenerator 代替”。简单翻译就是“用 UUID2”。所以最好使用 UUID2,如清单 5-2 所示。

清单 5-2。 使用 UUID2

import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.hibernate.annotations.GenericGenerator;
...

@Entity
@GenericGenerator(name="mongodb_uuidgg", strategy="uuid2")
public class Players implements Serializable {

    @Id
    @GeneratedValue(generator="mongodb_uuidgg")
    private String id;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
...
}

如果我现在使用 Hibernate OGM 持久化几个Players实体的实例,MongoDB 将显示如图图 5-9 所示的Players集合。

9781430257943_Fig05-09.jpg

图 5-9 。使用 UUID2 策略将几个玩家实例持久化到 MongoDB 集合中

@Id 和自定义生成器的示例

有时,世界上所有的主键生成器都不足以满足应用的需求。在这种情况下,定制生成器变得必不可少,但是在编写之前,您需要知道您的持久性环境是否支持它。在这种情况下,Hibernate OGM 和 MongoDB 与我的定制生成器配合得非常好,您将看到这一点。

如果遵循以下步骤,创建一个新的 Hibernate 定制生成器是一个非常简单的任务:

  • 创建一个实现org.hibernate.id.IdentifierGenerator接口的新类
  • 覆盖IdentifierGenerator.generate方法;提供生成器业务逻辑,并将新的主键作为一个Serializable对象返回

基于这两个步骤,我编写了一个自定义生成器来创建主键类型: XXXX _long-number(例如,SFGZ_3495832849584739405)。清单 5-3 显示了定制的生成器。

清单 5-3。 自定义主键生成器

package hogm.mongodb.generator;

import java.io.Serializable;
import java.util.Random;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.id.IdentifierGenerator;

public class CustomGenerator implements IdentifierGenerator {

    @Override
    public Serializable generate(SessionImplementor sessionImplementor,
            Object object) throws HibernateException {

        Random rnd = new Random();
        String str = "";

        for (int i = 0; i <= 3; i++) {
            str = str + (char) (rnd.nextInt(26) + 'a');
        }

        str = str + "_";
        str = str + String.valueOf(rnd.nextLong());
        str=str.toUpperCase();

        return str;
    }
}

测试定制的生成器 非常简单。首先,我使用了@GenericGenerator注解,并将定制生成器的全限定类名指定为生成器策略:

@GenericGenerator(name="mongodb_custom_generator",
                                  strategy="hogm.mongodb.generator.CustomGenerator")

接下来,我定义一个String主键字段,并使用清单 5-4 中所示的@GeneratedValue注解。

清单 5-4。 使用 GeneratedValue 标注

import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.hibernate.annotations.GenericGenerator;
...

@Entity
@GenericGenerator(name="mongodb_custom_generator",
                                    strategy="hogm.mongodb.generator.CustomGenerator")

public class Players implements Serializable {

    @Id
    @GeneratedValue(generator="mongodb_custom_generator")
    private String id;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
...
}

同样,我使用 Hibernate OGM 持久化了Players实体的几个实例,MongoDB 显示了图 5-10 中的Players集合。

9781430257943_Fig05-10.jpg

图 5-10 。使用定制生成器将几个播放器实例持久化到 MongoDB 集合中

演示@Id注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM_MONGODB_Id。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@EmbeddedId 批注

javax.persistence.EmbeddedId注解映射。

官方文件:http://docs.oracle.com/javaee/6/api/javax/persistence/EmbeddedId.html

简要概述

@EmbeddedId注解表示一个组合主键,它是一个可嵌入的类。你不得不写一个新的可序列化的类,这个类必须:用@Embeddable注解(这个类不需要@Entity或其他注解);定义主键字段;并为主键字段定义 getters 和 setters。@Embeddable允许您指定一个类,该类的实例存储为拥有实体的固有部分。实体本身必须定义一个用@Embeddable注解的类的主键字段。该字段应标注@EmbeddedId

如果您喜欢这种组合键,就不再需要指定@Id注解了。对于 MongoDB,组合键应该作为嵌入式文档存储在_id字段中。

OGM 支持

Hibernate OGM 支持用@EmbeddedId注解定义的组合键。它将 Java 组合键转换成 MongoDB 的_id字段中的嵌入式文档,主键字段变成嵌入式文档字段。

例子

创建这种组合键包括两个主要步骤:首先,编写可序列化的主键类并用@Embeddable对其进行注解,其次,选择将成为组合主键的适当的实体属性或持久性字段并用@EmbeddedId对其进行注解。例如,假设您有一个主键类:

import javax.persistence.Embeddable;
...

@Embeddable
public class RankingAndPrizeE implements Serializable {

    private int ranking;
    private String prize;

    //constructors, getters and setters
...
}

然后,在Players实体中,创建一个复合主键字段:

import javax.persistence.EmbeddedId;
...

@Entity
public class Players implements Serializable {

   @EmbeddedId
    private RankingAndPrizeE id;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
...
}

现在使用 Hibernate OGM 持久化Players实体的几个实例,MongoDB 将显示如图图 5-11 所示的Players集合。

9781430257943_Fig05-11.jpg

图 5-11 。使用@EmbeddedId 定义组合键

演示@EmbeddedId注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM_MONGODB_Id。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@IdClass Annotation

javax.persistence.IdClass注解映射。

官方文件:http://docs.oracle.com/javaee/6/api/javax/persistence/IdClass.html

简要概述

@IdClass注解表示映射到实体的多个字段或属性的复合主键。这种方法迫使您编写一个新的可序列化的类,该类定义主键字段并覆盖equalshashCode方法。主键类中定义的主键字段也必须以完全相同的方式出现在实体类中,只是它们必须具有 getter 和 setter 方法。而且,实体类用@IdClass标注。

如果您更喜欢这种组合键,那么在实体中会有多个@Id注解——每个主键字段一个。对于 MongoDB,组合键应该作为嵌入式文档存储在_id字段中。

OGM 支持

Hibernate OGM 支持用@IdClass注解定义的组合键。它将 Java 组合键转换成 MongoDB _id字段中的嵌入式文档,主键字段变成嵌入式文档字段。

例子

创建这种组合键包括两个主要步骤:首先,编写可序列化的主键类,其次,用@IdClass注解实体类,并像在主键类中一样定义主键字段。第一步如清单 5-5 所示。

清单 5-5。 可序列化主键类

package hogm.mongodb.entity;

import java.io.Serializable;

public class RankingAndPrizeC implements Serializable {

    private int ranking;
    private String prize;

    public RankingAndPrizeC() {
    }

    @Override
    public boolean equals(Object arg0) {

        //implement equals here
        return false;
    }

    @Override
    public int hashCode() {

        //implement hashCode here
        return 0;
    }
}

而第二步如清单 5-6 所示。

清单 5-6。 定义主键字段

import javax.persistence.Id;
import javax.persistence.IdClass;
...

@Entity
@IdClass(hogm.mongodb.entity.RankingAndPrizeC.class)
public class Players implements Serializable {

    @Id
    private int ranking;
    @Id
    private String prize;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
...

现在使用 Hibernate OGM 持久化几个Players实体的实例。MongoDB 将显示如图图 5-12 所示的Players集合。

9781430257943_Fig05-12.jpg

图 5-12 。使用@IdClass 定义一个组合键

演示@IdClass注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM_MONGODB_Id。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@表格注解

javax.persistence.Table注解映射。

官方文件:http://docs.oracle.com/javaee/6/api/javax/persistence/Table.html

简要概述

在关系数据库中,每个实体被表示为一个表(称为主表),默认情况下,其名称与实体相同(非限定的实体类名)。如果您想为一个表设置另一个名称,您可以使用@Table注解和name元素。您还可以通过添加catalogschema元素来指定一个目录和一个模式。

MongoDB 将表的概念与集合联系起来。默认集合名称与映射的实体相同。

OGM 支持

Hibernate OGM 支持@Table注解。它将提供name元素值作为相应集合的名称。此外,如果还指定了catalog元素,Hibernate OGM 会将目录值作为前缀添加到模式名(或者集合名,如果缺少模式的话)中,并用点号将其与模式名(或者集合名)分开。如果指定了schema元素,Hibernate OGM 将在目录名(如果存在的话)和集合名之间添加模式值,用点分隔。正如您所看到的,当目录、模式和集合名称存在时,Hibernate OGM 根据关系模型层次结构连接最终名称:目录包含模式,模式包含表。

例子

测试 @Table注解是一项简单的任务,因为您需要做的只是在类级别添加这个注解,然后看看会发生什么。下面是用@Table标注的Players实体:

import javax.persistence.Table;
...

@Entity
@Table(catalog="ATP", schema="public", name="atp_players")
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    private String name;
    private String surname;
    private int age;

    //constructors, getters and setters
    ...
}

图 5-13 显示了 MongoDB 上@Table注解的效果:

9781430257943_Fig05-13.jpg

图 5-13 。在 MongoDB 中映射@Table 注解

演示@Table注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM_MONGODB_TableColumn。它是一个 NetBeans 项目,并且是在 GlassFish 3 AS 下测试的。

@栏目注解

javax.persistence.Column注解映射。

官方文件:http://docs.oracle.com/javaee/6/api/javax/persistence/Column.html

简要概述

在关系数据库中,每个实体的持久属性或字段在数据库中表示为相应表的一列,字段名提供列名。您可以显式地提供一个列名(不同于字段名),方法是用@Column注解对其字段进行注解,并将所需的名称指定为name元素的值。此外,@Column元素允许您设置一些数据限制,比如长度(使用length元素),数据库列是否可以为空(nullable元素),等等。官方文档中列出了所有受支持的元素。

MongoDB 将每个实体实例存储为一个文档。每个文档都由文档的字段组成,这些字段以名称和值为特征。除了保留的_id字段,文档的其余字段名反映了实体持久性属性或字段名(或者,从关系模型的角度来看,反映了列名)。

OGM 支持

Hibernate OGM 支持@Column注解。它将提供每个name元素值作为相应文档字段的名称。除了name,其余的@Column元素似乎都被忽略了。此外,向主键持久字段添加一个@Column注解将被忽略,取而代之的是 MongoDB _id字段名,因此您可以为实体中的主键字段使用任何您喜欢的名称。

例子

测试@Column注解是一项简单的任务,因为您需要做的只是在字段(或属性)级别添加注解,然后看看会发生什么。下面是用@Column标注的Players实体:

import javax.persistence.Column;
...

@Entity
@Table(catalog="ATP", schema="public", name="atp_players")
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @Column(name="player_name")
    private String name;
    @Column(name="player_surname")
    private String surname;
    @Column(name="player_age")
    private int age;

    //constructors, getters and setters
    ...
}

图 5-14 展示了@Column标注在 MongoDB 上的效果。

9781430257943_Fig05-14.jpg

图 5-14 。在 MongoDB 中映射@Column 注解

演示@Column注解的完整应用可以在 Apress 存储库中获得,命名为HOGM_MONGODB_TableColumn。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@时态标注

javax.persistence.Temporal注解映射。

官方文件:http://docs.oracle.com/javaee/6/api/javax/persistence/Temporal.html

简要概述

@Temporal注解指示表示日期、时间或日期-时间(时间戳)值的持久性字段或属性。支持的值属于类型java.util.Datejava.util.Calendar。映射java.util.Datejava.util.Calendar时使用的类型可以用TemporalType表示为DATE(映射为java.sql.Date)、TIME(映射为java.sql.Time)或TIMESTAMP(映射为java.sql.Timestamp)。

MongoDB 在其文档中支持日期/时间字段。MongoDB 日期遵循 BSON 官方文档定义的格式(参见http://bsonspec.org/#/specification),可以在 MongoDB shell 中使用DateISODate构造函数创建,如下所示:

var mydate = new Date()
var mydate = new Date("Sun Feb 16 2013")
var mydate = new Date("Sun Feb 16 2013 08:22:05")
var mydate_iso = ISODate()
var mydate_iso = ISODate("2013-02-16T08:22:05")

OGM 支持

Hibernate OGM 支持@Temporal注解。每个时态字段(与其类型无关)都将被转换成由年、月、日、小时、分钟和秒组成的 MongoDB ISO 日期(year-month-day thour:minute:second)。例如,使用公历定义的 Java 日期如下所示:

private static final Calendar calendar = GregorianCalendar.getInstance();
calendar.clear();
calendar.set(1987, Calendar.MAY, 22); //22.05.1987

这个日期在 MongoDB 中是这样表示的:

ISODate("1987-05-22T00:00:00Z")

注意,在这个例子中,我没有指出小时、分钟和秒。添加样本时间会将日历设置转换为:

calendar.set(1987, Calendar.MAY, 22, 12, 40, 01); //22.05.1987 12:40:01

MongoDB 的表示变成了:

ISODate("1987-05-22T12:40:01Z")

如果您没有通过调用clear方法清除日历设置,并且没有指定时间(小时、分钟和秒),将自动设置当前时间。

例子

首先,我在实体中定义了一个代表每个玩家生日的java.util.Date字段。然后我用@Temporal (javax.persistence.TemporalType.DATE),来注解它,正如你在清单 5-7 中看到的。

清单 5-7。 定义一个字段来代表每个玩家的生日

import java.util.Date;
import javax.persistence.Temporal;
...

@Entity
@Table(catalog="ATP", schema="public", name="atp_players")
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @Column(name="player_name")
    private String name;
    @Column(name="player_surname")
    private String surname;
    @Column(name="player_age")
    private int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    private Date birth;

    //constructors, getters and setters
...
}

第二,我使用公历定义了玩家的生日,如下所示:

private static final Calendar calendar = GregorianCalendar.getInstance();
calendar.clear();
calendar.set(1987, Calendar.MAY, 22);   //22.05.1987
calendar.clear();
calendar.set(1981, Calendar.AUGUST, 8); //08.08.1981
...

现在,我将使用 Hibernate OGM 持久化Players实体的几个实例。MongoDB 将显示图 5-15 中的集合。注意birth文档字段。

9781430257943_Fig05-15.jpg

图 5-15 。在 MongoDB 中映射@Temporal 注解

演示@Temporal注解的完整应用可以在 Apress 存储库中获得,命名为HOGM_MONGODB_Temporal。它是一个 NetBeans 项目,并且是在 GlassFish 3 AS 下测试的。

@瞬态注解

javax.persistence.Transient注解映射。

官方文档:http://docs.oracle.com/javaee/6/api/javax/persistence/Transient.html .

简要概述

首先,提醒一句:如果您不熟悉@Transient注解,小心不要将其与 Java transient关键字混淆。transient关键字用于指示不可序列化的字段,而@Transient注解是特定于 JPA 的,用于指示不能保存到底层数据库的字段。此外,这个注解并不意味着来自数据库的任何支持;只有 JPA 提供者应该知道如何处理它。

OGM 支持

Hibernate OGM 支持@Transient注解。当一个实体类被传递给 OGM 时,它只保存没有用@Transient注解的字段。

例子

这里我用@Transient注解了一些Players实体字段,如下所示:

import javax.persistence.Transient;
...

@Entity
@Table(catalog="ATP", schema="public", name="atp_players")
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @Column(name="player_name")
    private String name;
    @Column(name="player_surname")
    private String surname;
    @Column(name="player_age")
    @Transient
    private int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    @Transient
    private Date birth;

    //constructors, getters and setters
...
}

如果您使用 Hibernate OGM 持久化了Players实体的几个实例,MongoDB 将显示出如图 5-16 所示的Players集合。注意,agebirth文档字段丢失了,这意味着 OGM 不会基于@Transient状态持久化它们。

9781430257943_Fig05-16.jpg

图 5-16 。在 MongoDB 中映射@Transient 注解

演示@Transient注解的完整应用可以在 Apress 存储库中获得,命名为HOGM_MONGODB_Transient。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@Embedded 和@ Embedded Annotations

javax.persistence.Embeddedjavax.persistence.Embeddable标注映射。

正式文件:http://docs.oracle.com/javaee/6/api/javax/persistence/Embedded.html

http://docs.oracle.com/javaee/6/api/javax/persistence/Embeddable.html

简要概述

当持久性字段或属性用@Embedded标注时,这表示一个可嵌入类的实例。此类不是实体,没有 id 或表;它只是包含嵌入字段的实体的一个逻辑部分,并且在类级别使用@Embeddable注解有意地将其分离并标记为可嵌入的。分离的原因各不相同,从希望拥有简单的代码到不想持久化可嵌入部分,并因此使用@Transient注解将其字段标记为瞬态的。默认情况下,嵌入对象的每个非瞬态属性或字段都映射到实体的数据库表。

从 MongoDB 的角度来看,可嵌入对象作为嵌套文档存储在实体的文档中。

OGM 支持

Hibernate OGM 支持@Embedded@Embeddable注解。此外,正如您在这里看到的,Hibernate OGM 还支持可嵌入字段的@Transient注解(由javax.persistence.Transient,映射,更多细节请见http://docs.oracle.com/javaee/6/api/javax/persistence/Transient.html)。OGM 知道如何将可嵌入类的每个实例转换成表示每个所有者实体实例的嵌套文档。被注解为瞬态的可嵌入类的任何字段都不会在嵌套文档中持久化。

不要试图使用@SecondaryTable注解(javax.persistence.SecondaryTable),因为 OGM 不支持它。

例子

首先,我定义了一个可嵌入的类,它包含每个玩家的一些细节:出生地、居住地、身高、体重等等。这个类非常简单,但是@Embeddable注解使它很特别:

import javax.persistence.Embeddable;
...

@Embeddable
public class Details implements Serializable {

    private String birthplace;
    private String residence;
    private String height;
    private String weight;
    private String plays;
    private int turnedpro;
    private String coach;
    private String website;

    //constructors, getters and setters
...
}

接下来,在Players实体中,我创建了一个类型为Details的字段,并将其标注为@Embedded,,如清单 5-8 所示。

清单 5-8。 创建嵌入明细字段

import javax.persistence.Embedded;
...

@Entity
@Table(catalog="ATP", schema="public", name="atp_players")
public class Players implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @Column(name="player_name")
    private String name;
    @Column(name="player_surname")
    private String surname;
    @Column(name="player_age")
    private int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    private Date birth;
    @Embedded
    private Details details;

   //constructors, getters and setters
...
}

如果您现在使用 Hibernate OGM 持久化了Players实体的几个实例,MongoDB 将显示出如图图 5-17 所示的Players集合。注意嵌套的文档。

9781430257943_Fig05-17.jpg

图 5-17 。在 MongoDB 中映射@Embedded 和@ Embedded 注解

我还将可嵌入字段“出生地”和“居住地”标注为瞬态:

import javax.persistence.Transient;
...

@Embeddable
public class Details implements Serializable {

    @Transient
    private String birthplace;
    @Transient
    private String residence;
...
}

我坚持了更多的球员和 Hibernate OGM 完美地工作。瞬态字段没有被持久化,正如你在图 5-18 中看到的。

9781430257943_Fig05-18.jpg

图 5-18 。对一些可嵌入的字段(或属性)使用@Transient

为了完整起见,值得注意的是,如果您将所有可嵌入字段都标注为瞬态的,OGM 将完全跳过嵌套文档,正如您在图 5-19 中看到的。

9781430257943_Fig05-19.jpg

图 5-19 。对所有可嵌入的字段(或属性)使用@Transient

演示了@Embeddable@Embedded注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM_MONGODB_Embedded。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

image 注意一个可嵌入的对象可以在多个类之间共享。在关系模型中,通过允许每个嵌入映射覆盖可嵌入映射中使用的列来支持这个特性,这是通过使用@AttributeOverride注解来实现的。在 MongoDB 和 Hibernate OGM 中,不需要覆盖列。不需要任何特殊处理,一切都会按预期工作;只需在每个你想要嵌入相同可嵌入类的类中使用@Embedded

@枚举注解

javax.persistence.Enumerated注解映射。

官方文件:http://docs.oracle.com/javaee/6/api/javax/persistence/Enumerated.html

简要概述

有时 Java 枚举类型可能适合于表示数据库中的列。JPA 通过@Enumerated注解提供数据库列和 Java 枚举类型之间的转换。默认情况下,枚举类型是序数;它将枚举的类型属性或字段作为一个整数保存,但是也可以通过将EnumType值设置为STRING来使其成为一个字符串。

MongoDB 将存储 Java enum 类型值的列视为普通的文档字段。

OGM 支持

Hibernate OGM 支持@Enumerated注解。它知道如何将 Java enum 类型转换成 MongoDB 文档字段,以及如何恢复它。支持EnumType.ORDINALEnumType.STRING。OGM 将STRING值存储在 MongoDB 的引号中,以表示字符串值。ORDINAL另一方面,值存储时不带引号,表示数值。

例子

首先,我定义了一个 Java enum 类型,表示我们的球员在 ATP 世界巡回赛历史上的最高排名。然后我定义了 Hibernate OGM 将持久化或恢复的相应字段,并用@Enumerated注解对其进行了标记。清单 5-9 展示了实体的部分代码。

清单 5-9。 一个 Java 枚举类型

import javax.persistence.EnumType;
import javax.persistence.Enumerated;
...

@Entity
@Table(catalog="ATP", schema="public", name="atp_players")
public class Players implements Serializable {

    public static enum Ratings {

        FIRST,
        SECOND,
        THIRD,
        FOURTH,
        FIFTH,
        SIXTH,
        SEVENTH,
        EIGHTH,
        NINTH,
        TENTH
    }

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @Column(name="player_name")
    private String name;
    @Column(name="player_surname")
    private String surname;
    @Column(name="player_age")
    private int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    private Date birth;
    @Column(name="player_best_rating")
    @Enumerated(EnumType.STRING)
    private Ratings best_rating;

//constructors, getters and setters
...
}

像往常一样,我现在使用 Hibernate OGM 持久化了Players实体的几个实例,MongoDB 显示了如图图 5-20 所示的Players集合。

9781430257943_Fig05-20.jpg

图 5-20 。MongoDB 中的映射@枚举

演示@Enumerated注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM_MONGODB_Enumerated。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@可缓存注解

javax.persistence.Cacheable注解映射。

官方文件:http://docs.oracle.com/javaee/6/api/javax/persistence/Cacheable.html

简要概述

缓存是提高性能的最重要的方法之一,它可以减少执行查询、连接等操作时的数据库流量。您可能知道,JPA 2.0 包含两级缓存:

  • The first-level cache is not directly related to performance and is meant for reducing the number of queries in transactions. It’s also known as the persistent context cache and it lives as long as the persistence context lives, usually until the end of transaction. When the persistent context is closed, the first-level cache is cleared, and further queries must use the database again. See Figure 5-21.

    9781430257943_Fig05-21.jpg

    图 5-21 。JPA 2.0 一级缓存

  • The second-level cache is directly related to performance. In this case, the caching mechanism is placed between the persistence context and the database and it acts a server-side device to keep objects loaded into memory. With this approach, the objects are available for the entire application directly from memory without involving the database. The JPA provider is responsible for implementing the second-level cache, but the implementation itself is pretty subjective, because the specification is not very clear. Therefore, each implementation is free to decide how to implement caching capabilities and how sophisticated they will be. See Figure 5-22.

    9781430257943_Fig05-22.jpg

    图 5-22 。JPA 2.0 二级缓存

默认情况下,实体不是二级缓存的一部分。JPA 2.0 提供了@Cacheable注解,可以用来明确地通知 JPA 提供者关于可缓存或不可缓存的实体。@Cacheable注解采用一个布尔值(true是可缓存实体的默认值;false,对于不可缓存的实体)。在将@Cacheable注解传播到所需的实体之后,您必须告诉 JPA 提供者使用哪种缓存机制,为此,您必须将shared-cache-mode标签添加到persistence.xml文件中。支持的值有:

  • NONE -无缓存
  • ENABLE_SELECTIVE—缓存所有标注有@Cacheable(true)的实体
  • DISABLE_SELECTIVE—缓存除了用@Cacheable(false)标注的实体之外的所有实体
  • ALL—缓存所有实体
  • UNSPECIFIED—未定义的行为(可能是 JPA 提供程序的默认选项)

OGM 支持

Hibernate OGM 支持@Cacheable注解和shared-cache-mode标签。您可能知道,Hibernate 有几个二级缓存提供者,比如 EHCache、OSCache 和 Infinispan。这些高速缓存提供者中的每一个都有一些特定的设置和特定的特性,具有优点和差距,并提供更好或更差的性能。但是这里我们不关注不同的缓存提供者,所以我们随意选择 EHCache 来测试 Hibernate OGM 对@Cacheable注解和shared-cache-mode标记的支持。请随意使用任何其他受支持的二级缓存提供程序。

例子

您可能只对最终的结果和结论感兴趣,但是,如果您想重现相同的测试,下面是设置 EHCache 二级缓存的主要步骤。(如果您从未使用过 Hibernate OGM 和二级缓存,这是一个尝试它们的好机会。)

  1. 为了将 EHCache 与 Hibernate OGM 和 MongoDB 一起使用,除了 Hibernate OGM 发行版和 MongoDB 驱动程序之外,还需要向应用的库中添加几个 jar。额外的 jar 有:ehcache-core-2.4.3.jarhibernate-ehcache-4.1.4.Final.jarslf4j-api-1.6.1.jar(都可以在 Hibernate 4.1.4 最终发行版的可选 jar 集中找到)和slf4j-simple-1.6.1.jar(可以从http://www.java2s.com/Code/Jar/s/Downloadslf4jsimple161jar.htm下载)。
  2. 接下来,你要写persistence.xml 文件。你必须:
  • shared-cache-mode设置为ENABLE_SELECTIVE(只有标注为@Cacheable(true)的实体才会被缓存)。
  • 打开二级缓存和查询缓存。
  • 指示二级缓存提供程序类。
  • 设置区域工厂类。
  • 指定高速缓存提供者/区域工厂使用的 EHCache 配置文件的位置ehcache.xml(ehcache.xml内容实际上并不相关,所以我不会在这里列出它。您可以在名为HOGM_MONGODB_Cache的应用下的 Apress 存储库中查看它。
  • 设置 JTA 平台。
  • 添加用于配置 MongoDB 连接的特定属性。

如果你完成了这些步骤,你将得到一个persistence.xml文件,就像清单 5-10 中的一样。

清单 5-10。 Persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence " xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance " xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">
    <persistence-unit name="HOGM_MONGODB_L2Cache-ejbPU" transaction-type="JTA">
        <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
        <class>hogm.mongodb.entity.Players</class>
        <class>hogm.mongodb.entity.Tournaments</class>
        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
        <properties>
            <property name="hibernate.cache.use_second_level_cache" value="true"/>
            <property name="hibernate.cache.use_query_cache" value="true"/>
            <property name="hibernate.cache.provider_class"
                            value="org.hibernate.cache.EhCacheProvider"/>
            <property name="hibernate.cache.region.factory_class"
                            value="org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory"/>
            <property name="hibernate.cache.provider_configuration_file_resource_path"
                            value="ehcache.xml"/>
            <property name="hibernate.transaction.jta.platform"
                            value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform"/>
            <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
            <property name="hibernate.ogm.datastore.grid_dialect"
                            value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
            <property name="hibernate.ogm.mongodb.database" value="mapping_entities_db"/>
            <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
            <property name="hibernate.ogm.mongodb.port" value="27017"/>
        </properties>
    </persistence-unit>
</persistence>

注意在persistence.xml文件中指定了两个实体— PlayersTournaments。为了测试ENABLE_SELECTIVE缓存机制,我用@Cacheable(true)注解了Players实体,用@Cacheable(false).注解了Tournaments实体。我们的测试将检查以确保Players对象是可缓存的,而Tournaments对象不应该是可缓存的。下面是Players实体的清单:

import javax.persistence.Cacheable;
...

@Entity
@Cacheable(true)
@Table(catalog = "ATP", schema = "public", name = "atp_players")
public class Players implements Serializable {

   //fields declaration
   //constructors, getters and setters
...
}

并且,Tournaments实体的列表是:

import javax.persistence.Cacheable;

@Entity
@Cacheable(false)
public class Tournaments implements Serializable {

   //fields declaration
   //constructors, getters and setters
...
}

在开始编写测试之前,您需要用至少五个文档填充与这两个实体相关联的 MongoDB 集合,每个文档具有 ids 1、2、3、4 和 5(您将在测试部分看到为什么我们需要五个文档)。完成后,您就可以编写一个简单的 JUnit 测试来检查二级缓存是否工作正常。要做到这一点,您需要使用二级缓存 API,这个 API 很差,但至少它允许我们使用javax.persistence.Cache接口查询和删除缓存中的实体。它提供了检查缓存中是否包含给定实体的数据的方法contains,以及从缓存中移除数据的两种方法:evict用于移除特定实体,而evictAll用于清除缓存.

所以,我们已经准备好编写测试了。我们只需要一个简单的场景来描述PlayersTournaments实体,如下所示:

  • 使用contains方法检查Players对象是否在缓存中(这将返回false)。
  • 使用EntityManager find方法查询Players对象(这个查询是针对 MongoDB 数据库执行的,由于ENABLE_SELECTIVE效应,提取的对象应该放在二级缓存中)。
  • 再次调用contains方法来检查Players对象是否在缓存中(这将返回true)。
  • 使用evict方法从缓存中移除Players对象。
  • 再次调用 contains 方法时,检查是否从缓存中删除了Players对象(这将返回 false)。

Tournaments 的场景如下:

  • 使用contains方法检查Tournaments对象是否在缓存中(这将返回false)。
  • 使用EntityManager find方法查询Tournaments对象(这个查询是针对 MongoDB 数据库执行的,由于ENABLE_SELECTIVE效应,提取的对象不应该放在二级缓存中)。
  • 再次调用contains方法来检查Tournaments对象是否在缓存中(这将返回false)。
  • 通过调用evictAll方法清除缓存。

最后,将这个场景翻译成一个 JUnit 测试,就像清单 5-11 中的测试一样。

清单 5-11。 一次 JUnit 测试

package tests;

import hogm.mongodb.entity.Players;
import hogm.mongodb.entity.Tournaments;
import javax.persistence.Cache;
import javax.persistence.CacheRetrieveMode;
import javax.persistence.CacheStoreMode;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import org.junit.After;
import org.junit.AfterClass;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class CacheTest {

    private static EntityManagerFactory emf;
    private EntityManager em;

    public CacheTest() {
    }

    @BeforeClass
    public static void setUpClass() {
    }

    @AfterClass
    public static void tearDownClass() {
    }

    @Before
    public void setUp() {
        emf = Persistence.createEntityManagerFactory("HOGM_MONGODB_L2Cache-ejbPU");
        em = emf.createEntityManager();

        em.setProperty("javax.persistence.cache.retrieveMode", CacheRetrieveMode.USE);
        em.setProperty("javax.persistence.cache.storeMode", CacheStoreMode.USE);
    }

    @After
    public void tearDown() {
        if (em != null) {
            em.clear();
            em.close();
        }
    }

    @Test
    public void testCache_ENABLE_SELECTIVE() {

        Cache cache = em.getEntityManagerFactory().getCache();

        //TESTING PLAYERS OBJECT CACHING

        // players objects shouldn't be in second-level cache at this moment
        for (int i = 1; i < 5; i++) {
            assertFalse(cache.contains(Players.class, i));
        }

        // finding the players objects should place them into second-level cache
        for (int i = 1; i < 5; i++) {
            em.find(Players.class, i);
        }

        // players objects should be in second-level cache at this moment,
        // but we delete them from cache one by one
        for (int i = 1; i < 5; i++) {
            assertTrue(cache.contains(Players.class, i));
            cache.evict(Players.class, i);
        }

        // players objects shouldn't be in second-level cache at this moment
        for (int i = 1; i < 5; i++) {
            assertFalse(cache.contains(Players.class, i));
        }

        //TESTING TOURNAMENTS OBJECT CACHING

        // tournaments objects shouldn't be in second-level cache at this moment
        for (int i = 1; i < 5; i++) {
            assertFalse(cache.contains(Tournaments.class, i));
        }

        // finding the tournaments objects shouldn't place them into second-level cache
        for (int i = 1; i < 5; i++) {
            em.find(Tournaments.class, i);
        }

        // players objects shouldn't be in second-level cache at this moment either
        for (int i = 1; i < 5; i++) {
            assertFalse(cache.contains(Tournaments.class, i));
        }

        cache.evictAll();
    }
}

测试结果 100%令人满意,如图图 5-23 所示,这意味着 Hibernate OGM 支持@Cacheableshared-cache-mode

9781430257943_Fig05-23.jpg

图 5-23 。测试@Cacheable 注解

此外,您可以通过编写自己的场景来轻松测试DISABLE_SELECTIVEALL

请注意,您可以通过设置以下EntityManager属性(在setUp方法中,如清单 5-11 )以编程方式控制检索和存储实体的缓存行为。为了完整起见,我将它们设置为默认值(使用),但是我也测试了BYPASSREFRESH值,一切都如预期的那样工作:

  • javax.persistence.cache.retrieveMode控制如何从调用EntityManager.find方法的缓存和查询中读取数据。它默认为值USE,这意味着从二级缓存中检索数据(如果可用的话)。如果不可用,则从数据库中检索数据。通过指定值BYPASS,可以轻松绕过二级缓存,直接进入数据库。
  • javax.persistence.cache.storeMode控制数据如何存储在缓存中。它默认为USE值,这意味着当从数据库读取数据或向数据库提交数据时,会创建或更新缓存数据,而不会在数据库读取时刷新缓存。通过设置REFRESH值可以强制刷新。最后,您可以通过设置BYPASS值来保持缓存不变。

理解 JPA 2.0 二级缓存 API 所需的一切都很好地浓缩在 Java EE 6 教程中,可从http://docs.oracle.com/javaee/6/tutorial/doc/gkjia.html获得。

演示@Cacheable注解的完整应用可以在 Apress 存储库中获得,命名为HOGM_MONGODB_Cache。它是一个 NetBeans 项目,并且是在 GlassFish 3 AS 下测试的。

@ mapped super class annotation

javax.persistence.MappedSuperclass注解映射。

官方文件:http://docs.oracle.com/javaee/6/api/javax/persistence/MappedSuperclass.html

简要概述

映射超类的作用域是为它的子类提供通用的行为和属性或字段映射。它类似于每类继承的表,但是不允许查询、持久化或者与超类的关系(这是这种方法的最大缺点)。也被称为具体类,映射超类不是一个实体,它在数据库中没有单独的表。可以使用AttributeOverrideAssociationOverride注解(或相应的 XML 元素)在相应的子类中覆盖映射信息。子类是实体,所以它们负责定义表。

MongoDB 将为每个实体(每个子类)包含一个集合,文档看起来将与实体中声明的字段完全一样(包括继承的字段)。如果您查看一个集合的内容,没有什么会泄露映射超类的存在。

OGM 支持

Hibernate OGM 支持@MappedSuperclass注解。它知道如何将每个子类转换成 MongoDB 集合,并用包含统一字段(继承字段+实体字段)的文档填充它。

例子

我的例子基于一个简单、常见的场景。我从某种通用或抽象的对象开始,比如玩家。“运动员”是一个非常通用的概念,因为运动员有很多种——网球运动员、棒球运动员等等。所有玩家都有一些共同的特征,比如名字、姓氏、年龄和生日,以及一些特定于他们的学科(类别)的特殊特征。

我们可以将它们放在一个超类中,一个用@MappedSuperclass标注的抽象类,而不是重复每种玩家实体的共同特征。然后,对于每个类别的玩家,我们可以定义一个从超类继承共同特征的实体,并提供更具体的特征。

因此,映射的超类被称为Players,看起来像这样:

import javax.persistence.MappedSuperclass;
...

@MappedSuperclass
public abstract class Players implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    protected int id;
    @Column(name="player_name")
    protected String name;
    @Column(name="player_surname")
    protected String surname;
    @Column(name="player_age")
    protected int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    protected Date birth;

    //getters and setters
...
}

接下来,我们设置两类球员:网球运动员和棒球运动员。网球运动员的一个显著特征可能是他或她用哪只手打球。对于一个棒球运动员来说,它可能是队中的位置。因此,我们可以编写TennisPlayers实体来继承超类字段并创建一个新的,如下所示:

import javax.persistence.AttributeOverride;
...

@Entity
@AttributeOverride(name="age", column=@Column(name="tenis_player_age"))
public class TennisPlayers extends Players implements Serializable {

    protected String handplay;

    //constructors, getters and setters
...
}

根据该规则,下面列出了BaseballPlayers实体:

import javax.persistence.AttributeOverride;
...

@Entity
@AttributeOverride(name="age", column=@Column(name="baseball_player_age"))
public class BaseballPlayers extends Playersimplements Serializable {

    protected String position;

    //constructors, getters and setters
...
}

现在使用 Hibernate OGM 持久化几个TennisPlayersBaseballPlayers实体的实例。MongoDB 将显示TennisPlayersBaseballPlayers集合,如图图 5-24 所示。注意继承的字段和新字段一起出现在文档中,以及@AttributeOverride注解的效果:

9781430257943_Fig05-24.jpg

图 5-24 。在 MongoDB 中测试@MappedSuperclass 注解

演示@MappedSuperclass注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM_MONGODB_MappedSuperclass。它是一个 NetBeans 项目,并且是在 GlassFish 3 AS 下测试的。

@ElementCollection 注解

javax.persistence.ElementCollection注解映射。

正式文件:http://docs.oracle.com/javaee/6/api/javax/persistence/ElementCollection.html

简要概述

@ElementCollection注解用于表示实例的集合(一个基本的 Java 类型或可嵌入的类)。不要混淆 Java 集合和 MongoDB 集合。Java 集合数据存储在一个单独的表中(集合表),可以使用@CollectionTable注解指定该表,该注解指示集合表名称和任何连接。由于数据存储在一个单独的表中,这与嵌入源对象表中的@Embeddable对象不同。更像是一对多的可嵌入关系。 @ElementCollection的一个关键特性是它能够轻松定义简单值(对象)的集合,而无需定义新的类,而是为它们创建单独的表。缺点是您不能控制持久化、合并或删除数据的传播级别,因为目标对象与源对象严格相关,并且它们作为一个整体。然而,获取类型(EAGERLAZY)是可用的,因此您可以加载源对象而不加载目标对象。

OGM 支持

Hibernate OGM 为@ElementCollection注解提供了部分支持。尽管我在测试中没有遇到任何错误或缺陷,但它并没有真正做到规范所说的那样。不支持@CollectionTable注解,Java 集合数据作为实体集合中的嵌套集合存储在 MongoDB 中,而不是存储在单独的集合中。

例子

为了演示一组可嵌入的类实例,我定义了一个简单的类,代表每个玩家在 2012 年赢得的锦标赛或决赛的列表:

import javax.persistence.Embeddable;
...

@Embeddable
public class Wins2012 implements Serializable {

   private String titlesfinals;

   //constructors, getters and setters
...
}

通常,这样的类会包含不止一个字段,但是出于测试的目的,没有必要添加更多的字段。

此外,对于一个简单对象的集合,我使用了一个List<String>来保存每个球员在 2008 年到 2012 年之间的排名历史。

两个集合都是在Players实体中定义的,如清单 5-12 所示(像targetClass (" 集合的元素类型的基本或可嵌入类)和fetch (" 集合是应该延迟加载还是必须立即获取)这样的元素是可选的)。

清单 5-12。 定义两个系列

import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.FetchType;
...

@Entity
@Table(catalog = "ATP", schema = "public", name = "atp_players")
public class Players implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    @Column(name = "player_name")
    private String name;
    @Column(name = "player_surname")
    private String surname;
    @Column(name = "player_age")
    private int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    private Date birth;
    @ElementCollection(targetClass=hogm.mongodb.entity.Wins2012.class,
                                        fetch = FetchType.EAGER)
    @CollectionTable(name = "EC_TABLE")  //not supported by OGM
    @AttributeOverrides({
        @AttributeOverride(name = "titlesfinals",
        column = @Column(name = "EC_titlesfinals"))
    })
    private List<Wins2012> wins = new ArrayList<Wins2012>();
    @ElementCollection(targetClass=java.lang.String.class,
                                        fetch = FetchType.LAZY)
    @CollectionTable(name = "RANKING_TABLE")  //not supported by OGM
    private List<String> rankinghistory08_12 = new ArrayList<String>();

//constructors, getters and setters
...
}

接下来,我持久化几个Players实例,结果如图图 5-25 所示。注意,这两个 Java 集合没有单独的 MongoDB 集合——@AttributeOverrides工作得非常好。

9781430257943_Fig05-25.jpg

图 5-25 。在 MongoDB 中测试@ElementCollection 注解

演示@ElementCollection注解的完整应用可以在 Apress 存储库中获得,命名为HOGM_MONGODB_ElementCollection。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。在继续本节之前,请下载相应的 NetBeans 项目,并确保您可以在 GlassFish AS 3 下成功运行该应用。

在测试过程中,您可能已经注意到了 web 阿桂中的标签为“查看延迟加载(您需要一个 id 为 1 的文档)”的按钮如果你按下这个按钮,wins集合使用急切机制加载,rankinghistory08_12集合使用懒惰机制加载(对于单个玩家,id:1)。结果将类似于图 5-26 所示。

9781430257943_Fig05-26.jpg

图 5-26 。在 MongoDB 中测试 @ElementCollection 注解的延迟加载

图 5-26 中的结果引出了一个明显的问题:我怎么知道 wins 集合被急切地加载了,而rankinghistory08_12被延迟地加载了?换句话说,我怎么知道延迟加载起作用了呢?

嗯,当涉及 Hibernate(包括 Hibernate OGM) JPA 时,这样的问题很常见,因为 Hibernate 在幕后使用的代理对象可能会令人困惑。然而,惰性加载是否有效的问题可以用几种方法来解决。您可以选择编写 JUnit 测试来监控数据库传输或任何其他复杂的解决方案,或者您可以创建一个简单的测试,就像我将要描述的那样。请注意,该测试是在 NetBeans IDE 中执行的,并且特定于本节中介绍的示例,但是可以很容易地调整到其他情况。测试步骤如下:

  • 为两个集合设置FetchType.EAGERwinsrankinghistory08_12

  • hogm.mongodb.ejb.SampleBean无状态 bean 中,找到方法loadAction中的以下代码行:

    Players p = em.find(Players.class, 1);
    
  • After this line, place a NetBeans line breakpoint as shown in Figure 5-27.

    9781430257943_Fig05-27.jpg

    图 5-27 。在 NetBeans 中添加行断点

  • 在调试模式下部署并启动应用(按 NetBeans 工具栏上的“调试项目”按钮)。

  • 应用启动后,按下标签为“去看懒加载(需要一个 _id:1 的文档)”的按钮。这将导致调试器执行代码,直到该行断点,并使应用在该点挂起。

  • The Players instance is loaded and the p variable is listed in NetBeans debugger (see the Variables window in Figure 5-28). Don’t expand the p tree node, since this will be interpreted as an explicit request to see the p content.

    9781430257943_Fig05-28.jpg

    图 5-28 。NetBeans 中的“变量”窗口

  • 接下来,关闭 MongoDB 服务器(可以在服务器 shell 中按 Ctrl+C)。

  • Now you can expand the p node and the wins and rankinghistory08_12 sub-nodes as shown in Figure 5-29. Since the MongoDB server is closed, and the collections data is available, we can conclude that the data was eagerly loaded.

    9781430257943_Fig05-29.jpg

    图 5-29 。展开“p”节点

  • 接下来,关闭应用,停止调试器并重启 MongoDB 服务器。

  • rankinghistory08_12集合设置FetchType.LAZY,为wins集合设置FetchType.EAGER

  • 再次以调试模式启动应用。

  • 应用启动后,按下标签为“去看懒加载(需要一个 _id:1 的文档)”的按钮。

  • 在 NetBeans Variables窗口中,您应该会看到一个代表p变量的折叠树节点。不要展开节点。

  • 再次关闭 MongoDB 服务器。

  • 现在,展开p节点以及winsrankinghistory08_12子节点,如图图 5-30 所示。注意,wins集合包含数据,因为它被急切地加载,但是rankinghistory08_12节点显示一个错误,表明它不能连接到 MongoDB 服务器。这意味着rankinghistory08_12集合的数据没有被急切地加载,应该在显式展开rankinghistory08_12节点时加载。因此,惰性加载在 Hibernate OGM 中是有效的。

9781430257943_Fig05-30.jpg

图 5-30 。展开“p”节点

您可以很容易地对其他情况执行类似的测试,比如关联。

JPA 生命周期事件@EntityListeners ,@ ExcludeDefaultListeners,@ ExcludeSuperclassListeners 注解

the javax.persistence.EntityListeners, javax.persistence.ExcludeDefaultListenersjavax.persistence.ExcludeSuperclassListeners标注映射。

官方文件:

http://docs.oracle.com/javaee/6/api/javax/persistence/EntityListeners.html
http://docs.oracle.com/javaee/6/api/javax/persistence/ExcludeDefaultListeners.html
http://docs.oracle.com/javaee/6/api/javax/persistence/ExcludeSuperclassListeners.html

简要概述

JPA 附带了一组反映实体生命周期的回调方法 。实际上,一个实体的生命周期由一系列事件组成,比如持久化、更新、删除等等。对于每个事件,JPA 允许您定义一个受支持的回调方法,当事件被触发时,JPA 会自动调用相应的回调方法。您负责编写回调方法实现。

当回调方法在实体体内定义时,它们是内部回调方法,当它们在实体体外定义时,在单独的类中,它们是外部回调方法。此外,默认回调方法是可以默认应用于所有实体类的侦听器。为了将这些概念与注解联系起来,下面是一些典型情况:

  • 内部回调方法不需要注解。回调方法简单地定义在实体体或映射的超类中。
  • 外部回调方法不需要注解。但是,使用这些方法的实体和映射超类需要用@EntityListeners({ ExternalListener_1.class, ExternalListeners_2.class, ...}).进行注解
  • 默认的回调方法不需要注解。实际上,这些回调没有注解;这就是为什么默认的监听器被定义在一个名为orm.xml,的 XML 文件中,这个文件和persistence.xml在同一个位置,或者在persistence.xml中指定的任何其他位置。
  • 默认情况下,默认侦听器应用于所有实体类。您可以通过用@ExcludeDefaultListeners对实体进行注解来关闭实体的这种行为。
  • 默认情况下,实体从它们映射的超类继承回调方法(超类侦听器的调用在实体类中继承)。用@ExcludeSuperclassListeners标注实体类可以获得相反的效果。此外,您可以在子类中覆盖映射的超类回调方法。

内部回调方法可以用以下注解进行标记:

  • @PrePersist在新实体持久化之前执行(添加到EntityManager)。
  • @PostPersist在数据库中存储新实体后执行(在提交或刷新期间)。
  • 从数据库中检索到实体后,执行@PostLoad
  • 当实体被EntityManager识别为已修改时,执行@PreUpdate
  • @PostUpdate在更新数据库中的实体后执行(在提交或刷新期间)。
  • 当一个实体在EntityManager中被标记为删除时,执行@PreRemove
  • @PostRemove在从数据库中删除实体后执行(在提交或刷新期间)。

外部回调方法和默认回调方法是相同的,只是它们采用一个参数来指定作为生命周期事件源的实体。

注意,当所有侦听器出现在应用中时,调用有严格的顺序。默认回调方法首先发生,外部回调方法其次,内部回调方法最后执行。

OGM 支持

Hibernate OGM 支持@EntityListeners, @ExcludeDefaultListeners,@ExcludeSuperclassListeners注解。它还支持实体和映射超类的监听器。

例子

对于这个例子,我使用了关于映射超类一节中定义的类——抽象映射超类Players,以及两个实体TennisPlayersBaseballPlayers。有了这三个类,我可以很好地测试监听器。注意,回调方法只通过一些日志消息来标记它们的存在。

按照调用的顺序,我首先在orm.xml 文件中定义了一个默认监听器(不要忘记将这个文件保存在与persistence.xml相同的位置):

<entity-mappings FontName2">http://java.sun.com/xml/ns/persistence/orm "
 xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
 xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence/orm
 http://java.sun.com/xml/ns/persistence/orm_1_0.xsd " version="1.0">
  <persistence-unit-metadata>
    <persistence-unit-defaults>
      <entity-listeners>
        <entity-listener class="hogm.mongodb.listeners.DefaultListener" />
      </entity-listeners>
    </persistence-unit-defaults>
  </persistence-unit-metadata>
</entity-mappings>

hogm.mongodb.listeners.DefaultListener只实现了onPrePersistonPostPersist方法,如清单 5-13 所示。

清单 5-13。onprerist 和 onPostPersist 方法

package hogm.mongodb.listeners;

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.PostPersist;
import javax.persistence.PrePersist;

public class DefaultListener {

    @PrePersist
    void onPrePersist(Object o) {
        Logger.getLogger(DefaultListener.class.getName()).
                                        log(Level.INFO, "PREPARING THE PERSIST SOME OBJECT ...");
    }

    @PostPersist
    void onPostPersist(Object o) {
        Logger.getLogger(DefaultListener.class.getName()).
                                        log(Level.INFO, "AN OBJECT WAS PERSISTED ...");
    }
}

默认情况下,当一个对象被持久化时,将为所有三个实体调用这些方法。

我还定义了两个外部侦听器,一个用于实现特定于更新操作的回调方法,另一个用于删除操作。使用@EntityListeners注解,这些监听器仅对BaseballPlayers实体可用。第一个监听器如清单 5-14 所示。

清单 5-14。 更新监听器

打包 hogm . MongoDB . listeners;

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.PostUpdate;
import javax.persistence.PreUpdate;

public class BaseballExternalUpdateListeners {

    @PreUpdate
    void onPreUpdate(Object o) {
        Logger.getLogger(BaseballExternalUpdateListeners.class.getName()).log(Level.INFO,
             "PREPARING THE UPDATE THE FIRST BASEBALL PLAYER OBJECT ...{0}", o.toString());
    }

    @PostUpdate
    void onPostUpdate(Object o) {
        Logger.getLogger(BaseballExternalUpdateListeners.class.getName()).log(Level.INFO,
             "THE FIRST BASEBALL PLAYER OBJECT WAS UPDATED...{0}", o.toString());
    }
}

而第二个在清单 5-15 中。

清单 5-15。 删除监听器

打包 hogm . MongoDB . listeners;

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.PostRemove;
import javax.persistence.PreRemove;

public class BaseballExternalRemoveListeners {

    @PreRemove
    void onPreRemove(Object o) {
        Logger.getLogger(BaseballExternalRemoveListeners.class.getName()).log(Level.INFO,
         "PREPARING THE DELETE FOR THE FIRST BASEBALL PLAYER OBJECT ...{0}", o.toString());
    }

    @PostRemove
    void onPostRemove(Object o) {
        Logger.getLogger(BaseballExternalRemoveListeners.class.getName()).log(Level.INFO,
         "THE FIRST TENNIS PLAYER OBJECT WAS REMOVED ...{0}", o.toString());
    }
}

映射的超类Players将拒绝默认侦听器,并实现三个内部回调方法:onPrePersistonPostPersistonPostLoad。这些侦听器只由BaseballPlayers实体继承,因为TennisPlayers实体将用@ExcludeSuperclassListeners进行注解。Players映射的超类如清单 5-16 所示。

清单 5-16。Players映射的超类

package hogm.mongodb.entity;

import java.io.Serializable;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.Column;
import javax.persistence.ExcludeDefaultListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.PostLoad;
import javax.persistence.PostPersist;
import javax.persistence.PrePersist;
import javax.persistence.Temporal;

@MappedSuperclass
@ExcludeDefaultListeners
public abstract class Players implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected int id;
    @Column(name = "player_name")
    protected String name;
    @Column(name = "player_surname")
    protected String surname;
    @Column(name = "player_age")
    protected int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    protected Date birth;

    @PrePersist
    void onPrePersist() {
        Logger.getLogger(Players.class.getName()).log(Level.INFO,
           "PREPARING THE PERSIST A (BASEBALL) PLAYER OBJECT ...");
    }

    @PostPersist
    void onPostPersist() {
        Logger.getLogger(Players.class.getName()).log(Level.INFO,
            "THE (BASEBALL) PLAYER OBJECT WAS PERSISTED ...");
    }

    @PostLoad
    void onPostLoad() {
        Logger.getLogger(Players.class.getName()).log(Level.INFO,
            "THE FIRST (BASEBALL) PLAYER OBJECT WAS LOADED ...");
    }

   //constructors, getters and setters
...
}

接下来是TennisPlayers实体,如清单 5-17 所示。它将实现所有内部侦听器,并接受默认侦听器,但不接受超类侦听器(注意@ExcludeSuperclassListeners注解的存在:

清单 5-17。TennisPlayers实体

import java.io.Serializable;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.ExcludeSuperclassListeners;
import javax.persistence.PostLoad;
import javax.persistence.PostPersist;
import javax.persistence.PostRemove;
import javax.persistence.PostUpdate;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;

@Entity
@ExcludeSuperclassListeners
@AttributeOverride(name = "age", column =
@Column(name = "tenis_player_age"))
public class TennisPlayers extends Players implements Serializable {

    protected String handplay;

    @PrePersist
    @Override
    void onPrePersist() {
        Logger.getLogger(TennisPlayers.class.getName()).log(Level.INFO,
           "PREPARING THE PERSIST A TENNIS PLAYER OBJECT ...");
    }

    @PostPersist
    @Override
    void onPostPersist() {
        Logger.getLogger(TennisPlayers.class.getName()).log(Level.INFO,
           "THE TENNIS PLAYER OBJECT WAS PERSISTED ...");
    }

    @PostLoad
    @Override
    void onPostLoad() {
        Logger.getLogger(TennisPlayers.class.getName()).log(Level.INFO,
            "THE FIRST TENNIS PLAYER OBJECT WAS LOADED ...");
    }

    @PreUpdate
    void onPreUpdate() {
        Logger.getLogger(TennisPlayers.class.getName()).log(Level.INFO,
           "PREPARING THE UPDATE THE FIRST TENNIS PLAYER OBJECT ...");
    }

    @PostUpdate
    void onPostUpdate() {
        Logger.getLogger(TennisPlayers.class.getName()).log(Level.INFO,
            "THE FIRST TENNIS PLAYER OBJECT WAS UPDATED...");
    }

    @PreRemove
    void onPreRemove() {
        Logger.getLogger(TennisPlayers.class.getName()).log(Level.INFO,
           "PREPARING THE DELETE FOR THE FIRST TENNIS PLAYER OBJECT ...");
    }

    @PostRemove
    void onPostRemove() {
        Logger.getLogger(TennisPlayers.class.getName()).log(Level.INFO,
            "THE FIRST TENNIS PLAYER OBJECT WAS REMOVED ...");
    }

    public String getHandplay() {
        return handplay;
    }

   //constructors, getters and setters
...
}

最后,BaseballPlayers实体显示在清单 5-18 中。它没有定义任何内部侦听器。它使用定义的外部侦听器,使用@EntityListeners注解指定,并从映射的超类继承侦听器。它不接受默认侦听器,因为映射的超类不包括默认侦听器。

清单 5-18。BaseballPlayers实体

package hogm.mongodb.entity;

import hogm.mongodb.listeners.BaseballExternalRemoveListeners;
import hogm.mongodb.listeners.BaseballExternalUpdateListeners;
import java.io.Serializable;
import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;

@Entity
@EntityListeners({BaseballExternalUpdateListeners.class,
                                BaseballExternalRemoveListeners.class})
@AttributeOverride(name = "age", column =
@Column(name = "baseball_player_age"))
public class BaseballPlayers extends Players implements Serializable {

    protected String position;

    //constructors, getters and setters
...

搞定了。我知道这令人困惑,但是在一个应用中测试实体和映射超类的所有三种注解是非常复杂的。在实际应用中,你不会把所有这些东西混合在一起。图 5-31 应该有助于澄清事情。

9781430257943_Fig05-31.jpg

图 5-31 。测试 JPA 监听器

下面是我测试的简单场景:

  • 插入两个网球运动员和一个棒球运动员。
  • 装载第一个网球运动员。
  • 更新第一个网球运动员。
  • 删除第一个网球运动员。
  • 更新第一个棒球运动员。
  • 装载第一个棒球运动员。
  • 删除第二名网球运动员。
  • 删除第一个棒球运动员。

在图 5-32 中,你可以从听者的调用角度看到每一步。看起来 Hibernate OGM 做了很好的工作,一切都如预期的那样工作,每个回调方法都在适当的时候被调用。

9781430257943_Fig05-32.jpg

图 5-32 。测试 JPA 监听器的结果

演示 JPA 监听器的完整应用可以在 Apress 存储库中获得,命名为HOGM_MONGODB_Listeners。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@版本注解

javax.persistence.Version注解映射。

正式文件:http://docs.oracle.com/javaee/6/api/javax/persistence/Version.html

简要概述

一个@Version字段或属性有双重作用:在执行合并操作(更新)时保证数据完整性,并提供乐观并发控制。版本字段(每个实体类只允许一个版本字段)与 JPA 乐观锁定 配合得很好,乐观锁定应用于事务提交,负责检查每个要更新或删除的对象。目标是避免当 JPA 处理两个并发线程(用户)对相同数据的同时更新时可能发生的冲突。当冲突发生时,持久提供者抛出异常。换句话说,乐观锁定假设数据在读写数据操作之间不会被修改。

@Version标注的字段被保存到数据库中,初始值通常为 0,并且对于每次更新操作(调用merge方法)它会自动递增(通常为 1)。实际上,当 JPA“烘烤”一个实体更新语句时,除了更新范围之外,它还向WHERE子句添加了正确的“单词”,用于递增版本字段和匹配旧版本值(读取值):

UPDATE *table_name* SET field_1 = *value_1, ... field_n = value_n* , version = (version + 1)
WHERE id = *some_id*  and version = *read_version*

同时,如果同一个实体被另一个用户(线程)更新,持久性提供者将抛出一个OptimisticLockException,因为它不能定位正确的旧版本值。乐观锁定可以提供更好的可伸缩性,但缺点是用户/应用必须刷新并重试失败的更新。

乐观锁定是 JPA 1.0 特有的,也是最常见的锁定方式(使用和推荐)。JPA 2.0 还附带了悲观锁定,它在数据被读取或写入时锁定数据库行。但是,这很少使用,因为它会阻碍可伸缩性并导致死锁和风险状态。乐观锁定和悲观锁定都位于@Version注解之上,并且可以通过 JPA API 进行控制。

关于 JPA 2.0 锁定的更多细节可以在这篇优秀的文章中找到,“ JPA 2.0 并发和锁定” ( https://blogs.oracle.com/carolmcdonald/entry/jpa_2_0_concurrency_and ))。

OGM 支持

Hibernate OGM 支持@Version注解,用@Version注解的字段像其他字段一样存储在 MongoDB 中。您还可以使用EntityManager find, refresh,lock方法控制锁定机制。因为 OGM 不支持本地查询或命名查询,所以不能使用QueryNamedQuery锁定方法。

例子

首先,我在Players实体中定义了一个@Version字段,如清单 5-19 所示。我把它命名为version,设置为类型Long(可以从intIntegershortShortlongjava.sql.Timestamp中选择)。

清单 5-19。 定义了@Version字段

import javax.persistence.Version;
...

@Entity
@Table(name = "atp_players")
public class Players implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    @Version
    private Long version;
    @Column(name = "player_name")
    private String name;
    @Column(name = "player_surname")
    private String surname;
    @Column(name = "player_age")
    private int age;
    private int facade; //used for simulating updated

    public Long getVersion() {
        return version;
    }

    protected void setVersion(Long version) {
        this.version = version;
    }

    //constructors, getters and setters
...
}

注意,因为应用通常不应该修改@Version字段,所以相应的 setter 方法被声明为protected

现在,让我们检查一下@Version字段是否在每次更新操作时自动递增。为此,持久化一些玩家,找个理由调用几次merge方法,比如用一些随机数更新facade字段。在合并的同时,在 MongoDB shell 中监控atp_players集合文档。在图 5-33 中,左侧在第一次调用merge之前呈现文件(_id:1)。在右边,注意在我调用了三次merge之后,版本字段的值从 0 增长到了 3。

9781430257943_Fig05-33.jpg

图 5-33 。调用合并方法时监控版本字段增量

因此,OGM 在每次调用merge时都成功地增加了版本字段。

image 注意如果您无法获得带有_id:1,的文档,您应该删除hibernate_sequences集合并重复持久化操作。您需要这个_id:1,因为在下一个测试中,我们将使用带有这个 id 的EntityManager find方法。我意识到像这样使用自动生成的键和find方法是不寻常的,也是不现实的,但这只是为了教学的目的。

测试乐观锁是否真的在工作(LockModeType.OPTIMISTIC)并不简单;它通常需要编写一个 JUnit 测试来模拟并发事务。然而,我更喜欢一种不同的方法,我想根据下面的场景塑造一个有状态的 bean:

  • 声明一个有状态 bean 并注入 OGMEntityManager;作为会话 bean,它将在多个请求中保持对话状态:

    @Named("bean")
    @Stateful
    @SessionScoped
    public class SampleBean {
    
        @PersistenceContext(unitName = " *PU_name* ")
        private EntityManager em;
    ...
    
  • 声明两个Players对象,p1p2:??

  • 创建一个业务方法,用数据库中的第一个玩家填充p1,并显示 read version字段。注意,我将锁定模式指定为OPTIMISTIC(这是默认设置):

    public void read_OPTIMISTIC_Action_1() {
            p1 = em.find(Players.class, 1, LockModeType.OPTIMISTIC);
            Logger.getLogger(SampleBean.class.getName()).
                         log(Level.INFO, "READ 1, version={0}", p1.getVersion());
        }
    
  • p2 :

    public void read_OPTIMISTIC_Action_2() {
            p2 = em.find(Players.class, 1, LockModeType.OPTIMISTIC);
            Logger.getLogger(SampleBean.class.getName()).
                         log(Level.INFO, "READ 2, version={0}", p2.getVersion());
        }
    

    重复上述步骤

  • 创建用于更新p1的业务方法。更新后,再次读取p1并显示version.,该值增加 1,更新成功完成,因为文档在读写操作之间没有被修改:

    public void update_OPTIMISTIC_Action_1() {
            p1.setFacade(new Random().nextInt(1000000));
            em.merge(p1);
            em.flush();
            p1 = em.find(Players.class, 1, LockModeType.OPTIMISTIC);
            Logger.getLogger(SampleBean.class.getName()).
                        log(Level.INFO, "UPDATE 1, version={0}",   p1.getVersion());
        }
    
  • 写一个更新p2的商业方法。在调用merge,之前显示读取的version.这个值应该小于数据库中的当前版本,表明在p2读写操作之间,另一个线程已经修改了文档。因此,当调用merge方法时,我将得到一个OptimisticLockException:

    public void update_OPTIMISTIC_Action_2() {
            Logger.getLogger(SampleBean.class.getName()).
                        log(Level.INFO, "UPDATE 2, version={0}", p2.getVersion());
            p2.setFacade(new Random().nextInt(1000000));
            em.merge(p2);
            em.flush();
            //there is no need to check version,
            // now the OptimisticLockException exception should be on screen
        }
    

为了一个成功的测试,我需要精确地按顺序调用这四个方法:read_OPTIMISTIC_Action_1(), read_OPTIMISTIC_Action_2(), update_OPTIMISTIC_Action_1()update_OPTIMISTIC_Action_2()。GlassFish 日志的输出如图图 5-34 所示。

9781430257943_Fig05-34.jpg

图 5-34 。获取 LockModeType 的 OptimisticLockException。乐观的

如果我把LockModeType.OPTIMISTIC改成LockModeType.OPTIMISTIC_FORCE_INCREMENT,,我可以很容易地测试乐观的力增量机制。如果您运行了前面的测试,删除所有的atp_players集合,并再次持久化一个Players实例。然后使用下面两个方法调用序列中的一个:read_OPTIMISTIC_Action_1, read_OPTIMISTIC_Action_2update_OPTIMISTIC_Action_1或 r ead_OPTIMISTIC_Action_1, read_OPTIMISTIC_Action_2, update_OPTIMISTIC_Action_2, update_OPTIMISTIC_Action_1.,因为版本字段在每次提交之前都会递增,而不仅仅是更新提交,您会看到类似于图 5-35 所示的内容(第一个调用序列)。

9781430257943_Fig05-35.jpg

图 5-35 。获取 LockModeType 的 OptimisticLockException。乐观 _ 力量 _ 增量

演示@Version注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM_MONGODB_Version。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@Access 注解

javax.persistence.Access注解映射

正式文件:http://docs.oracle.com/javaee/6/api/javax/persistence/Access.html

简要概述

默认情况下,实体通过其持久字段提供要持久化的数据。此外,当从数据库中提取数据时,它会填充相同的持久字段。用注解的术语来说,这就是@Access(AccessType.FIELD).另一种方法是使用get方法,通过间接访问字段作为属性来获取持久化的数据。类似地,提取的数据通过set方法填充实体。用注解术语来说,这就是@Access(AccessType.PROPERTY).

在 JPA 1.x 中,访问类型被限制为基于实体层次结构的字段或属性。从 JPA 2.0 开始,可嵌入类的访问类型可以不同于它所嵌入的实体的访问类型。

OGM 支持

根据 JPA 2.0 规范,Hibernate OGM 支持@Access注解。它可以通过一种访问类型从可嵌入的类中提取数据,并通过另一种访问类型从实体中提取数据。当然,我说的是嵌入可嵌入类的实体。

例子

对于这个例子,我定义了一个可嵌入的类,名为Details:

import javax.persistence.Access;
import javax.persistence.AccessType;
...

@Embeddable
@Access(AccessType.FIELD)
public class Details implements Serializable {

    private String birthplace;
    private String residence;
    private String height;
    private String weight;
    private String plays;
    private int turnedpro;
    private String coach;
    private String website;

    //constructors, getters and setters
...
}

注意@Access注解。(我任意选择使用访问类型FIELD)。现在,名为Players的实体被注解为@Access(AccessType.PROPERTY).,为了使用属性访问,我需要为非瞬态字段提供基于 Java bean 属性约定的getset方法。我还必须将所有 JPA 注解从字段级移动到它们的 getters 中。清单 5-20 显示了Players实体的完整清单。

清单 5-20。 完整的Players实体

import javax.persistence.Access;
import javax.persistence.AccessType;
...

@Entity
@Access(AccessType.PROPERTY)
@Table(catalog = "ATP", schema = "public", name = "atp_players")
public class Players implements Serializable {

    private int id;
    private String name;
    private String surname;
    private int age;
    private Date birth;
    private Details details;

    @Column(name = "player_name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Column(name = "player_surname")
    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Column(name = "player_age")
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Temporal(javax.persistence.TemporalType.DATE)
    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    @Embedded
    public Details getDetails() {
        return details;
    }

    public void setDetails(Details details) {
        this.details = details;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

现在,实体类具有PROPERTY访问类型,可嵌入类具有FIELD访问类型。这在 JPA 2.0 之前是不可能的,因为可嵌入对象的访问类型是由声明它的实体类的访问类型决定的。

搞定了。通过持久化几个实体实例来确保一切按预期工作。

演示@Access注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM_MONGODB_Access。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

image 注意显然,您并不总是需要显式指定访问类型,但有时您需要这样做以避免映射问题。例如,您可能有两个定义不同访问类型的实体,但是它们都嵌入了相同的可嵌入类。在这种情况下,您必须显式设置可嵌入类的访问类型。继承也会出现同样的情况——每个实体从其父实体继承访问类型,这可能并不总是令人满意的。从 JPA 2.0 开始,您可以在继承涉及的任何实体中本地显式覆盖访问类型。

关于 Hibernate 中的访问类型FIELD有一些误解。为了避免某些“陷阱”,您应该知道当设置了这种访问类型时,Hibernate 完全能够填充实体。当您需要从代码中访问这些值时,可能会出现问题,因为在这种情况下,Hibernate 需要专用的方法。这是众所周知的 Hibernate 代理陷阱之一。要了解细节,最好从http://blog.xebia.com/2008/03/08/advanced-hibernate-proxy-pitfalls/开始。

联合

在第二章的中,您看到了 OGM 如何使用IN_ENTITY, GLOBAL_COLLECTION,COLLECTION策略存储关联。现在我将讨论 OGM 如何存储一种不同的数据库关联。对于大多数例子,我将使用IN_ENTITY。有几种类型的数据库关联:

  • 一对一
  • 一对多和多对一
  • 多对多

实体关联中的方向

我想在这里对实体关联的方向做一个简短的概述,因为我认为这对本章的最后一部分会很有用。实体关联具有以下特征:

  • 关联的方向性可以来自关系的一侧(单向)或两侧(双向)。
  • 在单向关联中,其中一方被定义为拥有方;对面不知道有关联。
  • 在双向关联中,双方都有对另一方的引用。
  • 在双向关联中,一方被定义为拥有方(拥有者),另一方被定义为拥有方(非拥有者)。
  • 从编程角度来说,在双向关联中,声明是不对称的,这意味着只有一方通过在特定于关联的注解中设置mappedBy元素来提供关于方向性的信息。在双向一对一和多对多关联中,任何一方都可以使用mappedBy元素,而在双向一对多关联中,mappedBy不能在多对一方声明。
  • 注解元素的值是引用拥有方实体的关联拥有方的字段(或属性)的名称。

@OneToOne 注解

javax.persistence.OneToOne注解映射。

正式文件:http://docs.oracle.com/javaee/6/api/javax/persistence/OneToOne.html

简要概述

在关系数据库术语中,当一个表中正好有一条记录对应于相关表中的一条记录时,就发生了一对一的关联;两个表包含相同数量的记录,并且第一个表的每一行都链接到第二个表的另一行。JPA 使用@OneToOne注解映射单向和双向一对一关联。在双向关联中,非拥有方必须使用@OneToOne注解的mappedBy元素来指定拥有方的关联字段或属性(任何一方都可以是所有者)。这种关联支持抓取(急切或懒惰)、级联和孤儿移除。

OGM 支持

Hibernate OGM 支持符合 JPA 2.0 规范的@OneToOne注解。如您所知,默认情况下,OGM 使用IN_ENTITY策略在 MongoDB 中存储数据,这并不意味着任何额外的集合每个实体类都由一个集合表示。很容易区分以下情况:

  • 对于单向的一对一关联,OGM 将关联的导航信息存储在代表关联所有者的集合中。该集合中的每个文档都包含一个用于存储相应外键的字段。参见图 5-36 。

9781430257943_Fig05-36.jpg

图 5-36 。IN_ENTITY:一对一单向关联

  • 对于双向的一对一关联,导航信息是这样存储的:表示使用mappedBy的实体的集合(关联的非所有者端)包含为每个嵌入集合存储一个外键的字段,而表示关联的所有者端的集合在每个文档中包含一个存储相应外键的字段。参见图 5-37 。

9781430257943_Fig05-37.jpg

图 5-37 。IN_ENTITY:一对一双向关联

对于GLOBAL_COLLECTION策略 ,也有一些直截了当的案例:

  • 对于单向的一对一关联,GLOBAL_COLLECTION没有影响(类似于IN_ENTITY)。
  • 对于双向一对一关联,导航信息是这样存储的:表示使用mappedBy(非所有者端)的实体的集合不包含导航信息;它存储在全局Associations集合中。代表关联所有者端的集合在每个文档中都包含一个存储相应外键的字段。参见图 5-38 。

9781430257943_Fig05-38.jpg

图 5-38 。GLOBAL_COLLECTION:一对一双向关联

对于COLLECTION策略,以下是可能性:

  • 对于单向的一对一关联,COLLECTION没有影响(类似于IN_ENTITY)。
  • 对于双向一对一关联,导航信息是这样存储的:表示使用mappedBy(关联的非所有者方)的实体的集合不包含导航信息;它存储在一个单独的集合中,前缀为单词associations(每个这样的关联都有一个单独的集合)。代表关联所有者方的集合将在每个文档中包含一个存储相应外键的字段。参见图 5-39 。

9781430257943_Fig05-39.jpg

图 5-39 。集合:一对一双向关联

总结一下一对一关联的主要支持方面,有对单向和双向关联的支持;使用@JoinColumn指定用于加入实体关联或元素集合(的列的能力,使用@JoinTable@JoinColumns以及GLOBAL_COLLECTIONCOLLECTION策略支持从可嵌入类到另一个实体的一对一关联,级联(全部)和孤儿移除。此外,OGM 支持使用延迟加载进行抓取。

例子

为了说明一对一的关联(单向和双向),我需要两个逻辑上适合这个目的的实体。例如,网球运动员实体及其网站地址将具有这样的关联。因此,我可以创建映射网站地址的实体:

import java.io.Serializable;
...

@Entity
@Table(name = "players_websites")
public class Websites implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String http_address;
    //constructors, getters and setters
...
}

接下来,我创建了Players实体并定义了一个单向的一对一关联:

import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
...

@Entity
@Table(name = "atp_players")
public class Players implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    @Column(name = "player_name")
    private String name;
    @Column(name = "player_surname")
    private String surname;
    @Column(name = "player_age")
    private int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    @Column(name = "player_birth")
    private Date birth;
    @OneToOne(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    @JoinColumn(name = "website_fk", unique = true, nullable = false, updatable = false)
    private Websites website;

    //constructors, getters and setters
...
}

现在我将保存几个玩家和他们的网址,得到如图图 5-40 所示的东西。注意,atp_players集合中的每个文档都包含一个名为website_pk的字段,该字段存储来自players_websites集合的外键。这就是 OGM 如何使用IN_ENTITY策略映射一对一的单向关联。

9781430257943_Fig05-40.jpg

图 5-40 。一对一单向关联

此外,通过修改Websites实体,添加@OneToOne注解和mappedBy元素,我可以很容易地将这种关联转换成双向关联:

import javax.persistence.OneToOne;
...

@Entity
@Table(name = "players_websites")
public class Websites implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String http_address;
    @OneToOne(mappedBy = "website")
    private Players player_website;

    //constructors, getters and setters
...
}

这次,atp_playersplayers_websites系列如图图 5-41 所示。如您所见,关联的所有者atp_players仍然包含存储外键的字段,而非所有者方players_websites将外键存储在嵌入式集合中。

9781430257943_Fig05-41.jpg

图 5-41 。一对一双向关联

我的下一个目标是创建一个从可嵌入类到另一个实体的一对一关联。为此,我需要一个存储一些玩家细节的可嵌入类和一个存储更多细节的实体。可嵌入的类将为这个实体定义一个一对一的关联。下面是可嵌入的类,它被命名为Details:

import javax.persistence.Embeddable;
import javax.persistence.OneToOne;
...

@Embeddable
@Table(name = "player_details")
public class Details implements Serializable {

    private String birthplace;
    private String residence;
    private String height;
    private String weight;
    private String plays;
    private int turnedpro;
    private String coach;
    @OneToOne(cascade={CascadeType.PERSIST, CascadeType.REMOVE})
    private MoreDetails more;

    //constructors, getters and setters
...
}

MoreDetails字段引用以下实体:

import java.io.Serializable;
...

@Entity
@Table(name = "player_more_details")
public class MoreDetails implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private int ranking;
    private String prizes;
   //constructors, getters and setters
...
}

最后一步是在Players实体中添加可嵌入的类:

import javax.persistence.Embedded;

@Entity
@Table(name = "atp_players")
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    ...
    @Embedded
    private Details details;

    //constructors, getters and setters
...
}

现在,MongoDB 将揭示两个集合,atp_playersplayer_more_details,如图图 5-42 所示。注意,用于存储可嵌入类的atp_players嵌套文档(details字段)包含一个名为more_id的字段,该字段存储引用player_more_details文档的外键。

9781430257943_Fig05-42.jpg

图 5-42 。一对一关联和可嵌入类

我对存储、检索和删除一些Players实例的一对一关联做了一些尝试。在图 5-43 中,您可以看到一个简单场景产生的 GlassFish 日志消息示例:插入一个播放器,列出它,删除它,然后再次列出它。(注意持久化和移除的级联效应)。

9781430257943_Fig05-43.jpg

图 5-43 。测试一对一关联(持久化、检索、列出和删除)

演示@OneToOne注解的完整应用可以在 Apress 存储库中获得,并被命名为HOGM _ 蒙戈 DB_OneToOne 。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@OneToMany 和@ManyToOne 注解

javax.persistence.OneToManyjavax.persistence.ManyToOne标注映射

正式文件:http://docs.oracle.com/javaee/6/api/javax/persistence/OneToMany.html

http://docs.oracle.com/javaee/6/api/javax/persistence/ManyToOne.html

简要概述

在关系数据库术语中,当一个表中的每条记录对应于相关表中的多条记录时,就会发生一对多关联。这两个表包含的记录数量不同,第一个表中的每一行都链接到第二个表中的更多行。JPA 使用@OneToMany注解来映射这种关联。

当第二个表中的行与第一个表反向关联时,这是一个双向关联,由@ManyToOne注解表示。在双向关联中,mappedBy元素必须用于指定关联字段或关联所有者实体的属性。

在可嵌入类中,@OneToMany@ManyToOne都可以用来指定与实体集合的关联,或者指定从可嵌入类到实体类的关联。

这样的关联支持抓取(急切或懒惰)、级联和孤儿移除(只在@OneToMany上,不在@ManyToOne上)。

OGM 支持

Hibernate OGM 支持@OneToMany@ManyToOne注解。如您所知,默认情况下,OGM 使用IN_ENTITY策略在 MongoDB 中存储数据,这并不意味着任何额外的集合——每个实体类都由一个集合表示。我们可以很容易地区分以下几种情况:

  • 对于单向的一对多关联,OGM 将关联的导航信息存储在代表关联所有者的集合中,存储在存储嵌入集合中的外键的字段中。参见图 5-44 。

9781430257943_Fig05-44.jpg

图 5-44 。IN_ENTITY:一对多单向关联

  • 对于单向的多对一关联,OGM 将导航信息存储在代表关联所有者方的集合中;每个文档将包含一个用于存储相应外键的字段。参见图 5-45 。

9781430257943_Fig05-45.jpg

图 5-45 。IN_ENTITY:多对一单向关联

  • 对于双向的一对多关联,导航信息是这样存储的:表示使用mappedBy的实体(关联的非所有者方)的集合将包含存储嵌入集合中的外键的字段。代表关联所有者方的集合将在每个文档中包含一个存储相应外键的字段。参见图 5-46 。

9781430257943_Fig05-46.jpg

图 5-46 。IN_ENTITY:一对多双向关联

对于GLOBAL_COLLECTION策略 ,也有一些直截了当的情况:

  • 对于单向的一对多关联,OGM 将关联的导航信息存储在名为Associations的全局集合中。表示关联所有者的集合不包含任何导航信息。参见图 5-47 。

9781430257943_Fig05-47.jpg

图 5-47 。GLOBAL _ COLLECION:一对多单向关联

  • 对于单向的多对一关联,GLOBAL_COLLECTION没有任何作用。参见图 5-48 。

9781430257943_Fig05-48.jpg

图 5-48 。GLOBAL_COLLECTION:一对多双向关联

  • 对于双向一对多关联,导航信息是这样存储的:表示使用mappedBy的实体(非拥有@OneToMany实体)的集合将不包含导航信息。这些信息现在保存在Associations收藏中。另一方(所有者)将在每个文档中包含一个存储相应外键的字段。

对于COLLECTION策略 ,我们有:

  • 对于单向的一对多关联,OGM 将关联的导航信息存储在以单词associations为前缀的新集合中。表示关联所有者的集合不包含导航信息。参见图 5-49 。

9781430257943_Fig05-49.jpg

图 5-49 。集合:一对多单向关联

  • 对于单向的多对一关联,COLLECTION没有任何作用
  • 对于双向一对多关联,导航信息是这样存储的:表示使用mappedBy的实体(非拥有@ OneToMany实体)的集合不包含导航信息。这些信息被存储在一个以associations为前缀的新集合中。另一方(所有者)将在每个文档中包含一个存储相应外键的字段。参见图 5-50 。

9781430257943_Fig05-50.jpg

图 5-50 。集合:一对多双向关联

对于这些关联的主要方面,有对单向和双向关联的支持,指定用于加入实体关联或元素集合(@JoinColumn)的列的能力,对从一个可嵌入类到另一个实体或实体集合的一对多/多对一关联的支持,@JoinTable@JoinColumnsGLOBAL_COLLECTIONCOLLECTION策略,级联(全部),孤立移除,以及通过延迟加载进行提取。

例子

作为一个一对多关联(单向和双向)的例子,我需要两个逻辑上适合这个目的的实体。当我们存储球员和他的照片时,一个拥有很多照片的网球运动员可以作为一对多关联的一个很好的测试案例。照片可以映射到Photos实体中,就像这样:

import java.io.Serializable;
...

@Entity
@Table(name = "players_photos")
public class Photos implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String photo;

    //constructors, getters and setters
...
}

现在,每个玩家都有一个Photos的集合,所以Players实体应该定义一个@OneToMany关联,就像这样:

import javax.persistence.CascadeType;
import javax.persistence.OneToMany;
...

@Entity
@Table(name = "atp_players")
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
     ...

    @OneToMany(cascade=CascadeType.ALL)
    private Collection<Photos> photos;

    //constructors,getters and setters
...
}

坚持几个玩家和他们的照片,得到类似于图 5-51 所示的东西。注意,atp_players集合中的每个文档都包含一个名为photos的字段,该字段存储(在嵌套集合中)来自players_photos集合的相应外键。这就是 OGM 如何使用IN_ENTITY策略映射一对多单向关联。

9781430257943_Fig05-51.jpg

图 5-51 。单向一对多关联

因为我使用了泛型来指定元素类型,所以没有指定相关的目标实体类型。当不使用泛型时,我需要使用targetEntity元素指定目标实体类。例如,我可以重新定义@OneToMany关联,如下所示:

...
@OneToMany(targetEntity=hogm.mongodb.entity.Photos.class, cascade=CascadeType.ALL)
    private Collection photos;
...

如果从相反的方向去思考关联,很多照片属于同一个玩家,描述的是一种单向的多对一的关联。实现这样的关联意味着我们像这样编写实体:

import java.io.Serializable;
...

@Entity
@Table(name = "atp_players")
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    @Column(name = "player_name")
    private String name;
    @Column(name = "player_surname")
    private String surname;
    @Column(name = "player_age")
    private int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    @Column(name = "player_birth")
    private Date birth;

   //constructors, getters and setters
...
}

此外,Photos实体必须定义一个@ManyToOne字段(或属性),如下所示:

import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
...

@Entity
@Table(name = "players_photos")
public class Photos implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String photo;
    @ManyToOne
    @JoinColumn(name = "player_fk", unique = true, nullable = false, updatable = false)
    private Players player_photos;

   //constructors, getters and setters
...
}

坚持几个玩家和他们的照片,得到类似图 5-52 所示的东西。注意,players_photos集合中的每个文档都包含一个名为player_pk的字段,该字段存储来自atp_players集合的相应外键。这就是 OGM 如何使用IN_ENTITY 策略映射多对一单向关联。

9781430257943_Fig05-52.jpg

图 5-52 。单向多对一关联

我可以通过调整Players实体(Photos不变),轻松地将单向的一对多、多对一关联变成双向的。我需要指定作为关系所有者的实体的关联字段。因此,在Players实体中,我做了如下调整:

...
@OneToMany(cascade=CascadeType.ALL,mappedBy = "player_photos")
    private Collection<Photos> photos;
...

这次,atp_playersplayers_photos系列如图图 5-53 所示。

9781430257943_Fig05-53.jpg

图 5-53 。双向一对多关联

最后,我用这些关联来存储、检索和删除一些Players实例。在图 5-54 中,你可以看到一个简单场景下的 GlassFish 日志消息示例:插入一个播放器,列出它,删除它,然后再次列出它。(请注意“保留”和“删除”的级联效应。)

9781430257943_Fig05-54.jpg

图 5-54 。测试一对多关联

演示@OneToMany/@ManyToOne注解的完整应用可以在 Apress 存储库中获得,命名为HOGM_MONGODB_OneToMany。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

@ManyToManyAnnotation

javax.persistence.ManyToMany注解映射。

正式文件:http://docs.oracle.com/javaee/6/api/javax/persistence/ManyToMany.html

简要概述

在关系数据库术语中,当一个表中的许多记录都对应于相关表中的许多记录时,就会发生多对多关联。JPA 使用@ManyToMany注解来映射这种关联。

当第二个表中的行与第一个表反向关联时,这是双向关联。在双向多对多关联中,关系模型通常使用三个表,两个表用于数据,另一个表称为连接表,它保存由两个字段组成的组合键:两个外键字段引用第一个和第二个表的主键。同一对外键只能出现一次。在 JPA 中,可以在拥有方使用@JoinTable注解来指定连接表,拥有方可以是任何一方。

实际上,在 JPA 中,@ManyToMany@OneToMany的主要区别在于,@ManyToMany总是利用这个中间关系连接表来存储关联,而@OneToMany可以使用一个连接表或者引用源对象表主键的目标对象表中的外键。非拥有方(可以是双方中的任何一方)应该使用mappedBy元素来指定拥有方的关联字段或属性。从技术上来说,如果您只从拥有方添加或删除,那么mappedBy将保持数据库正确更新,但是这会导致一些问题,比如必须从应用代码中删除的孤儿(没有链接的记录)。如果没有mappedBy,连接表中可能会出现重复的记录,因为您将有两个不同的关联。在双向多对多关联中,建议您从两端添加数据。

@ManyToMany可以在一个可嵌入的类中用来指定一个实体集合的关联。这种关联支持抓取(急切的或懒惰的)和级联,但不支持孤立移除,这种移除只允许在源端具有单一基数的关联。

OGM 支持

Hibernate OGM 支持@ManyToMany注解。如您所知,默认情况下,OGM 使用IN_ENTITY策略在 MongoDB 中存储数据,这并不意味着任何额外的集合,只是实体集合。对于单向的多对多关联,OGM 将关联的导航信息存储在所有者集合中,存储在嵌入集合中存储外键的字段中。如果关联是双向的,那么双方都将包含用于存储相应导航信息(外键)的嵌入式集合。对于GLOBAL_COLLECTIONCOLLECTION策略,将使用第三个集合,如第二章中的“关联存储”一节所述。“在COLLECTION策略的情况下,如果没有指定mappedBy,则假设有两个差异关联,您将获得两个连接集合(每个关联一个)。

这些关联的主要方面包括支持单向和双向关联,能够指定一个列来加入一个实体关联或元素集合(@JoinColumn,支持从一个可嵌入类到另一个实体集合的一对多/多对一关联,@JoinTable@JoinColumnsGLOBAL_COLLECTIONCOLLECTION策略,以及级联(all)。此外,OGM 支持延迟加载抓取。

例子

为了演示多对多的关联,我需要两个逻辑上适合这个目的的实体。例如,一名网球运动员可能会参加几场比赛,而每场比赛都包含几名运动员。当我们存储球员、比赛和关联时,这可以是多对多关联的一个很好的测试用例。首先,让我们假设只有玩家知道锦标赛。换句话说,让我们实现一个单向的多对多关联。

为此,Players实体必须定义一个@ManyToMany关联,如下所示:

import javax.persistence.ManyToMany;
...

@Entity
@Table(name = "atp_players")
public class Players implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    @Column(name = "player_name")
    private String name;
    @Column(name = "player_surname")
    private String surname;
    @Column(name = "player_age")
    private int age;
    @Temporal(javax.persistence.TemporalType.DATE)
    @Column(name = "player_birth")
    private Date birth;
    @ManyToMany(cascade = CascadeType.PERSIST)
    Collection<Tournaments> tournaments;

    //constructors, getters and setters
...
}

Tournaments实体非常简单:

import java.io.Serializable;
...

@Entity
@Table(name = "atp_tournaments")
public class Tournaments implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String tournament;

    //constructors, getters and setters
...
}

坚持几个玩家和比赛,并定义一些从玩家到比赛的链接,得到类似于图 5-55 所示的东西。注意,atp_players集合中的每个文档都包含一个名为tournaments的字段,该字段存储(在嵌套集合中)来自atp_tournaments集合的相应外键。这就是 OGM 如何使用IN_ENTITY策略映射多对多单向关联。

9781430257943_Fig05-55.jpg

图 5-55 。单向多对多关联

通过将@ ManyToMany注解从Players实体翻译到Tournaments实体,并为Tournaments提供Players,而不是为Players提供Tournaments,可以从Tournaments角度定义相同类型的关联。

您可以轻松地将这种单向的多对多关联转换为双向关联。当Players实体保持不变时,Tournaments实体应该修改如下:

import javax.persistence.ManyToMany;
...

@Entity
@Table(name = "atp_tournaments")
public class Tournaments implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String tournament;
    @ManyToMany(mappedBy = "tournaments")
    Collection<Players> players;

     //constructors, getters and setters
...
}

现在,MongoDB 将在两个实体集合atp_playersatp_tournaments中包含嵌套集合。每个嵌套集合将存储另一端的外键。参见图 5-56 。

9781430257943_Fig05-56.jpg

图 5-56 。双向多对多关联

注意,在前面的例子中,我使用了泛型,所以我没有指定相关的目标实体类型。当不使用泛型时,您需要使用targetEntity元素指定目标实体类。例如,我可以这样重新定义@ManyToMany关联:

...
//in Players entity
@ManyToMany(targetEntity = hogm.mongodb.entity.Tournaments.class,cascade = CascadeType.PERSIST)
    Collection tournaments;
...
...
//in Tournaments entity
@ManyToMany(targetEntity = hogm.mongodb.entity.Players.class, mappedBy = "tournaments")
    Collection players;
...

当首选GLOBAL_COLLECTIONCOLLECTION策略时,我可以在关联的拥有方使用@JoinTable(包括@JoinColumn)来表示关联表和列的名称。对于GLOBAL_COLLECTION,我可以用:

...
@ManyToMany(targetEntity = hogm.mongodb.entity.Tournaments.class,
                          cascade = CascadeType.PERSIST)
    @JoinTable(name = "PLAYERS_AND_TOURNAMENTS", joinColumns =
    @JoinColumn(name = "PLAYER_ID", referencedColumnName = "id"),
    inverseJoinColumns =
    @JoinColumn(name = "TOURNAMENT_ID", referencedColumnName = "id"))
    Collection tournaments;
...

结果如图图 5-57 所示。

9781430257943_Fig05-57.jpg

图 5-57 。GLOBAL_COLLECTION 和@JoinTable

对于COLLECTION,结果如图 5-58 所示。

9781430257943_Fig05-58.jpg

图 5-58 。集合和@JoinTable

最后,我用这些关联来存储、检索和删除一些PlayersTournaments实例。您可以通过从 Apress 库下载整个应用来测试它;这是HOGM_MONGODB_ManyToMany应用(注意,该应用不提供孤儿移除)。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。

不支持的 JPA 2.0 注解

根据 Hibernate OGM Beta 4.0.0Beta 2 文档,以下内容不受支持:

  • 传承策略:@Inheritance也不@DiscriminatorColumn
  • 二级表:@SecondaryTables@SecondaryTable
  • 命名查询
  • 本地查询

摘要

在本章中,您看到了 Hibernate OGM 如何实现 JPA 2.0 注解来处理 MongoDB 存储。我讨论了主要的 JPA 2.0 注解,并重点讨论了受支持的注解:

  • @Entity
  • @Id
  • @EmbeddedId
  • @IdClass
  • @Table
  • @Column
  • @Temporal
  • @Transient
  • @Embedded and @Embeddable
  • @Enumerated
  • @Cacheable
  • @MappedSuperclass
  • @ElementCollection
  • @EntityListeners, @ExcludeDefaultListeners, @ExcludeSuperclassListeners
  • @Version
  • @Access
  • @OneToOne, @OneToMany, @ManyToOne, @ManyToMany

不支持的注解列表很短,在下一个版本中可能会减少到零。

六、Hibernate OGM 查询 MongoDB

在前面的章节中,我们完成了几个任务,以便在 NoSQL MongoDB 存储中组织和存储我们的数据。现在,我们将通过应用不同的查询技术来利用这些数据,从 NoSQL MongoDB 商店中只提取我们需要的信息。

正如我在第一章中提到的,查询 NoSQL 数据库是一项微妙而复杂的任务——根据对 NoSQL 查询的本地支持,有不同的情况和不同的方法。对于 MongoDB,有许多查询选项;根据查询的复杂性、性能参数等,您可以选择满足自己需求的方法:

  • 原生查询技术,这意味着使用 MongoDB 驱动程序查询功能,而不涉及 Hibernate OGM 或任何其他技术。
  • Hibernate ORM/OGM for CRUD ,其中创建/读取/更新/删除操作由 Hibernate ORM 引擎实现。
  • hibernate Search/Apache Lucene,它使用了全文索引和查询引擎(Apache Lucene)。Hibernate Search 是一个强大的查询机制,具有很好的性能和功能,并且提供了一个非常易用的到 Lucene 的桥梁。对于复杂的查询和索引支持,这是正确的选择。
  • Hibernate OGM JP-QL 解析器,使用 Hibernate Search 从 MongoDB 存储中检索所需的信息,适合简单的查询。这个 JP-QL 解析器还处于起步阶段,所以它需要时间变得强大并支持复杂的查询。
  • 其他工具,比如 DataNucleus、Morphia 等等,不会在本书中涉及。

image 注意目前,Hibernate OGM 通过 Hibernate Native API 不提供对 Hibernate 标准的支持。此外,它没有通过 JPA 提供对本地和命名查询的支持。

我们将深入研究这些查询的可能性,并尝试看看它是如何工作的。我们将重点讨论 Hibernate OGM,并从这个角度讨论 MongoDB。然而,为了完整起见,我们将通过首先查看基本的 MongoDB 查询功能来开始关于查询 MongoDB 的旅程,并将 Hibernate OGM 的主题保留到本章的第二部分。通过这种方式,您将对查询 MongoDB 有一个全面的了解,并且能够更好地选择适合您需求的查询解决方案。

MongoDB 原生查询

您可能知道,MongoDB 本身通过mongo shell(一个为 MongoDB 提供数据库接口的完全交互式 JavaScript 环境)提供交互式支持,并通过 MongoDB 驱动程序(可用于多种编程语言,如 Java、Ruby 和 PHP)提供编程支持。在这一节中,我们将跳过 shell,专注于使用 MongoDB Java 驱动程序查询 MongoDB 存储。您将需要这个驱动程序的 2.8.0 版本,可以在www.docs.mongodb.org/manual/applications/drivers/下载这个名为mongo-java-driver-2.8.0.jar的 JAR。

在执行任何查询之前,您需要配置一个 MongoDB 连接并创建一个数据库,然后创建一个集合并用数据填充它。为此,请回到第四章中的“Java SE 和 Mongo DB——hello world 示例”一节一旦知道如何将文档连接到 MongoDB 存储并持久化,就可以执行查询了。

我们将创建一个名为players的集合,并尝试对其进行一些查询。每个文档都存储一些网球运动员的数据:姓名、年龄和出生日期(允许有重复的文档)。在用几个文档填充集合之后,可以从众所周知的“全选”查询开始。您可以使用find方法,该方法返回包含许多文档的光标。如您所见,迭代结果非常容易。这段代码使用find提取所有文档:

...
Mongo mongo = new Mongo("127.0.0.1", 27017);
DB db = mongo.getDB("players_db");
DBCollection dbCollection = db.getCollection("players");
...
System.out.println("Find all documents in collection:");
            try (DBCursor cursor = dbCollection.find() ) {
                while (cursor.hasNext()) {
                    System.out.println(cursor.next());
                }
            }
...

该查询的结果如图 6-1 所示。

9781430257943_Fig06-01.jpg

图 6-1 。玩家收藏的所有文件

image 注意你可以通过调用getCount方法来统计一个集合中有多少个文档,就像这样:dbCollection.getCount();

您可以使用findOne方法找到单个文档;该方法不返回光标。截取的代码是:

...
System.out.println("Find the first document in collection:");
            DBObject first = dbCollection.findOne() ;
            System.out.println(first);
...

结果将是来自players集合的第一个文档,如图图 6-2 所示。

9781430257943_Fig06-02.jpg

图 6-2 。提取玩家集合的第一个文件

您还可以执行条件查询。例如,我们可以使用find方法提取对应于球员拉斐尔·纳达尔的文档,就像这样:

...
System.out.println("Find Rafael Nadal documents:");
            BasicDBObject query = new BasicDBObject("name", "Nadal").append("surname", "Rafael");
            try (DBCursor cursor = dbCollection.find(query) ) {
                while (cursor.hasNext()) {
                    System.out.println(cursor.next());
                }
            }
...

结果如图 6-3 所示。

9781430257943_Fig06-03.jpg

图 6-3 。仅提取包含拉斐尔·纳达尔的文档

结合了$gt(大于)操作符的find方法允许你提取所有年龄大于 25 岁的玩家:

...
System.out.println("Find players with age > 25:");
            BasicDBObject  query = new BasicDBObject("age", new BasicDBObject("$gt", 25));
            try (DBCursor cursor = dbCollection.find(query) ) {
                while (cursor.hasNext()) {
                    System.out.println(cursor.next());
                }
            }
...

你可以在图 6-4 中看到结果。

9781430257943_Fig06-04.jpg

图 6-4 。仅提取时间超过 25 年的文档

结合了$lt(小于)操作符的find方法允许您提取所有年龄小于 28 岁的玩家:

...
System.out.println("Find players with age < 28:");
            BasicDBObject  query = new BasicDBObject("age", new BasicDBObject("$lt", 28));
            try (DBCursor cursor = dbCollection.find(query) ) {
                while (cursor.hasNext()) {
                    System.out.println(cursor.next());
                }
            }
...

结果如图 6-5 所示。

9781430257943_Fig06-05.jpg

图 6-5 。仅提取时间小于 28 年的文档

使用$gt$lt,或$gte(大于或等于)和$lte(小于或等于)运算符和find方法,可以提取值区间之内(或之外)的数据。例如,您可以获得所有 1982 年 1 月 1 日至 1985 年 12 月 31 日之间出生的玩家,如下所示:

...
System.out.println("JAVA - Find players with birthday between 1 January, 1982 - 31 December, 1985:");
            Calendar calendar_begin = GregorianCalendar.getInstance();
            calendar_begin.clear();
            calendar_begin.set(1982, Calendar.JANUARY, 1);
            Calendar calendar_end = GregorianCalendar.getInstance();
            calendar_end.clear();
            calendar_end.set(1985, Calendar.DECEMBER, 31);
            BasicDBObject query = new BasicDBObject("birth", new BasicDBObject("$gte",
                                  calendar_begin.getTime()).append("$lte", calendar_end.getTime()));
            try (DBCursor cursor = dbCollection.find(query) ) {
                while (cursor.hasNext()) {
                    System.out.println(cursor.next());
                }
            }
...

结果如图 6-6 所示:

9781430257943_Fig06-06.jpg

图 6-6 。仅提取出生日期在 1982 年 1 月 1 日至 1985 年 12 月 31 日之间的文档

如果您喜欢使用 Joda Time(Java 日期和时间类的替代,可从http://joda-time.sourceforge.net获得),您可以像这样编写查询:

System.out.println("JODA - Find players with birthday between 1 January, 1982 - 31 December, 1985:");
            DateTime joda_calendar_begin = new DateTime(1982, 1, 1, 0, 0);
            DateTime joda_calendar_end = new DateTime(1985, 12, 31, 0, 0);
            query = new BasicDBObject("birth", new BasicDBObject("$gte", joda_calendar_begin.toDate()).append("$lte", joda_calendar_end.toDate()));
            try (DBCursor cursor = dbCollection.find(query) ) {
                while (cursor.hasNext()) {
                    System.out.println(cursor.next());
                }
            }

您还可以使用$in操作符和find方法提取具有特定值的数据。例如,您可以获得年龄为 25、27 和 30 岁的所有玩家,如下所示:

...
System.out.println("Find players with ages: 25, 27, 30");
            List<Integer> list = new ArrayList<>();
            list.add(25);
            list.add(27);
            list.add(30);
            BasicDBObject  query = new BasicDBObject("age", new BasicDBObject("$in", list));
            try (DBCursor cursor = dbCollection.find(query) ) {
                while (cursor.hasNext()) {
                    System.out.println(cursor.next());
                }
            }
...

结果如图 6-7 所示。

9781430257943_Fig06-07.jpg

图 6-7 。仅提取年龄等于 25、27 或 30 岁的文档

当需要取反提取数据时,可以使用$ne(不等于)运算符和find方法。例如,您可以轻松获得所有年龄不等于 27 岁的玩家,如下所示:

...
System.out.println("Find players with ages different from: 27");
            BasicDBObject  query = new BasicDBObject("age", new BasicDBObject("$ne", 27));
            try (DBCursor cursor = dbCollection.find(query) ) {
                while (cursor.hasNext()) {
                    System.out.println(cursor.next());
                }
            }
...

结果如图 6-8 所示:

9781430257943_Fig06-08.jpg

图 6-8 。仅提取年龄不在 27 岁的文档

在前面的例子中,我们使用 MongoDB Java 驱动程序从 MongoDB 创建(插入)和检索(读取)数据。您可以通过调用save方法来完成更新。例如,你可以用拉斐尔·纳达尔·帕雷拉代替拉斐尔·纳达尔,就像这样:

...
            System.out.println("UPDATING ...");
            BasicDBObject  query = new BasicDBObject("name", "Nadal").append("surname", "Rafael");
            try (DBCursor cursor = dbCollection.find(query)) {
                while (cursor.hasNext()) {
                    DBObject item = cursor.next();
                    item.put("name", "Nadal Parera");
                    dbCollection.save(item);
                }
            }
...

并且可以通过调用remove方法来删除数据。例如,您可以删除罗杰·费德勒的所有出现,如下所示:

...
            System.out.println("DELETING ...");
           BasicDBObject  query = new BasicDBObject("name", "Federer").append("surname", "Roger");
            try (DBCursor cursor = dbCollection.find(query)) {
                while (cursor.hasNext()) {
                    DBObject item = cursor.next();
                    dbCollection.remove(item);
                }
            }
...

image 注意关于使用 MongoDB 驱动程序的高级查询,请参见 Eelco Plugge、Tim Hawkins 和 Peter Membrey 编写的MongoDB 权威指南(a press,2010)。参观www.apress.com/9781430230519

包含前面代码片段的完整应用可以在 Apress 存储库中获得,并被命名为MONGODB_QUERY。它是一个 NetBeans 项目,在 Java 7 下进行了测试。

Hibernate OGM 和 CRUD 操作

针对 NoSQL 数据库执行的四个基本操作——创建、读取、更新和删除——在 Hibernate OGM 中现成可用。实际上,独立于 JPA 或 Hibernate 原生 API,Hibernate ORM 将持久性和加载查询委托给 OGM 引擎,OGM 引擎将 CRUD 操作委托给DatastoreProviderGridDialect,,它们与 NoSQL 存储交互。

在第三章和第四章中,您看到了如何通过 Hibernate Native API 和 Java Persistence API 开发基于 Hibernate OGM 的应用。因此,将清单 6-1 中的Players实体包装成这样一个应用应该是小菜一碟。

清单 6-1 。 玩家实体

package hogm.hnapi.entity;

import java.io.Serializable;
...

@Entity
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {

    @Id
    @GeneratedValue(generator = "mongodb_uuidgg")
    private String id;
    @Column(name = "player_name")
    private String name;
    @Column(name = "player_surname")
    private String surname;
    @Column(name = "player_age")
    private int age;
    @Column(name = "player_birth")
    @Temporal(javax.persistence.TemporalType.DATE)
    private Date birth;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

完成后,您就可以访问 CRUD 操作了。假设我们有一个名为playerPlayers实例

.

通过 Hibernate 本地 API 使用 Hibernate OGM,您可以用getCurrentSessionopenSession方法获得 Hibernate 会话。

  • 要持久化player实例,使用persist方法:

    HibernateUtil.getSessionFactory().getCurrentSession().persist(player);
    
  • 要更新player实例,使用merge方法:

    HibernateUtil.getSessionFactory().getCurrentSession().merge(player);
    
  • 通过id找到player实例,使用find方法:

    HibernateUtil.getSessionFactory().getCurrentSession().get(Players.class, *id* );
    
  • 要删除player实例,使用delete方法:

    HibernateUtil.getSessionFactory().getCurrentSession().delete(player);
    

您可以在一个名为HOGM_MONGODB_HNAPI_CRUD的示例应用中尝试所有这些方法,该应用可以在 Apress 存储库中找到。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。界面应用看起来像图 6-9 。

9781430257943_Fig06-09.jpg

图 6-9 。测试 Hibernate OGM 和 CRUD 操作

通过 Java 持久性 API ( em代表EntityManager)使用 Hibernate OGM:

  • 要持久化player实例,使用persist方法:

    em.persist(player);
    
  • 要更新player实例,使用merge方法:

    em.merge(player);
    
  • 通过id找到player实例,使用find方法:

    em.find(Players.class, *id* );
    
  • 要删除player实例,使用delete方法:

    em.delete (player);
    

您可以在一个名为HOGM_MONGODB_JPA_CRUD的示例应用中尝试所有这些方法,该应用可以在 Apress 存储库中找到。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。界面应用类似于图 6-9 中的。

Hibernate 搜索和 Apache Lucene

基本上,Hibernate/JPA 和 Apache Lucene 处理相同的领域——查询数据。它们都提供 CRUD 操作、基本数据单元(Hibernate 中的实体,Lucene 中的文档)和相同的编程概念。主要区别在于 Hibernate/JPA 提倡面向领域模型的编程,而 Lucene 只处理单一的内置数据模型——Document类,它太简单了,无法描述复杂的关联。然而,这两者结合起来就产生了一个更高级的 API,叫做 Hibernate Search。

Hibernate Search 和 Apache Lucene 都是强大、健壮的技术。虽然 Apache Lucene 是一个具有出色查询性能的全文索引和查询引擎,但是 Hibernate Search 将它的强大功能带到了持久性域模型中。共生关系运行得相当好:Hibernate Search“挤压”了 Apache Lucene 的查询能力,同时提供了对域模型、数据库和索引同步的支持,并将自由文本查询转换回托管对象。因为我们的重点是 Hibernate OGM 和 MongoDB,所以我不会提供 Hibernate 搜索或 Apache Lucene 教程。相反,我们将快速开发示例,我将为您提供足够的信息来理解新的 Hibernate Search/Apache Lucene 注释和类,而不会深入细节。我们将把 Hibernate ORM、OGM 和 Search 与 Apache Lucene 和 MongoDB 结合到具有查询功能的应用中,以便您可以探索查询过程的复杂性。一旦你有了一个功能性的应用,你将能够尝试各种各样的查询。

我们将开发两个应用。第一个是 Hibernate OGM/via Hibernate Native API 应用,第二个是 Hibernate OGM via JPA(详见第三章和第四章)。这两个应用将遵循一个共同的、简单的场景:我们将创建一个实体(和相应的 POJO,只针对 Hibernate Native API),将几个实例持久化到一个 MongoDB 集合中,并通过 Hibernate Search 和 Apache Lucene 执行一些查询示例。

POJO 被命名为Players,如清单 6-2 所示(这个 POJO 被映射到一个hbm.xml文件中)。

清单 6-2 。 玩家类

public class Players {

    private String id;
    private String name;
    private String surname;
    private int age;
    private Date birth;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}
And thePlayers.hbm.xml fileis shown in Listing 6-3.

清单 6-3 。players . hbm . XML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" " http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd ">
<hibernate-mapping>
    <class name="hogm.hnapi.pojo.Players" table="atp_players">
        <id name="id" type="string">
            <column name="id" />
            <generator class="uuid2" />
        </id>
        <property name="name" type="string">
            <column name="player_name"/>
        </property>
        <property name="surname" type="string">
            <column name="player_surname"/>
        </property>
        <property name="age" type="int">
            <column name="player_age"/>
        </property>
        <property name="birth" type="date">
            <column name="player_birth"/>
        </property>
    </class>
</hibernate-mapping>

或者,如果你喜欢实体版本,POJO 就变成清单 6-4 中所示的样子。(两个应用中都使用了该实体。)

清单 6-4 。 实体版玩家

import java.io.Serializable;
...

@Entity
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {

    @Id
    @GeneratedValue(generator = "mongodb_uuidgg")
    private String id;
    @Column(name = "player_name")
    private String name;
    @Column(name = "player_surname")
    private String surname;
    @Column(name = "player_age")
    private int age;
    @Column(name = "player_birth")
    @Temporal(javax.persistence.TemporalType.DATE)
    private Date birth;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

常见步骤

无论是哪种应用类型(通过 Hibernate Native API 的 OGM 还是通过 JPA ),都有一些添加 Hibernate Search 或 Apache Lucene 支持的通用步骤:

  1. 除了 Hibernate OGM 和 MongoDB 库(记得是从第一章开始的),我们至少还需要再添加两个 jar:hibernate-search-orm-4.2.0.Final.jaravro-1.6.3.jar。这两个版本都可以在 Hibernate Search 发行版 4.2.0 最终版中获得。注意,许多其他 jar,包括 Apache Lucene 和 Object/Lucene core mapper,都可以在 Hibernate OGM 和 MongoDB 库中找到。
  2. 接下来,我们需要关注我们的 POJO(或实体)类。这是将 Hibernate Search 引入等式的第一步——特定于 Hibernate Search 的配置通过注释来表达。更准确地说,我们需要使用一些注释来映射 POJO(实体)。
  • 我们将使用@Indexed注释将Players类标记为可索引的(可搜索的)。没有用@Indexed标注的实体将被索引过程忽略。

  • 然后,我们指定如何在字段或属性级别使用@Field注释进行索引。有一些受支持的属性,但是现在,它足以表明字段或属性是否被索引(使用index属性);是否分析字段或属性(使用analyze属性);以及字段或属性是否存储在 Lucene 索引中(使用store属性)。官方文档中提供了更多属性和详细描述。

  • 因为我们有一个Date字段,所以我们需要知道一些关于 Hibernate Search 如何处理日期的事情。日期以 GMT 时间(200611072203012,2006 年 11 月 7 日下午 4:03 点和美国东部时间 12 点)存储为“yyyyyymmdhhmmssss”,”但是我们可以使用@DateBridge注释指定在索引中存储日期的适当分辨率(分辨率可以是DAYHOURYEARMINUTESECONDMONTHMILISECOND)。我们采用YEAR分辨率。

  • 对于数值字段,比如player年龄,我们可以使用@NumericField注释。这是可选的,但是对于支持高效的范围查询、排序以及加速查询来说,这是非常有用的。

  • 最后,为了将一个字段或属性表示为文档 id(主键),我们需要用@DocumentId对其进行注释。对于已经包含一个@Id注释的实体,该注释是可选的。

  • For our needs, the @Indexed, @Field, @NumericField, @DateBridge and @DocumentId annotations are enough to configure the indexing process. Listing 6-5 shows the Players POJO after it has been marked with the Hibernate Search annotations.

    清单 6-5 。 玩家 POJO 带注释

    package hogm.hnapi.pojo;
    
    import org.hibernate.search.annotations.Analyze;
    import org.hibernate.search.annotations.DateBridge;
    import org.hibernate.search.annotations.DocumentId;
    import org.hibernate.search.annotations.Field;
    import org.hibernate.search.annotations.Index;
    import org.hibernate.search.annotations.Indexed;
    import org.hibernate.search.annotations.NumericField;
    import org.hibernate.search.annotations.Resolution;
    import org.hibernate.search.annotations.Store;
    ...
    
    @Indexed
    public class Players {
    
        @DocumentId
        private String id;
        @Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
        private String name;
        @Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
        private String surname;
        @NumericField
        @Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
        private int age;
        @Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
        @DateBridge(resolution = Resolution.YEAR)
        private Date birth;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getSurname() {
            return surname;
        }
    
        public void setSurname(String surname) {
            this.surname = surname;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public Date getBirth() {
            return birth;
        }
    
        public void setBirth(Date birth) {
            this.birth = birth;
        }
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    }
    

或者,如果我们将这些注释应用于Players实体,我们会得到清单 6-6 中所示的内容。

清单 6-6 。 带注释的实体版玩家

package hogm.hnapi.entity;

import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.NumericField;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;
...

@Entity
@Indexed
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {

    private static final long serialVersionUID = 1L;
    @DocumentId
    @Id
    @GeneratedValue(generator = "mongodb_uuidgg")
    private String id;
    @Column(name = "player_name")
    @Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
    private String name;
    @Column(name = "player_surname")
    @Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
    private String surname;
    @Column(name = "player_age")
    @NumericField
    @Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
    private int age;
    @Column(name = "player_birth")
    @Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
    @DateBridge(resolution = Resolution.YEAR)
    @Temporal(javax.persistence.TemporalType.DATE)
    private Date birth;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}
  • 3.接下来,我们必须通过 Hibernate 本地 API 应用在hibernate.cfg.xml(或者在HibernateUtil)中为 OGM 提供一些基本配置信息,或者通过 JPA 在persistence.xml中为 OGM 提供一些基本配置信息。

我们必须指定目录提供者;对于 Apache Lucene,目录代表存储索引文件的类型和位置,它与文件系统(FSDirectoryProvider)和内存实现(RAMDirectoryProvider)捆绑在一起,尽管它也支持定制实现。Hibernate Search 负责 Lucene 资源的配置和初始化,包括通过DirectoryProviders的目录。我们想要容易地访问索引文件(能够用外部工具,比如 Luke,物理地检查索引),所以我们将通过设置hibernate.search.default.directory_provider属性为filesystem来使用文件系统存储它们。除了目录提供者之外,我们还必须通过hibernate.search.default.indexBase属性为所有索引指定默认的基本目录。最后,我们可以通过将hibernate.search.default.locking_strategy属性设置为single;来指定锁定策略(在本例中是文件系统级的锁)。这是一个保存在内存中的 Java 对象锁。通过 Hibernate Native API 将这些配置添加到 OGM 的hibernate.cfg.xml(或HibernateUtil)中,或通过 JPA 将这些配置添加到 OGM 的persistence.xml中,如下所示:

//in hibernate.cfg.xml
<property name="hibernate.search.default.directory_provider">filesystem</property>
<property name="hibernate.search.default.indexBase">./Indexes</property>
<property name="hibernate.search.default.locking_strategy">single</property>...

或者:

//in HibernateUtil
OgmConfiguration cfgogm = new OgmConfiguration();
...
cfgogm.setProperty("hibernate.search.default.directory_provider","filesystem");
cfgogm.setProperty("hibernate.search.default.indexBase","./Indexes");
cfgogm.setProperty("hibernate.search.default.locking_strategy", "single");
...

或者:

...
//in persistence.xml
<property name="hibernate.search.default.directory_provider" value="filesystem"/>
<property name="hibernate.search.default.indexBase" value="./Indexes"/>
<property name="hibernate.search.default.locking_strategy" value="single"/>
...

最后,一切都配置好了,我们准备开始编写 Lucene 查询。但是,从现在开始,代码将特定于两个应用中的每一个。所以让我们从 OGM via Hibernate Native API 应用开始。

Hibernate 搜索/Apache Lucene 查询——通过本地 API 进行 OGM

第一个目标是编写一个“select all”查询,通过本地 API 应用帮助您熟悉 OGM 中的 Lucene 风格。遵循一步一步的方法,我们可以编写这样一个查询,就像这样:

  1. 创建一个org.hibernate.search.FullTextSession。这个接口将为 Hibernate 会话增加全文搜索和索引功能。这个会话提供了两种编写查询的方法:使用 Hibernate 搜索查询 DSL(域搜索语言)或本地 Lucene 查询。完成这项工作的代码是:

    FullTextSession fullTextSession =
    Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
    
  2. 创建一个org.hibernate.search.query.dsl.QueryBuilder and使用新的会话获得一个查询构建器,帮助简化查询定义。注意,我们指出我们的查询只影响Players类:

    QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
        buildQueryBuilder().forEntity(Players.class).get();
    
  3. 创建一个 Lucene 查询。正如您将在官方文档中看到的,有几种方法可以使用QueryBuilder构建 Lucene 查询。对于这个例子,我们可以使用queryBuilder.all方法,这是一种获取整个文档的简单方法:

    org.apache.lucene.search.Query query = queryBuilder.all().createQuery();
    
  4. 定义排序规则(可选)。我们可以使用 Lucene 的排序功能轻松定义一个排序规则。例如,我们可能需要按照名字对提取的球员进行排序:

    org.apache.lucene.search.Sort sort = new Sort(new SortField("name", SortField.STRING));
    
  5. 将 Lucene 查询包装在一个org.hibernate.FullTextQuery中。为了配置排序规则和执行查询,我们需要将 Lucene 查询包装成一个FullTextQuery,就像这样:

    FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
    
  6. 指定对象查找方法和数据库检索方法。对于 OGM,您必须指定对象查找和数据库检索方法(SKIP指定不检查对象是否已经存在于二级缓存或持久性上下文中;FIND_BY_ID按标识符逐一加载每个对象):

    fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID);
    
  7. 设置排序规则。您可以通过调用setSort方法:

    fullTextQuery.setSort(sort);
    

    来设置排序规则

  8. 执行查询。最后,我们执行查询并在java.util.List :

    List<Players> results = fullTextQuery.list();
    

    中获得结果

  9. 或者,清除会话:

    fullTextSession.clear();
    

我们可以将这九个步骤放在一个名为selectAllAction的方法中,以创建我们的第一个 Hibernate Search/Lucene 查询。您可以在名为 SampleBean 的会话 bean 中找到这个方法,它在包hogm.hnapi.ejb shown in Listing 6-7.

清单 6-7 。??【选择动作法】

package hogm.hnapi.ejb;
...
public class SampleBean {
  ...
  public List<Players> selectAllAction() {

        log.info("Select all Players instance ...");

        FullTextSession fullTextSession =
Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
        QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
        org.apache.lucene.search.Query query = queryBuilder.all().createQuery();
        org.apache.lucene.search.Sort sort = new Sort(new SortField("name", SortField.STRING));

        FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);

        fullTextQuery.setSort(sort);
        List<Players> results = fullTextQuery.list();

        fullTextSession.clear();

        log.info("Search complete ...");

        return results;
    }
...
}

这九个步骤可以作为编写许多其他类型查询的快速指南。现在让我们看看如何编写一些常见的查询:

  • 选择所有 1987 年出生的玩家。这个查询(以及类似的查询)可以使用三种方法轻松编写:queryBuilder.keyword,这表示我们正在搜索一个特定的单词;TermContext.onField,指定在哪个 Lucene 字段中查找;还有TermMatchingContext.matching,它告诉我们要寻找什么。因此,将这个查询包装成一个名为selectByYearAction的方法,如清单 6-8 所示。

清单 6-8 。??【selectByYearAction】方法

package hogm.hnapi.ejb;
...
public class SampleBean {
  ...
public List<Players> selectByYearAction() {

        log.info("Search only Players instances 'born in 1987' ...");

        Calendar calendar = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"));
        calendar.clear();

        calendar.set(Calendar.YEAR, 1987);

        FullTextSession fullTextSession =
Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
        QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();

   org.apache.lucene.search.Query query =
queryBuilder.keyword().onField("birth").matching(calendar.getTime()).createQuery();

        FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
       fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);

        List<Players> results = fullTextQuery.list();

        fullTextSession.clear();

        log.info("Search complete ...");

        return results;
    }
}
  • 只选择一个叫拉斐尔·纳达尔的球员。这个查询(以及类似的查询)在两个不同的字段中搜索两个单词,“Rafael”和“Nadal”该查询在player_surname列(surname字段)中查找第一个单词,在player_name column (name字段中查找第二个单词。为此,您可以使用一个名为must.的聚合操作符(聚合操作符允许您将简单的查询组合成更复杂的查询。)将必要的代码包装到一个名为selectRafaelNadalAction的方法中就说明了这一点。bool方法表明我们已经创建了一个布尔查询——一个查找与其他查询的布尔组合相匹配的文档的查询。(参见清单 6-9 。)

清单 6-9 。??【selectRafaelNadalAction】方法

package hogm.hnapi.ejb;
...
public class SampleBean {
  ...
public List<Players> selectRafaelNadalAction() {

        log.info("Search only Players instances that have the name 'Nadal' and surname 'Rafael' ...");

        FullTextSession fullTextSession =
Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
        QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
        org.apache.lucene.search.Query query = queryBuilder.bool().must(queryBuilder.keyword()
       .onField("name").matching("Nadal").createQuery()).must(queryBuilder.keyword()
      .onField("surname").matching("Rafael").createQuery()).createQuery();

        FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);

        List<Players> results = fullTextQuery.list();
        fullTextSession.clear();

        log.info("Search complete ...");

        return results;
    }
}
  • 选择姓氏以字母‘j’开头的球员这个查询(以及类似的查询)可以使用通配符来编写。?代表单个字符,*代表任何字符序列。TermContext.wildcard方法表明后面是通配符查询。将必要的代码包装到名为selectJAction的方法中就说明了这一点。(参见清单 6-10 。)

清单 6-10 。?? 的选择方法

package hogm.hnapi.ejb;
...
public class SampleBean {
  ...
public List<Players> selectJAction() {

        log.info("Search only Players that surnames begins with 'J' ...");

        FullTextSession fullTextSession =
        Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
        QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
                                                       buildQueryBuilder().forEntity(Players.class).get();

        org.apache.lucene.search.Query query = queryBuilder.keyword().wildcard()
                                                  .onField("surname").matching("J*").createQuery();

         FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
         fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                                                  DatabaseRetrievalMethod.FIND_BY_ID);

        List<Players> results = fullTextQuery.list();
        fullTextSession.clear();

        log.info("Search complete ...");

        return results;
    }
}
  • 选择年龄在区间(25,28)的球员。这个查询(以及类似的查询)可以被视为范围查询。这种查询搜索区间中的值(包括或不包括边界),或者低于或高于区间边界的值(包括或不包括边界)。您通过调用QueryBuilder.range方法来指示一个范围查询。通过调用fromto方法来设置间隔,通过调用excludeLimit method可以排除间隔的边界。将必要的代码包装到一个名为select25To28AgeAction的方法中就会显示这一点。(参见清单 6-11 。)

清单 6-11 。?? 选择 25 到 28 岁的动作方法

package hogm.hnapi.ejb;
...
public class SampleBean {
  ...
public List<Players> select25To28AgeAction() {

        log.info("Search only Players that have ages between 25 and 28, excluding limits ...");

        FullTextSession fullTextSession =
                  Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
        QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
                                   buildQueryBuilder().forEntity(Players.class).get();

        org.apache.lucene.search.Query query = queryBuilder.range()
                                  .onField("age").from(25).to(28).excludeLimit().createQuery();

        FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                                                DatabaseRetrievalMethod.FIND_BY_ID);

        List<Players>results = fullTextQuery.list();
        fullTextSession.clear();

        log.info("Search complete ...");

        return results;
    }
}

image 注意正如你所看到的,你可以使用fromtoexcludeLimit方法简单地模拟一个范围。除此之外,Lucene 还提供了belowabove方法。在逻辑方法中使用它们,您可以获得众所周知的运算符" "(大于)、"< = "(小于或等于)和"> = "大于或等于)。

您可以编写许多其他类型的查询,您只需浏览更多关于 Hibernate Search 和 Apache Lucene 的文档。对于提到的查询,我开发了一个完整的应用,可以在 Apress 存储库中找到,并命名为HOGM_MONGODB_HNAPI_HS。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。图 6-10 显示了这种应用。

9781430257943_Fig06-10.jpg

图 6-10 。HOGM_MONGODB_HNAPI_HS 应用

image 注意您可以通过调用startAndWait方法:fullTextSession.createIndexer().startAndWait();来重建索引(删除它,然后从数据库中重新加载所有实体)

当您有关联(或嵌入对象)时,您需要提供更多的注释。关联对象(和嵌入对象)可以作为根实体索引的一部分进行索引。为此,该关联标有@IndexedEmbedded。当关联是双向时,另一方必须用@ContainedIn标注。这有助于 Hibernate Search 更新关联索引过程。

例如,我们假设Players实体与Tournaments实体是多对多的关联关系(每个玩家参加多个锦标赛,每个锦标赛包含多个玩家)。(请记住,POJOs 注释是在.hbm.xml文件中指定的。)带注释的 POJOs 如清单 6-12 中的和清单 6-13 中的所示。

清单 6-12 。PlayersPOJO

package hogm.hnapi.pojo

import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;
...

@Indexed
public class Players {

    @DocumentId
    private String id;
    @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
    private String name;
    @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
    private String surname;
    @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
    private int age;
    @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
    @DateBridge(resolution = Resolution.YEAR)
    private Date birth;
    @IndexedEmbedded
    Collection<Tournaments> tournaments = new ArrayList<Tournaments>(0);

    //getters and setters
...
}

清单 6-13 。 锦标赛 POJO

package hogm.hnapi.pojo

import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.ContainedIn;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;

@Indexed
public class Tournaments {

    @DocumentId
    private String id;
    @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
    private String tournament;
    @ContainedIn
    Collection<Players> players = new ArrayList<Players>(0);

    //getters and setters
...
}

现在将这些 POJOs 包装成实体,如清单 6-14 中的和清单 6-15 中的所示。

清单 6-14 。 玩家实体

package hogm.hnapi.entity;

import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;

@Entity
@Indexed
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {

    @DocumentId
    @Id
    @GeneratedValue(generator = "mongodb_uuidgg")
    private String id;
    @Column(name = "player_name")
    @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
    private String name;
    @Column(name = "player_surname")
    @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
    private String surname;
    @Column(name = "player_age")
    @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
    private int age;
    @Column(name = "player_birth")
    @Field
    @DateBridge(resolution = Resolution.YEAR)
    @Temporal(javax.persistence.TemporalType.DATE)
    private Date birth;
    @ManyToMany(cascade = CascadeType.PERSIST,fetch=FetchType.EAGER)
    @IndexedEmbedded
    private Collection<Tournaments> tournaments= new ArrayList<Tournaments>(0);

    //getters and setters
...
}

清单 6-15 。 锦标赛实体

package hogm.hnapi.entity;

import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.ContainedIn;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;

@Entity
@Indexed
@Table(name = "atp_tournaments")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Tournaments implements Serializable {

    @DocumentId
    @Id
    @GeneratedValue(generator = "mongodb_uuidgg")
    private String id;
    @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
    private String tournament;
    @ManyToMany(mappedBy = "tournaments", fetch = FetchType.EAGER)
    @ContainedIn
    private Collection<Players> players = new ArrayList<Players>(0);

    //getters and setters
...
}

现在可以编写 Hibernate Search/Apache Lucene 查询了。(官方文档是开始测试关联查询的好地方。)出于测试目的,我将前面的 POJOs 和实体集成到一个名为HOGM_MONGODB_HNAPI_ASSOCIATIONS_HS的应用中,该应用可以从 Apress 存储库中下载(这里涉及到两个查询)。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。图 6-11 显示了这种应用。

9781430257943_Fig06-11.jpg

图 6-11 。HOGM _ MONGODB _ HNAPI _ ASSOCIATIONS _ HS 应用

image 注意您可以通过键入命令db.dropDatabase();轻松地从 shell 中删除 MongoDB 数据库。

Hibernate 搜索/Apache Lucene 查询—通过 JPA 的 OGM

还记得我们之前写的“全选”查询吗?这一次,我们将通过 JPA 为基于 OGM 的应用编写相同的查询。完成该任务的步骤是:

  1. 创建一个org.hibernate.search.jpa.FullTextEntityManager。这个接口为 OGM EntityManager增添了全文搜索和索引功能。下面是实现这一点的代码(emEntityManager实例):

    FullTextEntityManager fullTextEntityManager =
                            org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
    
  2. 创建一个org.hibernate.search.query.dsl.QueryBuilder。使用新的实体管理器获得查询构建器,这将有助于简化查询定义。请注意,您指出该查询只影响Players类:

    QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
    buildQueryBuilder().forEntity(Players.class).get();
    
  3. 创建一个 Lucene 查询。正如官方文档所示,使用queryBuilder构建 Lucene 查询有几种方法。对于这个例子,我们可以使用queryBuilder.all方法,这是一种获取整个文档的简单方法:

    org.apache.lucene.search.Query query = queryBuilder.all().createQuery();
    
  4. 定义排序规则(可选)。您可以使用 Lucene sort功能轻松定义一个排序规则。例如,您可能需要按name :

    org.apache.lucene.search.Sort sort = new Sort(new SortField("name", SortField.STRING));
    

    对提取的球员进行排序

  5. 将 Lucene 查询包装在一个org.hibernate.FullTextQuery中。为了设置排序规则并执行查询,您需要将 Lucene 查询包装在一个FullTextQuery中,就像这样:

    FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
    
  6. 指定对象查找方法和数据库检索方法。对于 OGM,您必须指定对象查找和数据库检索方法,就像这样:

    fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID);
    
  7. 设置排序规则。您可以通过调用setSort方法来设置排序规则,比如:

    fullTextQuery.setSort(sort);
    
  8. 执行查询。最后,您可以执行查询并在java.util.List :

    ...
    List<Players> results = fullTextQuery.getResultList();
    ...
    

    中获得结果

  9. 清除会话(可选):

    fullTextEntityManager.clear();
    

现在,您可以将这九个步骤放入一个名为selectAllAction的方法中,以获得清单 6-16 中所示的 Hibernate Search/Lucene 查询。

清单 6-16 。selectAllAction

package hogm.jpa.ejb;
...
public class SampleBean {
...
public List<Players> selectAllAction() {

        log.info("Select all Players instance ...");

        FullTextEntityManager fullTextEntityManager =
                                     org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
                                                       buildQueryBuilder().forEntity(Players.class).get();
        org.apache.lucene.search.Sort sort = new Sort(new SortField("name", SortField.STRING));
        org.apache.lucene.search.Query query = queryBuilder.all().createQuery();

        FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                                                 DatabaseRetrievalMethod.FIND_BY_ID);

        fullTextQuery.setSort(sort);
        List<Players> results = fullTextQuery.getResultList();

        fullTextEntityManager.clear();

        log.info("Search complete ...");

        return results;
    }
}

这九个步骤可以作为编写许多其他类型查询的快速指南。此外,您还可以看到如何编写一些常见的查询(这些查询与“Hibernate Search/Apache Lucene 通过本机 API 查询 OGM”一节中的查询相同,“通过 JPA 为 OGM 重写的案例”)。

  • 选择所有 1987 年出生的玩家。这个查询(以及类似的查询)可以使用三种方法轻松编写:QueryBuilder.keyword,这表示我们正在搜索一个特定的单词;TermContext.onField,指定在哪个 Lucene 字段中查找;还有TermMatchingContext.matching,它告诉我们要寻找什么。因此,将这个查询包装成一个名为selectByYearAction的方法,如清单 6-17 所示。

清单 6-17 。??【selectByYearAction】方法

package hogm.jpa.ejb;
...
public class SampleBean {
...

public List<Players> selectByYearAction() {

        log.info("Search only Players instances born in 1987 ...");

        Calendar calendar = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"));
        calendar.clear();

        calendar.set(Calendar.YEAR, 1987);

        FullTextEntityManager fullTextEntityManager =
                                     org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
                                             buildQueryBuilder().forEntity(Players.class).get();

         org.apache.lucene.search.Query query = queryBuilder.keyword()
                                       .onField("birth").matching(calendar.getTime()).createQuery();

        FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                                                 DatabaseRetrievalMethod.FIND_BY_ID);

        List<Players> results = fullTextQuery.getResultList();

        fullTextEntityManager.clear();

        log.info("Search complete ...");

        return results;
    }
}
  • 只选择名叫拉斐尔·纳达尔的球员。这个查询(以及类似的查询)在两个不同的字段中搜索两个单词,“Rafael”和“Nadal”。该查询在player_surname column (surname字段中查找第一个单词,在player_name列(name字段)中查找第二个单词。为此,您可以使用一个名为 must 的聚合操作符。将必要的代码包装到名为selectRafaelNadalAction的方法中就说明了这一点。bool方法表明我们已经创建了一个布尔查询。(参见清单 6-18 。)

清单 6-18 。??【selectRafaelNadalAction】方法

package hogm.jpa.ejb;
...
public class SampleBean {
...
public List<Players> selectRafaelNadalAction() {

        log.info("Search only Players instances that have the name 'Nadal' and surname 'Rafael' ...");

        FullTextEntityManager fullTextEntityManager =
                                    org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
                                             buildQueryBuilder().forEntity(Players.class).get();

        org.apache.lucene.search.Query query = queryBuilder.bool().must(queryBuilder.keyword()
                           .onField("name").matching("Nadal").createQuery()).must(queryBuilder.keyword()
                           .onField("surname").matching("Rafael").createQuery()).createQuery();

        FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                                                 DatabaseRetrievalMethod.FIND_BY_ID);

        List<Players> results = fullTextQuery.getResultList();
        fullTextEntityManager.clear();

        log.info("Search complete ...");

        return results;
    }
}
  • 选择姓氏以字母“j”开头的玩家。该查询(以及类似的查询)可以使用通配符编写。?代表单个字符,*代表任何字符序列。TermContext.wildcard方法表明后面是通配符查询。将必要的代码包装到一个名为selectJAction的方法中将会显示这一点。(参见清单 6-19 。)

清单 6-19 。?? 的选择方法

package hogm.jpa.ejb;
...
public class SampleBean {
...
public List<Players> selectJAction() {

        log.info("Search only Players that surnames begins with 'J' ...");

        FullTextEntityManager fullTextEntityManager =
                                       org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory()
                                                      .buildQueryBuilder().forEntity(Players.class).get();

        org.apache.lucene.search.Query query = queryBuilder.keyword().wildcard()
                                                      .onField("surname").matching("J*").createQuery();

        FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                                                 DatabaseRetrievalMethod.FIND_BY_ID);

        List<Players> results = fullTextQuery.getResultList();
        fullTextEntityManager.clear();

        log.info("Search complete ...");

        return results;
    }
}
  • 选择年龄在区间(25,28)的球员。该查询(以及类似的查询)可以被视为范围查询。这种查询搜索区间中的值(包括或不包括边界),或者低于或高于区间边界的值(包括或不包括边界)。您通过调用queryBuilder.range方法来指示一个范围查询。通过调用fromto方法来设置间隔,通过调用excludeLimit方法可以排除间隔的边界。将必要的代码包装到一个名为select25To28AgeAction的方法中就会显示这一点。(参见清单 6-20 。)

清单 6-20 。?? 选择 25 到 28 岁的动作方法

package hogm.jpa.ejb;
...
public class SampleBean {
...
public List<Players> select25To28AgeAction() {

        log.info("Search only Players that have ages between 25 and 28, excluding limits ...");

        FullTextEntityManager fullTextEntityManager =
                                      org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory()
                                            .buildQueryBuilder().forEntity(Players.class).get();

         org.apache.lucene.search.Query query = queryBuilder.range().onField("age")
                                            .from(25).to(28).excludeLimit().createQuery();

        FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                                                 DatabaseRetrievalMethod.FIND_BY_ID);

        List<Players> results = fullTextQuery.getResultList();
        fullTextEntityManager.clear();

        log.info("Search complete ...");

        return results;
    }
}

您可以编写许多其他类型的查询,您只需钻研关于 Hibernate Search 和 Apache Lucene 的可用文档。对于所涉及的查询,我开发了一个完整的应用,可以在 Apress 存储库中获得,并命名为HOGM_MONGODB_JPA_HS。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。图 6-12 显示了这种应用。

9781430257943_Fig06-12.jpg

图 6-12 。HOGM_MONGODB_JPA_HS 应用

image 注意您可以通过调用startAndWait方法:fullTextEntityManager.createIndexer().startAndWait();来重建索引(删除它,然后从数据库中重新加载所有实体)

当您有关联(或嵌入对象)时,您需要提供更多的注释。关联对象(和嵌入对象)可以作为根实体索引的一部分进行索引。为此,该关联标有@IndexedEmbedded。当关联是双向时,另一方必须用@ContainedIn标注。这有助于 Hibernate Search 保持关联索引过程是最新的。

例如,我们假设Players实体与Tournaments实体是多对多的关联关系(每个玩家参加多个锦标赛,每个锦标赛包含多个玩家)。带注释的Players实体列表如列表 6-21 所示。

清单 6-21 。 标注玩家实体

package hogm.jpa.entity;

import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;

@Entity
@Indexed
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {

    @DocumentId
    @Id
    @GeneratedValue(generator = "mongodb_uuidgg")
    private String id;
    @Column(name = "player_name")
    @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
    private String name;
    @Column(name = "player_surname")
    @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
    private String surname;
    @Column(name = "player_age")
    @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
    private int age;
    @Column(name = "player_birth")
    @Field
    @DateBridge(resolution = Resolution.YEAR)
    @Temporal(javax.persistence.TemporalType.DATE)
    private Date birth;
    @ManyToMany(cascade = CascadeType.PERSIST,fetch=FetchType.EAGER)
    @IndexedEmbedded
    private Collection<Tournaments> tournaments= new ArrayList<Tournaments>(0);

    //getters and setters
...
}

Tournaments实体如清单 6-22 所示。

清单 6-22 。 标注赛事实体

package hogm.jpa.entity;

import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.ContainedIn;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;

@Entity
@Indexed
@Table(name = "atp_tournaments")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Tournaments implements Serializable {

    @DocumentId
    @Id
    @GeneratedValue(generator = "mongodb_uuidgg")
    private String id;
    @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
    private String tournament;
    @ManyToMany(mappedBy = "tournaments", fetch = FetchType.EAGER)
    @ContainedIn
    private Collection<Players> players = new ArrayList<Players>(0);

    //getters and setters
...
}

现在可以编写 Hibernate Search/Apache Lucene 查询了。(官方文档是开始测试关联查询的好地方。)出于测试目的,我将前面的实体集成到一个名为HOGM_MONGODB_JPA_ASOCIATIONS_HS的应用中,该应用可以从 Apress 存储库中下载(这里涉及到两个查询)。它是一个 NetBeans 项目,并在 GlassFish 3 AS 下进行了测试。图 6-13 显示了这种应用。

9781430257943_Fig06-13.jpg

图 6-13 。HOGM _ MONGODB _ JPA _ ASSOCIATIONS _ HS 应用

我们到此为止,但这可能只是你探索 Hibernate Search 和 Apache Lucene 结合起来的惊人力量的开始。我已经为您提供了一个通过 OGM 和 Hibernate Search/Apache Lucene 查询 MongoDB 集合的起点。从现在开始,在 Hibernate Search/Apache Lucene 领域走多远取决于你自己。

Hibernate OGM JP-QL 解析器

根据 Hibernate OGM 文档,版本 4.0.0Beta1 包括一个 JP-QL 基本解析器,能够使用 Hibernate Search 转换简单的查询。目前,使用它有几个限制,包括:

  • 不隐含任何连接、聚合或其他关系操作。
  • 使用 Hibernate 会话 API(JPA 集成即将到来)。
  • Hibernate Search 对目标实体和属性进行索引(目前还没有验证)。

我试图解决这些限制,但是还不能开发一个功能应用来利用 JP-QL 解析器进行简单的查询。对于用@Indexed@Field,等标注的Players实体,我尝试了一个简单的查询,如下所示:

Query query = HibernateUtil.getSessionFactory().getCurrentSession().createQuery("from Players p");

不幸的是,我的多种方法都失败了,只有一个恼人的错误:java.lang.NullPointerException。索引过程似乎工作得很好,但是查询结果列表总是空的。

无论如何,这不是一个大问题,因为 JP-QL 解析器非常年轻,当您阅读本节时,这些信息可能已经过时了。到那时,JP-QL 解析器的查询支持可能会更加慷慨。现在,您可以使用 MongoDB Java 驱动程序,当然还有 Hibernate Search 和 Apache Lucene。

摘要

经过前几章的努力,这一章我们收获了果实。我们能够通过编写针对 MongoDB 数据库的查询来处理存储的数据。特别是,在本章中,您学习了如何使用 Hibernate Search/Apache Lucene 和 MongoDB Java 驱动程序编写查询。我的目标是提供关于编写一个纯 MongoDB Java 驱动程序应用和一个 OGM 的基本信息,通过本地 API 和/或通过 JPA 应用查询 MongoDB 数据库。

七、MongoDB 电子商务数据库模型

开源电子商务软件的市场每年都在增长。为了证明这一点,只需看看今天被用作各种电子商务应用起点的许多流行平台。例如,Magento、Zen Cart 和 Spree 都提供了用于存储和查询类别、产品、订单、库存等的数据库模式。尽管这些平台之间存在差异,但它们都有一些共同点:它们都提供 SQL 数据库。

对于 NoSQL 商店来说,电子商务软件市场是一个挑战,大多数 NoSQL 商店被认为不适合电子商务。然而,MongoDB 是健壮和灵活的,它的特性包括支持丰富的数据模型、索引查询、原子操作和复制写入,这促使我们问:MongoDB 适合电子商务应用吗?嗯,这个问题需要一个权威的答案,这个答案可能会在关于 MongoDB 是否适合电子商务应用的热情和误解开始消退,事情开始平静下来之后出现。

人们普遍认为 MongoDB 很快,通过使用文档(概念上比表简单)减少了表和关联的数量,并提供了灵活的模式。但是它有一些缺点,主要集中在事务、一致性和持久性方面。相比之下,SQL 数据库提供了安全性,但它们没有那么快,具有僵化的模式,需要几十个表(关联),并且会减慢开发进度(有时我们需要编写复杂的查询)。然而,“安全”似乎是关键词,因为没有一个电子卖家(电子零售商)愿意因为数据库不一致而失去订单或金钱。

尽管如此,名为 Forward ( http://getfwd.com/)的“一个功能齐全、以开发人员为中心的电子商务平台,通过强大的模板&富有表现力的语法,使定制代码变得容易,已经准备好向所有人展示 MongoDB 非常适合电子商务应用。因此,在较小的范围内,我将通过使用 MongoDB 开发一个电子商务数据模型,并通过 JPA 和 Hibernate Search/Apache Lucene 在一个基于 Hibernate OGM 的企业应用中使用它,来保持这种肯定。

在这一章中,我将把电子商务应用的特定 SQL 模式转换(或改编)为 MongoDB 模式。在图 7-1 中,你可以看到一个中等复杂程度的电子商务应用的数据库模式;在电子商务环境中,大多数表格都是不言自明的。主表是类别、产品、订单和用户。

9781430257943_Fig07-01.jpg

图 7-1 。SQL 电子商务数据库架构

主要目标是开发一个类似于图 7-1 中的 MongoDB 数据库模式。通过相似,我的意思是我们想要再现主要的功能(相同的查询能力),而不是相同的表、关联和字段。此外,我们将为它编写相应的 JPA 实体。我们将通过 JPA 使用 Hibernate OGM,所以我们需要 JPA 注释。我们将使用 Hibernate Search 和 Apache Lucene 进行查询,因此我们需要 Hibernate Search 特有的注释来索引 Lucene 中的数据。

即使你不是电子零售商,从客户的角度来看,你可能非常熟悉许多电子商务术语,尤其是类别、产品、促销、订单、购物车、采购订单、付款、送货地址等等。这样的术语是每个互联网用户都熟知的,所以我不会在这里试图解释它们。

MongoDB 电子商务数据库架构

在图 7-2 中,可以看到我提出的 MongoDB 电子商务数据库架构,我将其命名为 eshop_db 。该图包含 MongoDB 集合、它们的关联以及相应的 JPA 实体(但不是字段)。

9781430257943_Fig07-02.jpg

图 7-2 。MongoDB 电子商务数据库模式

对类别集合建模(categories_c)

categories_c 集合对应于categories表。

按类别对产品进行分类是大多数电子商务网站的常见功能。特定于类别的 SQL 表很可能存储每个类别的名称,以及与负责存储产品的表的一对多(或者有时是多对多)惰性关联。这个想法是非常快速地加载类别名称(没有他们的产品),因为它们出现在电子商务网站的第一页。在用户选择一个类别后,可以在以后加载产品。但是,尽管这在 SQL 中是可行的,但是在 MongoDB 中,您需要非常小心关联,因为它们可能会启动事务。我们的目的是尽可能避免事务,所以我没有在categories_c集合中定义任何关联。

我创建了类别集合(categories_c),其结构如图图 7-3 所示。如您所见,每个文档都存储了一个标识符和类别名称:

9781430257943_Fig07-03.jpg

图 7-3 。categories_c 集合中的文档示例

这个集合的 JPA 实体显示在清单 7-1 的中。

清单 7-1 。??【JPA 实体】为类别 _c

1       package eshop.entities;
2
3       import java.io.Serializable;
4       import javax.persistence.Column;
5       import javax.persistence.Entity;
6       import javax.persistence.GeneratedValue;
7       import javax.persistence.Id;
8       import javax.persistence.Table;
9       import org.hibernate.annotations.GenericGenerator;
10      import org.hibernate.search.annotations.Analyze;
11      import org.hibernate.search.annotations.DocumentId;
12      import org.hibernate.search.annotations.Field;
13      import org.hibernate.search.annotations.Index;
14      import org.hibernate.search.annotations.Indexed;
15      import org.hibernate.search.annotations.Store;
16
17      @Entity
18      @Indexed
19      @Table(name = "categories_c")
20      public class Categories implements Serializable {
21
22          private static final long serialVersionUID = 1L;
23          @DocumentId
24          @Id
25          @GeneratedValue(generator = "uuid")
26          @GenericGenerator(name = "uuid", strategy = "uuid2")
27          private String id;
28          @Column(name = "category_name")
29          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.YES)
30          private String category;
31
32          public String getId() {
33              return id;
34          }
35
36          public void setId(String id) {
37              this.id = id;
38          }
39
40          public String getCategory() {
41              return category;
42          }
43
44          public void setCategory(String category) {
45              this.category = category;
46          }
47
48          @Override
49          public int hashCode() {
50              int hash = 0;
51              hash += (id != null ? id.hashCode() : 0);
52              return hash;
53          }
54
55          @Override
56          public boolean equals(Object object) {
57              if (!(object instanceof Categories)) {
58                  return false;
59              }
60              Categories other = (Categories) object;
61              if ((this.id == null && other.id != null) || (this.id != null &&
                  !this.id.equals(other.id))) {
62                  return false;
63              }
64              return true;
65          }
66
67          @Override
68          public String toString() {
69              return "eshop.entities.Categories[ id=" + id + " ]";
70          }
71      }
72

注意,第 29 行指定类别 id ( id 字段)和类别名称( category_name 字段)应该可以用 Lucene 进行搜索,并禁用了分析器。我们不需要分析器,因为我们按原样搜索类别(而不是按它包含的单词),我们将按名称对类别进行排序(Lucene 不允许您分析用于排序操作的字段)。此外,类别名称存储在 Lucene 索引中。这消耗了索引中的空间,但不是很大,因为你不想要太多的类别而引起关注。这允许我们利用投影(注意,id 是自动存储的)。使用 projection 允许我们在将来向这个集合中添加更多可搜索的、非惰性的字段,比如类别代码、类别描述等等,但是仍然只提取类别名称。当然,这只是针对 Lucene 的一种方法(不是规则)。如果您选择使用 JP-QL 查询(当 Hibernate OGM 提供对这种查询的支持时),事情会有所不同。

为产品集合建模(products_c )

products_c集合对应于productsproductoptions表。

在专用于产品的集合(products_c)中,每个产品的文档存储两种信息:一般数据,如 SKU、名称、价格、描述等;关系模型中通常需要附加表的数据,如产品的图库和产品选项(例如,颜色、大小、类型等)。我将在嵌入式集合中存储每个产品的图库和选项,而不是使用额外的表和关联。这是有意义的,因为这些物理细节是产品的独特特征。而且, products_c 集合是与categories_c集合单向多对一关联的所有者方,所以它存储相应类别的外键。

在图 7-4 中,可以看到这样一个文档样本。

9781430257943_Fig07-04.jpg

图 7-4 。products_c 集合中的示例文档

每个产品都将由这样一个文档来表示。colorssizes嵌入式系列仅对拥有这些选项的产品可见。

这个集合的 JPA 实体如清单 7-2 中的所示。

清单 7-2 。 产品的 JPA 实体 _c

1        package eshop.entities;
2
3        import java.io.Serializable;
4        import java.util.ArrayList;
5        import java.util.List;
6        import javax.persistence.Column;
7        import javax.persistence.ElementCollection;
8        import javax.persistence.Entity;
9        import javax.persistence.FetchType;
10       import javax.persistence.GeneratedValue;
11       import javax.persistence.Id;
12       import javax.persistence.ManyToOne;
13       import javax.persistence.Table;
14       import org.hibernate.annotations.GenericGenerator;
15       import org.hibernate.search.annotations.Analyze;
16       import org.hibernate.search.annotations.DocumentId;
17       import org.hibernate.search.annotations.Field;
18       import org.hibernate.search.annotations.Index;
19       import org.hibernate.search.annotations.Indexed;
20       import org.hibernate.search.annotations.IndexedEmbedded;
21       import org.hibernate.search.annotations.NumericField;
22       import org.hibernate.search.annotations.Store;
23
24       @Entity
25       @Indexed
26       @Table(name = "products_c")
27       public class Products implements Serializable {
28
29           private static final long serialVersionUID = 1L;
30           @DocumentId
31           @Id
32           @GeneratedValue(generator = "uuid")
33           @GenericGenerator(name = "uuid", strategy = "uuid2")
34           private String id;
35           @Column(name = "product_sku")
36           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
37           private String sku;
38           @Column(name = "product_name")
39           @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
40           private String product;
41           @Column(name = "product_price")
42           @NumericField
43           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
44           private double  price;
45           @Column(name = "product_old_price")
46           @NumericField
47           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
48           private double old_price;
49           @Column(name = "product_description")
50           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
51           private String description;
52           @IndexedEmbedded
53           @ManyToOne(fetch = FetchType.LAZY)
54           private Categories category;
55           @IndexedEmbedded
56           @ElementCollection(targetClass = java.lang.String.class,
                              fetch = FetchType.EAGER)
57           @Column(name = "product_gallery")
58           private List<String> gallery = new ArrayList<String>();
59           @IndexedEmbedded
60           @ElementCollection(targetClass = java.lang.String.class,
                              fetch = FetchType.EAGER)
61           @Column(name = "product_colors")
62           private List<String> colors = new ArrayList<String>();
63           @IndexedEmbedded
64           @ElementCollection(targetClass = java.lang.String.class,
                              fetch = FetchType.EAGER)
65           @Column(name = "product_sizes")
66           private List<String> sizes = new ArrayList<String>();
67
68           public String getId() {
69               return id;
70           }
71
72           public void setId(String id) {
73               this.id = id;
74           }
75
76           public List<String> getGallery() {
77               return gallery;
78           }
79
80           public void setGallery(List<String> gallery) {
81               this.gallery = gallery;
82           }
83
84           public double getPrice() {
85               return price;
86           }
87
88           public void setPrice(double price) {
89               this.price = price;
90           }
91
92           public double getOld_price() {
93               return old_price;
94           }
95
96           public void setOld_price(double old_price) {
97               this.old_price = old_price;
98           }
99
100          public String getProduct() {
101              return product;
102          }
103
104          public void setProduct(String product) {
105              this.product = product;
106          }
107
108          public String getSku() {
109              return sku;
110          }
111
112          public void setSku(String sku) {
113              this.sku = sku;
114          }
115
116          public String getDescription() {
117              return description;
118          }
119
120          public List<String> getColors() {
121              return colors;
122          }
123
124          public void setColors(List<String> colors) {
125              this.colors = colors;
126          }
127
128          public List<String> getSizes() {
129              return sizes;
130          }
131
132          public void setSizes(List<String> sizes) {
133              this.sizes = sizes;
134          }
135
136          public void setDescription(String description) {
137              this.description = description;
138          }
139
140          public Categories getCategory() {
141              return category;
142          }
143
144          public void setCategory(Categories category) {
145              this.category = category;
146          }
147
148          @Override
149          public int hashCode() {
150              int hash = 0;
151              hash += (id != null ? id.hashCode() : 0);
152              return hash;
153          }
154
155          @Override
156          public boolean equals(Object object) {
157              if (!(object instanceof Products)) {
158                  return false;
159              }
160              Products other = (Products) object;
161              if ((this.id == null && other.id != null) || (this.id != null &&
                    !this.id.equals(other.id))) {
162                  return false;
163              }
164              return true;
165          }
166
167          @Override
168          public String toString() {
169              return "eshop.entities.Products[ id=" + id + " ]";
170          }
171      }

让我们仔细看看一些主要的代码行。

在第 39 行,对应于产品名( product_name )的字段是为 Lucene 准备的。我们要注意的部分是analyze = Analyze.YES,它告诉 Lucene 对这个字段使用默认的分析器。我们可以通过产品名称中包含的任何单词来搜索产品,而不是通过名称(通常由几个单词组成)来搜索。这有助于我们轻松实现通过产品名称搜索的功能。

如您所见,在第 42 行和第 48 行中,产品价格( product_priceproduct_old_price )是数值(double s)。将它们存储为数字而不是字符串是有意义的,这样您就可以执行范围查询和计算,如小计、总计、货币换算等。您可以告诉 Lucene,通过用@NumericField对字段进行注释,字段表示数值。当一个属性被索引为数值字段时,它支持高效的范围查询,并且排序比在标准的@Field属性上做同样的查询要快。

第 52-54 行定义了categories_cproducts_c集合之间的单向、多对一的关联。对于 Lucene 来说,这个关联应该被标记为@IndexedEmbedded,它被用来索引作为拥有实体一部分的关联实体。可能我之前已经说过了,但是现在是时候再次指出 Lucene 不知道关联,这就是为什么它需要@IndexedEmbedded@ContainedIn注释。如果没有这些注释,像@ManyToMany@*ToOne@Embedded,@ElementCollection这样的关联将不会被索引,因此将无法被搜索。关联让您可以轻松地编写类似于 SQL 查询的 Lucene 查询,其中包含类型为:从类别字段等于某个值的类别中选择所有产品(在 JP-QL 中通常是一个连接)的 WHERE 子句。

第 55-66 行定义了产品的选项和图片集。对于这个例子,我们使用最常见的选项,颜色和大小,但你可以添加更多。与其将它们放入另一个表并创建另一个关联,我更喜欢使用@ElementCollection来存储它们。当一个产品没有颜色和大小,它只是被跳过。MongoDB 文档允许灵活的结构,所以当没有指定选项时,相应的集合将不会出现在文档中。最后,我们使用 eager 机制加载选项和图库,因为我们希望加载和显示每个产品及其图库和选项。如果您想分两个阶段加载产品:首先是产品的简要概述,然后是用户请求的选项,请使用惰性机制。

为客户集合建模(customers_c)

customers_c集合对应于users表。

对于用户(潜在客户),我们需要一个单独的集合来存储个人数据;我们将这个集合命名为customers_c。个人数据包括姓名、电子邮件地址、密码、地址等信息(显然,您可以添加更多字段)。当用户登录系统时,您可以通过电子邮件地址和密码轻松识别他,并加载他的个人资料。他的订单没有加载到与他的配置文件相同的查询中。它们只有在执行显式请求时才被延迟加载;这允许我们只装载请求的订单,而不是全部。通常,客户只检查他最近的订单状态,很少想查看过时的订单。许多电子商务网站不提供过期订单,只提供最新订单。

customers_c集合中的每个文档(条目)如图图 7-5 所示。

9781430257943_Fig07-05.jpg

图 7-5 。customers_c 集合中的示例文档

请注意,客户的地址是作为嵌入文档存储的;这让我们可以使用快速查询和延迟加载,在没有额外表的情况下提供多个地址。

这个集合的 JPA 实体显示在清单 7-3 中。

清单 7-3 。 客户的 JPA 实体 _c

1        package eshop.entities;
2
3        import eshop.embedded.Addresses;
4        import java.io.Serializable;
5        import java.util.Date;
6        import javax.persistence.Basic;
7        import javax.persistence.Column;
8        import javax.persistence.Embedded;
9        import javax.persistence.Entity;
10       import javax.persistence.FetchType;
11       import javax.persistence.GeneratedValue;
12       import javax.persistence.Id;
13       import javax.persistence.Table;
14       import javax.persistence.Temporal;
15       import org.hibernate.annotations.GenericGenerator;
16       import org.hibernate.search.annotations.Analyze;
17       import org.hibernate.search.annotations.DateBridge;
18       import org.hibernate.search.annotations.DocumentId;
19       import org.hibernate.search.annotations.Field;
20       import org.hibernate.search.annotations.Index;
21       import org.hibernate.search.annotations.Indexed;
22       import org.hibernate.search.annotations.IndexedEmbedded;
23       import org.hibernate.search.annotations.Resolution;
24       import org.hibernate.search.annotations.Store;
25
26       @Entity
27       @Indexed
28       @Table(name = "customers_c")
29       public class Customers implements Serializable {
30
31           private static final long serialVersionUID = 1L;
32           @DocumentId
33           @Id
34           @GeneratedValue(generator = "uuid")
35           @GenericGenerator(name = "uuid", strategy = "uuid2")
36           private String id;
37           @Column(name = "customer_email")
38           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
39           private String email;
40           @Column(name = "customer_password")
41           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
42           private String password;
43           @Column(name = "customer_name")
44           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
45           private String name;
46           @Column(name = "customer_surname")
47           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
48           private String surname;
49           @DateBridge(resolution = Resolution.DAY)
50           @Temporal(javax.persistence.TemporalType.DATE)
51           @Column(name = "customer_registration")
52           private Date registration;
53           @Embedded
54           @IndexedEmbedded
55           @Basic(fetch = FetchType.LAZY)
56           private Addresses customer_address_1;
57           @Embedded
58           @IndexedEmbedded
59           @Basic(fetch = FetchType.LAZY)
60           private Addresses customer_address_2;
61
62           public String getId() {
63               return id;
64           }
65
66           public void setId(String id) {
67               this.id = id;
68           }
69
70           public String getEmail() {
71               return email;
72           }
73
74           public void setEmail(String email) {
75               this.email = email;
76           }
77
78           public String getPassword() {
79               return password;
80           }
81
82           public void setPassword(String password) {
83               this.password = password;
84           }
85
86           public String getName() {
87               return name;
88           }
89
90           public void setName(String name) {
91               this.name = name;
92           }
93
94           public String getSurname() {
95               return surname;
96           }
97
98           public void setSurname(String surname) {
99               this.surname = surname;
100          }
101
102          public Date getRegistration() {
103              return registration;
104          }
105
106          public void setRegistration(Date registration) {
107              this.registration = registration;
108          }
109
110          public Addresses getCustomer_address_1() {
111              return customer_address_1;
112          }
113
114          public void setCustomer_address_1(Addresses customer_address_1) {
115              this.customer_address_1 = customer_address_1;
116          }
117
118          public Addresses getCustomer_address_2() {
119              return customer_address_2;
120          }
121
122          public void setCustomer_address_2(Addresses customer_address_2) {
123              this.customer_address_2 = customer_address_2;
124          }
125
126          @Override
127          public int hashCode() {
128              int hash = 0;
129              hash += (id != null ? id.hashCode() : 0);
130              return hash;
131          }
132
133          @Override
134          public boolean equals(Object object) {
135              if (!(object instanceof Customers)) {
136                  return false;
137              }
138              Customers other = (Customers) object;
139              if ((this.id == null && other.id != null) || (this.id != null &&
                    !this.id.equals(other.id))) {
140                  return false;
141              }
142              return true;
143          }
144
145          @Override
146          public String toString() {
147              return "eshop.entities.Customers[ id=" + id + " ]";
148          }
149      }
150

这段代码有一些重要的方面值得解释。

第 53-60 行的代码非常有趣。可以看到,同一个可嵌入对象类型在同一个实体中出现了两次(可嵌入对象在一个名为 Addresses 的类中映射地址坐标、城市、邮政编码、街道等)。如果您在 SQL 和 JPA 提供程序(如 EclipseLink 或 Hibernate)中使用过这种技术,您就会知道必须显式设置至少一个列,因为列名 default 不起作用。在这种情况下,通用 JPA 用@AttributeOverride注释解决了这个问题(参见www.docs.oracle.com/javaee/6/api/javax/persistence/AttributeOverride.html)。然而,在 NoSQL 和 Hibernate OGM 中,您不需要对列名进行这种调整。

代表地址的可嵌入类如清单 7-4 中的所示。

清单 7-4 。 可嵌入地址类

1       package eshop.embedded;
2
3       import java.io.Serializable;
4       import javax.persistence.Embeddable;
5       import org.hibernate.search.annotations.Analyze;
6       import org.hibernate.search.annotations.Field;
7       import org.hibernate.search.annotations.Index;
8       import org.hibernate.search.annotations.Store;
9
10      @Embeddable
11      public class Addresses implements Serializable {
12
13          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
14          private String city;
15          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
16          private String state;
17          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
18          private String street;
19          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
20          private String number;
21          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
22          private String zip;
23          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
24          private String country;
25          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
26          private String phone;
27          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
28          private String fax;
29
30          public String getCity() {
31              return city;
32          }
33
34          public void setCity(String city) {
35              this.city = city;
36          }
37
38          public String getNumber() {
39              return number;
40          }
41
42          public void setNumber(String number) {
43              this.number = number;
44          }
45
46          public String getState() {
47              return state;
48          }
49
50          public void setState(String state) {
51              this.state = state;
52          }
53
54          public String getStreet() {
55              return street;
56          }
57
58          public void setStreet(String street) {
59              this.street = street;
60          }
61
62          public String getZip() {
63              return zip;
64          }
65
66          public void setZip(String zip) {
67              this.zip = zip;
68          }
69
70          public String getCountry() {
71              return country;
72          }
73
74          public void setCountry(String country) {
75              this.country = country;
76          }
77
78          public String getPhone() {
79              return phone;
80          }
81
82          public void setPhone(String phone) {
83              this.phone = phone;
84          }
85
86          public String getFax() {
87              return fax;
88          }
89
90          public void setFax(String fax) {
91              this.fax = fax;
92          }
93      }

对订单集合建模(orders_c)

orders_c集合对应于ordersdetails表。

订单存储在一个单独的集合中,名为orders_c。对于每个订单,我们存储状态(一个订单可以经过多种状态,比如已购买、已发货、已取消等等);小计(这表示订单金额);订单创建日期;送货地址;以及订单的产品。您可以添加更多的字段,比如订单标识符(例如, nnnn,)、订单友好名称、订单到期日期等等。

送货地址由一个嵌入式文档表示,订单的产品存储为一个嵌入式集合。因此,我们不需要额外的集合或关联,查询非常容易执行,我们可以缓慢或急切地加载送货地址和订单产品,这取决于我们如何实现 web 站点 GUI。

在这个集合中,我们需要存储表示购买订单的客户的外键。为此,我定义了订单和客户之间的单向多对一关联。

我还没有说任何关于当前购物车的事情—订单还没有提交。购物车可以在客户的单个(或多个)会话中支持多个内容修改,添加新产品、删除其他产品、清空购物车、修改产品数量等等。在数据库中反映所有这些修改是没有用的,因为每个修改都需要至少一个查询来更新客户和购物车之间的“对话”。为此,您可以采用编程方法,将购物车存储在客户会话中,或者视图范围或对话范围中。您还可以使用 cookies,或者任何有助于实现这一任务的特定设计模式。这个想法是只有在实际下订单时才修改数据库。

当然,如果您的数据非常关键或者您需要在多个会话中持久化(例如,如果用户可能在一周后回来),那么使用单独的集合或者作为orders_c集合中的文档将购物车持久化到数据库是一个好主意。毕竟,购物车只是一个尚未下单的订单,所以它可以像一个正常订单一样存储,状态可能是未采购。如果您决定持久化购物车,请注意正确地将其与库存同步。这是防止“超售”的必要条件在某些情况下,应用必须将商品从库存移动到购物车,然后再移动回来,例如,如果用户丢弃了一个或多个产品,甚至放弃了整个购买。从库存中取出产品并将其移动到购物车(或相反)是特定于事务的操作,因此您必须处理回滚问题。显然,如果你没有库存,事情就简单多了。

在图 7-6 中,可以看到一个订单的单据样本。

9781430257943_Fig07-06.jpg

图 7-6 。orders_c 集合中的示例文档

按照惯例,当产品没有颜色或尺寸时,我们会存储一个类似“不可用”的标志。

这个集合的 JPA 实体显示在清单 7-5 中:

清单 7-5 。JPA 实体为订单 _c

1        package eshop.entities;
2
3        import eshop.embedded.Addresses;
4        import eshop.embedded.CartProducts;
5        import java.io.Serializable;
6        import java.util.ArrayList;
7        import java.util.Date;
8        import java.util.List;
9        import javax.persistence.AttributeOverride;
10       import javax.persistence.AttributeOverrides;
11       import javax.persistence.Basic;
12       import javax.persistence.Column;
13       import javax.persistence.ElementCollection;
14       import javax.persistence.Embedded;
15       import javax.persistence.Entity;
16       import javax.persistence.FetchType;
17       import javax.persistence.GeneratedValue;
18       import javax.persistence.Id;
19       import javax.persistence.ManyToOne;
20       import javax.persistence.Table;
21       import javax.persistence.Temporal;
22       import org.hibernate.annotations.GenericGenerator;
23       import org.hibernate.search.annotations.Analyze;
24       import org.hibernate.search.annotations.DateBridge;
25       import org.hibernate.search.annotations.DocumentId;
26       import org.hibernate.search.annotations.Field;
27       import org.hibernate.search.annotations.Index;
28       import org.hibernate.search.annotations.Indexed;
29       import org.hibernate.search.annotations.IndexedEmbedded;
30       import org.hibernate.search.annotations.NumericField;
31       import org.hibernate.search.annotations.Resolution;
32       import org.hibernate.search.annotations.Store;
33
34       @Entity
35       @Indexed
36       @Table(name = "orders_c")
37       public class Orders implements Serializable {
38
39           private static final long serialVersionUID = 1L;
40           @DocumentId
41           @Id
42           @GeneratedValue(generator = "uuid")
43           @GenericGenerator(name = "uuid", strategy = "uuid2")
44           private String id;
45           @Column(name = "order_status")
46           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
47           private String status;
48           @Column(name = "order_subtotal")
49           @NumericField
50           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
51           private double subtotal;
52           @DateBridge(resolution = Resolution.HOUR)
53           @Temporal(javax.persistence.TemporalType.DATE)
54           private Date orderdate;
55           @Embedded
56           @IndexedEmbedded
57           @Basic(fetch = FetchType.EAGER)
58           private Addresses shipping_address;
59           @IndexedEmbedded
60           @ElementCollection(targetClass = eshop.embedded.CartProducts.class,
61           fetch = FetchType.EAGER)
62           @AttributeOverrides({
63               @AttributeOverride(name = "sku",
64               column =
65               @Column(name = "product_sku")),
66               @AttributeOverride(name = "name",
67               column =
68               @Column(name = "product_name")),
69               @AttributeOverride(name = "price",
70               column =
71               @Column(name = "product_price")),
72               @AttributeOverride(name = "color",
73               column =
74               @Column(name = "product_color")),
75               @AttributeOverride(name = "size",
76               column =
77               @Column(name = "product_size")),
78               @AttributeOverride(name = "quantity",
79               column =
80               @Column(name = "product_quantity")),
81               @AttributeOverride(name = "uin",
82               column =
83               @Column(name = "unique_identification_number")),})
84           private List<CartProducts> cart = new ArrayList<CartProducts>(0);
85           @IndexedEmbedded
86           @ManyToOne(fetch = FetchType.LAZY)
87           private Customers customer;
88
89           public String getId() {
90               return id;
91           }
92
93           public void setId(String id) {
94               this.id = id;
95           }
96
97           public String getStatus() {
98               return status;
99           }
100
101          public void setStatus(String status) {
102              this.status = status;
103          }
104
105          public Addresses getShipping_address() {
106              return shipping_address;
107          }
108
109          public void setShipping_address(Addresses shipping_address) {
110              this.shipping_address = shipping_address;
111          }
112
113          public List<CartProducts> getCart() {
114              return cart;
115          }
116
117          public void setCart(List<CartProducts> cart) {
118              this.cart = cart;
119          }
120
121          public Customers getCustomer() {
122              return customer;
123          }
124
125          public void setCustomer(Customers customer) {
126              this.customer = customer;
127          }
128
129          @Override
130          public int hashCode() {
131              int hash = 0;
132              hash += (id != null ? id.hashCode() : 0);
133              return hash;
134          }
135
136          public double getSubtotal() {
137              return subtotal;
138          }
139
140          public void setSubtotal(double subtotal) {
141              this.subtotal = subtotal;
142          }
143
144          public Date getOrderdate() {
145              return orderdate;
146          }
147
148          public void setOrderdate(Date orderdate) {
149              this.orderdate = orderdate;
150          }
151
152          @Override
153          public boolean equals(Object object) {
154              if (!(object instanceof Orders)) {
155                  return false;
156              }
157              Orders other = (Orders) object;
158              if ((this.id == null && other.id != null) || (this.id != null &&
                    !this.id.equals(other.id))) {
159                  return false;
160              }
161              return true;
162          }
163
164          @Override
165          public String toString() {
166              return "eshop.entities.Orders[ id=" + id + " ]";
167          }
168      }

让我们讨论这个实体的主要代码行。

第 55-58 行表示送货地址的映射。如您所见,我更喜欢为每个订单使用嵌入式文档。我急切地加载了它,但延迟加载也是一个选项,这取决于您在加载订单时希望显示什么。

从 Lucene 的角度来看,我需要@IndexedEmbedded注释,因为我想将这个可嵌入的类作为拥有实体的一部分进行索引。Addresses可嵌入类(注释为@可嵌入)如上面的清单 7-4 所示。

在第 59-84 行,一个 element-collection(在 MongoDB 中映射为嵌入式集合)存储了一个订单的产品。元素集合的类型是一个可嵌入的类。这里要注意的主要事情是我使用了@AttributeOverrides注释;如果我们不覆盖可嵌入集合的列名,它们默认为类似于cart . collection&&element . price的名称。这不是很友好,所以在这种情况下@AttributeOverrides会非常有用。

这个可嵌入的类被命名为CartProducts,如清单 7-6 所示。

清单 7-6 。 可嵌入CartProducts

1       package eshop.embedded;
2
3       import java.io.Serializable;
4       import javax.persistence.Embeddable;
5       import org.hibernate.search.annotations.Analyze;
6       import org.hibernate.search.annotations.Field;
7       import org.hibernate.search.annotations.Index;
8       import org.hibernate.search.annotations.NumericField;
9       import org.hibernate.search.annotations.Store;
10
11      @Embeddable
12      public class CartProducts implements Serializable {
13
14          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
15          private String sku;
16          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
17          private String name;
18          @NumericField
19          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
20          private double price;
21          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
22          private String color;
23          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
24          private String size;
25          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
26          private String quantity;
27          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
28          private String uin;
29
30          public String getSku() {
31              return sku;
32          }
33
34          public void setSku(String sku) {
35              this.sku = sku;
36          }
37
38          public String getName() {
39              return name;
40          }
41
42          public void setName(String name) {
43              this.name = name;
44          }
45
46          public double getPrice() {
47              return price;
48          }
49
50          public void setPrice(double price) {
51              this.price = price;
52          }
53
54          public String getColor() {
55              return color;
56          }
57
58          public void setColor(String color) {
59              this.color = color;
60          }
61
62          public String getSize() {
63              return size;
64          }
65
66          public void setSize(String size) {
67              this.size = size;
68          }
69
70          public String getQuantity() {
71              return quantity;
72          }
73
74          public void setQuantity(String quantity) {
75              this.quantity = quantity;
76          }
77
78          public String getUin() {
79              return uin;
80          }
81
82          public void setUin(String uin) {
83              this.uin = uin;
84          }
85      }
86

从 Lucene 的角度来看,我们需要@IndexedEmbedded注释,因为我们想要在实体所有者索引中索引这个可嵌入的集合。

第 85-87 行定义了订单 _ccustomers_c集合之间的单向关联。对于 Lucene 来说,这个关联应该被标记为@IndexedEmbedded ,它用于索引作为拥有实体一部分的关联实体。这种关联允许我们轻松地编写类似于 SQL 查询的 Lucene 查询,包含类型为:从客户字段等于某个值的订单中选择所有订单(在 JP-QL 中,这通常是一个连接)的 WHERE 子句。

对库存集合进行建模 (inventory_c)

该集合在图 7-1 中没有对应的表格。不是所有的电子商务网站都需要库存管理。但是,对于那些这样做的人,MongoDB 提供了一些解决方案。一种解决方案是为仓库中的每种实物产品存储一个单独的文档。这将防止对数据的并发访问,因为每个文档在该产品上都有一个唯一的锁。在这种方法中,我们依赖于 MongoDB 支持对单个文档进行原子操作这一事实。对于仓库不包含太多产品的情况(这取决于您对“太多”的定义),这种方法会非常有效。

另一种方法是存储一组相同产品的文档,并在该文档中使用一个字段来表示产品的数量。在这种情况下,您需要处理多个用户更新该字段的情况,从同一个组中提取或返回一个产品(还有一个管理员偶尔会重新填充库存)。我选择这种方法,并通过使用乐观锁定来处理并发更新。如果您需要锁定一个文档供您独占使用,直到您完成它,使用悲观锁定,但是要小心避免(或处理)死锁。一般来说,当您不期望即将发生冲突时,乐观锁定是好的,但是,由于事务被中止(而不是回滚),您需要付出代价并以某种方式处理它。另一方面,悲观锁定在预期会发生冲突时使用,在冲突即将发生时使用。决定选择哪个锁定选项可能非常棘手,但这里有一个经验法则:如果您必须保证重要数据(如银行数据)的完整性,请使用悲观锁定,而对其他所有数据使用乐观锁定。

存储库存的 MongoDB 集合被命名为inventory_c。对于每组相同的产品,我已经根据产品 SKU 以及颜色和大小创建了一个组合键。除了 id 之外,每个文档还包含一个用于存储可用产品数量的数字字段,名为inventoryversion字段用于乐观锁定。参见图 7-7 。

9781430257943_Fig07-07.jpg

图 7-7 。customers_c 集合中显示库存字段的示例文档

清单 7-7 中的显示了 inventory_c 的 JPA 实体。

清单 7-7 。JPA 实体为库存 _c

1       package eshop.entities;
2
3       import java.io.Serializable;
4       import javax.persistence.Column;
5       import javax.persistence.Entity;
6       import javax.persistence.Id;
7       import javax.persistence.IdClass;
8       import javax.persistence.Table;
9       import javax.persistence.Version;
10
11      @Entity
12      @IdClass(eshop.embedded.InventoryPK.class)
13      @Table(name = "inventory_c")
14      public class Inventory implements Serializable {
15
16          private static final long serialVersionUID = 1L;
17          @Id
18          private String sku;
19          @Id
20          private String sku_color;
21          @Id
22          private String sku_size;
23          @Version
24          private Long version;
25          @Column(name = "inventory")
26          private int inventory;
27
28          public int getInventory() {
29              return inventory;
30          }
31
32          public void setInventory(int inventory) {
33              this.inventory = inventory;
34          }
35
36          public String getSku() {
37              return sku;
38          }
39
40          public void setSku(String sku) {
41              this.sku = sku;
42          }
43
44          public String getSku_color() {
45              return sku_color;
46          }
47
48          public void setSku_color(String sku_color) {
49              this.sku_color = sku_color;
50          }
51
52          public String getSku_size() {
53              return sku_size;
54          }
55
56          public void setSku_size(String sku_size) {
57              this.sku_size = sku_size;
58          }
59
60          public Long getVersion() {
61              return version;
62          }
63
64          protected void setVersion(Long version) {
65              this.version = version;
66          }
67
68          @Override
69          public int hashCode() {
70              int hash = 7;
71              hash = 13 * hash + (this.sku != null ? this.sku.hashCode() : 0);
72              return hash;
73          }
74
75          @Override
76          public boolean equals(Object obj) {
77              if (obj == null) {
78                  return false;
79              }
80              if (getClass() != obj.getClass()) {
81                  return false;
82              }
83              final Inventory other = (Inventory) obj;
84              if ((this.sku == null) ? (other.sku != null) :
                   !this.sku.equals(other.sku)) {
85                  return false;
86              }
87              return true;
88          }
89      }

而组合键类 是:

1       package eshop.embedded;
2
3       import java.io.Serializable;
4
5       public class InventoryPK implements Serializable{
6
7           private String sku;
8           private String sku_color;
9           private String sku_size;
10
11          public InventoryPK(){
12          }
13
14          public InventoryPK(String sku, String sku_color, String sku_size) {
15              this.sku = sku;
16              this.sku_color = sku_color;
17              this.sku_size = sku_size;
18          }
19
20          @Override
21          public int hashCode() {
22              int hash = 7;
23              hash = 83 * hash + (this.sku != null ? this.sku.hashCode() : 0);
24              hash = 83 * hash + (this.sku_color != null ?
               this.sku_color.hashCode() : 0);
25              hash = 83 * hash + (this.sku_size != null ?
               this.sku_size.hashCode() : 0);
26              return hash;
27          }
28
29          @Override
30          public boolean equals(Object obj) {
31              if (obj == null) {
32                  return false;
33              }
34              if (getClass() != obj.getClass()) {
35                  return false;
36              }
37              final InventoryPK other = (InventoryPK) obj;
38              if ((this.sku == null) ? (other.sku != null) :
                   !this.sku.equals(other.sku)) {
39                  return false;
40              }
41              if ((this.sku_color == null) ? (other.sku_color != null) :
                   !this.sku_color.equals(other.sku_color)) {
42                  return false;
43              }
44              if ((this.sku_size == null) ? (other.sku_size != null) :
                   !this.sku_size.equals(other.sku_size)) {
45                  return false;
46              }
47              return true;
48          }
49      }

摘要

在本章中,您看到了我对 MongoDB 电子商务数据库的提议。当然,这只是一个草图,显然还有待改进。我介绍了提议的架构和数据库集合,并且我们已经创建了必要的实体和可嵌入的类。在下一章中,我们将继续开发基于这个数据库架构的企业应用。

八、MongoDB 电子商务数据库查询

在第七章中,我们为一个电子商务应用开发了一个 MongoDB 数据库模型。现在,我们将编写使用数据库的必要查询,并了解如何执行电子商务平台的常见任务,包括:

  • 显示产品类别。
  • 展示促销产品。
  • 显示一个类别中的产品(带分页)。
  • 按名称(或名称中的单词)搜索产品。
  • 查找客户(用于登录、编辑配置文件、保存订单等)。
  • 保存订单,以便将购物车与数据库同步。
  • 检查特定产品和数量的库存。
  • 从购物车中移除产品时恢复数量。

这些任务中的每一项都将在 Hibernate Search/Apache Lucene 查询中完成(由于 JP-QL 还不够完善,我们需要使用 Apache 提供的全文搜索引擎)。Hibernate 搜索查询将以 JPA 风格编写。

为了测试数据库,我开发了一个电子商务网站,灵感来自网球运动员拉斐尔·纳达尔(www.rafaelnadal-shop.com/en)的官方电子商店。网站基于:

  • Java EE 6 (EJB 3.0,JSF 2.0)
  • Hibernate OGM 4.0.0 Beta2
  • MongoDB 2.2.2
  • MongoDB Java 驱动程序 2.8.0
  • Hibernate 搜索 4.2.0 Beta 1
  • Apache Lucene 3.6.0
  • 原始面孔 3.4.2

如果你不熟悉 JSF 或超级面孔,也不用担心。没有它们,您也可以使用 JSP 和 servlet 等其他方法实现相同的功能。此外,您可以放弃 EJB,按照自己的意愿实现业务层。也可以使用 Hibernate 原生 API 来代替 JPA。这些技术并不是必需的,只要您理解了电子商务数据库模型和我们将要讨论的查询,您就可以使用您喜欢的技术将所有东西粘合到电子商务应用中。

您将在 Apress 资源库中找到名为RafaEShop的应用的完整源代码。该应用是作为 NetBeans 7.2.1 项目开发的,并在 GlassFish v3 下进行了测试。图 8-1 显示了这些类的交互。

9781430257943_Fig08-01.jpg

图 8-1 。RafaEShop 应用中的类的交互

出于本地主机测试的目的,遵循以下步骤(假设应用已经部署并且 MongoDB 服务器正在运行):

  1. 确保在 MongoDB 中没有名为eshop_db的数据库。

  2. Access the page http://localhost:8080/RafaEShop/faces/db.xhtml, as shown in Figure 8-2. (Obviously, you need to adjust the address and port to reflect your application server).

    9781430257943_Fig08-02.jpg

    图 8-2 。用于填充 eshop_db 数据库的用户界面

  3. 仅按一次标记为“的按钮,填充拉斐尔·纳达尔电子商店 MongoDB 数据库;“多次按下按钮会导致错误。

  4. 通过按标有“转到网站”的按钮导航到网站。"此按钮导航到网站起始页。

现在你应该会看到类似于图 8-3 所示的东西。

9781430257943_Fig08-03.jpg

图 8-3 。拉斐尔·纳达尔电子商店 GUI

如果您需要恢复数据库(无论出于什么原因),请遵循以下步骤:

  1. 删除eshop_db数据库。您可以从 MongoDB shell 中这样做,就像这样:

    mongo eshop_db
    db.dropDatabase()
    
  2. 导航到D root文件夹并删除eshop文件夹(这是 Lucene 索引数据的地方)。

  3. 从上面开始重复步骤 1-4。

现在让我们根据 Lucene 查询来“剖析”图 8-2 。

显示产品类别

第一个查询将从categories_c集合(Categories实体)中提取类别名称和 id。名称对用户可见,id 有助于识别类别,以便检索其产品;我们显示按名称排序的类别。你可以在EshopBean.java中找到这段代码,如清单 8-1 所示。

清单 8-1 。EshopBean.java

package eshop.beans;
...
public class EShopBean {
...
public List<String> extractCategories() {

   FullTextEntityManager fullTextEntityManager =
                                        org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

   QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().buildQueryBuilder().
                                        forEntity(Categories.class).get();

        org.apache.lucene.search.Query query = queryBuilder.all().createQuery();

        FullTextQuery fullTextQuery = fullTextEntityManager
.createFullTextQuery(query, Categories.class);
        fullTextQuery.setProjection(FullTextQuery.ID, "category");
        Sort sort = new Sort(new SortField("category", SortField.STRING));
        fullTextQuery.setSort(sort);

        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                        DatabaseRetrievalMethod.FIND_BY_ID);

        List<String> results = fullTextQuery.getResultList();

        return results;
    }
}

这个查询非常简单。我们通过投射类别名称和 id 来提取所有的Categories实例(按照类别名称排序)。在图 8-4 中,你可以看到类别是如何在浏览器中列出的。

9781430257943_Fig08-04.jpg

图 8-4 。显示产品类别

展示促销产品

除了类别名称,我们网站的第一页还包含促销产品列表;这些产品可以属于不同的类别。这是许多电子商务网站的常见做法,但你也可以展示最新的产品或畅销书。在这种情况下,通过检查products_c集合(Products实体)中文档的 MongoDB 字段product_old_price(产品实体中的old_price)很容易识别促销产品。所有旧价格大于 0 的产品都被认为是促销产品。因此,该查询看起来像清单 8-2 中的代码。

清单 8-2 。 查询用于展示的促销产品

package eshop.beans;
...
public class EShopBean {
...
public List<Products> extractPromotionalProducts() {

        FullTextEntityManager fullTextEntityManager = org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

        org.apache.lucene.search.Query query = NumericRangeQuery
.newDoubleRange("old_price", 0.0d, 1000d, false, true);
        FullTextQuery fullTextQuery = fullTextEntityManager
.createFullTextQuery(query, Products.class);
        Sort sort = new Sort(new SortField("price", SortField.DOUBLE));
        fullTextQuery.setSort(sort);

        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,                                           DatabaseRetrievalMethod.FIND_BY_ID);

        List results = fullTextQuery.getResultList();

        return results;
    }
}

请注意,促销产品是按价格升序显示的。显然,您可以用多种不同的方式在 web 浏览器中展示产品。在图 8-5 中,你可以看到我们的定制设计。请注意,促销产品的旧价格位于当前价格的右侧。

9781430257943_Fig08-05.jpg

图 8-5 。展示促销产品

如你所见,我们还没有为促销产品提供分页。接下来,我们将研究如何在显示选定类别的产品时提供分页,您可以在这里采用相同的机制。

显示某一类别的产品

当用户选择一个类别时,我们需要提供该类别下的产品列表。既然我们有了类别 id,提取产品就很容易了,如清单 8-3 所示。

清单 8-3 。 提取产品

package eshop.beans;
...
public class EShopBean {
...
public Map<Integer, List<Products>> extractProducts(String id, int page) {

        FullTextEntityManager fullTextEntityManager =
                                     org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
                                             buildQueryBuilder().forEntity(Products.class).get();
        org.apache.lucene.search.Query query = queryBuilder.keyword().
                                             onField("category.id").matching(id).createQuery();

        FullTextQuery fullTextQuery = fullTextEntityManager
.createFullTextQuery(query, Products.class);
        Sort sort = new Sort(new SortField("price", SortField.DOUBLE));
        fullTextQuery.setSort(sort);

        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                             DatabaseRetrievalMethod.FIND_BY_ID);

        fullTextQuery.setFirstResult(page * 3);
        fullTextQuery.setMaxResults(3);
        List<Products> results = fullTextQuery.getResultList();

        Map<Integer, List<Products>> results_and_total = new HashMap<Integer, List<Products>>();
        results_and_total.put(fullTextQuery.getResultSize(), results);

        return results_and_total;
    }
}

返回类型Map<Integer, List<Products>>可能看起来很奇怪,但实际上理解起来非常简单。由于一个类别可能包含许多产品,我们需要实现分页机制,并且每次查询只从数据库加载一个页面(页面大小设置为三个产品)。为了计算页面数量,我们需要知道所选类别中的产品数量,即使我们只提取其中的一部分。Lucene 能够返回产品的总数,即使你只查询一些产品。产品总数存储为返回的映射的键,而产品列表是该映射的值。下面是它的代码:

  • fullTextQuery.setFirstResult(int n);设置检索数据的第一个结果的位置,或者换句话说,它跳过结果集中的前“n”个元素。fullTextQuery.setMaxResults(int n);, which is用于设置从第一个结果开始检索的结果数。
  • 返回与查询匹配的所有结果的数量,即使我们只检索结果的子集。

例如,在图 8-6 中,你可以看到Racquets类别中的最后一个产品。在产品列表下,您可以看到上一页的导航链接和类型current_page of total_pages的分页状态:

9781430257943_Fig08-06.jpg

图 8-6。使用分页显示某个类别的产品

按名称搜索产品

电子商务网站必须执行的一项任务是提供一种简单的方法来搜索特定产品或大量产品,而无需浏览产品的类别和页面。通常,用户知道产品名称或者知道他在找什么。例如,他可能知道产品名为“ Babolat AeroPro Drive GT 球拍,”,或者他可能只知道他正在寻找一款“球拍。“最难的是用户只知道应该出现在产品名称中的关键词。

许多查询引擎用自定义查询来处理这样的问题,但是 Lucene 是专门为在文本中搜索而设计的,所以在文本中搜索关键字是小菜一碟。完成这种搜索最简单的方法是激活Products实体(set analyze = Analyze.YES)中产品字段的默认分析器。对于复杂的搜索,您可以编写自己的分析器,或者混合分析器等等。如果需要对关键字进行更细粒度的控制,可以使用通配符。

清单 8-4 中的代码定位名称中包含一个关键字(或由空格分隔的关键字列表)的产品(或一组产品)。(我武断地选择不对结果进行排序。)

清单 8-4 。 通过关键字定位产品

package eshop.beans;
...
public class EShopBean {
...
public List<Products> searchProducts(String search) {

        FullTextEntityManager fullTextEntityManager =
                                       org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
                                             buildQueryBuilder().forEntity(Products.class).get();
        org.apache.lucene.search.Query query = queryBuilder.keyword().
                                             onField("product").matching(search).createQuery();

        FullTextQuery fullTextQuery = fullTextEntityManager
.createFullTextQuery(query, Products.class);

        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                             DatabaseRetrievalMethod.FIND_BY_ID);
        fullTextQuery.setMaxResults(3);

        List results = fullTextQuery.getResultList();

        return results;
    }
}

我们搜索的一个限制是它最多返回三个结果(前三个)。如果您想返回更多,甚至全部,您将需要实现分页机制,以避免在单个查询中返回太多数据。

例如,我测试了关键词“ t 恤衫的搜索,得到了如图 8-7 所示的结果。

9781430257943_Fig08-07.jpg

图 8-7 。通过关键字搜索产品

通过电子邮件和密码找到客户

每个客户必须有一个唯一的帐户,在Customers实体(customers_c集合)中包含他的名字、姓氏、电子邮件地址、密码等等。当客户登录网站、查看或修改他的个人资料、下订单或采取其他行动时,我们需要能够从数据库中提取客户详细信息。清单 8-5 中的查询通过电子邮件地址和密码在customers_c集合中定位一个客户。

清单 8-5 。 寻找客户

package eshop.beans;
...
public class EShopBean {
...
public Customers extractCustomer(String email, String password) {

        FullTextEntityManager fullTextEntityManager =
                                       org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().buildQueryBuilder().
                                             forEntity(Customers.class).get();
        org.apache.lucene.search.Query query = queryBuilder.bool().must(queryBuilder.keyword()
                                            .onField("email").matching(email).createQuery()).
                                            must(queryBuilder.keyword()
                                            .onField("password").matching(password).createQuery()).createQuery();

        FullTextQuery fullTextQuery = fullTextEntityManager
.createFullTextQuery(query, Customers.class);

        fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
                                            DatabaseRetrievalMethod.FIND_BY_ID);

        List results = fullTextQuery.getResultList();

        if (results.isEmpty()) {
            return null;
        }

        return (Customers) results.get(0);
    }
}

定购

这个查询不需要 Lucene。当客户下订单时,应用应该有客户(因为他或她已登录);送货地址(由客户提供);和购物车(存储在客户的会话中)。有了这些,保持订单就非常容易了,就像这样:

package eshop.beans;
...
public class EShopBean {
...
private EntityManager em;
...
Orders new_order = new Orders();
...
//for each product
new_order.getCart().add( *cart_product*);
...
new_order.setShipping_address( *shipping_address*);
new_order.setCustomer( *customer*);

new_order.setOrderdate(Calendar.getInstance().getTime());
new_order.setSubtotal( *payment*);
new_order.setStatus("PURCHASED");

...
em.persist(new_order);
...
}

这个查询只影响单个文档,提供了原子性。

检查库存

只有在仓库库存中有产品时,客户才能将产品添加到购物车中。从程序上讲,这意味着我们需要知道产品细节和所需数量;检查它在库存中是否可用;如果是,从库存中删除该数量。

然而,从库存中删除会导致数据不一致,这显然是不可取的。这可以通过使用乐观锁定(甚至悲观锁定)来避免,但是当抛出乐观锁定异常时,这是要付出代价的。一个简单的解决方案是提供一条消息,如“产品未添加到您的购物车。抱歉给您带来不便,请再试一次。。。",或者等待几秒钟,重复查询一定次数,或者直到库存中不再有该产品。第一个解决方案给客户一个快速的响应,而第二个解决方案则让他等待。我选择返回一条消息,敦促用户再试一次。代码如清单 8-6 所示。

清单 8-6 。 盘点库存

package eshop.beans;
...
public class EShopBean {
...
public int checkInventory(String sku, String color, String size, int quantity) {

        InventoryPK pk = new InventoryPK(sku, color, size);

        Inventory inventory = em.find(Inventory.class, pk, LockModeType.OPTIMISTIC);
        int amount = inventory.getInventory();
        if (amount > 0) {
            if (amount >= quantity) {
                amount = amount - quantity;
                inventory.setInventory(amount);
                try {
                    em.merge(inventory);
                } catch (OptimisticLockException e) {
                    return -9999;
                }

                return quantity;
            } else {
                inventory.setInventory(0);
                try {
                    em.merge(inventory);
                } catch (OptimisticLockException e) {
                    return -9999;
                }

                return amount;
            }
        } else {
            return amount;
        }
    }
}

当库存包含的产品少于所需数量时,我们只将可用的数量添加到购物车中,并通过消息通知用户。图 8-8 显示了当用户试图将产品添加到购物车时可能出现的消息。

9781430257943_Fig08-08.jpg

图 8-8 。将产品添加到购物车时可能出现的消息

当然,有许多方法可以改善这一点,例如在每个产品旁边显示一条消息,说明“有货或“无货”,并为后者取消激活Add to Cart按钮。

恢复库存

客户可以在下订单之前从购物车中放下产品,或者如果用户分心而没有及时完成订单,会话可能会过期(我们的应用没有实现这种情况)。当这种情况发生时,我们需要通过将丢弃的产品添加回库存来恢复库存。实际上,该过程与从库存中移除产品相反,因此可能会出现相同的数据不一致问题。乐观锁定(或悲观锁定)可以解决这个问题,但是,我们必须再次处理一个可能的乐观锁定异常。很明显,你不能给顾客回复一条信息说,“抱歉,我们不能从你的购物车中删除产品。。。“因为那样会很烦。在我们的例子中,我们只是从购物车中删除产品(因为它存储在会话中),并且只尝试一次恢复库存。但是您可以重复查询,将数量存储在其他地方,稍后再尝试恢复它;或者您可以使用内存中的二级库存;或者找到任何其他符合您需求的方法。

下面是恢复库存的代码:

package eshop.beans;
...
public class EShopBean {
...
public int refreshInventory(String sku, String color, String size, int quantity) {

        InventoryPK pk = new InventoryPK(sku, color, size);

        Inventory inventory = em.find(Inventory.class, pk, LockModeType.OPTIMISTIC);
        int amount = inventory.getInventory();

        amount = amount + quantity;

        inventory.setInventory(amount);

        try {
            em.merge(inventory);
        } catch (OptimisticLockException e) {
            return -9999;
        }

        return quantity;
    }
}

当产品从购物车中移除时(即使库存实际上无法恢复),用户应该会看到类似于图 8-9 中的消息。

9781430257943_Fig08-09.jpg

图 8-9 。指示从购物车中移除产品成功的消息

此时,我们有了一组与许多电子商务网站相当的查询。显然,还可以添加许多其他的,或者使用这个数据库模型,或者通过修改模型本身。

开发管理 GUI 的注意事项

到目前为止,我们只从一个客户(用户)的角度谈论了电子商务平台。但行政方面对电子商务平台也很重要。只需编写适当的查询,您就可以基于我们的数据库模型开发一个强大的管理 GUI。例如,我们的数据库模型简化了管理员必须完成的最常见的任务:

  • 您可以轻松地创建一个新的类别,重命名或删除现有的类别,等等。
  • 您可以在类别中插入新产品、删除现有产品或修改产品特征。
  • 您可以查看或修改客户资料和订单。
  • 您可以轻松地填充库存和跟踪状态。
  • 您可以创建几个关于销售、畅销书等的统计数据。

所有这些任务都可以自动完成(每个查询只影响一个文档)。

摘要

在本章中,您学习了如何查询在第七章中建模的 MongoDB 电子商务数据库。您看到了编写 Lucene 查询来实现电子商务平台的主要功能并避免交易是多么容易。使用 MongoDB 每个文档的原子性、嵌入式集合、嵌套文档和一些棘手的查询,我们能够创建一个电子商务站点,它提供了真正的电子商务平台的大多数公共设施。此时,您可以轻松地编写一个管理端,添加一个强大的登录机制,修改某些参数,比如产品页面大小等等。

九、将 MongoDB 数据库迁移到云

在本章中,您将看到如何将一个 MongoDB 数据库从您的本地计算机迁移到两个云平台,MongoHQ 和 MongoLab。云计算通常意味着硬件和软件资源可以作为服务通过网络(通常是互联网)获得。

我将向您展示如何将在第七章的中开发的 MongoDB eshop_db数据库迁移到云中,但是您可以使用任何其他数据库,只要您按顺序遵循这些步骤。将该过程应用于任何其他 MongoDB 数据库都非常容易。

将 MongoDB 数据库迁移到 MongoHQ 云

我要介绍的第一个云计算平台是 MongoHQ ( www.mongohq.com/home )。当你访问这个链接时,你应该会看到类似于图 9-1 所示的东西。

9781430257943_Fig09-01.jpg

图 9-1 。MongoHQ 云平台—首页

假设您在本地计算机上有一个 MongoDB 数据库(例如,eshop_db数据库),并且您希望它在 MongoHQ 云平台上运行。以下是您需要遵循的步骤:

  1. To create a free account, first press the Sign Up button. You’ll see a simple form like the one in Figure 9-2. Fill out the form and create the account (for this exercise, you can skip the credit card information).

    9781430257943_Fig09-02.jpg

    图 9-2 。MongoHQ 云平台—创建新账户

  2. Use these credentials to authenticate yourself in the MongoHQ system. Enter your e-mail address and password and press the Sign In button, as shown in Figure 9-3.

    9781430257943_Fig09-03.jpg

    图 9-3 。MongoHQ 云平台—登录

  3. After you log in, you’ll see the New Database panel, where you can choose a database type. For testing purposes, you can choose a free database, such as Sandbox or Azure Sandbox. Once you select a database type, additional information will be provided below it. As you can see in Figure 9-4, I chose Sandbox.

    9781430257943_Fig09-04.jpg

    图 9-4 。MongoHQ 云平台—选择沙盒数据库类型

  4. After selecting the database type, scroll down and locate the Name your database input text field. Type the name of the MongoDB database exactly as you want it to appear in the cloud (see Figure 9-5). Then press the Create Database button and wait until the empty database is prepared for you.

    9781430257943_Fig09-05.jpg

    图 9-5 。MongoHQ 云平台—命名您的 MongoDB 数据库

  5. After a few seconds, the database should be ready. A popup will inform you that the database is empty, but you can copy an external database or a MongoHQ database or start creating collections. In addition, the popup displays the information you need to connect to the database either from the MongoDB shell or by using a MongoDB URI (see Figure 9-6). The MongoDB URI is specific to each user, which means you have to adjust each command to your own URI.

    9781430257943_Fig09-06.jpg

    图 9-6 。MongoHQ 云平台 MongoDB 数据库已经可以使用了

  6. 现在,我们不需要这个弹出窗口。在其左侧,找到Collections标签下的Admin标签并将其打开。Admin向导提供了所有可用于操作数据库的操作,包括那些来自弹出窗口的操作。

  7. Now you have to create at least one user for your database. To do so, switch to the Users tab and fill in the fields, as shown in Figure 9-7. Press the Add user button.

    9781430257943_Fig09-07.jpg

    图 9-7 。MongoHQ 云平台—为 MongoDB 数据库创建一个新用户

  8. If the user is successfully created, you’ll see the entry, as shown in Figure 9-8.

    9781430257943_Fig09-08.jpg

    图 9-8 。MongoHQ 云平台—新用户文档

  9. So far, so good! Now you can export the eshop_db collections from your local computer to the brand-new eshop_db database created in the MongoHQ cloud. You can accomplish this task by using two MongoDB utilities: mongodump and mongorestore. Both are available as executables in the { MongoDB_HOME }/bin folder. Start the MongoDB server, open a shell command, and navigate to the /bin folder.

    image 注意你可以在http://docs.mongodb.org/manual/reference/mongodump/http://docs.mongodb.org/manual/reference/mongorestore/找到更多关于 MongoDB 手册中mongodumpmongorestore实用程序的信息。

  10. Use the mongodump utility to export the eshop_db database content in binary format (you can get either JSON or CSV as the output format using the mongoexport command). The output of this utility should be stored in a separate folder. I specified a folder named eshop_tmp within the { MongoDB_HOME} folder (it will be automatically created). Here’s the complete command (shown also in Figure 9-9):

```java
mongodump -h localhost:27017 -d eshop_db -o ../eshop_tmp
```

![9781430257943_Fig09-09.jpg](https://gitee.com/OpenDocCN/vkdoc-javaweb-zh/raw/master/docs/pro-hbn-mongodb/img/9781430257943_Fig09-09.jpg)

图 9-9 。以二进制格式导出 eshop_db 数据库(仍在本地计算机上)
  1. The database, in binary format, can now be imported to the cloud using the mongorestore utility. Basically, mongorestore is used to import the content from a binary database dump into a specific database. Here’s the command (also shown in Figure 9-10):
```java
mongorestore -h linus.mongohq.com:10039 -d eshop_db -u admin -p eshop ../eshop_tmp/eshop_db
```

![9781430257943_Fig09-10.jpg](https://gitee.com/OpenDocCN/vkdoc-javaweb-zh/raw/master/docs/pro-hbn-mongodb/img/9781430257943_Fig09-10.jpg)

图 9-10 。在 MongoHQ 云中导入 eshop_db 数据库

Each collection was successfully imported。导航到Collections选项卡可以看到收藏的名称,如图图 9-11 所示。

9781430257943_Fig09-11.jpg

图 9-11 。MongoHQ 中列出的 eshop_db 数据库集合

任务完成!eshop_db数据库在 MongoHQ 云中。

注意,您可以在Admin向导中完成许多其他任务:删除数据库、克隆数据库、创建集合等等。每个任务都非常直观,并有友好的 MongoHQ 界面辅助。

将 MongoDB 数据库迁移到 MongoLab 云

MongoLab ( https://mongolab.com/welcome/)是我将在本章介绍的第二个云计算平台。当您访问该链接时,您应该会看到类似于图 9-12 中所示的内容。

9781430257943_Fig09-12.jpg

图 9-12 。MongoLab 云平台-起始页

我们将在一台本地计算机上从一个 MongoDB 数据库开始,比如eshop_db数据库。同样,你想让它运行在云上。以下是使用 MongoLab 完成此操作的步骤:

  1. To create a free account, first press the Sign Up button. You’ll see a simple form, such as the one in Figure 9-13. Fill out the form and create the account.

    9781430257943_Fig09-13.jpg

    图 9-13 。MongoLab 云平台—创建新账户

  2. Use these credentials to authenticate yourself in the MongoLab system. Fill in the username and password and press the Log In button, as shown in Figure 9-14.

    9781430257943_Fig09-14.jpg

    图 9-14 。MongoLab 云平台—登录表单

  3. After logging in, you’ll see the Databases administration panel where you can create new databases, remote connections, and dedicated clusters. For testing purposes, you can create a new MongoDB database by pressing the Create new button in the Databases section (see Figure 9-15).

    9781430257943_Fig09-15.jpg

    图 9-15 。MongoLab 云平台—数据库部分

  4. Next, you need to fill in some fields and make some selections in the Create Shared Plan database wizard. Start by typing the database name as eshop_db, then select the cloud provider. I just accepted the default. Select the free, shared plan because it’s perfect for testing purposes. Finally, create at least one user for this database by filling in the fields in the New database user section. I used admin for the username and eshop for the password. Press the Create database button (see Figure 9-16).

    9781430257943_Fig09-16.jpg

    图 9-16 。MongoLab 云平台—创建新的 MongoDB 数据库

  5. After a few seconds the database is created and listed in the Databases section, as shown in Figure 9-17:

    9781430257943_Fig09-17.jpg

    图 9-17 。MongoLab 云平台—MongoLab 中列出的 eshop_db 数据库

  6. Select this database to see further details, such as the connection information, collections, system collections, users, stats, and so on (see Figure 9-18). This information is specific to your account.

    9781430257943_Fig09-18.jpg

    图 9-18 。MongoLab 云平台 eshop _ db 数据库详细信息

您已经准备好将eshop_db数据库内容导入到 MongoLab 云中。正如您之前所做的,您可以使用mongodumpmongorestore实用程序。假设你已经使用mongodump将数据库内容导出为二进制格式,你需要做的就是根据数据库名称下面列出的连接信息调用mongorestore,如图图 9-18 所示。下面是mongostore命令(也显示在图 9-19 ):

mongorestore -h ds029107.mongolab.com:29107 -d eshop_db -u admin -p eshop ../eshop_tmp/eshop_db

9781430257943_Fig09-19.jpg

图 9-19 。在 MongoLab cloud 中导入 eshop_db 数据库内容

快速刷新页面会在eshop_db下显示导入的集合,如图 9-20 中的所示。

9781430257943_Fig09-20.jpg

图 9-20 。MongoLab 中列出的 eshop_db 数据库集合

任务完成!eshop_db数据库现在位于 MongoLab 云中。

注意,Tools向导提供了关于在 MongoLab 中导入和导出数据的详细信息。除了mongodumpmongorestore,你还可以访问mongoimportmongoexport工具。

连接到 MongoHQ 或 MongoLab 云数据库

只要正确地将连接数据(主机、端口、用户和密码)集成到应用上下文中,就可以轻松地测试部署到 MongoHQ 或 MongoLab 云的eshop_db数据库的连接。清单 9-1 中的应用基于 MongoDB Java 驱动程序。它连接到eshop_db数据库并显示集合大小(文档的数量)。如果提供的值不起作用,调整MONGO_*常量以符合您的常量。

清单 9-1。 。测试与 eshop_db 数据库的连接

package testcloudauth;

import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import java.net.UnknownHostException;

public class TestCloudAuth {

    //for MongoHQ
    private static final String MONGO_HOST_HQ = "linus.mongohq.com";
    private static final int MONGO_PORT_HQ = 10039;
    private static final String MONGO_USER_HQ = "admin";
    private static final String MONGO_PASSWORD_HQ = "eshop";
    private static final String MONGO_DATABASE_HQ = "eshop_db";

    //for MongoLab
    private static final String MONGO_HOST_LAB = "ds029107.mongolab.com";
    private static final int MONGO_PORT_LAB = 29107;
    private static final String MONGO_USER_LAB = "admin";
    private static final String MONGO_PASSWORD_LAB = "eshop";
    private static final String MONGO_DATABASE_LAB = "eshop_db";

    public static void main(String[] args) {
        try {

            Mongo mongo_hq = new Mongo(MONGO_HOST_HQ, MONGO_PORT_HQ);
            DB db_hq = mongo_hq.getDB(MONGO_DATABASE_HQ);
            Mongo mongo_lab = new Mongo(MONGO_HOST_LAB, MONGO_PORT_LAB);
            DB db_lab = mongo_lab.getDB(MONGO_DATABASE_LAB);

            boolean auth_hq = db_hq.authenticate(MONGO_USER_HQ,
                                           MONGO_PASSWORD_HQ.toCharArray());
            boolean auth_lab = db_lab.authenticate(MONGO_USER_LAB,
                                           MONGO_PASSWORD_LAB.toCharArray());

            if (auth_hq) {

                System.out.println("Connected at MongoHQ:");
                DBCollection collection_categories_c_hq = db_hq.getCollection("categories_c");
                DBCollection collection_customers_c_hq = db_hq.getCollection("customers_c");
                DBCollection collection_inventory_c_hq = db_hq.getCollection("inventory_c");
                DBCollection collection_products_c_hq = db_hq.getCollection("products_c");
                DBCollection collection_orders_c_hq = db_hq.getCollection("orders_c");
                System.out.println("TOTAL DOCUMENTS IN categories_c (MongoHQ):" +
                                              collection_categories_c_hq.count());
                System.out.println("TOTAL DOCUMENTS IN customers_c (MongoHQ):" +
                                              collection_customers_c_hq.count());
                System.out.println("TOTAL DOCUMENTS IN inventory_c (MongoHQ):" +
                                              collection_inventory_c_hq.count());
                System.out.println("TOTAL DOCUMENTS IN products_c (MongoHQ):" +
                                              collection_products_c_hq.count());
                System.out.println("TOTAL DOCUMENTS IN orders_c (MongoHQ):" +
                                              collection_orders_c_hq.count());
            } else {
                System.out.println("Sorry, connection to MongoHQ (eshop_db database) failed ...");
            }

            if (auth_lab) {
                System.out.println("Connected at Mongolab:");
                DBCollection collection_categories_c_lab = db_lab.getCollection("categories_c");
                DBCollection collection_customers_c_lab = db_lab.getCollection("customers_c");
                DBCollection collection_inventory_c_lab = db_lab.getCollection("inventory_c");
                DBCollection collection_products_c_lab = db_lab.getCollection("products_c");
                DBCollection collection_orders_c_lab = db_lab.getCollection("orders_c");
                System.out.println("TOTAL DOCUMENTS IN categories_c (Mongolab):" +
                                              collection_categories_c_lab.count());
                System.out.println("TOTAL DOCUMENTS IN customers_c (Mongolab):" +
                                              collection_customers_c_lab.count());
                System.out.println("TOTAL DOCUMENTS IN inventory_c (Mongolab):" +
                                              collection_inventory_c_lab.count());
                System.out.println("TOTAL DOCUMENTS IN products_c (Mongolab):" +
                                              collection_products_c_lab.count());
                System.out.println("TOTAL DOCUMENTS IN orders_c (Mongolab):" +
                                              collection_orders_c_lab.count());
            } else {
                System.out.println("Sorry, connection to Mongolab (eshop_db database) failed ...");
            }
        } catch (UnknownHostException | MongoException e) {
            System.err.println(e.getMessage());
        }
    }
}

如果连接成功建立,输出将类似于您在图 9-21 中看到的内容。

9781430257943_Fig09-21.jpg

图 9-21 。TestCloudAuth 应用的输出 ??

这个名为TestMongoHQAuth 的应用的完整源代码可以在 Apress 资源库中找到。它是一个 NetBeans 项目,并针对所展示的案例进行了测试。

可以通过 JPA 或 Hibernate Native API 在 Hibernate OGM 中配置相同的连接。例如,可以修改persistence.xml文件以连接到 MongoHQ 下的eshop_db数据库,如下所示:

...
<property name="hibernate.ogm.mongodb.database" value="eshop_db"/>
<property name="hibernate.ogm.mongodb.host" value="linus.mongohq.com"/>
<property name="hibernate.ogm.mongodb.port" value="10039"/>
<property name="hibernate.ogm.mongodb.username" value="admin"/>
<property name="hibernate.ogm.mongodb.password" value="eshop"/>
...

摘要

在本章中,您看到了如何将 MongoDB 数据库从本地计算机迁移到 MongoHQ 和 MongoLab 云平台。在这两种情况下,我都使用免费账户,并将在《??》第七章中建模的eshop_db数据库的二进制版本导出到云中。我使用 MongoDB mongodump实用程序来获得这个数据库的二进制版本,并且使用 MongoDB mongorestore实用程序来实现导出。此外,您还看到了如何从 Java 应用测试连接并对每个云提供商进行一些查询。该应用使用 Java MongoDB 驱动程序,但是我也向您展示了如何使用 JPA persistence.xml文件配置相同的连接。

十、在 OpenShift 上迁移 RafaEShop 应用

在第九章中,您看到了如何将 MongoDB 数据库迁移到两个云平台——MongoHQ 和 MongoLab。顾名思义,这些平台是基于云的托管数据库解决方案,专用于 MongoDB,这意味着使用这些数据库的应用必须托管在另一个地方。但是如果你没有这样的地方,或者你想把整个应用(不仅仅是数据库)放在云中,你就必须更多地关注云计算平台,比如 Red Hat 的 OpenShift。如www.openshift.com网站所述,“ OpenShift 是 Red Hat 为应用提供的免费、自动扩展的平台即服务(PaaS)。作为云中的一个应用平台,OpenShift 管理堆栈,这样你就可以专注于你的代码

OpenShift 允许您使用几乎任何编程语言、框架和中间件,支持多种架构和服务器,为各种类型的应用提供现成的模板,维护用于开发和迁移应用的专用工具,并始终专注于为开发人员提供帮助和文档。

OpenShift 使用插件的概念来指代所有受支持的服务器、框架、数据库管理系统等等。例如,GlassFish AS、MongoDB、MySQL、SwitchYard、Cron、RockMongo 和 JBoss AS 都是模块,应用构建在一个或多个模块上。正如您将看到的,OpenShift 提供了一个用户界面,用于添加、删除和配置应用模块,但它真正的力量来自于 OpenShift 客户端工具,称为 rhc 。对于简单的应用(如一些 web 应用),使用 OpenShift GUI 非常方便,而对于更复杂的应用(带有数据库、web 服务等的 web 应用),GUI 和 rhc 的混合提供了完全的控制。

在本章中,您将看到如何将RafaEShop应用迁移到 OpenShift 云。该应用在运行于localhost上的 GlassFish AS 3 上托管和测试,但现在我们将把它迁移到运行于云中的 GlassFish AS 3 和 JBoss AS 7 上。目的是对源代码(MongoDB 连接凭证、配置文件等)进行必要的修改,并将这些代码迁移到云中,首先在 GlassFish AS 3 上作为 Web 归档(WAR),其次在 JBoss AS 7 上作为 WAR 和 Maven 项目。最后,您将在云中拥有三个应用:一个部署在 GlassFish AS 3 上,两个部署在 JBoss AS 7 上。

在 OpenShift 上创建免费帐户

在开始使用 OpenShift 之前,您需要在www.openshift.com/创建一个免费帐户。这可以通过按下SIGN UP链接快速轻松地完成,这将打开如图图 10-1 所示的表单。

9781430257943_Fig10-01.jpg

图 10-1 。创建免费的 OpenShift 账户

请注意,您必须提供一个有效的电子邮件地址,因为您将收到一封来自 OpenShift 的电子邮件,其中包含一个用于激活帐户的链接(图 10-2 )。

9781430257943_Fig10-02.jpg

图 10-2 。通过电子邮件激活您的帐户

创建并激活您的帐户后,您必须接受法律条款和条件。阅读它们,然后按下I Accept按钮(图 10-3 )。

9781430257943_Fig10-03.jpg

图 10-3 。OpenShift 法律术语

一旦你接受法律条款,你将被重定向到你的个人管理控制台,在那里你可以看到和创建应用,获得帮助,并修改帐户设置(见图 10-4 )。默认情况下,Create Application向导将被激活。

9781430257943_Fig10-04.jpg

图 10-4 。用户管理控制台选项卡

从现在开始,点击 OpenShift 起始页上的MY APPS链接,使用您的邮箱和密码登录即可进入管理控制台页面,如图图 10-5 所示。

9781430257943_Fig10-05.jpg

图 10-5 。登录到 OpenShift

如果登录成功,应该会看到Create Application向导 ( 图 10-6 )。该向导会自动启动,因为您还没有任何可用的应用。一旦你这样做,默认向导将是My Applications。请注意,一个免费的 OpenShift 帐户允许您在云中最多拥有三个应用。当您达到三个应用时,您不能创建新的应用,除非您删除或缩减现有的应用。(您将得到的消息是:“目前您没有足够的可用空闲设备来创建新的应用。您可以缩减或删除现有应用来释放资源。)

9781430257943_Fig10-06.jpg

图 10-6 。OpenShift 创建应用向导

在创建你的第一个应用之前,你必须创建一个名称空间,它是你的帐户独有的,并且是 OpenShift 将分配给你的应用的公共 URL 的后缀。(如果您现在不创建名称空间,稍后会提示您创建名称空间。)首先,在你的个人管理控制台中,切换到My Account向导,如图 图 10-7 所示。(如果您没有看到此图片,请点击Domain部分的Create a domain for your application链接)。

9781430257943_Fig10-07.jpg

图 10-7 。打开转移我的帐户向导

键入有效的名称空间并保存。正如你在图 10-7 中看到的,我输入了 hogm 。名称空间成功创建后,您会看到一条类似于图 10-8 中的消息。(请注意,在Public Keys消息的位置,您可能会看到一个SSH Keys部分,其中有一个指向upload your public key to access the code的链接。)

9781430257943_Fig10-08.jpg

图 10-8 。域已成功创建

在图 10-8 中有两条重要信息。第一个确认域创建成功,第二个告诉您需要一个 SSH 公钥来安全地加密本地机器和应用之间的连接。稍后我将向您展示如何从 shell 中完成这项工作,这样您就可以注销了。

此时,您已经有了一个帐户和一个域,但是在您可以开始迁移RafaEShop应用之前还有几个步骤。要从本地 shell 与 OpenShift 平台通信,您需要在您的机器上安装和配置 OpenShift RHC 客户端工具(RHC)。这些工具是使用 Ruby 编程语言构建和打包的,将帮助您完成许多任务,比如将应用上传到云或者从云中移除应用;监控服务器状态和日志;控制可用服务(启动/停止/重启);添加和删除安全权限;转发端口等等。这些工具提供的一些功能也可以通过 OpenShift 向导获得,但是这些工具的功能超出了 OpenShift web GUIs。

在 Windows 上安装 OpenShift RHC 客户端工具

在本节中,您将了解如何在 Windows 上安装 OpenShift RHC 客户端工具。您还将安装 Git 版本控制系统,rhc 使用它来为控制您的应用提供强大的命令行支持。

安装 Ruby

因为 rhc 是用 Ruby 构建和打包的,所以需要在电脑上安装 Ruby。推荐版本是 Ruby 1.9.x 我安装了 Ruby 1.9.3-p392,在http://rubyinstaller.org/downloads/可用。对于 Windows,Ruby 是一个可执行文件,因此安装过程由一个直观的向导监控和指导。在安装过程中,许多设置都有适合大多数情况的默认值,但是您必须选中“将 Ruby 可执行文件添加到您的路径中”复选框,以便从 shell 中运行 Ruby(图 10-9 )。

9781430257943_Fig10-09.jpg

图 10-9 。正在安装 Ruby 将 Ruby 可执行文件添加到路径中

image 注意如果你决定安装 1.9.x 以后的 Ruby 版本(例如 Ruby 2.0.0),你可能会得到一个类型为“ DL 被弃用,请在 shell 中使用小提琴”的警告。其他一切都应该像预期的那样工作。

安装 Git

正如www.git-scm.com网站上引用的那样,“Git 是一个免费的开源分布式版本控制系统,旨在快速高效地处理从小到大的项目。” OpenShift rhc 需要 Git 为你的源代码提供版本控制,所以你需要从www.git-scm.com/downloads下载安装。

我下载了 Git1.8.1.2,并使用安装向导安装了它。在安装过程中,确保将 Git 添加到您的PATH中,这样您就可以从 shell 中运行它(图 10-10 )。

9781430257943_Fig10-10.jpg

图 10-10 。正在安装 Git 将 Git 可执行文件添加到路径中

如果你不想改变你的PATH,,你可以使用 Git Bash shell(图 10-10 中的第一个单选按钮),它会在你的桌面上放一个快捷方式。

从 Shell 中测试 Ruby 和 Git

在继续之前,最好通过执行一些简单的命令来快速测试一下 Ruby 和 Git。要测试 Ruby,打开一个 shell 并输入下面的命令,这也显示在图 10-11 中:

ruby -e 'puts "Hello from Ruby"'

9781430257943_Fig10-11.jpg

图 10-11 。测试 Ruby

此命令的输出也在此图中可见。

现在输入下面的命令,这也显示在图 10-12 中,以测试 Git 已经成功安装并且可以从 shell 中获得:

git –version

9781430257943_Fig10-12.jpg

图 10-12 。测试 Git

在该图中也可以看到预期的输出。

有些情况下,在安装过程中向窗口PATH添加 Ruby 和 Git 路径不会产生预期的结果。在这种情况下,Ruby 和/或 Git 将无法从 shell 中获得,您将得到一条错误消息,而不是预期的结果,该消息指出“‘Ruby 或 Git’未被识别为内部或外部命令、可操作程序或批处理文件。“如果这发生在你身上,继续读下去。如果您得到了预期的结果,请跳到下一部分。

至少有两种方法可以解决这个问题。我喜欢用批处理(*.bat)文件。这个想法很简单:

  • 在你的计算机上的任何地方创建一个名为autoexec.bat的文件(这个名字实际上不必是 autoexec )。
  • 在这个文件中,添加一行类似于图 10-13 中的SET PATH条目,调整 Ruby 和 Git 路径以符合你的路径。

9781430257943_Fig10-13.jpg

图 10-13 。创建一个窗口。蝙蝠文件

  • 打开一个 shell,导航到.bat文件的位置,输入文件名(参见图 10-14 )。

9781430257943_Fig10-14.jpg

图 10-14 。运行批处理文件

现在,应该可以从 shell 访问 Ruby 了。请记住,每次打开新的 shell 时,都需要运行这个命令来访问 Ruby 和 Git。这可能会很痛苦,但很有效。如果您已经有这样一个.bat文件,那么只需在SET PATH部分中添加这些条目。比如我的autoexec.bat如图图 10-15 所示。

9781430257943_Fig10-15.jpg

图 10-15 。设置路径的 Windows 批处理文件示例

另一种方法是使用 Windows 向导:

  1. Desktop中,右键点击My Computer并点击Properties

  2. Click the Advanced System Settings link in the left column (Figure 10-16).

    9781430257943_Fig10-16.jpg

    图 10-16 。Windows 7 控制面板

  3. System Properties窗口中,点击Environment Variables按钮。

  4. Locate the Path variable (Figure 10-17) and add to it the Ruby and Git paths.

    9781430257943_Fig10-17.jpg

    图 10-17 。在 Windows 7 中添加 Ruby 和 Git 路径

  5. 重启机器,从 shell 中测试 Ruby 和 Git。

安装 OpenShift Gem

最后,我们将安装 OpenShift gem 。正确安装 Ruby 和 Git 之后,我们将使用 RubyGems 包管理器(包含在 Ruby 中)来安装 OpenShift 客户端工具。这很简单,由一个简单的命令gem install rhc组成,如图图 10-18 所示。该命令从www.rubygems.org/gems/rhc下载并安装 rhc gem。

9781430257943_Fig10-18.jpg

图 10-18 。下载并安装 OpenShift gem

安装完成后,运行rhc setup命令(第一次安装 rhc 工具时推荐使用)。为此,请使用您的电子邮件地址和密码登录,然后系统会提示您回答几个问题。第一个是关于在你的磁盘上创建一个令牌,用于不使用你的密码访问服务器。键入 yes ,令牌将被保存在C:/Users/{USER}/.openshift/express.conf文件中,如图图 10-19 所示。

9781430257943_Fig10-19.jpg

图 10-19 。rhc 安装外壳向导;生成令牌

还记得安全加密本地机器和应用之间的通信所需的 SSH 公钥吗?现在应该通知您没有这样的密钥,OpenShift 可以为您创建一个 SSH 密钥并上传到服务器。键入是;SSH 密钥将保存在本地的C:/Users/{USER}/.ssh/id_rsa.pub文件中,并上传到服务器,如图图 10-20 所示。

9781430257943_Fig10-20.jpg

图 10-20 。rhc 安装外壳向导;创建和上传 SSH 密钥

在几条信息性消息和一个可以在 OpenShift 上创建的应用列表之后,您应该会看到一条类似于“您的客户端工具现在已经配置好了”的消息(参见图 10-21 )。

9781430257943_Fig10-21.jpg

图 10-21 。rhc 安装外壳向导;配置成功

该消息确认 rch 已成功创建,并已准备好开始开发应用。

SSH 密钥已成功生成并上传。你可以在你的个人管理控制台的My Account向导中查看,如图图 10-22 所示。

9781430257943_Fig10-22.jpg

图 10-22 。使用我的帐户向导访问您的 SSH 密钥

image 本节介绍仅在 Windows 上安装 rhc 工具。但是 rhc 也可以在其他操作系统上运行,比如 Mac OS X、Fedora 16、17 和 18、Red Hat Enterprise Linux 6.4、Ubuntu 等等。要了解如何在这些操作系统上安装 rhc,请参见www.openshift.com/developers/rhc-client-tools-install处的说明。

修复已知问题

在 Windows 7 中,当你试图执行 Git 命令时,很有可能得到错误“权限被拒绝(publickey,gssapi-keyex,gssapi-with-mic) ”。解决这个问题最简单的方法是将两个名为id_rsa的文件从 C :/Users/{USER}/.ssh文件夹复制到{GIT_HOME}/.ssh文件夹。这应该可以解决问题!

使用 JBoss AS 7 将 RafaEShop 应用迁移到 OpenShift】

现在让我们看看如何将 RafaEShop 应用从您的计算机迁移到 JBoss AS 7 上的 OpenShift cloud。我们将看两个场景:一个是作为 WAR (Web Archive )迁移这个应用,另一个是作为 Maven 项目。

image 注意当然,如果你只对其中一个场景感兴趣,就只阅读那个场景,忽略其他所有的参考。

不过,首先要完成两者共有的几个步骤。

步骤 1:创建一个基本文件夹。在你的一个本地磁盘上创建一个名为JBossAS的文件夹(比如D:/JBossAS)。我们将使用它作为我们两个场景的基本文件夹。

第二步:创建两个场景文件夹。在D:/JBossAS文件夹中,创建两个子文件夹,一个名为war,另一个名为mvn

步骤 3:基于 JBoss Application Server 7.1 插件创建一个默认项目。

在部署一个应用(如 RafaEShop )之前,需要创建一个默认的 JBoss Application Server 7.1 应用。这是 OpenShift 支持的之一,如图图 10-23 所示。

9781430257943_Fig10-23.jpg

图 10-23 。JBoss 应用服务器 7.1 卡盒

这一步可以从 OpenShift GUI 或 shell 中完成。我更喜欢后者,所以打开一个 shell 并导航到D:/JBossAS/war文件夹。使用 rhc 工具通过以下命令创建新项目,该命令也显示在图 10-24 中:

rhc app create -a RafaEShopW -t jbossas-7

9781430257943_Fig10-24.jpg

图 10-24 。创建 JBoss Application Server 7.1 默认应用

image 注意在此步骤中,您可能会收到问题“您确定要继续连接吗(是/否)?“答案是肯定的。OpenShift 需要将该主机添加到可信主机列表中。

切换到D:/JBossAS/mvn文件夹并重复这一步,这次键入以下内容:

rhc app create -a RafaEShopM -t jbossas-7

现在您有了两个相同的默认应用。我们将把 RafaEShop 应用作为 WAR 文件部署在RafaEShopW 下,作为 Maven 项目部署在RafaEShopM 下。

此时,如果您检查D:/JBossAS/warD:/JBossAS/mvn文件夹,您会看到应用是在RafaEShopWRafaEShopM子文件夹中创建的。在这里你会发现几个文件夹和文件,在D:/JBossAS/war/RafaEShopW/README.txtD:/JBossAS/war/RafaEShopM/README.txt文件中有描述,我从中复制了下面的片段:

deployments/ -建造战争的地点

src/ - Maven src 结构

pom.xml - Maven 构建文件

.openshift/-open shift 特定文件的位置

.openshift/config/ -配置文件的位置,如standalone.xml(用于修改 jboss 配置,如数据源)

.openshift/action_hooks/pre_build -在构建之前每次 git 推送时运行的脚本(在 CI 系统上,如果可用的话)

.openshift/action_hooks/build -作为构建过程的一部分,在每次 git 推送时运行的脚本(在 CI 系统上,如果可用的话)

.openshift/action_hooks/deploy -在构建之后、应用重启之前,每次 git 推送时运行的脚本

.openshift/action_hooks/post_deploy -应用重启后每次 git 推送时运行的脚本

.openshift/action_hooks/pre_start_jbossas-7 -在启动 AS7 之前运行的脚本

openshift/action_hooks/post_start_jbossas-7-AS7 启动后运行的脚本

.openshift/action_hooks/pre_stop_jbossas-7 -在停止 AS7 之前运行的脚本

.openshift/action_hooks/post_stop_jbossas-7-AS7 停止后运行的脚本

.openshift/markers -用于控制应用行为的文件目录。请参见标记目录中的自述文件

阅读整个文件的完整细节。应用链接在您的个人管理控制台中可用,如图图 10-25 所示。这些链接是功能性的,它们打开应用的默认欢迎页面。

9781430257943_Fig10-25.jpg

图 10-25 。两个 JBoss 应用服务器 7.1 应用链接

如果你点击一个应用的链接,你会看到应用的详细信息,比如与应用相关联的 Git 存储库,你将能够管理模块(参见图 10-26 )。

9781430257943_Fig10-26.jpg

图 10-26 。RafaEShopW 应用详细信息

现在您已经看到了默认 JBoss Application Server 7.1 应用的结构,是时候进一步了解了。

第四步:添加一个 MongoDB NoSQL 数据库 2.2 插件(见图 10-27 )。

9781430257943_Fig10-27.jpg

图 10-27 。MongoDB NoSQL 数据库 2.2 卡盒

这将添加一个准备好填充数据的 MongoDB 服务器实例。默认情况下,OpenShift 将创建一个与应用同名的 MongoDB 数据库。正如你在图 10-27 中看到的,有一个Select按钮可以让你使用一个专用的向导来添加这个墨盒。我将让您自己探索这种方法,同时向您展示如何使用 rhc 在 shell 中实现这一点。使用下面的 rhc 命令,如图 10-28 所示,将 MongoDB 添加到 RafaEShopW 应用中:

rhc cartridge add -a RafaEShopW -c mongodb-2.2

9781430257943_Fig10-28.jpg

图 10-28 。使用 rhc 工具添加 MongoDB NoSQL 数据库 2.2 插件

重复这个步骤,通过切换到D:/JBossAS/mvn文件夹并键入以下命令,为 RafaEShopM 应用获得一个 MongoDB 实例:

rhc cartridge add -a RafaEShopM -c mongodb-2.2

注意,添加了 MongoDB 数据库,您可以通过列出的凭证访问它。MongoDB 插件现在可以在您的个人管理控制台中使用,如图 10-29 所示。

9781430257943_Fig10-29.jpg

图 10-29 。MongoDB 插件列在 RafaEShopW 应用中

第五步:添加 RockMongo 1.1 弹药筒,如图图 10-30 所示。(这一步是可选的。)

9781430257943_Fig10-30.jpg

图 10-30 。RockMongo 1.1 卡盒

添加 RockMongo 管理工具并不是强制性的,但是通过一个友好的 web GUI 访问 MongoDB 数据库非常有用,这样可以轻松管理数据库内容(添加和删除集合、查询数据、管理用户、导入和导出数据等等)。您可以使用下面的命令从外壳添加这个插件(对于 RafaEShopW 应用),这个命令也显示在图 10-31 中:

rhc cartridge add -a RafaEShopW -c rockmongo-1.1

9781430257943_Fig10-31.jpg

图 10-31 。使用 rhc 工具添加 RockMongo 1.1 插件

当然,你也可以通过按下Select按钮来尝试可视化的方法。

添加 RockMongo 插件后,您可以从列出的 URL https://RafaEShopW-hogm.rhcloud.com/rockmongo/访问它。(凭证与 MongoDB 托管实例相同:用户:管理员,密码: hi_qnUdFqEBg )。在图 10-32 中,我访问了属于 RafaEShopW 应用的 MongoDB 实例的 RockMongo 接口。

9781430257943_Fig10-32.jpg

图 10-32 。RafaEShopW 数据库的 RockMongo 接口

重复这个步骤,将 RockMongo 插件添加到 RafaEShopM 应用中。切换到D:/JBossAS/mvn并键入以下内容:

rhc cartridge add -a RafaEShopM -c rockmongo-1.1

登录 OpenShift 查看 RockMongo 弹夹,如图图 10-33 所示。

9781430257943_Fig10-33.jpg

图 10-33 。RockMongo 墨盒列在 RafaEShopW 应用中

第六步:将org.hibernate:ogm模块添加到 JBoss AS 7 中。

在第四章中,在“在内置 JTA 环境中 Hibernate OGM(EJB 3,JBoss AS 7),”一节中,您看到了如何在 JBoss AS 7 中添加 Hibernate OGM 的模块。必须将相同的模块添加到D:/JBossAS/war/RafaEShopW/.openshift/config/modules文件夹(以及RafaESHopM的等效文件夹)。简单复制{JBOSS_HOME}/modules/org/hibernate/main{JBOSS_HOME}/modules/org/hibernate/ogm文件夹,如图图 10-34 所示。

9781430257943_Fig10-34.jpg

图 10-34 。将 Hibernate OGM 特有的模块添加到 JBoss AS 7

第七步:调整persistence.xml设置。你需要根据你的云应用修改persistence.xml文件。我建议你复印一份这份文件。原件位于{RafaEShop_HOME}/src/conf文件夹中,目前包含清单 10-1 中显示的设置。

清单 10-1。 原文persistence.xml File

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence"
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="HOGM_eSHOP-ejbPU" transaction-type="JTA">
        <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
        <class>eshop.entities.Categories</class>
        <class>eshop.entities.Customers</class>
        <class>eshop.entities.Inventory</class>
        <class>eshop.entities.Orders</class>
        <class>eshop.entities.Products</class>
    <properties>
        <property name="hibernate.search.default.directory_provider" value="filesystem"/>
        <property name="hibernate.search.default.indexBase" value="D:/eshop"/>
        <property name="hibernate.search.default.locking_strategy" value="single"/>
        <property name="hibernate.transaction.jta.platform"
                        value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform"/>
        <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
        <property name="hibernate.ogm.datastore.grid_dialect"
                        value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
        <property name="hibernate.ogm.mongodb.database" value="eshop_db"/>
        <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
        <property name="hibernate.ogm.mongodb.port" value="27017"/>
    </properties>
</persistence-unit>
</persistence>

您必须调整几个设置,如以下一组说明所示(这是针对RafaEShopW应用的)。

Apache Lucene 索引存储在文件系统的D:/eshop文件夹中。您需要使用有效的云文件夹路径修改此文件夹路径(基本文件夹)。或者,为了更简单,您可以通过替换以下代码来使用基于内存的目录:

<property name="hibernate.search.default.directory_provider" value="filesystem"/>
<property name="hibernate.search.default.indexBase" value="D:/eshop"/>
<property name="hibernate.search.default.locking_strategy" value="single"/>

使用此代码:

<property name="hibernate.search.default.directory_provider" value="ram"/>
<property name="hibernate.search.default.locking_strategy" value="single"/>

因为应用是在 JBoss AS 上部署的,所以您需要通过替换以下设置来调整 JTA 平台:

<property name="hibernate.transaction.jta.platform"
        value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform"/>

使用此设置:

<property name="hibernate.transaction.jta.platform"
        value="org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform"/>

此外,您需要添加一些属性来帮助 JBoss AS 定位和使用org.hibernate:ogm模块,如第四章中的所述,在“在内置的 JTA 环境中 Hibernate OGM(EJB 3,JBoss AS 7) 一节中:”

<property name="jboss.as.jpa.adapterModule" value="org.jboss.as.jpa.hibernate:4"/>
<property name="jboss.as.jpa.providerModule" value="org.hibernate:ogm"/>
<property name="jboss.as.jpa.classtransformer" value="false"/>
<property name="hibernate.listeners.envers.autoRegister" value="false"/>

最后,您需要设置 MongoDB 数据库名称、主机、端口、用户和密码。为此,请替换以下代码:

<property name="hibernate.ogm.mongodb.database" value="eshop_db"/>
<property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
<property name="hibernate.ogm.mongodb.port" value="27017"/>

使用此代码:

<property name="hibernate.ogm.mongodb.database" value="RafaEShopW"/>
<property name="hibernate.ogm.mongodb.host" value="127.7.182.129"/>
<property name="hibernate.ogm.mongodb.port" value="27017"/>
<property name="hibernate.ogm.mongodb.username" value="admin"/>
<property name="hibernate.ogm.mongodb.password" value="hi_qnUdFqEBg"/>

image 注意如果使用 RockMongo 连接到 MongoDB 服务器,可以很容易地获得 MongoDB 远程服务器的 IP 地址。在图 10-32 中,你可以看到在Host字段中列出的 IP 地址 127.7.182.129。端口始终是 27017,而用户名和密码是用于与 RockMongo 连接的端口,由 OpenShift 在您添加 MongoDB 插件时提供。

“新的”persistence.xml如清单 10-2 中的所示。

清单 10-2。persistence.xml File

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">
    <persistence-unit name="HOGM_eSHOP-ejbPU" transaction-type="JTA">
        <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
        <class>eshop.entities.Categories</class>
        <class>eshop.entities.Customers</class>
        <class>eshop.entities.Inventory</class>
        <class>eshop.entities.Orders</class>
        <class>eshop.entities.Products</class>
    <properties>
        <property name="hibernate.search.default.directory_provider" value="ram"/>
        <property name="hibernate.search.default.locking_strategy" value="single"/>
        <property name="jboss.as.jpa.adapterModule" value="org.jboss.as.jpa.hibernate:4"/>
        <property name="jboss.as.jpa.providerModule" value="org.hibernate:ogm"/>
        <property name="jboss.as.jpa.classtransformer" value="false"/>
        <property name="hibernate.listeners.envers.autoRegister" value="false"/>
        <property name="hibernate.transaction.jta.platform"
                        value="org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform"/>
        <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
        <property name="hibernate.ogm.datastore.grid_dialect"
                        value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
        <property name="hibernate.ogm.mongodb.database" value="RafaEShopW"/>
        <property name="hibernate.ogm.mongodb.host" value="127.7.182.129"/>
        <property name="hibernate.ogm.mongodb.port" value="27017"/>
        <property name="hibernate.ogm.mongodb.username" value="admin"/>
        <property name="hibernate.ogm.mongodb.password" value="hi_qnUdFqEBg"/>
    </properties>
</persistence-unit>
</persistence>

RafaEShopM应用重复步骤 7。您需要修改的只是 MongoDB 数据库名称和凭证。现在,您有两个persistence.xml文件,一个用于RafaEShopW应用,一个用于RafaEShopM应用。把它们放在手边。

现在我们已经完成了 WAR 和 Maven 项目共有的步骤。

监控 JBoss AS 7 日志

OpenShift 将重启 JBoss 作为每个 Git 提交会话的实例。更具体地说,当您提交更改时,OpenShift 将 JBoss 作为实例停止,上传并处理更改,然后再次作为实例启动 JBoss。如果在重启过程中出现问题(例如,如果代码中有错误或者 JAR 丢失),那么 JBoss AS 实例启动时会出现错误,这意味着应用将无法在线使用。在这种情况下,出于调试目的,将 JBoss 作为日志来查看是非常有帮助的;否则,很难知道发生了什么。

您可以通过向您的应用打开一个安全外壳(SSH)会话 来(实时)监控服务器日志。

image 注意找到连接到应用所需的特定 SSH 命令的最简单方法是访问应用页面并从那里复制(如图图 10-35 中的RafaEShopW应用所示)。关于使用 SSH 进行远程访问的更多详细信息,请访问www.openshift.com/developers/remote-access

9781430257943_Fig10-35.jpg

图 10-35 。为应用查找特定的 SSH 命令(RafaEShopW 示例)

这可以从外壳使用下面的ssh命令来完成,它也显示在图 10-36 中:

ssh 514ffd57500446021e000087@RafaEShopW-hogm.rhcloud.com

这是为了向 RafaEShopW 应用打开一个安全 Shell 会话;对 RafaEShopM 应用做同样的事情非常直观。

9781430257943_Fig10-36.jpg

图 10-36 。从 shell 执行 ssh 命令

接下来,键入tail_all命令,如图图 10-37 所示。请注意,该命令将跟踪当前应用的所有可用日志。(特定于 JBoss AS 的日志是jbossas-7/logs/boot.logjbossas-7/logs/server.log.)

9781430257943_Fig10-37.jpg

图 10-37 。将 JBoss 作为日志结尾

现在,您可以实时监控日志。不要在提交过程中关闭监控器,因为该进程“连接”到日志文件;只需为其他命令打开另一个 shell。

image 注意除了tail_all,安全 Shell 会话还允许你执行其他命令。要查看这些命令的列表及其描述,请在 SSH 会话建立后键入help命令。

提交更改

在本地应用文件夹中所做的每个更改都应该在 OpenShift 上提交(您必须将该文件夹的内容与 OpenShift 上的应用同步)。为此,您可以使用 Git 命令(打开一个 shell 来监控服务器日志,如果您还没有这样做的话)。

打开一个新的 shell 并导航到D:/JBossAS/war/RafaEShopW文件夹(这对RafaEShopM应用也有效)。键入命令git add .,如图图 10-38 所示,为下一次提交准备暂存的内容。不要担心低频 CRLF 警告。

9781430257943_Fig10-38.jpg

图 10-38 。执行 git 添加。命令

值得一提(详情见www.kernel.org/pub/software/scm/git/docs/git-add.html):

git add -A阶段所有

git add.阶段新增和修改,不删除

git add -u修改和删除的阶段,没有新增

键入命令git commit -m "first commit"(文本 first commit 可以是任何文本,只要该命令的每次执行都不同)。在这种情况下,更改作为模块文件存储和列出在新的 JBoss 中,如图 10-39 中的所示。

9781430257943_Fig10-39.jpg

图 10-39 。执行 git commit -m“第一次提交”命令

值得一提(详情见www.kernel.org/pub/software/scm/git/docs/git-commit.html):

如果您提交了一个命令,然后立即发现了一个错误,那么您可以使用git reset命令来恢复它。

使用git push命令将更改传播到 OpenShift(参见图 10-40 )。在此命令的执行过程中(可能需要几秒到几分钟),您可以检查实时更新的服务器日志。注意服务器在push期间是如何停止和启动的。如果没有检测到任何更改,您将看到一条消息,通知您所有内容都是最新的。

9781430257943_Fig10-40.jpg

图 10-40 。执行 git push 命令

值得一提(详情见www.kernel.org/pub/software/scm/git/docs/git-push.html):

从远程储存库中删除所有列出的更改。

将 RafaEShop 应用迁移为一场战争

做完这些辛苦的工作,是时候准备RafaEShop战了,准备在云端部署在 JBoss AS 7 下。首先,在您的本地项目RafaEShop/dist文件夹中或者在RafaEShop/dist文件夹中的一个 press 存储库中找到 WAR。将这场战争复制到D:/JBossAS/war/RafaEShopW/deployments文件夹。最后,覆盖RafaEShop WAR 存档中的persistence.xml文件(可以使用任何存档工具,比如 WinRAR)。

在发起云战争之前,还有一个步骤需要完成。您需要在/lib文件夹中再添加两个 jar,分别命名为jackson-core-asl-1.9.12.jar ( http://mvnrepository.com/artifact/org.codehaus.jackson/jackson-core-asl)和jackson-mapper-asl-1.9.12.jar ( http://mvnrepository.com/artifact/org.codehaus.jackson/jackson-mapper-asl)。

最后,提交更改,如图 10-41 所示。

9781430257943_Fig10-41.jpg

图 10-41 。进行拉斐尔商店战争

应用已成功部署并启动。JBoss 服务器日志应该类似于图 10-42 中所示。

9781430257943_Fig10-42.jpg

图 10-42 。JBoss AS 日志

如果您仅对这个应用感兴趣,您可以跳到“测试它!”部分。

将 RafaEShop 应用作为 Maven 项目进行迁移

现在我将重点介绍一下 RafaEShopM应用。快速浏览一下/mvn/RafaEShopM文件夹,会发现一个pom.xml文件和一个/src文件夹,其中有三个子文件夹:/main/java/main/resources,/main/webapp。这其实是 OpenShift 创建的默认应用;这是一个运行在 JBoss AS 7 上的简单演示,可以作为开发人员的起点。

正如您所看到的,这个演示有一个 Maven 项目结构,这意味着我们应该能够用RafaEShop应用替换它。为此,我们必须在正确的位置添加RafaEShop组件,并相应地调整pom.xml

以下是将RafaEShop应用部署为 Maven 项目的步骤:

步骤 1:找到RafaEShop NetBeans 项目。您可以从 Apress 存储库中下载RafaEShop NetBeans 项目。

第二步:清空/webapp文件夹的内容。删除D:/JBossAS/mvn/RafaEShopM/src/main/webapp文件夹的当前内容即可。

第 3 步:复制RafaEShop源。将文件夹{RafaEShop_HOME}/src/java/eshop复制到D:/JBossAS/mvn/RafaEShopM/src/main/java文件夹,如图图 10-43 所示。

9781430257943_Fig10-43.jpg

图 10-43 。将/eshop 文件夹从 RafaEShop 应用复制到 RafaEShopM 应用

第四步:复制RafaEShop /web文件夹内容。将{RafaEShop_HOME}/web文件夹的内容复制到D:/JBossAS/mvn/RafaEShopM/src/main/webapp文件夹,如图图 10-44 所示。

9781430257943_Fig10-44.jpg

图 10-44 。将/web 文件夹内容从 RafaEShop 应用复制到 RafaEShopM 应用

第五步:创建/META-INF文件夹。在D:/JBossAS/mvn/RafaEShopM/src/main/resources中创建一个名为META-INF的空文件夹(参见图 10-45 )。

9781430257943_Fig10-45.jpg

图 10-45 。创建空的 META-INF 文件夹

第 6 步:复制persistence.xml.在前面的章节中,“使用 JBoss AS 7 将 RafaEShop 应用迁移到 OpenShift,,您为RafaEShopM应用创建了一个persistence.xml文件。现在,将其复制到D:/JBossAS/mvn/RafaEShopM/src/main/resources/META-INF文件夹中(参见图 10-46 )。

9781430257943_Fig10-46.jpg

图 10-46 。复制 persistence.xml 文件

第七步:调整pom.xml.编辑默认的pom.xml文件,如清单 10-3 所示。您必须添加必要的依赖项(Hibernate OGM、Hibernate Search 和 PrimeFaces)。

清单 10-3。 编辑pom.xml File

<project FontName2">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>RafaEShopM</groupId>
  <artifactId>RafaEShopM</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>
  <name>RafaEShopM</name>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.6</maven.compiler.source>
    <maven.compiler.target>1.6</maven.compiler.target>
  </properties>

  <repositories>
       <repository>
            <id>prime-repo</id>
            <name>PrimeFaces Maven Repository</name>
            <url> http://repository.primefaces.org</url >
            <layout>default</layout>
        </repository>
       <repository>
          <id>jboss-public-repository-group</id>
          <name>JBoss Public Maven Repository Group</name>
          <url> https://repository.jboss.org/nexus/content/groups/public-jboss/</url >
          <layout>default</layout>
          <releases>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
          </releases>
          <snapshots>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
          </snapshots>
        </repository>
    </repositories>

    <!--
    <pluginRepositories>
        <pluginRepository>
          <id>jboss-public-repository-group</id>
          <name>JBoss Public Maven Repository Group</name>
          <url> https://repository.jboss.org/nexus/content/groups/public-jboss/</url >
          <layout>default</layout>
          <releases>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
          </releases>
          <snapshots>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
          </snapshots>
        </pluginRepository>
      </pluginRepositories>
       -->

 <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.primefaces</groupId>
                <artifactId>primefaces</artifactId>
                <version>3.4.2</version>
            </dependency>
            <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-search</artifactId>
    <version>4.2.0.Beta1</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate.ogm</groupId>
                <artifactId>hibernate-ogm-core</artifactId>
                <version>4.0.0.Beta2</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate.ogm</groupId>
                <artifactId>hibernate-ogm-mongodb</artifactId>
                <version>4.0.0.Beta1</version>
            </dependency>
        </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.primefaces</groupId>
      <artifactId>primefaces</artifactId>
      <version>3.4.2</version>
    </dependency>
 <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-search</artifactId>
   <version>4.2.0.Beta1</version>
      </dependency>
   <dependency>
        <groupId>org.hibernate.ogm</groupId>
        <artifactId>hibernate-ogm-core</artifactId>
        <version>4.0.0.Beta2</version>
      </dependency>
      <dependency>
        <groupId>org.hibernate.ogm</groupId>
        <artifactId>hibernate-ogm-mongodb</artifactId>
        <version>4.0.0.Beta1</version>
      </dependency>
    <dependency>
      <groupId>org.jboss.spec</groupId>
      <artifactId>jboss-javaee-6.0</artifactId>
      <version>1.0.0.Final</version>
      <type>pom</type>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  <profiles>
    <profile>
     <!-- When built in OpenShift the 'openshift' profile will be used when invoking mvn. -->
     <!-- Use this profile for any OpenShift specific customization your app will need. -->
     <!-- By default that is to put the resulting archive into the 'deployments' folder. -->
     <!-- http://maven.apache.org/guides/mini/guide-building-for-different-environments.html -->
     <id>openshift</id>
     <build>
        <finalName>RafaEShopM</finalName>
        <plugins>
          <plugin>
            <artifactId>maven-war-plugin</artifactId>
            <version>2.1.1</version>
            <configuration>
              <outputDirectory>deployments</outputDirectory>
              <warName>RafaEShop</warName>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
</project>

最后,提交更改,如图图 10-47 所示。

9781430257943_Fig10-47.jpg

图 10-47 。将 RafaEShop 提交为 Apache Maven 项目

应用已成功部署并启动。JBoss 服务器日志应该类似于图 10-48 中所示。

9781430257943_Fig10-48.jpg

图 10-48 。RafaEShop 已成功部署

如果您仅对这个应用感兴趣,现在可以跳到“测试它!”部分。

使用 GlassFish 3 作为将 RafaEShop 应用迁移到 OpenShift

在本章的第一部分,你看到了 OpenShift 为 JBoss AS 提供了出色的支持。只需几分钟,您就可以获得一个在 JBoss AS 上运行的默认应用,并且只需几次点击和命令,您就可以构建和部署自己的应用作为 WARs,甚至作为 Apache Maven 项目。

在本章的第二部分,我们将了解如何将RafaEShop应用从您的计算机迁移到 GlassFish 3 AS 上的 OpenShift cloud。在撰写本文时,OpenShift 没有提供默认的 GlassFish 插件,但它允许您使用自己动手的应用类型扩展 OpenShift 以支持 GlassFish(或其他不支持的语言、框架和中间件),如图图 10-49 所示。

9781430257943_Fig10-49.jpg

图 10-49 。自己动手做墨盒

以下是创建此类应用的步骤:

第 1 步:像准备 OpenShift 一样准备 GlassFish。在创建 DIY 应用之前,您需要为使用 OpenShift 准备好 GlassFish。有几处修改会影响 GlassFish 域配置,所以不建议您在本地 GlassFish 发行版上执行这些修改。最好从download.java.net/glassfish/3.1.2.2/release/glassfish-3.1.2.2.zip下载一个新的 GlassFish 发行版(本例中使用的是 3.1.2.2 版本),并在计算机上一个方便的地方解压缩 ZIP 存档内容。

以下是你需要做的修改:

  • 将 HTTP 侦听器绑定到应用 IP(由环境变量$OPENSHIFT_INTERNAL_IP表示)。
  • 禁用管理员控制台。
  • 禁用其他侦听器。
  • 将一些端口更新为允许的端口。

这些修改将影响单个 GlassFish 文件,{GlassFish_HOME}/glassfish/domains/domain1/config/domain.xml.在我展示修改列表之后,这个文档的修改版本将被完整显示。您将在http://docs.oracle.com/cd/E19798-01/821-1753/abhar/index.html找到关于这个文件的内容和格式的更多细节。

您应该进行以下修改。这些修改由 OpenShift 博客在https://www.openshift.com/blogs处指明,并按照从文档顶部到底部的顺序显示:

修改 1:将 localhost 替换为 OPENSHIFT_INTERNAL_IP ,如图图 10-50 所示。

9781430257943_Fig10-50.jpg

图 10-50 。修改 1

修改 2:将 localhost 替换为 OPENSHIFT_INTERNAL_IP ,如图图 10-51 所示。

9781430257943_Fig10-51.jpg

图 10-51 。修改 2

修改 3:删除“ http-listener-2 ”,如图图 10-52 所示。

9781430257943_Fig10-52.jpg

图 10-52 。修改 3

修改 4:注释如图 10-53 所示的线条。

9781430257943_Fig10-53.jpg

图 10-53 。修改 4

修改 5:将 0.0.0.0 替换为 OPENSHIFT_INTERNAL_IP ,将 8686 替换为 7600 ,如图图 10-54 所示。

9781430257943_Fig10-54.jpg

图 10-54 。修改 5

修改 6:将 localhost 替换为 OPENSHIFT_INTERNAL_IP ,将 7676 替换为 5445 ,如图图 10-55 所示。

9781430257943_Fig10-55.jpg

图 10-55 。修改 6

修改 7:将 127.0.0.1 替换为 OPENSHIFT_INTERNAL_IP ,如图图 10-56 所示。

9781430257943_Fig10-56.jpg

图 10-56 。修改 7

修改 8:对图 10-57 中的 <协议></协议>、行进行注释。

9781430257943_Fig10-57.jpg

图 10-57 。修改 8

修改 9:增加address = " open shift _ INTERNAL _ IP ",,如图图 10-58 所示。

9781430257943_Fig10-58.jpg

图 10-58 。修改 9

修改 10:注释 <网络监听器></网络监听器>、如图图所示的线路。

9781430257943_Fig10-59.jpg

图 10-59 。修改 10

修改 11:删除 http-listener-2,如图图 10-60 所示。

9781430257943_Fig10-60.jpg

图 10-60 。修改 11

修改 12:注释线,如图 10-61 所示。

9781430257943_Fig10-61.jpg

图 10-61 。修改 12

修改 13:将 127.0.0.1 替换为 OPENSHIFT_INTERNAL_IP ,如图图 10-62 所示。

9781430257943_Fig10-62.jpg

图 10-62 。修改 13

修改 14:对图 10-63 中 <协议></协议>、行进行注释。

9781430257943_Fig10-63.jpg

图 10-63 。修改 14

修改 15:将 0.0.0.0 替换为 OPENSHIFT_INTERNAL_IP ,将 ${HTTP_LISTENER_PORT} 替换为 9999 ,如图 10-64 所示。

9781430257943_Fig10-64.jpg

图 10-64 。修改 15

修改 16:注释 <网络监听器></网络监听器>、如图 10-65 所示的线路。

9781430257943_Fig10-65.jpg

图 10-65 。修改 16

image 真实的环境变量是$OPENSHIFT_INTERNAL_IP。字符串OPENSHIFT_INTERNAL_IP只是一个占位符,因此您可以使用任何其他文本。

所有的修改完成后,domain.xml就变成了你在清单 10-4 中看到的样子。

清单 10-4。 修改后的domain.xml File

<!--

    DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.

    Copyright (c) 2010-2012 Oracle and/or its affiliates. All rights reserved.

    The contents of this file are subject to the terms of either the GNU
    General Public License Version 2 only ("GPL") or the Common Development
    and Distribution License("CDDL") (collectively, the "License").  You
    may not use this file except in compliance with the License.  You can
    obtain a copy of the License at
    https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
    or packager/legal/LICENSE.txt.  See the License for the specific
    language governing permissions and limitations under the License.

    When distributing the software, include this License Header Notice in each
    file and include the License file at packager/legal/LICENSE.txt.

    GPL Classpath Exception:
    Oracle designates this particular file as subject to the "Classpath"
    exception as provided by Oracle in the GPL Version 2 section of the License
    file that accompanied this code.

    Modifications:
    If applicable, add the following below the License Header, with the fields
    enclosed by brackets [] replaced by your own identifying information:
    "Portions Copyright [year] [name of copyright owner]"

    Contributor(s):
    If you wish your version of this file to be governed by only the CDDL or
    only the GPL Version 2, indicate your decision by adding "[Contributor]
    elects to include this software in this distribution under the [CDDL or GPL
    Version 2] license."  If you don't indicate a single choice of license, a
    recipient has the option to distribute your version of this file under
    either the CDDL, the GPL Version 2 or to extend the choice of license to
    its licensees as provided above.  However, if you add GPL Version 2 code
    and therefore, elected the GPL Version 2 license, then the option applies
    only if the new code is made subject to such option by the copyright
    holder.

-->

<?xml version="1.0" encoding="UTF-8"?>
<domain log-root="${com.sun.aas.instanceRoot}/logs" application-root="${com.sun.aas.instanceRoot}/applications" version="10.0">
  <system-applications />
  <applications />
  <resources>
    <jdbc-resource pool-name="__TimerPool" jndi-name="jdbc/__TimerPool" object-type="system-admin" />
    <jdbc-resource pool-name="DerbyPool" jndi-name="jdbc/__default" />
    <jdbc-connection-pool name="__TimerPool" datasource-classname="org.apache.derby.jdbc.EmbeddedXADataSource" res-type="javax.sql.XADataSource">
      <property value="${com.sun.aas.instanceRoot}/lib/databases/ejbtimer" name="databaseName" />
      <property value=";create=true" name="connectionAttributes" />
    </jdbc-connection-pool>
    <jdbc-connection-pool is-isolation-level-guaranteed="false" name="DerbyPool" datasource-classname="org.apache.derby.jdbc.ClientDataSource" res-type="javax.sql.DataSource">
      <property value="1527" name="PortNumber" />
      <property value="APP" name="Password" />
      <property value="APP" name="User" />
      <property value="OPENSHIFT_INTERNAL_IP" name="serverName" />
      <property value="sun-appserv-samples" name="DatabaseName" />
      <property value=";create=true" name="connectionAttributes" />
    </jdbc-connection-pool>
  </resources>
  <servers>
    <server name="server" config-ref="server-config">
      <resource-ref ref="jdbc/__TimerPool" />
      <resource-ref ref="jdbc/__default" />
    </server>
  </servers>
  <nodes>
    <node name="localhost-domain1" type="CONFIG" node-host="OPENSHIFT_INTERNAL_IP" install-dir="${com.sun.aas.productRoot}" />
  </nodes>
  <configs>
    <config name="server-config">
      <http-service>
        <access-log />
        <virtual-server id="server" network-listeners="http-listener-1" />
        <virtual-server id="__asadmin" network-listeners="admin-listener" />
      </http-service>
      <admin-service auth-realm-name="admin-realm" type="das-and-server" system-jmx-connector-name="system">
        <jmx-connector auth-realm-name="admin-realm" security-enabled="false" address="OPENSHIFT_INTERNAL_IP" port="7600" name="system" />
        <property value="/admin" name="adminConsoleContextRoot" />
        <property value="${com.sun.aas.installRoot}/lib/install/applications/admingui.war" name="adminConsoleDownloadLocation" />
        <property value="${com.sun.aas.installRoot}/.." name="ipsRoot" />
      </admin-service>
      <connector-service shutdown-timeout-in-seconds="30" />
      <web-container>
        <session-config>
          <session-manager>
            <manager-properties />
            <store-properties />
          </session-manager>
          <session-properties />
        </session-config>
      </web-container>
      <ejb-container steady-pool-size="0" max-pool-size="32" session-store="${com.sun.aas.instanceRoot}/session-store" pool-resize-quantity="8">
        <ejb-timer-service />
      </ejb-container>
      <mdb-container steady-pool-size="0" max-pool-size="32" pool-resize-quantity="8" />
      <jms-service type="EMBEDDED" default-jms-host="default_JMS_host">
        <jms-host name="default_JMS_host" host="OPENSHIFT_INTERNAL_IP" port="5445" admin-user-name="admin" admin-password="admin" lazy-init="true" />
      </jms-service>
      <security-service>
        <auth-realm classname="com.sun.enterprise.security.auth.realm.file.FileRealm" name="admin-realm">
          <property value="${com.sun.aas.instanceRoot}/config/admin-keyfile" name="file" />
          <property value="fileRealm" name="jaas-context" />
        </auth-realm>
        <auth-realm classname="com.sun.enterprise.security.auth.realm.file.FileRealm" name="file">
          <property value="${com.sun.aas.instanceRoot}/config/keyfile" name="file" />
          <property value="fileRealm" name="jaas-context" />
        </auth-realm>
        <auth-realm classname="com.sun.enterprise.security.auth.realm.certificate.CertificateRealm"name="certificate" />
        <jacc-provider policy-configuration-factory- provider="com.sun.enterprise.security.provider.PolicyConfigurationFactoryImpl" policy-provider="com.sun.enterprise.security.provider.PolicyWrapper" name="default">
          <property value="${com.sun.aas.instanceRoot}/generated/policy" name="repository" />
        </jacc-provider>
        <jacc-provider policy-configuration-factory-provider="com.sun.enterprise.security.jacc.provider.SimplePolicyConfigurationFactory" policy-provider="com.sun.enterprise.security.jacc.provider.SimplePolicyProvider" name="simple" />
        <audit-module classname="com.sun.enterprise.security.Audit" name="default">
          <property value="false" name="auditOn" />
        </audit-module>
        <message-security-config auth-layer="SOAP">
          <provider-config provider-id="XWS_ClientProvider" class-name="com.sun.xml.wss.provider.ClientSecurityAuthModule" provider-type="client">
            <request-policy auth-source="content" />
            <response-policy auth-source="content" />
            <property value="s1as" name="encryption.key.alias" />
            <property value="s1as" name="signature.key.alias" />
            <property value="false" name="dynamic.username.password" />
            <property value="false" name="debug" />
          </provider-config>
          <provider-config provider-id="ClientProvider" class-name="com.sun.xml.wss.provider.ClientSecurityAuthModule" provider-type="client">
            <request-policy auth-source="content" />
            <response-policy auth-source="content" />
            <property value="s1as" name="encryption.key.alias" />
            <property value="s1as" name="signature.key.alias" />
            <property value="false" name="dynamic.username.password" />
            <property value="false" name="debug" />
            <property value="${com.sun.aas.instanceRoot}/config/wss-server-config-1.0.xml"name="security.config" />
          </provider-config>
          <provider-config provider-id="XWS_ServerProvider" class-name="com.sun.xml.wss.provider.ServerSecurityAuthModule" provider-type="server">
            <request-policy auth-source="content" />
            <response-policy auth-source="content" />
            <property value="s1as" name="encryption.key.alias" />
            <property value="s1as" name="signature.key.alias" />
            <property value="false" name="debug" />
          </provider-config>
          <provider-config provider-id="ServerProvider" class-name="com.sun.xml.wss.provider.ServerSecurityAuthModule" provider-type="server">
            <request-policy auth-source="content" />
            <response-policy auth-source="content" />
            <property value="s1as" name="encryption.key.alias" />
            <property value="s1as" name="signature.key.alias" />
            <property value="false" name="debug" />
            <property value="${com.sun.aas.instanceRoot}/config/wss-server-config-1.0.xml"name="security.config" />
          </provider-config>
        </message-security-config>
        <message-security-config auth-layer="HttpServlet">
          <provider-config provider-type="server" provider-id="GFConsoleAuthModule" class-name="org.glassfish.admingui.common.security.AdminConsoleAuthModule">
            <request-policy auth-source="sender" />
            <response-policy />
            <property name="restAuthURL" value=" http://localhost:${ADMIN_LISTENER_PORT}/management/sessions " />
            <property name="loginPage" value="/login.jsf" />
            <property name="loginErrorPage" value="/loginError.jsf" />
          </provider-config>
        </message-security-config>
        <property value="SHA-256" name="default-digest-algorithm" />
      </security-service>
      <transaction-service tx-log-dir="${com.sun.aas.instanceRoot}/logs" />
      <java-config classpath-suffix="" system-classpath="" debug-options="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=9009">
        <jvm-options>-XX:MaxPermSize=192m</jvm-options>
        <jvm-options>-XX:PermSize=64m</jvm-options>
        <jvm-options>-client</jvm-options>
        <jvm-options>-Djava.awt.headless=true</jvm-options>
        <jvm-options>-Djavax.management.builder.initial=com.sun.enterprise.v3.admin.AppServerMBeanServerBuilder</jvm-options>
        <jvm-options>-XX:+UnlockDiagnosticVMOptions</jvm-options>
        <jvm-options>-Djava.endorsed.dirs=${com.sun.aas.installRoot}/modules/endorsed${path.separator}${com.sun.aas.installRoot}/lib/endorsed</jvm-options>
        <jvm-options>-Djava.security.policy=${com.sun.aas.instanceRoot}/config/server.policy</jvm-options>
        <jvm-options>-Djava.security.auth.login.config=${com.sun.aas.instanceRoot}/config/login.conf</jvm-options>
        <jvm-options>-Dcom.sun.enterprise.security.httpsOutboundKeyAlias=s1as</jvm-options>
        <jvm-options>-Xmx512m</jvm-options>
        <jvm-options>-Djavax.net.ssl.keyStore=${com.sun.aas.instanceRoot}/config/keystore.jks</jvm-options>
        <jvm-options>-Djavax.net.ssl.trustStore=${com.sun.aas.instanceRoot}/config/cacerts.jks</jvm-options>
        <jvm-options>-Djava.ext.dirs=${com.sun.aas.javaRoot}/lib/ext${path.separator}${com.sun.aas.javaRoot}/jre/lib/ext${path.separator}${com.sun.aas.instanceRoot}/lib/ext</jvm-options>
        <jvm-options>-Djdbc.drivers=org.apache.derby.jdbc.ClientDriver</jvm-options>
        <jvm-options>-DANTLR_USE_DIRECT_CLASS_LOADING=true</jvm-options>
        <jvm-options>-Dcom.sun.enterprise.config.config_environment_factory_class=com.sun.enterprise.config.serverbeans.AppserverConfigEnvironmentFactory</jvm-options>
        <!-- Configuration of various third-party OSGi bundles like
             Felix Remote Shell, FileInstall, etc. -->
        <!-- Port on which remote shell listens for connections.-->
        <jvm-options>-Dosgi.shell.telnet.port=6666</jvm-options>
        <!-- How many concurrent users can connect to this remote shell -->
        <jvm-options>-Dosgi.shell.telnet.maxconn=1</jvm-options>
        <!-- From which hosts users can connect -->
        <jvm-options>-Dosgi.shell.telnet.ip=OPENSHIFT_INTERNAL_IP</jvm-options>
        <!-- Gogo shell configuration -->
        <jvm-options>-Dgosh.args=--nointeractive</jvm-options>
        <!-- Directory being watched by fileinstall. -->
        <jvm-options>-Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/</jvm-options>
        <!-- Time period fileinstaller thread in ms. -->
        <jvm-options>-Dfelix.fileinstall.poll=5000</jvm-options>
        <!-- log level: 1 for error, 2 for warning, 3 for info and 4 for debug. -->
        <jvm-options>-Dfelix.fileinstall.log.level=2</jvm-options>
        <!-- should new bundles be started or installed only?
             true => start, false => only install
        -->
        <jvm-options>-Dfelix.fileinstall.bundles.new.start=true</jvm-options>
        <!-- should watched bundles be started transiently or persistently -->
        <jvm-options>-Dfelix.fileinstall.bundles.startTransient=true</jvm-options>
        <!-- Should changes to configuration be saved in corresponding cfg file? false: no, true: yes
             If we don't set false, everytime server starts from clean osgi cache, the file gets rewritten.
        -->
        <jvm-options>-Dfelix.fileinstall.disableConfigSave=false</jvm-options>
        <!-- End of OSGi bundle configurations -->
        <jvm-options>-XX:NewRatio=2</jvm-options>
      </java-config>
      <network-config>
        <protocols>
          <protocol name="http-listener-1">
            <http default-virtual-server="server" max-connections="250">
              <file-cache enabled="false" />
            </http>
          </protocol>
          <protocol name="admin-listener">
            <http default-virtual-server="__asadmin" max-connections="250" encoded-slash-enabled="true">
              <file-cache enabled="false" />
            </http>
          </protocol>
        </protocols>
        <network-listeners>
          <network-listener address="OPENSHIFT_INTERNAL_IP" port="8080" protocol="http-listener-1" transport="tcp" name="http-listener-1" thread-pool="http-thread-pool" />
        </network-listeners>
        <transports>
          <transport name="tcp" />
        </transports>
      </network-config>
      <thread-pools>
        <thread-pool name="admin-thread-pool" max-thread-pool-size="50" max-queue-size="256" />
        <thread-pool name="http-thread-pool" max-queue-size="4096" />
        <thread-pool name="thread-pool-1" max-thread-pool-size="200" />
      </thread-pools>
    </config>
    <config name="default-config" dynamic-reconfiguration-enabled="true">
      <http-service>
        <access-log />
        <virtual-server id="server" network-listeners="http-listener-1">
          <property name="default-web-xml" value="${com.sun.aas.instanceRoot}/config/default-web.xml" />
        </virtual-server>
        <virtual-server id="__asadmin" network-listeners="admin-listener" />
      </http-service>
      <admin-service system-jmx-connector-name="system" type="server">
        <!-- JSR 160  "system-jmx-connector" -->
        <jmx-connector address="0.0.0.0" auth-realm-name="admin-realm" name="system" port="${JMX_SYSTEM_CONNECTOR_PORT}" protocol="rmi_jrmp" security-enabled="false" />
        <!-- JSR 160  "system-jmx-connector" -->
        <property value="${com.sun.aas.installRoot}/lib/install/applications/admingui.war"                                                             name="adminConsoleDownloadLocation" />
      </admin-service>
      <web-container>
        <session-config>
          <session-manager>
            <manager-properties />
            <store-properties />
          </session-manager>
          <session-properties />
        </session-config>
      </web-container>
      <ejb-container session-store="${com.sun.aas.instanceRoot}/session-store">
        <ejb-timer-service />
      </ejb-container>
      <mdb-container />
      <jms-service type="EMBEDDED" default-jms-host="default_JMS_host"
addresslist-behavior="priority">
        <jms-host name="default_JMS_host" host="localhost" port="${JMS_PROVIDER_PORT}"
admin-user-name="admin" admin-password="admin" lazy-init="true" />
      </jms-service>
      <log-service log-rotation-limit-in-bytes="2000000"
file="${com.sun.aas.instanceRoot}/logs/server.log">
        <module-log-levels />
      </log-service>
      <security-service>
        <auth-realm classname="com.sun.enterprise.security.auth.realm.file.FileRealm" name="admin-realm">
          <property name="file" value="${com.sun.aas.instanceRoot}/config/admin-keyfile" />
          <property name="jaas-context" value="fileRealm" />
        </auth-realm>
        <auth-realm classname="com.sun.enterprise.security.auth.realm.file.FileRealm" name="file">
          <property name="file" value="${com.sun.aas.instanceRoot}/config/keyfile" />
          <property name="jaas-context" value="fileRealm" />
        </auth-realm>
        <auth-realm classname="com.sun.enterprise.security.auth.realm.certificate.CertificateRealm" name="cetificate" />
        <jacc-provider policy-provider="com.sun.enterprise.security.provider.PolicyWrapper" name="default" policy-configuration-factory-provider="com.sun.enterprise.security.provider.PolicyConfigurationFactoryImpl">
          <property name="repository" value="${com.sun.aas.instanceRoot}/generated/policy" />
        </jacc-provider>
        <jacc-provider policy-provider="com.sun.enterprise.security.jacc.provider.SimplePolicyProvider" name="simple" policy-configuration-factory-provider="com.sun.enterprise.security.jacc.provider.SimplePolicyConfigurationFactory" />
        <audit-module classname="com.sun.enterprise.security.Audit" name="default">
          <property name="auditOn" value="false" />
        </audit-module>
        <message-security-config auth-layer="SOAP">
          <provider-config provider-type="client" provider-id="XWS_ClientProvider"
class-name="com.sun.xml.wss.provider.ClientSecurityAuthModule">
            <request-policy auth-source="content" />
            <response-policy auth-source="content" />
            <property name="encryption.key.alias" value="s1as" />
            <property name="signature.key.alias" value="s1as" />
            <property name="dynamic.username.password" value="false" />
            <property name="debug" value="false" />
          </provider-config>
          <provider-config provider-type="client" provider-id="ClientProvider"
class-name="com.sun.xml.wss.provider.ClientSecurityAuthModule">
            <request-policy auth-source="content" />
            <response-policy auth-source="content" />
            <property name="encryption.key.alias" value="s1as" />
            <property name="signature.key.alias" value="s1as" />
            <property name="dynamic.username.password" value="false" />
            <property name="debug" value="false" />
            <property name="security.config"
value="${com.sun.aas.instanceRoot}/config/wss-server-config-1.0.xml" />
          </provider-config>
          <provider-config provider-type="server" provider-id="XWS_ServerProvider"
class-name="com.sun.xml.wss.provider.ServerSecurityAuthModule">
            <request-policy auth-source="content" />
            <response-policy auth-source="content" />
            <property name="encryption.key.alias" value="s1as" />
            <property name="signature.key.alias" value="s1as" />
            <property name="debug" value="false" />
          </provider-config>
          <provider-config provider-type="server" provider-id="ServerProvider"
class-name="com.sun.xml.wss.provider.ServerSecurityAuthModule">
            <request-policy auth-source="content" />
            <response-policy auth-source="content" />
            <property name="encryption.key.alias" value="s1as" />
            <property name="signature.key.alias" value="s1as" />
            <property name="debug" value="false" />
            <property name="security.config"
value="${com.sun.aas.instanceRoot}/config/wss-server-config-1.0.xml" />
          </provider-config>
        </message-security-config>
      </security-service>
      <transaction-service tx-log-dir="${com.sun.aas.instanceRoot}/logs" automatic-recovery="true" />
      <diagnostic-service />
      <java-config debug-options="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,
address=${JAVA_DEBUGGER_PORT}" system-classpath="" classpath-suffix="">
        <jvm-options>-XX:MaxPermSize=192m</jvm-options>
        <jvm-options>-XX:PermSize=64m</jvm-options>
        <jvm-options>-server</jvm-options>
        <jvm-options>-Djava.awt.headless=true</jvm-options>
        <jvm-options>-XX:+UnlockDiagnosticVMOptions</jvm-options>
        <jvm-options>-Djava.endorsed.dirs=${com.sun.aas.installRoot}/modules/endorsed${path.separator}${com.sun.aas.installRoot}/lib/endorsed</jvm-options>
        <jvm-options>-
Djava.security.policy=${com.sun.aas.instanceRoot}/config/server.policy</jvm-options>
        <jvm-options>-Djava.security.auth.login.config=${com.sun.aas.instanceRoot}/config/                                                                        login.conf</jvm-options>
        <jvm-options>-Dcom.sun.enterprise.security.httpsOutboundKeyAlias=s1as</jvm-options>
        <jvm-options>-Djavax.net.ssl.keyStore=${com.sun.aas.instanceRoot}/config/keystore.jks</jvm-options>
        <jvm-options>-Djavax.net.ssl.trustStore=${com.sun.aas.instanceRoot}/config/cacerts.jks</jvm-options>
        <jvm-options>- Djava.ext.dirs=${com.sun.aas.javaRoot}/lib/ext${path.separator}${com.sun.aas.javaRoot}/jre/lib/ext${path.separator}${com.sun.aas.instanceRoot}/lib/ext</jvm-options>
        <jvm-options>-Djdbc.drivers=org.apache.derby.jdbc.ClientDriver</jvm-options>
        <jvm-options>-DANTLR_USE_DIRECT_CLASS_LOADING=true</jvm-options>
        <jvm-options>-Dcom.sun.enterprise.config.config_environment_factory_class=com.sun.enterprise.config.serverbeans.AppserverConfigEnvironmentFactory</jvm-options>
        <jvm-options>-XX:NewRatio=2</jvm-options>
        <jvm-options>-Xmx512m</jvm-options>
        <!-- Port on which remote shell listens for connections.-->
        <jvm-options>-Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT}</jvm-options>
        <!-- How many concurrent users can connect to this remote shell -->
        <jvm-options>-Dosgi.shell.telnet.maxconn=1</jvm-options>
        <!-- From which hosts users can connect -->
        <jvm-options>-Dosgi.shell.telnet.ip=OPENSHIFT_INTERNAL_IP</jvm-options>
        <!-- Gogo shell configuration -->
        <jvm-options>-Dgosh.args=--noshutdown -c noop=true</jvm-options>
        <!-- Directory being watched by fileinstall. -->
        <jvm-options>-Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/</jvm-options>
        <!-- Time period fileinstaller thread in ms. -->
        <jvm-options>-Dfelix.fileinstall.poll=5000</jvm-options>
        <!-- log level: 1 for error, 2 for warning, 3 for info and 4 for debug. -->
        <jvm-options>-Dfelix.fileinstall.log.level=3</jvm-options>
        <!-- should new bundles be started or installed only?
                 true => start, false => only install
             -->
        <jvm-options>-Dfelix.fileinstall.bundles.new.start=true</jvm-options>
        <!-- should watched bundles be started transiently or persistently -->
        <jvm-options>-Dfelix.fileinstall.bundles.startTransient=true</jvm-options>
        <!-- Should changes to configuration be saved in corresponding cfg file? false: no, true: yes
                  If we don't set false, everytime server starts from clean osgi cache, the file gets rewritten.
             -->
        <jvm-options>-Dfelix.fileinstall.disableConfigSave=false</jvm-options>
        <!-- End of OSGi bundle configurations -->
      </java-config>
      <availability-service>
        <web-container-availability />
        <ejb-container-availability sfsb-store-pool-name="jdbc/hastore" />
        <jms-availability />
      </availability-service>
      <network-config>
        <protocols>
          <protocol name="http-listener-1">
            <http default-virtual-server="server">
              <file-cache />
            </http>
          </protocol>
          <protocol name="admin-listener">
            <http default-virtual-server="__asadmin" max-connections="250">
              <file-cache enabled="false" />
            </http>
          </protocol>
          <protocol security-enabled="true" name="sec-admin-listener">
            <http default-virtual-server="__asadmin" encoded-slash-enabled="true">
              <file-cache />
            </http>
            <ssl client-auth="want" classname="com.sun.enterprise.security.ssl.GlassfishSSLImpl"                                                              cert-nickname="glassfish-instance" />
          </protocol>
          <protocol name="admin-http-redirect">
            <http-redirect secure="true" />
          </protocol>
          <protocol name="pu-protocol">
            <port-unification>
              <protocol-finder protocol="sec-admin-listener" name="http-finder"                                           classname="com.sun.grizzly.config.HttpProtocolFinder" />
              <protocol-finder protocol="admin-http-redirect" name="admin-http-redirect"                                           classname="com.sun.grizzly.config.HttpProtocolFinder" />
            </port-unification>
          </protocol>
        </protocols>
        <network-listeners>
          <network-listener address="OPENSHIFT_INTERNAL_IP" port="9999" protocol="http-listener-1"
transport="tcp" name="http-listener-1" thread-pool="http-thread-pool" />
        </network-listeners>
        <transports>
          <transport name="tcp" />
        </transports>
      </network-config>
      <thread-pools>
        <thread-pool name="http-thread-pool" />
        <thread-pool max-thread-pool-size="200" idle-thread-timeout-in-seconds="120"                                                                            name="thread-pool-1" />
      </thread-pools>
      <group-management-service />
      <management-rules />
      <system-property name="ASADMIN_LISTENER_PORT" value="24848" />
      <system-property name="HTTP_LISTENER_PORT" value="28080" />
      <system-property name="HTTP_SSL_LISTENER_PORT" value="28181" />
      <system-property name="JMS_PROVIDER_PORT" value="27676" />
      <system-property name="IIOP_LISTENER_PORT" value="23700" />
      <system-property name="IIOP_SSL_LISTENER_PORT" value="23820" />
      <system-property name="IIOP_SSL_MUTUALAUTH_PORT" value="23920" />
      <system-property name="JMX_SYSTEM_CONNECTOR_PORT" value="28686" />
      <system-property name="OSGI_SHELL_TELNET_PORT" value="26666" />
      <system-property name="JAVA_DEBUGGER_PORT" value="29009" />
    </config>
  </configs>
  <property name="administrative.domain.name" value="domain1" />
  <secure-admin special-admin-indicator="3047aff3-3214-4ac9-aa5e-a5dad78b2eea">
    <secure-admin-principal dn="CN=localhost,OU=GlassFish,O=Oracle Corporation,L=Santa Clara,ST=California,C=US" />
    <secure-admin-principal dn="CN=localhost-instance,OU=GlassFish,O=Oracle Corporation,L=Santa Clara,ST=California,C=US" />
  </secure-admin>
</domain>

搞定了。GlassFish 已经准备好了,您可以继续下一步了。

步骤 2:创建 DIY 应用存根。是时候创建 OpenShift 默认 DIY 应用了:

  • 在本地磁盘D:上创建一个空文件夹,并将其命名为GlassFishAS.
  • 打开一个 shell 并导航到GlassFishAS文件夹。
  • 键入命令rhc app create -a RafaEShop -t diy-0.1,,如图图 10-66 所示。

9781430257943_Fig10-66.jpg

图 10-66 。创建默认 DIY 应用

快速检查一下,登录 OpenShift 网站,在你的管理控制台中找到应用链接(图 10-67 )。

9781430257943_Fig10-67.jpg

图 10-67 。管理控制台中列出的应用链接

image 注意在继续深入之前,请慢慢阅读D:/GlassFishAS/RafaEShop/README.txt文件。该文件描述了应用文件夹和一些环境变量。

第 3 步:复制 GlassFish 文件。现在将 GlassFish 文件复制到D:/GlassFishAS/RafaEShop/diy文件夹中,如图图 10-68 所示。

9781430257943_Fig10-68.jpg

图 10-68 。复制 GlassFish 文件

第四步:修改开始和停止动作钩子(你应该从README.txt文件熟悉这些文件)。

您需要在启动 GlassFish 服务器之前调整启动文件。找到文件D:/GlassFishAS/RafaEShop/.openshift/action_hooks/start,在它的代码后面添加以下几行。这些修改由 OpenShift 博客在https://www.openshift.com/blogs指出:

cd $OPENSHIFT_REPO_DIR/diy/glassfish3/glassfish/domains/domain1/config/
mv domain.xml domain.xml_2
sed 's/'$( grep serverName domain.xml_2 | cut -d\" -f 2 )'/'$OPENSHIFT_INTERNAL_IP'/g' domain.xml_2 > domain.xml
../../../bin/asadmin start-domain &> $OPENSHIFT_DIY_LOG_DIR/server.log

开始文件应该像你在图 10-69 中看到的那样。

9781430257943_Fig10-69.jpg

图 10-69 。修改开始文件

根据D:/GlassFishAS/RafaEShop/.openshift/action_hooks/stop文件停止 GlassFish。该文件的默认内容应该替换为以下几行。这些修改由 OpenShift 博客在https://www.openshift.com/blogs指出:

#!/bin/bash
# The logic to stop your application should be put in this script.
kill `ps -ef | grep glassfish3 | grep -v grep | awk '{ print $2 }'` > /dev/null 2>&1
exit 0

现在它应该看起来像你在图 10-70 中看到的。

9781430257943_Fig10-70.jpg

图 10-70 。修改停止文件

第 5 步:添加 MongoDB NoSQL 数据库 2.2 插件,正如你之前在图 10-27 中看到的。在本章的第一部分,您看到了如何添加 MongoDB 插件。过程完全相同,所以在 shell 中键入命令rhc cartridge add -a RafaEShop -c mongodb-2.2,如图图 10-71 所示。

9781430257943_Fig10-71.jpg

图 10-71 。添加 MongoDB 插件

第六步:添加 RockMongo 1.1 弹药筒,就像你之前在图 10-30 中看到的那样。

添加 RockMongo 管理工具来使用可视化方法管理 MongoDB 数据库是非常有用的。使用以下命令添加 RockMongo,如图图 10-72 所示:

rhc cartridge add -a RafaEShop -c rockmongo-1.1

9781430257943_Fig10-72.jpg

图 10-72 。添加 RockMongo 卡盒

第七步:调整persistence.xml设置。在RafaEShop应用中找到persistence.xml文件。您需要修改这些文件设置来使用新的 MongoDB 数据库;我建议您在修改之前复制一份。此时,persistence.xml文件应该具有如清单 10-5 所示的内容。

清单 10-5。 修改 persistence.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">
    <persistence-unit name="HOGM_eSHOP-ejbPU" transaction-type="JTA">
        <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
        <class>eshop.entities.Categories</class>
        <class>eshop.entities.Customers</class>
        <class>eshop.entities.Inventory</class>
        <class>eshop.entities.Orders</class>
        <class>eshop.entities.Products</class>
    <properties>
        <property name="hibernate.search.default.directory_provider"
                  value="filesystem"/>
        <property name="hibernate.search.default.indexBase" value="D:/eshop"/>
        <property name="hibernate.search.default.locking_strategy"
                  value="single"/>
        <property name="hibernate.transaction.jta.platform"
                  value="org.hibernate.service.jta.platform.
                                                 internal.SunOneJtaPlatform"/>
        <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
        <property name="hibernate.ogm.datastore.grid_dialect"
                  value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
        <property name="hibernate.ogm.mongodb.database" value="eshop_db"/>
        <property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
        <property name="hibernate.ogm.mongodb.port" value="27017"/>
    </properties>
</persistence-unit>
</persistence>

您必须调整几个设置,如下面的一组说明所示。

Apache Lucene 索引存储在文件系统中(在D:/eshop文件夹中)。您需要使用有效的云文件夹路径修改此文件夹路径(基本文件夹)。作为一个更简单的选择,您可以通过替换以下代码来使用基于内存的目录:

<property name="hibernate.search.default.directory_provider"
value="filesystem"/>
<property name="hibernate.search.default.indexBase" value="D:/eshop"/>
<property name="hibernate.search.default.locking_strategy"
          value="single"/>

使用此代码:

<property name="hibernate.search.default.directory_provider"
          value="ram"/>
<property name="hibernate.search.default.locking_strategy"
          value="single"/>

设置 MongoDB 数据库的名称、主机、端口、用户和密码。为此,请替换以下代码片段:

<property name="hibernate.ogm.mongodb.database" value="eshop_db"/>
<property name="hibernate.ogm.mongodb.host" value="127.0.0.1"/>
<property name="hibernate.ogm.mongodb.port" value="27017"/>

使用此代码:

<property name="hibernate.ogm.mongodb.database" value="RafaEShop"/>
<property name="hibernate.ogm.mongodb.host" value="127.9.57.129"/>
<property name="hibernate.ogm.mongodb.port" value="27017"/>
<property name="hibernate.ogm.mongodb.username" value="admin"/>
<property name="hibernate.ogm.mongodb.password" value="YhH7s7eLYrR4"/>

persistence.xml中的“新”应该如下:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" FontName2">http://java.sun.com/xml/ns/persistence "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd ">
    <persistence-unit name="HOGM_eSHOP-ejbPU" transaction-type="JTA">
        <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
        <class>eshop.entities.Categories</class>
        <class>eshop.entities.Customers</class>
        <class>eshop.entities.Inventory</class>
        <class>eshop.entities.Orders</class>
        <class>eshop.entities.Products</class>
    <properties>
        <property name="hibernate.search.default.directory_provider"
                  value="ram"/>
        <property name="hibernate.search.default.locking_strategy"
                  value="single"/>
        <property name="hibernate.transaction.jta.platform"
                  value="org.hibernate.service.jta.platform.
                                              internal.SunOneJtaPlatform"/>
        <property name="hibernate.ogm.datastore.provider" value="mongodb"/>
        <property name="hibernate.ogm.datastore.grid_dialect"
                  value="org.hibernate.ogm.dialect.mongodb.MongoDBDialect"/>
        <property name="hibernate.ogm.mongodb.database" value="RafaEShop"/>
        <property name="hibernate.ogm.mongodb.host" value="127.9.57.129"/>
        <property name="hibernate.ogm.mongodb.port" value="27017"/>
        <property name="hibernate.ogm.mongodb.username" value="admin"/>
        <property name="hibernate.ogm.mongodb.password" value="YhH7s7eLYrR4"/>
    </properties>
</persistence-unit>
</persistence>

第八步:在 GlassFish 中添加RafaEShop战争。在您的本地项目RafaEShop/dist文件夹中或者在 Apress 存储库中的RafaEShop/dist文件夹中找到RafaEShop应用 WAR。

将这场战争复制到D:/GlassFishAS/RafaEShop/diy/glassfish3/glassfish/domains/domain1/autodeploy文件夹中(参见图 10-73 )。最后,覆盖RafaEShop战争档案中的persistence.xml文件。你可以使用任何存档工具,如 WinRAR。

9781430257943_Fig10-73.jpg

图 10-73 。复制 RafaEShop 战争

注意,在这个文件夹中,除了你的战争之外,还有一个名为openshift的战争。这是您使用 DIY 墨盒时 OpenShift 生成的默认应用。

您已经完成了最后一步。现在,您可以将应用上传到 OpenShift 平台。但是,在提交更改之前,最好打开一个单独的进程来监控 GlassFish 的启动/停止状态。

监控 GlassFish 启动/停止

如果您阅读了本章的第一部分,您已经熟悉了使用安全 shell 会话建立的连接,以及如何从您的计算机打开这样的连接。您现在可以使用以下命令打开一个 SSH 会话来实时监控 GlassFish 的启动/停止状态,如图 10-74 中的所示:

ssh 515466444382ece1bb0001c2@RafaEShop-hogm.rhcloud.com.

9781430257943_Fig10-74.jpg

图 10-74 。监控 GlassFish 的启动/停止状态

接下来,键入tail_all命令。该命令将跟踪当前应用的所有可用日志,包括服务器启动/停止状态。最有可能的是,此时您会从 RockoMongo 日志中看到一些错误或通知。暂时忽略它们,让这个过程开放。

提交更改

每次您提交对应用的更改时,OpenShift 将自动停止 GlassFish AS,提交更改,并再次启动 GlassFish AS。打开一个新的 shell,导航到D:/GlassFishAS/RafaEShop文件夹,并键入以下三个命令:

git add .
git commit -m "first commit"
git push

因为这是第一次提交,所以将所有内容推送到应用需要一些时间。

在提交结束时,GlassFish 启动,在监控该操作的 shell 中,您应该看到类似于图 10-75 所示的内容。

9781430257943_Fig10-75.jpg

图 10-75 。GlassFish 域已成功启动

显然,这正是我们所期待的信息。如果您看到此消息,请直接跳到“监控 GlassFish 日志一节。“如果不是,你有问题,也许是下一个描述的问题。

修复已知问题

有时,你得到的不是成功,而是一条消息,说明“权限被拒绝 (见图 10-76 )。

9781430257943_Fig10-76.jpg

图 10-76 。权限被拒绝

要解决这个问题,您需要授予自己对应用文件和目录的某些权限。打开一个新的 SSH 会话(不要关闭监控 GlassFish 启动/停止状态的会话),并键入以下chmod命令(也显示在图 10-77 中)。

chmod +x app-root/runtime/repo/diy/glassfish3/bin/*
chmod +x diy-0.1/repo/diy/glassfish3/glassfish/bin/*
chmod +x diy-0.1/runtime/repo/.openshift/action_hooks/*

9781430257943_Fig10-77.jpg

图 10-77 。授予对应用文件的权限

通过输入命令ctl_app start 再次启动应用(参见图 10-78 )。

9781430257943_Fig10-78.jpg

图 10-78 。从 shell 启动应用

image 注意要停止应用,键入ctl_app stop。要重新启动应用,请键入ctl_app restart。关于这些命令(以及其他命令)的更多细节可以通过键入help命令获得。

这一次,在监控 GlassFish 域启动/停止状态的 shell 中,您应该会看到类似于图 10-75 中的成功消息。

监控 GlassFish 日志

当 GlassFish 域成功启动时,您可以看到 GlassFish 日志文件的位置和名称,如图 10-79 (这是从图 10-75 中提取的)。

9781430257943_Fig10-79.jpg

图 10-79 。将 GlassFish 定位为日志文件

server.log 的内容可以在一个 shell 中列出(在应用部署之前,您需要耐心等待)。打开一个新的 SSH 会话并键入以下命令,该命令也显示在图 10-80 中:

tail app-root/rutime/repo/diy/glassfish3/glassfish/domains/domain1/logs/server.log

9781430257943_Fig10-80.jpg

图 10-80 。将 GlassFish 的内容作为日志文件列出

基于这些日志内容,您可以轻松地调试您的应用。

在图 10-80 中,日志消息表明应用RafaEShop已成功部署。现在你可以关闭所有的外壳,享受应用。

测试一下!

既然您已经做到了这一步,那么您可能已经成功地用三种方法中的至少一种部署了应用。无论您选择哪种方法,测试都可以以相同的方式进行。如果你尝试了所有三种方法,你的 OpenShift 管理控制台应该看起来像图 10-81 中的那个。

9781430257943_Fig10-81.jpg

图 10-81 。管理控制台中列出的所有应用链接

image 注意我将介绍在 GlassFish AS 3 上测试作为 WAR 部署的RafaEShopW应用的步骤。您可以轻松地将这些步骤用于其他两种方法。

因为这是您第一次运行该应用,所以您需要填充 MongoDB 电子商务数据库。如您所知,这可以从db.xhml管理页面完成。这个的链接是http://rafaeshopw-hogm.rhcloud.com/RafaEShop/faces/db.xhtml

填充数据库后,您可以在http://rafaeshopw-hogm.rhcloud.com/RafaEShop/faces/index.xhtml访问电子商店(参见图 10-82 )。

9781430257943_Fig10-82.jpg

图 10-82 。运行 RafaEShopW 应用

谨慎的

如果您需要重新填充 MongoDB 数据库(从db.xhtml管理页面),不要忘记删除现有的集合。你可以在 RockMongo 界面通过按下Drop All按钮轻松完成,如图图 10-83 所示。

9781430257943_Fig10-83.jpg

图 10-83 。从 RockMongo 接口中删除数据库集合

然后重新启动服务器。你可以使用ctl_app restart命令或者管理控制台,如图图 10-84 所示。

9781430257943_Fig10-84.jpg

图 10-84 。重新启动应用

重启服务器将重置 Lucene 索引(因为它们存储在 RAM 中),这意味着您还需要删除数据库集合并从db.xhml管理页面重新填充数据库。

试图在不使用db.xhtml管理页面的情况下填充 MongoDB 数据库将会导致错误。因为应用不能索引现有的数据库,所以 Lucene 索引不会被更新。

不要担心最初的orders_c集合会丢失。这将在提交第一份采购订单时创建。

很高兴知道

OpenShift 允许您使用端口转发远程连接到可用的服务(一般来说,这种技术允许您将远程计算机连接到私有局域网内的服务)。这可以使用 rhc 命令rhc port-forward -a RafaEShopW来完成,如图图 10-85 所示。

9781430257943_Fig10-85.jpg

图 10-85 。端口转发

在图 10-85 中,您可以看到RafaEShopW应用的可用端口。请注意,mongod服务器也在列表中。

放弃

在写这本书的时候,我讨论的那些应用在网上都是可用的。我不能保证当你阅读这本书时,OpenShift 会让这些应用可用(如果应用没有太大的流量,OpenShift 可能会关闭服务器)。

摘要

在这最后一章中,您看到了如何将RafaEShop应用迁移到 OpenShift PaaS。本章从几个介绍性任务开始,比如在 OpenShift 上创建一个账户,激活并登录这个账户,熟悉 OpenShift web 界面。此外,您还看到了如何将RafaEShop部署为 WAR 和运行在云中的 JBoss AS 7 上的 Apache Maven 项目。在本章的第二部分中,您配置了一个 GlassFish AS 3 域用于在 OpenShift 中运行,并且在这个域上部署了RafaEShop应用。

posted @ 2024-08-13 14:30  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报