App Engine(Python)数据存储预调用API钩子

5

背景

假设我正在为GAE制作应用程序,并且我想使用API Hooks

大修改:在原始版本的问题中,我描述了我的用例,但一些人正确指出它并不适合API Hooks。承认!请帮助我。但现在我的问题是学术性的:我仍然不知道如何在实践中使用hooks,我想知道。我已经重写了我的问题,使其更加通用。


代码

那么我创建一个像这样的模型:

class Model(db.Model):
    user = db.UserProperty(required=True)
    def pre_put(self):
        # Sets a value, raises an exception, whatever.  Use your imagination

然后我创建了一个db_hooks.py文件:
from google.appengine.api import apiproxy_stub_map

def patch_appengine(): 
    def hook(service, call, request, response):
        assert service == 'datastore_v3'
        if call == 'Put':
            for entity in request.entity_list():
                entity.pre_put()

    apiproxy_stub_map.apiproxy.GetPreCallHooks().Append('preput',
                                                        hook,
                                                        'datastore_v3')

由于我深受TDD之苦,所以我使用GAEUnit来完成所有这些工作。因此,在gaeunit.py文件中的主方法上方,我添加了以下内容:

import db_hooks
db_hooks.patch_appengine()

然后我编写了一个测试,实例化并放置了一个模型。


问题

虽然patch_appengine()肯定被调用了,但是钩子却从来没有被调用过。我错过了什么?如何使pre_put函数真正被调用?

3个回答

2
问题在于,在hook()函数的上下文中,entity并不是你所期望的db.Model的实例。

在这种情况下,entity是协议缓冲区类,令人困惑地称为entity (entity_pb)。把它看作你真正实体的JSON表示,所有数据都在那里,你可以从中构建一个新的实例,但没有对等的内存驻留实例在等待回调。
我知道,猴子补丁所有各种put/delete方法是设置模型级别回调的最佳方式†。
由于似乎没有太多关于如何在较新的异步调用中安全地执行此操作的资源,因此这里有一个实现before_put、after_put、before_delete和after_delete钩子的BaseModel:
class HookedModel(db.Model):

    def before_put(self):
        logging.error("before put")

    def after_put(self):
        logging.error("after put")

    def before_delete(self):
        logging.error("before delete")

    def after_delete(self):
        logging.error("after delete")

    def put(self):
        return self.put_async().get_result()

    def delete(self):
        return self.delete_async().get_result()

    def put_async(self):
        return db.put_async(self)

    def delete_async(self):
        return db.delete_async(self)

将你的模型类从HookedModel继承,并根据需要覆盖before_xxx、after_xxx方法。

将以下代码放置在应用程序全局加载的某个位置(例如,如果您使用标准的布局,则为main.py)。这是调用我们的钩子的部分:

def normalize_entities(entities):
    if not isinstance(entities, (list, tuple)):
        entities = (entities,)
    return [e for e in entities if hasattr(e, 'before_put')]

# monkeypatch put_async to call entity.before_put
db_put_async = db.put_async
def db_put_async_hooked(entities, **kwargs):
    ents = normalize_entities(entities)
    for entity in ents:
        entity.before_put()
    a = db_put_async(entities, **kwargs)
    get_result = a.get_result
    def get_result_with_callback():
        for entity in ents:
            entity.after_put()
        return get_result()
    a.get_result = get_result_with_callback
    return a
db.put_async = db_put_async_hooked


# monkeypatch delete_async to call entity.before_delete
db_delete_async = db.delete_async
def db_delete_async_hooked(entities, **kwargs):
    ents = normalize_entities(entities)
    for entity in ents:
        entity.before_delete()
    a = db_delete_async(entities, **kwargs)
    get_result = a.get_result
    def get_result_with_callback():
        for entity in ents:
            entity.after_delete()
        return get_result()
    a.get_result = get_result_with_callback
    return a
db.delete_async = db_delete_async_hooked

你可以通过model.put()或db.put()、db.put_async()等方法来保存或删除你的实例,并获得所需的效果。
†如果有更好的解决方案,我们很乐意了解!

2
钩子对于手头的任务来说有点底层。你可能需要一个自定义属性类。DerivedProperty,来自aetycoon,正是你需要的。
但请记住,用户对象的“昵称”字段可能不是你想要的 - 根据文档,如果他们使用的是gmail账户,则它只是电子邮件字段中的用户部分,否则它是他们的完整电子邮件地址。你可能想让用户自己设置昵称。

1

我认为钩子并不能真正解决这个问题。钩子只会在您的AppEngine应用程序上下文中运行,但用户可以使用Google帐户设置在应用程序外更改其昵称。如果他们这样做,它不会触发任何在您的钩子中实现的逻辑。

我认为解决您的问题的真正方法是让您的应用程序管理自己的昵称,独立于用户实体公开的昵称。


啊,该死,你说得完全正确。那么关于每个账户强制唯一性的问题呢?在进行 put 操作之前检查不是最好的方法吗? - Nick Novitski
如果您想强制唯一性,那么您必须在数据存储中创建实体之前进行检查。如果禁止用户更改该值 - 这是一个好主意 - 您只需要在创建时进行检查。 - Adam Crossland
真的,在这两种情况下,我都只会编写一个方法来进行我想要的检查和更新,然后使用它来代替put()。谢谢。 - Nick Novitski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接