共计 8632 个字符,预计需要花费 22 分钟才能阅读完成。
这篇文章将为大家详细讲解有关 NoSQL 数据建模的示例分析,丸趣 TV 小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。
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 = new Entity(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 基类
package com.b50.nosql import com.google.appengine.api.datastore.DatastoreServiceFactory import com.google.appengine.api.datastore.Entity abstract class Model { def entity static def datastore = DatastoreServiceFactory.datastoreService public Model(){ super() } public Model(params){ this.@entity = new Entity(this.getClass().simpleName) params.each{ key, val - this.setProperty key, val } } def getProperty(String name) { if(name.equals( id)){ return entity.key.id }else{ return entity. ${name} } } void setProperty(String name, value) { entity. ${name} = value } def save(){ this.entity.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 子类
package com.b50.nosql class Race extends Model { public Race(params){ super(params) } }
当一个子类使用一列参数(即一个包含多个“键 / 值”对的 Map)实例化时,一个对应的 entity 将在内存中创建。要持久存储它,只需调用 save 方法。
清单 4. 创建一个 Race 实例并将其保存到 GAE 的数据存储
import com.b50.nosql.Runner def iparams = [:] def formatter = new SimpleDateFormat(MM/dd/yyyy) def rdate = formatter.parse(04/17/2010) iparams[name] = Charlottesville Marathon iparams[date] = rdate iparams[distance] = 26.2 as double def race = new Race(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 = new Query(Race.class.simpleName) query.addFilter(name , Query.FilterOperator.EQUAL, name) def preparedQuery = this.datastore.prepare(query) if(preparedQuery.countEntities() 1){ return new Race(preparedQuery.asList(withLimit(1))[0]) }else{ return new Race(preparedQuery.asSingleEntity()) } }
这个简单的查找程序使用 Google App Engine 的 Query 和 PreparedQuery 类型来查找一个类型为“Race”的实体,其名称(完全)等同于传入的名称。如果有超过一个 Race 符合这个标准,查找程序将返回一个列表的第一项,这是分页限制 1(withLimit(1))所指定的。
对应的 findAllByName 与上述方法类似,但添加了一个参数,指定 您想要的实体个数,如清单 6 所示:
清单 6. 通过名称找到全部实体
static def findAllByName(name, pagination=10){ def query = new Query(Race.class.getSimpleName()) query.addFilter(name , Query.FilterOperator.EQUAL, name) def preparedQuery = this.datastore.prepare(query) def entities = preparedQuery.asList(withLimit(pagination as int)) return entities.collect { new Race(it as Entity) } }
与前面定义的查找程序类似,findAllByName 通过名称找到 Race 实例,但是它返回 所有 Race。顺便说一下,Groovy 的 collect 方法非常灵活:它允许删除创建 Race 实例的对应的循环。注意,Groovy 还支持方法参数的默认值;这样,如果我没有传入第 2 个值,pagination 将拥有值 10。
清单 7. 查找程序的实际运行
def nrace = Race.findByName(Charlottesville Marathon) assert nrace.distance == 26.2 def races = Race.findAllByName(Charlottesville Marathon) assert races.class == ArrayList.class
清单 7 中的查找程序按照既定的方式运行:findByName 返回一个实例,而 findAllByName 返回一个 Collection(假定有多个“Charlottesville Marathon”)。
“参赛者”对象没有太多不同
现在我已能够创建并找到 Race 的实例,现在可以创建一个快速的 Runner 对象了。这个过程与创建初始的 Race 实例一样简单,只需如清单 8 所示扩展 Model:
清单 8. 创建一个参赛者很简单
package com.b50.nosql class Runner extends Model{ public Runner(params){ super(params) } }
看看 清单 8,我感觉自己几乎完成工作了。但是,我还需创建参赛者和比赛之间的链接。当然,我将把它建模为一个“多对多”关系,因为我希望我的参赛者可以参加多项比赛。
没有架构的域建模
Google App Engine 在 Bigtable 上面的抽象不是一个面向对象的抽象;即,我不能原样存储关系,但可以共享键。因此,为建模多个 Race 和多个 Runner 之间的关系,我将在每个 Race 实例中存储一列 Runner 键,并在每个 Runner 实例中存储一列 Race 键。
我必须对我的键共享机制添加一点逻辑,但是,因为我希望生成的 API 比较自然 —我不想询问一个 Race 以获取一列 Runner 键,因此我想要一列 Runner。幸运的是,这并不难实现。
在清单 9 中,我已经添加了两个方法到 Race 实例。但一个 Runner 实例被传递到 addRunner 方法时,它的对应 id 被添加到底层 entity 的 runners 属性中驻留的 id 的 Collection。如果有一个现成的 runners 的 collection,则新的 Runner 实例键将添加到它;否则,将创建一个新的 Collection,且这个 Runner 的键(实体上的 id 属性)将添加到它。
清单 9. 添加并检索参赛者
def addRunner(runner){ if(this.@entity.runners){ this.@entity.runners runner.id }else{ this.@entity.runners = [runner.id] } } def getRunners(){ return this.@entity.runners.collect { new Runner( this.getEntity(Runner.class.simpleName, it) ) } }
当清单 9 中的 getRunners 方法调用时,一个 Runner 实例集合将从底层的 id 集合创建。这样,一个新方法(getEntity)将在 Model 类中创建,如清单 10 所示:
清单 10. 从一个 id 创建一个实体
def getEntity(entityType, id){ def key = KeyFactory.createKey(entityType, id) return this.@datastore.get(key) }
getEntity 方法使用 Google 的 KeyFactory 类来创建底层键,它可以用于查找数据存储中的一个单独实体。
最后,定义一个新的构造函数来接受一个实体类型,如清单 11 所示:
清单 11. 一个新添加的构造函数
public Model(Entity entity){ this.@entity = entity }
如清单 9、10 和 11、以及 图 1 的对象模型所示,我可以将一个 Runner 添加到任一 Race,也可以从任一 Race 获取一列 Runner 实例。在清单 12 中,我在这个等式的 Runner 方上创建了一个类似的联系。清单 12 展示了 Runner 类的新方法。
清单 12. 参赛者及其比赛
def addRace(race){ if(this.@entity.races){ this.@entity.races race.id }else{ this.@entity.races = [race.id] } } def getRaces(){ return this.@entity.races.collect { new Race( this.getEntity(Race.class.simpleName, it) ) } }
这样,我就使用一个无模式数据存储创建了两个域对象。
通过一些参赛者完成这个比赛
此前我所做的是创建一个 Runner 实例并将其添加到一个 Race。如果我希望这个关系是双向的,如图 1 中我的对象模型所示,那么我也可以添加一些 Race 实例到一些 Runner,如清单 13 所示:
清单 13. 参加多个比赛的多个参赛者
def runner = new Runner([fname: Chris , lname: Smith , date:34]) runner.save() race.addRunner(runner) race.save() runner.addRace(race) runner.save()
将一个新的 Runner 添加到 race 并添加对 Race 的 save 的调用后,这个数据存储已使用一列 ID 更新,如图 3 中的屏幕快照所示:
图 3. 查看一项比赛中的多个参赛者的新属性
通过仔细检查 Google App Engine 中的数据,可以看到,一个 Race 实体现在拥有了一个 Runners 的 list,如图 4 所示。
图 4. 查看新的参赛者列表
同样,在将一个 Race 添加到一个新创建的 Runner 实例之前,这个属性并不存在,如图 5 所示。
图 5. 一个没有比赛的参赛者
但是,将一个 Race 关联到一个 Runner 后,数据存储将添加新的 races ids 的 list。
图 6. 一个参加比赛的参赛者
无模式数据存储的灵活性正在刷新 —属性按照需要自动添加到底层存储。作为开发人员,我无须更新或更改架构,更谈不上部署架构了!
NoSQL 的利弊
当然,无模式数据建模也有利有弊。回顾上面的比赛应用程序,它的一个优势是非常灵活。如果我决定将一个新属性(比如 SSN)添加到一个 Runner,我不必进行大幅更改 —事实上,如果我将该属性包含在构造函数的参数中,那么它就会自动添加。对那些没有使用一个 SSN 创建的旧实例而言,发生了什么事情?什么也没发生!它们拥有一个值为 null 的字段。
另一方面,我已经明确表明要牺牲一致性和完整性来换取效率。这个应用程序的当前数据架构没有向我施加任何限制 —理论上我可以为同一个对象创建无限个实例。在 Google App Engine 引擎的键处理机制下,它们都有惟一的键,但其他属性都是一致的。更糟糕的是,级联删除不存在,因此如果我使用相同的技术来建模一个“一对多”关系并删除父节点,那么我得到一些无效的子节点。当然,我可以实现自己的完整性检查 —但关键是,我必须亲自动手(就像完成其他任务一样)。
使用无模式数据存储需要严明的纪律。如果我创建各种类型的 Races —有些有名称,有些没有,有些有 date 属性,而另一些有 race_date 属性 —那么我只是在搬起石头砸自己(或使用我的代码的人)的脚。
当然,也有可能联合使用 JDO、JPA 和 Google App Engine。在多个项目上使用过关系模型和无模式模型后,我可以说 Gaelyk 的低级 API 最灵活,使用最方便。使用 Gaelyk 的另一个好处是能够深入了解 Bigtable 和一般的无模式数据存储。
关于“NoSQL 数据建模的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。