Java - Google App Engine - 在Google Datastore中建模图形结构

时间:2014-04-15 13:17:03

标签: java google-app-engine google-cloud-datastore nosql

Google Apps Engine将Google Datastore作为唯一的NoSQL数据库提供(我认为它基于BigTable)。

在我的应用程序中,我有一个类似社交的数据结构,我想像在图形数据库中那样对其进行建模。我的应用程序必须保存异构对象(用户,文件,...)和它们之间的关系(例如user1 OWNS file2,user2 FOLLOWS user3等)。

我正在寻找一种模拟这种典型情况的好方法,我想到了两个解决方案系列:

  1. 基于列表的解决方案:任何对象都包含其他相关对象的列表,列表中的对象存在本身就是关系(正如Google在JDO部分https://developers.google.com/appengine/docs/java/datastore/jdo/relationships中所说的那样)。

  2. 基于图形的解决方案:节点和关系都是对象。对象独立于关系存在,而每个关系包含对两个(或更多)连接对象的引用。

  3. 这两种方法的优点和缺点是什么?

    关于方法1:这是一个人们可以想到的更简单的方法,它也出现在官方文档中,但是:

    • 每个有向关系都会使对象记录增长:例如,对象维度限制给出的可能关系的数量有限制吗?
    • 这是JDO功能还是数据存储结构允许自然实现该方法?
    • 关系搜索时间会随着列表而增加,这个解决方案适合大(百万)关系吗?

    关于方法2:每个关系可以具有更高级别的特征(它是一个对象,它可以具有属性)。我认为内存大小不是Google的问题,但是:

    • 每个关系都需要自己的记录,因此每个相关夫妇的搜索时间将随着关系总数的增加而增加。这适合大量的关系(数百万,数十亿)?即如果记录结构合理,Google是否有很好的搜索记录?或者我将很快处于这样一种情况:如果我想搜索User1的朋友User4,我必须等待几秒钟?
    • 另一方面,随着新关系的增加,每个对象的尺寸都不会增加。

    你能帮助我找到两种方法中的其他重点,以这种方式选择最佳模型吗?

3 个答案:

答案 0 :(得分:5)

首先,数据存储区中的搜索时间不依赖于您存储的实体数量,而只取决于您检索的实体数量。因此,如果您需要在十亿之中找到一个关系对象,那么就需要花费相同的时间,就像您只有一个对象一样。

其次,列表方法有一个严重的限制,称为#34;爆炸索引"。您必须索引包含列表的属性以使其可搜索。如果您使用的参数不仅仅引用了此属性,那么您将遇到此问题 - 谷歌它可以了解其含义。

第三,列表方法要贵得多。每次添加新关系时,都会以相当高的写入成本重写整个实体。如果您不能使用仅限密钥查询,那么阅读成本也会更高。使用对象方法,您可以使用仅键查询来查找关系,此类查询现在是免费的。

更新:

如果您的关系是定向的,您可以考虑将关系实体作为User实体的子项,并使用Object id作为Relationship实体的id。那么您的Relationship实体将根本没有属性,这可能是最具成本效益的解决方案。您将能够使用仅限密钥ancestor queries检索用户拥有的所有对象。

答案 1 :(得分:1)

我有一个AppEngine应用程序,我使用这两种方法。哪个更好取决于两件事:可以存在多少关系的实际限制以及关系改变的频率。

注1:我的回答是基于Objectify的经验和大量使用缓存。里程可能因其他方法而异。

注意2:我已经使用了“id'而不是正确的DataStore术语' name'这里。名称会令人困惑,而且id会更好地符合客观化条款。

考虑与他们所在学校相关的用户,反之亦然。在这种情况下,你会两个都做。将用户链接到学校,其中包含'列表'方法。将用户参加的学校ID列表存储为具有不同类型/种类但ID与用户相同的UserSchoolLinks实体。例如,如果用户的id =' 6h30n'存储ID为#6; 6h30n'的UserSchoolLinks对象。每当您需要获取用户的学校列表时,通过键查找加载此单个实体。

但是,不要为上学的用户做相反的事情。对于该关系,请插入链接实体。使用学校的id和用户ID的组合作为链接实体的id。将两个id作为单独的属性存储在实体中。例如,用户' 6h30n'的SchoolUserLink;上学' g3g0a3'获得id' g3g0a3~6h30n'并包含以下字段:school = g3g0a3和user = 6h30n。使用学校财产上的查询来获取学校的所有SchoolUserLinks。

这就是原因:

  1. 用户会经常看到他们的学校但很少更改。使用这种方法,用户的学校将被缓存,并且每次打到他们的个人资料时都不会被取得。

  2. 由于您将通过密钥查找到达用户的学校,因此您无法使用查询。因此,您不必为用户的学校处理最终的一致性。

  3. 学校可能有很多用户参加。通过将这种关系存储为链接实体,我们避免创建一个巨大的单个对象。

  4. 上过学校的用户会发生很大变化。这样我们就不必经常写一个大的实体。

  5. 通过使用User实体的id作为UserSchoolLinks实体的id,我们可以只知道用户的ID来获取链接。

  6. 将学校ID和用户ID组合为SchoolUser链接的ID。我们可以进行密钥查找,以查看用户和学校是否已链接。再一次,无需担心最终的一致性。

  7. 通过将用户ID作为SchoolUserLink的属性包含在内,我们不需要解析SchoolUserLink对象来获取用户的id。我们也可以使用这个字段来检查两个方向之间的一致性,并在某些人参加数百所学校的情况下进行回退。

  8. 缺点:  这种方法违反了DRY原则。看起来像这里最少的邪恶。  我们仍然需要使用查询来获得上过学校的用户。这意味着要处理最终的一致性。

    不要忘记更新UserSchoolLinks实体,并在交易中添加/删除SchoolUserLink实体。

答案 2 :(得分:-1)

你的问题太复杂了,但我尝试解释最好的解决方案(我将在Python中回答,但同样可以用Java完成)。

class User(db.User):
  followers = db.StringListProperty()

简单添加关注者。

user = User.get(key)
user.followers.append(str(followerKey))

这允许快速查询被关注者和关注者

User.all().filter('followers', followerKey) # -> followed

此查询的成本非常高,因此您可以在i / o写入中使其更快但更复杂且成本更高:

class User(db.User):
  followers = db.StringListProperty()
  follows = db.StringListProperty()

无论在更改期间这是多么复杂,因为删除用户需要更新,因此您需要2次写入。

你也可以存储关系,但这是更糟糕的情况,因为它比追随者的第二个例子更复杂,并且遵循...... - 请记住,实体可以拥有1Mb它不是限制但可以。