Как обеспечить целостность данных для объектов в Google App Engine без использования имен ключей? - PullRequest
5 голосов
/ 19 августа 2010

У меня возникли некоторые проблемы в Google App Engine, обеспечивающей правильность моих данных при использовании отношения предка без имен ключей.

Позвольте мне объяснить немного подробнее: у меня есть родительский объект категория , и я хочу создать дочернюю сущность item .Я хотел бы создать функцию, которая принимает имя категории и имя элемента, и создает обе сущности, если они не существуют.Первоначально я создал одну транзакцию и, при необходимости, создал обе транзакции, используя имя ключа, и это работало нормально.Однако я понял, что не хочу использовать имя в качестве ключа, поскольку оно может потребоваться изменить, и я попытался в своей транзакции сделать это:

def add_item_txn(category_name, item_name):
  category_query = db.GqlQuery("SELECT * FROM Category WHERE name=:category_name", category_name=category_name)
category = category_query.get()
if not category:
    category = Category(name=category_name, count=0)

item_query = db.GqlQuery("SELECT * FROM Item WHERE name=:name AND ANCESTOR IS :category", name=item_name, category=category)
item_results = item_query.fetch(1)
if len(item_results) == 0:
  item = Item(parent=category, name=name)

db.run_in_transaction(add_item_txn, "foo", "bar")

Что я нашел, когда попытался запуститьэто то, что App Engine отклоняет это, поскольку не позволяет выполнить запрос в транзакции: Only ancestor queries are allowed inside transactions.

Рассматривая пример , Google дает о том, как решить эту проблему:

def decrement(key, amount=1):
    counter = db.get(key)
    counter.count -= amount
    if counter.count < 0:    # don't let the counter go negative
        raise db.Rollback()
    db.put(counter)

q = db.GqlQuery("SELECT * FROM Counter WHERE name = :1", "foo")
counter = q.get()
db.run_in_transaction(decrement, counter.key(), amount=5)

Я попытался переместить выборку категории до транзакции:

def add_item_txn(category_key, item_name):
    category = category_key.get()
    item_query = db.GqlQuery("SELECT * FROM Item WHERE name=:name AND ANCESTOR IS :category", name=item_name, category=category)
    item_results = item_query.fetch(1)
    if len(item_results) == 0:
         item = Item(parent=category, name=name)

category_query = db.GqlQuery("SELECT * FROM Category WHERE name=:category_name", category_name="foo")
category = category_query.get()
if not category:
    category = Category(name=category_name, count=0)
db.run_in_transaction(add_item_txn, category.key(), "bar")

Это, казалось бы, сработало, но я обнаружил, когда запустил это с несколькими запросами, которые у меня былиСоздаются дублированные категории, что имеет смысл, поскольку категория запрашивается вне транзакции, и несколько запросов могут создать несколько категорий.

Кто-нибудь знает, как правильно создать эти категории?Я попытался поместить создание категории в транзакцию, но только снова получил ошибку о запросах предков.

Спасибо!

Симон

Ответы [ 2 ]

2 голосов
/ 20 августа 2010

Вот подход к решению вашей проблемы.Во многих отношениях это не идеальный подход, и я искренне надеюсь, что кто-то другой AppEnginer предложит более точное решение, чем я.Если нет, попробуйте.

Мой подход использует следующую стратегию: он создает сущности, которые действуют как псевдонимы для сущностей категории.Имя категории может измениться, но сущность псевдонима сохранит свой ключ, и мы можем использовать элементы ключа псевдонима для создания имени ключа для ваших сущностей категории, поэтому мы сможем найти категорию по ее имени, ноего хранилище отделено от его имени.

Все псевдонимы хранятся в одной группе сущностей, и это позволяет нам использовать запрос предка, дружественный к транзакциям, поэтому мы можем искать или создавать CategoryAlias, не рискуя тем, что несколькокопии будут созданы.

Когда я захочу поискать или создать комбинацию категории и элемента, я могу использовать имя ключа категории для программной генерации ключа внутри транзакции, и мы можем получить сущность через ее ключвнутри транзакции.

class CategoryAliasRoot(db.Model):
    count = db.IntegerProperty()
    # Not actually used in current code; just here to avoid having an empty
    # model definition.

    __singleton_keyname = "categoryaliasroot"

    @classmethod
    def get_instance(cls):
            # get_or_insert is inherently transactional; no chance of
            # getting two of these objects.
        return cls.get_or_insert(cls.__singleton_keyname, count=0)

class CategoryAlias(db.Model):
    alias = db.StringProperty()

    @classmethod
    def get_or_create(cls, category_alias):
        alias_root = CategoryAliasRoot.get_instance()
        def txn():
            existing_alias = cls.all().ancestor(alias_root).filter('alias = ', category_alias).get()
            if existing_alias is None:
                existing_alias = CategoryAlias(parent=alias_root, alias=category_alias)
                existing_alias.put()

            return existing_alias

        return db.run_in_transaction(txn)

    def keyname_for_category(self):
        return "category_" + self.key().id

    def rename(self, new_name):
        self.alias = new_name
        self.put()

class Category(db.Model):
    pass

class Item(db.Model):
    name = db.StringProperty()

def get_or_create_item(category_name, item_name):

    def txn(category_keyname):
        category_key = Key.from_path('Category', category_keyname)

        existing_category = db.get(category_key)
        if existing_category is None:
            existing_category = Category(key_name=category_keyname)
            existing_category.put()

        existing_item = Item.all().ancestor(existing_category).filter('name = ', item_name).get()
        if existing_item is None:
            existing_item = Item(parent=existing_category, name=item_name)
            existing_item.put()

        return existing_item

    cat_alias = CategoryAlias.get_or_create(category_name)
    return db.run_in_transaction(txn, cat_alias.keyname_for_category())

Предостережение emptor: Я не проверял этот код.Очевидно, вам нужно будет изменить его в соответствии с вашими реальными моделями, но я думаю, что принципы, которые он использует, являются надежными.

ОБНОВЛЕНИЕ: Саймон, в своем комментарии вы в основном имеете правильную идею;хотя есть важная тонкость, которую вы не должны пропустить.Вы заметите, что сущности категории не являются потомками фиктивного корня.Они не имеют общего родителя и сами являются корневыми объектами в своих группах объектов.Если бы у всех сущностей Category был один и тот же родительский элемент, это превратилось бы в одну гигантскую группу сущностей, и у вас был бы кошмар производительности, потому что в каждой группе сущностей одновременно могла быть запущена только одна транзакция.сущности CategoryAlias ​​являются потомками фиктивного корневого лица.Это позволяет мне делать запросы внутри транзакции, но группа сущностей не становится слишком большой, потому что элементы, принадлежащие каждой категории, не привязаны к CategoryAlias.

Кроме того, данные в сущности CategoryAlias ​​могутизменить, не меняя ключ полномочий, и я использую ключ Alias ​​в качестве точки данных для генерации имени ключа, которое можно использовать при создании самих сущностей категории.Таким образом, я могу изменить имя, которое хранится в CategoryAlias, не теряя своей способности сопоставлять эту сущность с той же категорией.

0 голосов
/ 20 августа 2010

Обратите внимание на пару вещей (я думаю, что они, вероятно, просто опечатки) -

  1. Первая строка ваших транзакционных методов вызывает get () для ключа - это не документированная функция. В любом случае вам не нужно иметь фактический объект категории в функции - достаточно ключа в обоих местах, где вы используете объект категории.

  2. Вы, кажется, не вызываете put () ни для категории, ни для элемента (но поскольку вы говорите, что получаете данные в хранилище данных, я предполагаю, что вы оставили это для краткости?)

Что касается решения, вы можете попытаться добавить значение в memcache с разумным сроком действия -

if memcache.add("category.%s" % category_name, True, 60): create_category(...)

Это, по крайней мере, останавливает создание множества. Еще немного сложно узнать, что делать, если запрос не возвращает категорию, но вы не можете получить блокировку из memcache. Это означает, что категория находится в процессе создания.

Если исходящий запрос поступает из очереди задач, просто сгенерируйте исключение, чтобы задача была перезапущена.

В противном случае вы могли бы немного подождать и запросить снова, хотя это немного хитроумно.

Если запрос приходит от пользователя, вы можете сказать ему, что произошел конфликт, и попытаться снова.

...