Google Apps Engine将Google Datastore作为唯一的NoSQL数据库提供(我认为它基于BigTable)。
在我的应用程序中,我有一个类似社交的数据结构,我想像在图形数据库中那样对其进行建模。我的应用程序必须保存异构对象(用户,文件,...)和它们之间的关系(例如user1 OWNS file2,user2 FOLLOWS user3等)。
我正在寻找一种模拟这种典型情况的好方法,我想到了两个解决方案系列:
基于列表的解决方案:任何对象都包含其他相关对象的列表,列表中的对象存在本身就是关系(正如Google在JDO部分https://developers.google.com/appengine/docs/java/datastore/jdo/relationships中所说的那样)。
基于图形的解决方案:节点和关系都是对象。对象独立于关系存在,而每个关系包含对两个(或更多)连接对象的引用。
这两种方法的优点和缺点是什么?
关于方法1:这是一个人们可以想到的更简单的方法,它也出现在官方文档中,但是:
关于方法2:每个关系可以具有更高级别的特征(它是一个对象,它可以具有属性)。我认为内存大小不是Google的问题,但是:
你能帮助我找到两种方法中的其他重点,以这种方式选择最佳模型吗?
答案 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。
这就是原因:
用户会经常看到他们的学校但很少更改。使用这种方法,用户的学校将被缓存,并且每次打到他们的个人资料时都不会被取得。
由于您将通过密钥查找到达用户的学校,因此您无法使用查询。因此,您不必为用户的学校处理最终的一致性。
学校可能有很多用户参加。通过将这种关系存储为链接实体,我们避免创建一个巨大的单个对象。
上过学校的用户会发生很大变化。这样我们就不必经常写一个大的实体。
通过使用User实体的id作为UserSchoolLinks实体的id,我们可以只知道用户的ID来获取链接。
将学校ID和用户ID组合为SchoolUser链接的ID。我们可以进行密钥查找,以查看用户和学校是否已链接。再一次,无需担心最终的一致性。
通过将用户ID作为SchoolUserLink的属性包含在内,我们不需要解析SchoolUserLink对象来获取用户的id。我们也可以使用这个字段来检查两个方向之间的一致性,并在某些人参加数百所学校的情况下进行回退。
缺点: 这种方法违反了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它不是限制但可以。