【问题标题】:How would I achieve this using Google App Engine Datastore?我将如何使用 Google App Engine Datastore 实现这一目标?
【发布时间】:2013-11-29 21:53:14
【问题描述】:

我是 Datastore 的初学者,我想知道如何使用它来实现我想做的事情。

例如,我的应用需要跟踪客户及其所有购买。

来自关系数据库,我可以通过创建 [Customers] 和 [Purchases] 表来实现这一点。 在 Datastore 中,我可以创建 [Customers] 和 [Purchases] 种类。

我苦苦挣扎的是 [Purchases] 类型的结构。

如果我将 [Purchases] 作为 [Customers] 类的子对象,那么 [Customers] 中的一个实体和 [Purchases] 中的一个实体是否共享相同的密钥?这是否意味着在这个 [Purchases] 实体内部,我的财产会随着他们每次购买而不断增加?

或者我是否会为他们进行的每次购买拥有一个 [Purchases] 实体,并且在每个实体中我都会有一个指向 [Customers] 类型实体的属性?

Datastore 在这些场景中的表现如何?

【问题讨论】:

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


    【解决方案1】:

    听起来你并不完全了解祖先。让我们先使用非祖先版本,这是一种合法的方式:

    class Customer(ndb.Model):
        # customer data fields
        name = ndb.StringProperty()
    
    class Purchase(ndb.Model):
        customer = ndb.KeyProperty(kind=Customer)
        # purchase data fields
        price = ndb.IntegerProperty
    

    这是基本的方法。对于每个客户,您将在数据存储中拥有一个实体。对于每次购买,您将在数据存储区中拥有一个实体,其密钥属性指向客户。

    如果您有购买商品,并且需要找到相关客户,它就在那里。

    purchase_entity.customer.get()
    

    如果您有客户,您可以发出查询以查找属于该客户的所有购买:

    Purchase.query(customer=customer_entity.key).fetch()
    

    在这种情况下,每当您写入客户或购买实体时,GAE 数据存储都会将该实体写入该实体,该实体会在云中运行的任何一台不忙的数据存储机器中写入。通过这种方式,您可以获得非常高的写入吞吐量。但是,当您查询给定客户的所有购买时,您只需读回索引中的最新数据。如果添加了新购买,但索引尚未更新,那么您可能会得到陈旧的数据(最终一致性)。除非您使用祖先,否则您会被这种行为困扰。

    现在至于祖先版本。基本概念基本相同。您仍然有一个客户实体,并且每次购买都有单独的实体。购买不是客户实体的一部分。但是,当您使用客户作为祖先创建购买时,它(大致)意味着购买存储在存储客户实体的数据存储中的同一台机器上。在这种情况下,您的写入性能仅限于那台机器的性能,并被宣传为每秒一次写入。不过,作为一个好处,您可以使用祖先查询来查询该机器,并获取给定客户所有购买的最新列表。

    使用祖先的语法有点不同。客户部分是一样的。但是,当您创建购买时,您将其创建为:

    purchase1 = Purchase(ancestor=customer_entity.key)
    purchase2 = Purchase(ancestor=customer_entity.key)
    

    此示例创建两个单独的购买实体。每次购买都有不同的密钥,客户也有自己的密钥。但是,每个购买密钥都将嵌入 customer_entity 的密钥。所以你可以认为购买密钥的长度是原来的两倍。但是,您不再需要为客户保留单独的 KeyProperty(),因为您可以在购买键中找到它。

    class Purchase(ndb.Model):
        # you don't need a KeyProperty for the customer anymore
        # purchase data fields
        price = ndb.IntegerProperty
    
    purchase.key.parent().get()
    

    为了查询给定客户的所有购买:

    Purchase.query(ancestor=customer_entity.key).fetch()
    

    实体的实际结构没有太大变化,主要是语法。但是祖先查询是完全一致的。

    不推荐您描述的第三个选项。我只是为了完整性而将其包括在内。这有点令人困惑,并且会像这样:

    class Purchase(ndb.Model):
        # purchase data fields
        price = ndb.IntegerProperty()
    
    class Customer(ndb.Model):
        purchases = ndb.StructuredProperty(Purchase, repeated=True)
    

    这是使用 ndb.StructuredProperty 的特殊情况。在这种情况下,您将在数据存储区中只有一个客户实体。虽然有一个购买类,但您的购买不会作为单独的实体存储 - 它们只会作为数据存储在 Customer 实体中。

    这样做可能有几个原因。您只处理一个实体,因此您的数据获取将是完全一致的。当您必须更新大量购买时,您还可以降低写入成本,因为您只编写一个实体。您仍然可以查询 Purchase 类的属性。但是,这是为仅具有有限数量或重复对象而设计的,而不是数百或数千个对象。每个实体的总大小限制为 1MB,因此您最终会达到该大小,并且您将无法添加更多购买。

    【讨论】:

    • 第三个选项绝对不是要走的路。所以那个被检查了。因此,根据您的有用解释,如果数据强一致性对我的应用程序来说不是太大问题,我似乎会首选。如果这确实很重要,请排在第二位。如果我选择第一个选项但使用数据存储提供的这些“事务”怎么样?我会实现类似于祖先选项的东西吗?
    • 事务允许您将多个数据存储操作作为一个原子单元进行。假设用户实体有购买计数,您可以添加购买,并一次性更新用户实体的购买计数,而无需担心竞争条件。但是,您无法实现返回所有购买的查询,这保证是最新的。您可能会做一个小技巧,例如在 User 实体中存储所有购买的列表,然后在事务中更新该列表,然后您不需要发出查询。事务和祖先解决不同的事情。
    • 关于事务的附加注释 - 一个事务最多可以对 5 个不同的实体组(一组祖先 + 后代)进行操作。因此,将用户构建为祖先或购买实际上将允许您进行涉及更多实体的更复杂的交易。
    • 这就是我目前所理解的。事务通过将数据存储操作保持为原子操作来保证数据完整性,但它不能保证最新的查询。您必须使用祖先查询才能看到最新的数据集。
    【解决方案2】:

    (根据你的个人标签,我假设你是一个 java 人,使用 GAE+java)

    首先,不要使用the ancestor relationships - 这有一个特殊目的来定义事务范围(也称为实体组)。它有几个限制,不应用于实体之间的正常关系。

    其次,一定要使用 ORM 而不是低级 API:我个人最喜欢的是 objectify。 GAE 还提供JDOJPA

    在 GAE 中,实体之间的关系只是通过在另一个实体中存储对一个实体的引用(键)来创建的。

    在您的情况下,有两种可能性可以在客户与其购买之间创建一对多关系。

    public class Customer {
        @Id
        public Long customerId;  // 'Long' identifiers are autogenerated
    
        // first option: parent-to-children references
        public List<Key<Purchase>> purchases; // one-to-many parent-to-child
    }
    
    public class Purchase {
        @Id
        public Long purchaseId;
    
        // option two: child-to-parent reference
        public Key<Customer> customer;
    }
    

    您是否使用选项 1 或选项 2(或两者)取决于您计划访问数据的方式。区别在于您使用的是get 还是query。两者之间的区别在于成本和速度,get 总是更快更便宜。

    注意:GAE 数据存储区中的引用是手动的,没有参照完整性:删除关系的一部分将不会从数据存储区产生警告/错误。当您删除实体时,由您的代码来修复引用 - 使用事务来一致地更新两个实体(提示:无需使用实体组 - 要更新事务中的两个实体,您可以使用 XG transactions,在 objectify 中默认启用) .

    【讨论】:

    • 如果我遵循选项 1,我最终会遇到每个实体 1 MB 的限制,对吗?我肯定会使用 ORM 进行检查,但我想知道将它与 memcache 一起使用是多么容易。如果以后性能成为问题,我最终将不得不利用 memcache。
    • Objectify 透明地使用缓存:code.google.com/p/objectify-appengine/wiki/Caching
    • 谢谢。我肯定会给 Objectify 一个机会。
    • 我已经下载了最新的 Objectify 并尝试做一些基本的操作。看起来 ofy() 静态并不像文档所说的那样存在于他们的网站上。官方文档过时了吗?
    【解决方案3】:

    我认为在这种特定情况下最好的方法是使用父结构。

    class Customer(ndb.Model):
        pass
    
    class Purchase(ndb.Model):
        pass
    
    customer = Customer()
    customer_key = customer.put()
    
    purchase = Purchase(parent=customer_key)
    

    然后您可以使用

    获得客户的所有购买
    purchases = Purchase.query(ancestor=customer_key)
    

    或让购买的客户使用

    customer = purchase.key.parent().get()
    

    当您经常使用该值时,确实跟踪购买计数可能是一个好主意。 您可以使用_pre_put_hook_post_put_hook 来做到这一点

    class Customer(ndb.Model):
        count = ndb.IntegerProperty()
    
    class Purchase(ndb.Model):
        def _post_put_hook(self):
            # TODO check whether this is a new entity.
            customer = self.key.parent().get()
            customer.count += 1
            customer.put()
    

    在事务中执行此操作也是一种好习惯,因此当购买失败时会重置计数,反之亦然。

    @ndb.transactional
    def save_purchase(purchase):
        purchase.put()
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-10-23
      • 2013-08-11
      • 1970-01-01
      • 1970-01-01
      • 2016-04-03
      • 2013-03-19
      • 2015-07-25
      相关资源
      最近更新 更多