IBM,DW,NoSQL,数据建模
关系数据库已经统治数据存储30 多年了,但是无模式(或NoSQL)数据库的逐渐流行表明变化正在发生。尽管 RDBMS 为在传统的客户端服务器架构中存储数据提供了一个坚实的基础,但它不能轻松地(或便宜地)扩展到多个节点。在高度可伸缩的 Web 应用程序(比如 Facebook 和 Twitter)的时代,这是一个非常不幸的弱点。
尽管关系数据库的早期替代方案(还记得面向对象的数据库吗?)不能解决真正紧急的问题,NoSQL 数据库(比如 Google 的 Bigtable 和 Amazon 的 SimpleDB)却作为对 Web 的高可伸缩性需求的直接响应而崛起。本质上,NoSQL 可能是一个杀手问题的杀手应用程序—随着 Web 2.0 的演变,Web 应用程序开发人员可能会遇到更多,而不是更少这样的应用程序。
在这期 Java 开发 2.0 中,我将向您介绍无模式数据建模,这是经过关系思维模式训练的许多开发人员使用 NoSQL 的主要障碍。您将了解到,从一个域模型(而不是关系模型)入手是简化您的改变的关键。如果您使用 Bigtable(如我的示例所示),您可以借助 Gaelyk:Google App Engine 的一个轻量级框架扩展。
NoSQL:一种新的思维方式?
当开发人员谈论非关系或 NoSQL 数据库时,经常提到的第一件事是他们需要改变思维方式。我认为,那实际上取决于您的初始数据建模方法。如果您习惯通过首先建模数据库结构(即首先确定表及其关联关系)来设计应用程序,那么使用一个无模式数据存储(比如 Bigtable)来进行数据建模则需要您重新思考您的做事方式。但是,如果您从域模型开始设计您的应用程序,那么 Bigtable 的无模式结构将看起来更自然。
非关系数据存储没有联接表或主键,甚至没有外键这个概念(尽管这两种类型的键以一种更松散的形式出现)。因此,如果您尝试将关系建模作为一个 NoSQL 数据库中的数据建模的基础,那么您可能最后以失败告终。从域模型开始将使事情变得简单;实际上,我已经发现,域模型下的无模式结构的灵活性正在重新焕发生机。
从关系数据模型迁移到无模式数据模型的相对复杂程度取决于您的方法:即您从基于关系的设计开始还是从基于域的设计开始。当您迁移到 CouchDB 或 Bigtable 这样的数据库时,您的确会丧失 Hibernate(至少现在)这样的成熟的持久存储平台的顺畅感觉。另一方面,您却拥有能够亲自构建它的“绿地效果”。在此过程中,您将深入了解无模式数据存储。
实体和关系
无模式数据存储赋予您首先使用对象来设计域模型的灵活性(Grails 这样的较新的框架自动支持这种灵活性)。您的下一步工作是将您的域映射到底层数据存储,这在使用 Google App Engine 时再简单不过了。
在文章“Java 开发 2.0:针对 Google App Engine 的 Gaelyk”中,我介绍了 Gaelyk ——一个基于 Groovy的框架,该框架有利于使用 Google 的底层数据存储。那篇文章的主要部分关注如何利用 Google 的 Entity对象。下面的示例(来自那篇文章)将展示对象实体如何在 Gaelyk 中工作。
清单1.使用 Entity的对象持久存储
def ticket =newEntity("ticket") ticket.officer = params.officer ticket.license = params.plate ticket.issuseDate = offensedate ticket.location = params.location ticket.notes = params.notes ticket.offense = params.offense
这种对象持久存储方法很有效,但容易看出,如果您频繁使用票据实体—例如,如果您正在各种 servlet 中创建(或查找)它们,那么这种方法将变得令人厌烦。使用一个公共 servlet(或 Groovlet)来为您处理这些任务将消除其中一些负担。一种更自然的选择——我将稍后展示——将是建模一个 Ticket对象。
返回比赛
我不会重复 Gaelyk 简介中的那个票据示例,相反,为保持新鲜感,我将在本文中使用一个赛跑主题,并构建一个应用程序来展示即将讨论的技术。
如图 1 中的“多对多”图表所示,一个 Race拥有多个 Runner,一个 Runner可以属于多个 Race。
图1.比赛和参赛者
如果我要使用一个关系表结构来设计这个关系,至少需要 3 个表:第 3 表将是链接一个“多对多”关系的联接表。所幸我不必局限于关系数据模型。相反,我将使用 Gaelyk(和 Groovy代码)将这个“多对多”关系映射到 Google 针对 Google App Engine 的 Bigtable 抽象。事实上,Gaelyk 允许将 Entity当作 Map,这使得映射过程相当简单。
无模式数据存储的好处之一是无须事先知道所有事情,也就是说,与使用关系数据库架构相比,可以更轻松地适应变化。(注意,我并非暗示不能更改架构;我只是说,可以更轻松地适应变化。)我不打算定义我的域对象上的属性—我将其推迟到 Groovy的动态特性(实际上,这个特性允许创建针对 Google 的 Entity对象的域对象代理)。相反,我将把我的时间花费在确定如何查找对象并处理关系上。这是 NoSQL 和各种利用无模式数据存储的框架还没有内置的功能。
Model 基类
我将首先创建一个基类,用于容纳 Entity对象的一个实例。然后,我将允许一些子类拥有一些动态属性,这些动态属性将通过 Groovy的方便的 setProperty方法添加到对应的 Entity实例。setProperty针对对象中实际上不存在的任何属性设置程序调用。(如果这听起来耸人听闻,不用担心,您看到它的实际运行后就会明白。)
清单2展示了位于我的示例应用程序的一个 Model实例的第一个 stab:
清单2.一个简单的 Model 基类
packagecom.b50.nosqlimportcom.google.appengine.api.datastore. DatastoreServiceFactoryimportcom.google. appengine.api.datastore.EntityabstractclassModel { def entitystaticdef datastore = DatastoreServiceFactory.datastoreServicepublicModel() publicModel(params) { this.@entity=newEntity(this.getClass().simpleName) params.each{ key,val ->this.setProperty key,val } }def getProperty(String name) else" } }voidsetProperty(String name,value)"= value}def save()}
注意抽象类如何定义一个构造函数,该函数接收属性的一个 Map ——我总是可以稍后添加更多构造函数,稍后我就会这么做。这个设置对于 Web 框架十分方便,这些框架通常采用从表单提交的参数。Gaelyk 和 Grails 将这样的参数巧妙地封装到一个称为 params的对象中。这个构造函数迭代这个 Map并针对每个“键/值”对调用 setProperty方法。
检查一下 setProperty方法就会发现“键”设置为底层 entity的属性名称,而对应的“值”是该 entity的值。
Groovy技巧
如前所述,Groovy的动态特性允许我通过 get和 set Property方法捕获对不存在的属性的方法调用。这样,清单 2 中的 Model的子类不必定义它们自己的属性—它们只是将对一个属性的所有调用委托给这个底层 entity对象。
清单 2 中的代码执行了一些特定于 Groovy的操作,值得一提。首先,可以通过在一个属性前面附加一个 @来绕过该属性的访问器方法。我必须对构造函数中的 entity对象引用执行上述操作,否则我将调用 setProperty方法。很明显,在这个关头调用 setProperty将打破这种模式,因为 setProperty方法中的 entity变量将是 null。
其次,构造函数中的调用 this.getClass().simpleName将设置 entity的“种类”—— simpleName属性将生成一个不带包前缀的子类名称(注意,simpleName的确是对 getSimpleName的调用,但 Groovy允许我不通过对应的 JavaBeans 式的方法调用来尝试访问一个属性)。
最后,如果对 id属性(即,对象的键)进行一个调用,getProperty方法很智能,能够询问底层 key以获取它的 id。在 Google App Engine 中,entities的 key属性将自动生成。
Race 子类
定义 Race子类很简单,如清单 3 所示:
清单3.一个 Race 子类
packagecom.b50.nosqlclassRaceextendsModel }
当一个子类使用一列参数(即一个包含多个“键/值”对的 Map)实例化时,一个对应的 entity将在内存中创建。要持久存储它,只需调用 save方法。
清单4.创建一个 Race 实例并将其保存到 GAE 的数据存储
importcom.b50.nosql.Runnerdef iparams= [:] def formatter =newSimpleDateFormat("MM/dd/yyyy") def rdate = formatter.parse("04/17/2010") iparams["name"] ="Charlottesville Marathon" iparams["date"] = rdate iparams["distance"] =26.2asdouble def race =newRace(iparams)race.save()
清单4 是一个 Groovlet,其中,一个 Map(称为 iparams)创建为带有 3 个属性——一次比赛的名称、日期和距离。(注意,在 Groovy中,一个空白 Map通过 [:]创建。)Race的一个新实例被创建,然后通过 save方法存储到底层数据存储。
可以通过 Google App Engine 控制台来查看底层数据存储,确保我的数据的确在那里,如图 2 所示:
图2.查看新创建的Race
查找程序方法生成持久存储的实体
现在我已经存储了一个 Entity,拥有查找它的能力将有所帮助。接下来,我可以添加一个“查找程序”方法。在本例中,我将把这个“查找程序”方法创建为一个类方法(static)并且允许通过名称查找这些 Race(即基于 name属性搜索)。稍后,总是可以通过其他属性添加其他查找程序。
我还打算对我的查找程序采用一个惯例,即指定:任何名称中不带单词 all的查找程序都企图找到一个实例。名称中包含单词 all的查找程序(如 findAllByName)能够返回一个实例 Collection或 List。清单 5 展示了 findByName查找程序:
清单5.一个基于 Entity名称搜索的简单查找程序
static def findByName(name) {def query =newQuery(Race.class.simpleName) query.addFilter("name", Query.FilterOperator.EQUAL, name) def preparedQuery =this.datastore.prepare(query) if (preparedQuery.countEntities()>1) else}
这个简单的查找程序使用 Google App Engine 的 Query和 PreparedQuery类型来查找一个类型为“Race”的实体,其名称(完全)等同于传入的名称。如果有超过一个 Race符合这个标准,查找程序将返回一个列表的第一项,这是分页限制 1(withLimit(1))所指定的。
对应的 findAllByName与上述方法类似,但添加了一个参数,指定您想要的实体个数,如清单 6 所示:
清单 6.通过名称找到全部实体
static def findAllByName(name, pagination=10) { def query =newQuery(Race.class.getSimpleName()) query.addFilter("name", Query.FilterOperator.EQUAL, name) def preparedQuery =this.datastore.prepare(query) def entities = preparedQuery.asList(withLimit(pagination asint)) return entities.collect {newRace(it as Entity) } }
与前面定义的查找程序类似,findAllByName通过名称找到 Race实例,但是它返回所有 Race。顺便说一下,Groovy的 collect方法非常灵活:它允许删除创建 Race实例的对应的循环。注意,Groovy还支持方法参数的默认值;这样,如果我没有传入第 2 个值,pagination将拥有值 10。