我如何使用Google App Engine数据存储区实现这一目标?

时间:2013-11-29 21:53:15

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

我是Datastore的初学者,我想知道如何使用它来实现我想要的目标。

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

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

我正在努力的是[购买]类型的结构。

如果我将[购买]作为[客户]类型的子女,那么[客户]中的一个实体和[购买]中的一个实体是否共享相同的密钥?这是否意味着在[Purchases]实体内部,我会为每次购买而持续增加一个属性?

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

数据存储在这些情况下的表现如何?

3 个答案:

答案 0 :(得分:5)

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

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,因此您最终会达到这个目标,并且您将无法添加更多购买。

答案 1 :(得分:3)

(根据您的个人标签,我认为您是一名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,默认情况下已启用物化)。

答案 2 :(得分:0)

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

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()