Java 开发 2.0: MongoDB:拥有 RDBMS 特性的 NoSQL 数据存储

使用 Java 代码和 Groovy 创建和查询文档

如果您正在探索 NoSQL 数据库的世界,则 MongoDB(有时被誉为 NoSQL RDBMS)应在您的清单上获得一个位置。了解所有有关 MongoDB 的自定义 API、交互式 shell、RDBMS 类型动态查询的支持、以及快速、容易的 MapReduce 计算。然后使用 MongoDB 的本地 Java™ 语言驱动程序和被名为 GMongo 的方便 Groovy 包装来开始创建、寻找并操纵数据。

Andrew Glover, 作家和开发人员, Beacon50

Andrew GloverAndrew Glover 是具有行为驱动开发、持续集成和敏捷软件开发激情的开发人员、作家、演说家和企业家。他是 easyb 行为驱动开发(Behavior-Driven Development,BDD)框架的创建者和三本书的合著者:持续集成Groovy 在行动Java 测试模式。您可以通过他的博客与他保持一致并在 Twitter(http://twitter.com/aglover)上关注他。



2010 年 12 月 13 日

面向文档的数据库(如 MongoDB 和 CouchDB)与关系数据库有很大的不同,即它们不能在表中存储数据;相反,它们是用文档形式来存储数据的。从开发人员的角度来看,面向文档的(或无模式)数据比关系数据更简单且管理更加灵活。而不是将数据存储到刚性模式的表、行和列中,通过关系加入,文档被单独编写,包含它们需要的任何数据。

更多关于来自源的 MongoDB 的信息

在即时更新的技术播客上,查找更多有关此来自 Eliot Horowitz (10gen 的 CTO)的开源文档数据库。现在就收听

在开源的、面向文档的数据库中,MongoDB 经常被誉为具有 RDBMS 功能的 NoSQL 数据库。这方面的一个例子是动态查询的 MongoDB 支持无需预定义的 MapReduce 函数。MongoDB 还带有交互式 shell,这使得访问其数据存储变得简单,且其对于分块的即装即用的支持能够使高可伸缩性跨多个节点。

MongoDB 的 API 是 JSON 对象和 JavaScript 函数的本地混合物。通过 shell 程序开发人员可与 MongoDB 进行交互,即允许命令行参数,或通过使用语言驱动程序来访问数据存储实例。这里不存在类 JDBC 驱动程序,这意味着您不必处理 ResultSetPreparedStatement

速度是 MongoDB 的另外一个优势,主要是由于它处理写入的方式:它们存储在内存中,然后通过后台线程写入磁盘。

关于本系列

自 Java 技术首次诞生以来,Java 开发格局已发生了翻天覆地的变化。得益于成熟的开源框架和可靠的租赁部署基础设施,现在可以迅速而经济地组装、测试、运行和维护 Java 应用程序。在 本系列 中,Andrew Glover 探索使这种新的 Java 开发风格成为可能的各种技术和工具。

在本文中,您将了解到 MongoDB。我将再次使用停车票据示例来介绍 CouchDB(请参考 参考资料),演示无模式数据存储的灵活性。因为 MongoDB 的 API 和对于动态查询的支持是其两个主要卖点,所以我将专注于这两点,并带您了解一些在操作中演示了 MongoDB 的 shell 和 Java 语言驱动程序的示例。在本文的后面,我还将为您介绍 GMongo,在一定程度上缓解了 MongoDB 的 MapReduce 实现的 Groovy 包装 — 这也恰好是此特殊 NoSQL 选项的一大亮点。

为什么采取无模式?

无模式存储并不适用于每一个域,因此这对于了解您为什么可能选择面向文档的关系方法来说是一个好主意。虽然在数据可被表现为不同形式的域中文档的灵活性具有意义,但是基本的架构是相同的。一个典型的示例就是名片。如果有一叠名片,您将看到它们所表现出来的不同数据:一些包括传真号码或公司 URL,而另外一些则是邮件地址或两个电话号码,甚至是一个 Twitter 手柄。虽然数据是不同的,但是架构或函数却是相同的 — 即名片具有的联系信息。

虽然在关系方面为名片建模是可行的,但是这是令人费解的。在关系数据库中,在每一个或两个利用空值的传真列中,通过该空值(例如)您完成了许多记录。因为您可能发现自己受制于地址字段的长度,所以您还必须在关系系统中指定列类型。(我敢打赌您绝对不想存储类似 Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch 这样的地址,是不是?然而,这个小镇真的存在。)

使用面向文档的数据存储为名片建模是一项更容易的工作。没有模式就意味着文档可以拥有任何其需要的任何长度的数据。鉴于名片的性质,对于将它们建模为具有不同属性的文档来说是有意义的。

大部分无模式数据存储都不能完全支持 ACID(原子性、一致性、隔离性和持久性),然而,这可以在以可靠性和一致性为关键的域中提出挑战。NoSQL 方法的支持者认为只要您不对宕机负有责任,那么 ACID 就会工作,一旦您开始努力在规模上引用多个节点,这就是不可避免的。也许底线是倾向于规模的无模式数据存储,其比关系数据存储更容易,这是为基于 web 的应用程序制定面向文档存储的好选择。


MongoDB 入门

MongoDB 入门再简单不过了,尤其因为其为目标操作系统提供了下载。如果您想在 Mac OS X 上建立 MongoDB,例如,您只下载适当的二进制,并对其进行解压,创建一个数据目录(MongoDB 在此目录中写入数据存储的内容),然后通过 mongodb 命令启动一个实例。(确保进程告诉您想让其在何处写入您的数据!)

在清单 1 中,我正在启动 MongoDB 并告诉它要在 data/db 目录中存储我的数据。您将会注意到我传入了一些 verbose 标志 — v 越多,越详细。

清单 1. MongoDB for Mac OS X
iterm$ ./bin/mongod — dbpath ./data/db/ — vvvvvvvv

一旦您启动了 MongoDB,您很快就会感受到使用其交互式 shell 的感觉。只需调用 mongo 命令,您就应该会看到一些类似于清单 2 的内容:

清单 2. 启动 MongoDB shell
iterm$ ./bin/mongo
MongoDB shell version: 1.6.0
connecting to: test
>

在您启动该 shell 的时候,您将看到您最初会与 “test” 数据存储连接。现在我将一直使用它来演示创建和查找文档,其中涉及到写入一些 JavaScript 和 JSON。


创建和查找文档

像 CouchDB 一样,MongoDB 便于通过 JSON 创建文档(尽管它们被存储为 BSON,但是 JSON 的二进制形式也被高效地利用)。要在交互式 shell 中创建一张停车票据,您只需创建一个 JSON 文档,如同清单 3 中所示:

清单 3. 简单的 JSON 文档
> ticket =  { officer: "Kristen Ree" , location: "Walmart parking lot", vehicle_plate: 
  "Virginia 5566",  offense: "Parked in no parking zone", date: "2010/08/15"}

按 Return 键,MongoDB 将返回一个格式化的 JSON 文档,如清单 4 所示:

清单 4. MongoDB 的响应
{
  "officer" : "Kristen Ree",
  "location" : "Walmart parking lot",
  "vehicle_plate" : "Virginia 5566",
  "offense" : "Parked in no parking zone",
  "date" : "2010/08/15"
}

我刚刚创建了一张停车票据的 JSON 表示,并将称其为 “票据”。要持久化此文档,我不得不将其与 collection 关联在一起,这类似于关系术语中的 “模式”。在清单 5 中,我将该票据与 tickets 集合关联在一起:

清单 5. 保存票据实例
> db.tickets.save(ticket)

请注意在 MongoDB 中,我的 tickets 集合无需提前创建。集合将在它们被引用的第一时间创建。

现在继续创建更多的票据。这将使得在下一部分中寻找它们变得更有意思。

查找文档

在集合中查找所有文档比较容易:只要调用如清单 6 所示的 find 命令就可以了:

清单 6. 在 MongoDB 中查找所有文档
> db.tickets.find()
{ "_id" : ObjectId("4c7aca17dfb1ab5b3c1bdee8"), "officer" : "Kristen Ree", "location" : 
  "Walmart parking lot", "vehicle_plate" : "Virginia 5566", "offense" : 
  "Parked in no parking zone", "date" : "2010/08/15" }
{ "_id" : ObjectId("4c7aca1ddfb1ab5b3c1bdee9"), "officer" : "Kristen Ree", "location" : 
  "199 Baldwin Dr", "vehicle_plate" : "Maryland 7777", "offense" : 
  "Parked in no parking zone", "date" : "2010/08/29" }

不带任何参数的 find 命令简单地返回一个特定集合中的所有文档,在本例中名为 tickets

请注意在 清单 6 中 MongoDB 为每一个文档都创建了一个 ID,如 _id 键所示。

您可以在 JSON 文档中搜索单一的键。例如,如果我想寻找由 Walmart 停车场开出的所有票据,则我会使用如清单 7 所示的查询:

清单 7. 通过查询寻找
> db.tickets.find({location:"Walmart parking lot"})

您可以在 JSON 文档中搜索任何可用键(在这种情况下是 offense、_iddate 等)。另外一种选择(如清单 8 所示)是使用正则表达式搜索键值(如 location),其工作方式大致相当于 SQL 中的 LIKE 语句:

清单 8. 通过 regex 寻找
> db.tickets.find({location:/walmart/i})

Regex 语句(本例中为 walmart)后面的 i 表明该语句不区分大小写。


MongoDB 的 Java 驱动程序

MongoDB 的 Java 语言驱动程序旨在抽象您在前面部分中所见到的大部分 JSON 和 JavaScript 代码,以便您可以留下一个简单的 Java API。要启动 MongoDB 的 Java 驱动程序,只需下载它并将生成的 .jar 文放入您的类路径中(请参考 参考资料)。

现在假如您想在 tickets 集合中创建另外一张票据,该集合存储在 test 数据存储中。使用 Java 驱动程序,您将首先连接到一个 MongoDB 实例,然后获取 test 数据库和 tickets 集合,如清单 9 所示:

清单 9. 使用 MongoDB 的 Java 驱动程序
Mongo m = new Mongo();
DB db = m.getDB("test");
DBCollection coll = db.getCollection("tickets");

要使用 Java 驱动程序创建 JSON 文档,只需创建 BasicObject 并将名称和值与其关联在一起,如清单 10 所示:

清单 10. 通过 Java 驱动程序创建文档
BasicDBObject doc = new BasicDBObject();

doc.put("officer", "Andrew Smith");
doc.put("location", "Target Shopping Center parking lot");
doc.put("vehicle_plate", "Virginia 2345");
doc.put("offense", "Double parked");
doc.put("date", "2010/08/13");

coll.insert(doc);

通过 Java 驱动程序查找文档并在由此产生的光标上迭代也非常容易,如清单 11 所示:

清单 11. 通过 Java 驱动程序查找文档
DBCursor cur = coll.find();
while (cur.hasNext()) {
 System.out.println(cur.next());
}

不少 MongoDB 库对于 Java 开发人员来说都是可用的,包括 Groovy 中的一个一流抽象,该抽象构建于 Java 驱动程序之上。在下一小节中,我将构建一个应用程序,展示默认 Java 驱动程序和更具 Groovy 风格的驱动程序的实际运行情况。这个很酷的应用程序还将演示 MongoDB 的 MapReduce 功能,我将用该功能来处理一个文档集合。


使用 MongoDB 进行 Twitter 分析

仅位于数据库中的数据并不都那样有趣;真正强大的是我们要如何使用它。通过此应用程序,我将首先从 Twitter 捕获一些信息并将其存储在 MongoDB 中。然后我会计算两个指标:谁转发给我最多,以及我的哪些帖子被转发最多。

要执行此应用程序,我首先需要一种方法来与 Twitter 连接在一起并捕获数据。为此,我会使用被称为 Twitter4J 的库,其将 Twitter 中或多或少的 RESTful API 抽象为简单的 Java API(请参考 参考资料)。我将使用此 API 来查找我的转发。一旦我拥有了数据,我会将其格式化为 JSON 文档,如清单 12 所示:

清单 12. 通过 JSON 存储转发
{ 
  "user_name" : "twitter user",
  "tweet" : "Podcast ...", 
  "tweet_id" :  9090...., 
  "date" : "08/12/2010" 
}

在清单 13 中,在我的简单的驱动应用程序中(也是用 Java 代码编写的),我使用 MongoDB 的具有 Twitter4J 的本地 Java 驱动程序,它将捕获数据,而后在 MongoDB 中存储数据:

清单 13. 将 Twitter 数据插入 MongoDB
Mongo m = new Mongo();
DB db = m.getDB("twitter_stats");
DBCollection coll = db.getCollection("retweets");

Twitter twitter = new TwitterFactory().getInstance("<some user name>", "<some password>");
List<Status> statuses = twitter.getRetweetsOfMe();
for (Status status : statuses) { 
  ResponseList<User> users = twitter.getRetweetedBy(status.getId());
  
  for (User user : users) {
    BasicDBObject doc = new BasicDBObject();
    doc.put("user_name", user.getScreenName());
    doc.put("tweet", status.getText());
    doc.put("tweet_id", status.getId());
    doc.put("date", status.getCreatedAt());
    coll.insert(doc);
 }
}

请注意 清单 13 中的 “twitter_stats” 数据库是按需创建的,因为它在驱动程序运行以前是不存在的。对于 “retweets” 集合来说同样是真实的。一旦创建了数据库和集合,就获得了 Twitter4J 的 Twitter 对象,顺序是根据最近的 20 条转发排列的。

从 Twitter4J 返回的 StatusList 对象现在对我的转发做出了响应。要查询每一个以获取相关数据,然后创建 MongoDB 的 BasicDBObject 实例并填入相关数据。最后,保存每一个文档。


MongoDB 的 MapReduce

一旦我存储了所有的数据,我将准备开始操纵它。要获得想要的信息,需要两个批处理操作:首先,我会汇总每一个 Twitter 用户被列出来的次数。然后,我将汇总每一个 tweet(或 tweet_id)弹出的次数。

为了批处理数据操作,MongoDB 利用了 MapReduce。在较高的层面上,MapReduce 算法将一个问题分为两个步骤:Map 函数被用于采用大输出并将其分为更小的块,然后将此数据交给其他可以用它做一些事情的进程。Reduce 函数旨在将单一答案从 Map 带给最终输出。

因为 MongoDB 的核心 API 是 JavaScript,所以 MapReduce 函数必须用 JavaScript 编写。因此,即使使用 Java 驱动程序,我也一直需要为 MapReduce 函数编写 JavaScript,尽管我可以在 String 中定义 JavaScript,或者一个类似于 BasicDBObject 的对象。我要进一步简化事情,并亲自保存一些编码,这有助于 MongoDB 的默认驱动程序顶端的小包装库。此包装 — 名为 GMongo — 是用 Groovy 编写的并旨在利用 Groovy。虽然我还是要用 JavaScript 编写 MapReduce 函数,但是 Groovy 的多行字符串功能将使工作少一点麻烦,这是因为我不必转义字符串。

JavaScript 中的 MapReduce 函数

为了找出谁转发给我最多,我不得不做两件事情:首先,我需要编写 map 函数,它可以锁定在 JSON 文档结构的 user_name 属性上。这是很容易的,如清单 14 所示:

清单 14. 使用 JavaScript 编写的简单 Map 函数
function map() {
  emit(this.user_name, 1); 
}

我的 map 函数是很简单的 — 它只获取传递给它的所有文档的 user_name 属性。对 emit 的调用是必需的,且其第二个参数是一个值。这个值基本上就是键的计数,其对于单一文档来说就是 1。您将看到在我使用该计数值来汇总事情时,它如何工作。

因此在 清单 14 中,通过我自己的键,我调用了 emit 函数(user_name 属性)和一个值。在函数上下文中的 this 变量表示为 JSON 文档本身。

下一步,我不得不定义 reduce 函数(如清单 15 所示),其采用了所有相应分组文档并汇总这些值:

清单 15. 使用 JavaScript 编写的 Reduce 函数
function reduce(key, vals) {
  var sum = 0;
  for(var i in vals) sum += vals[i];
  return sum;
}

正如您在 清单 15 中看到的那样,传递给 reducekeyvals 变量表现出类似 function reduce("asmith", [1,1,1,1]) 一样的事情;当然,这意味着 asmithuser_name 已经在四个不同的文档中出现。A. Smith 已经转发给我四次了!

通过在 vals 变量上迭代,我确认了这一点,即返回一个简单的 sum

使用 Groovy 的 MapReduce 函数

下一步,我将编写一个 Groovy 脚本,该脚本使用 GMongo,然后适当地插入我的 mapreduce 函数中,如清单 16 所示:

清单 16. MapReduce 的 Groovy 脚本
  mongo = new GMongo()
def db = mongo.getDB("twitter_stats")

def res = db.retweets.mapReduce(
    """
    function map() {
        emit(this.user_name, 1); 
    }
    """,
    """
    function reduce(key, vals) {
        var sum = 0;
        for(var i in vals) sum += vals[i];
        return sum;
    }
    """,
    "result",
    [:] 
)

def cursor = db.result.find().sort(new BasicDBObject("value":-1))
       
cursor.each{
  println "${it._id} has retweeted you ${it.value as int} times"
}

清单 16 中,我首先创建了一个 GMongo 的实例并获取 “twitter_stats” 数据存储,如果我使用默认 Java 驱动程序,则所有这些都很类似于我所看到的。

下一步,我在 retweets 集合上制定 mapReduce 方法调用。GMongo 驱动程序让我直接引用集合而不是获取它,而在 清单 13 中,我不得不获取该集合。mapReduce 方法采用四个参数:
头两个是 String,其表示用 JavaScript 定义的 mapreduce 函数。第三个参数是持有 MapReduce 结果的对象的名称。最后一个参数完成操作需要的任何输入查询 — 例如,我可以只传递给 MapReduce 函数某些 JSON 文档(比如某个日期范围内的文档)或它们中的一部分。

此后我查询了 result 对象(它是一个 JSON 文档)并发布了一个 sort 调用。清单 16 中的 sort 调用需要一个看上去像 {value:-1} 的 JSON 文档,这意味着我希望按降序排列,即最大值位于顶端。返回的 cursor 对象基本上是一个迭代器,我可以在其上直接使用 Groovy 的一流 each,以便打印出一个可爱的小报告。我的报告列示最多的转发,从多到少进行排序。

尝试运行 清单 16 中的脚本,您应该看到类似清单 17 的输出:

清单 17. MapReduce 输出
bglover has retweeted you 3 times
bobama has retweeted you 3 times
sjobs has retweeted you 2 times
...

现在,虽然我知道了谁转发给我的最多,但是如何报告哪些帖子被转发得最多呢?这是一个非常简单的练习:我只定义 map 函数,该函数去除了 tweet 属性而不是 user_name,如清单 18 所示:

清单 18. 另一个 Map 函数
function map() {
  emit(this.tweet, 1); 
}

作为一个额外的奖励,请注意 reduce 函数可以是相同的,因为它只汇总已分组的键!


结束语

这是一个 MongoDB 的速度与激情之旅,只涉及它的功能的皮毛。但是,我希望您已经看到 MongoDB 的无模式性质实现了很大的灵活性。这在数据元素不同但通常相关的域中非常有用 — 就像我在本文开头展示的名片示例那样。

在 MongoDB 和 CouchDB 都支持无模式灵活性时,它们在其他方面是非常不同的:MongoDB 的类 RDBMS 功能使其更容易操作 — 从 RDBMS 的观点来说,这也是熟悉的。MongoDB 让您能执行动态查询并使用本地语言(如 Java、Ruby 或 PHP)工作。所有这一切都具有 MapReduce 的力量。

面向文档的数据库并不适用于每一个领域。处理财务数据的事务繁忙的领域可能最适合使用传统的、ACID 稳定的 RDBMS。但是对于那些需要高性能流量和灵活数据模型的应用程序来说,MongoDB 值得一看。

参考资料

学习

获得产品和技术

  • MongoDB.org:下载 MongoDB 和 Java 语言驱动程序。
  • 下载 GMongo:默认 Java 语言驱动程序的 Groovy 替代品。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=600453
ArticleTitle=Java 开发 2.0: MongoDB:拥有 RDBMS 特性的 NoSQL 数据存储
publish-date=12132010