App Engine (Python) Datastore 预调用 API 挂钩

5 投票
3 回答
1711 浏览
提问于 2025-04-15 19:49

背景

假设我正在为谷歌应用引擎(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()确实被调用了,但hook却没有被触发。我漏掉了什么呢?我该如何让pre_put函数真正被调用?

3 个回答

1

我觉得 Hooks 并不能真正解决这个问题。Hooks 只会在你的 AppEngine 应用程序中运行,但用户可以通过 Google 账户设置在你的应用之外更改他们的昵称。如果他们这样做了,就不会触发你在 Hooks 中实现的任何逻辑。

我认为解决你问题的真正方法是让你的应用程序自己管理昵称,这个昵称和用户实体中显示的昵称是独立的。

2

这里的问题是,在 hook() 函数中,entity 并不是你想象中的 db.Model 的实例。

在这个上下文中,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

Hooks在这个任务上有点底层,可能不太适合。你可能更想要的是一个自定义的属性类。DerivedProperty,来自aetycoon,正好可以满足这个需求。

不过要注意,用户对象中的“昵称”字段可能不是你想要的。根据文档,如果用户使用的是gmail账号,这个昵称字段只是他们邮箱的一部分;如果不是的话,就是他们的完整邮箱地址。你可能更希望让用户自己设置昵称。

撰写回答