PAYATHON 2026

通过 Jinja2 获取 ndb entity 是否计入读取计费

支付阿杰

结论

ent.rel.get() 是否计为一次 Datastore 读取,要看它有没有命中 NDB 缓存:

  • 如果命中当前请求的 context cache,通常不会访问 Datastore,也不会新增 Datastore entity read。
  • 如果没有命中 context cache,NDB 会从 Datastore 读取关联实体,并计入相应的读取用量。
  • 某些旧版 NDB 环境还会使用 Memcache。命中 Memcache 同样可以避免 Datastore 读取,具体行为取决于所用的 NDB 实现和配置。
  • Jinja2 不会改变计费规则。在模板和 Python 代码中调用同一个 get(),效果没有本质区别。

因此,下面的表达式不只是读取已有属性,它还会在模板渲染期间查找实体:

{% ent.rel.get().property %}

原因

如果 ent.rel 是 ndb.Key,调用:

ent.rel.get()

时,NDB 会先尝试从可用缓存中获取实体。只有缓存未命中时,才会访问 Datastore。

这里包含两步操作:

  1. ent.rel.get():获取关联 entity,可能触发 Datastore 读取。
  2. .property:读取已获取 entity 的属性,不会再次访问 Datastore。

在一次请求或同一个 NDB context 中,对同一个 key 多次调用 get(),后续调用通常会命中 context cache。因此,不能只根据模板中表达式出现的次数推算计费次数。批量读取、缓存配置、事务以及 NDB 版本都可能影响实际结果。

推荐做法

不要在 Jinja2 模板中访问数据。先在视图或业务逻辑中获取关联实体,再把渲染所需的数据传给模板:

related_entity = ent.rel.get() if ent.rel else None

return render_template(
    "page.html",
    ent=ent,
    related_entity=related_entity,
)

模板只读取准备好的值:

{% if related_entity %}
  {{ related_entity.property }}
{% endif %}

这样处理有几个好处:

  • 数据库访问发生在哪里一目了然,也更容易判断读取次数。
  • 可以统一处理 key 为空或关联实体不存在的情况。
  • 能避免循环渲染造成 N+1 读取。
  • Datastore 调用失败时,不会等到模板渲染阶段才报错。

多个关联实体应批量读取

如果页面要展示多条记录的关联实体,不要在循环里逐个调用 get():

{% for ent in entities %}
  {{ ent.rel.get().property }}
{% endfor %}

更合适的做法是在 Python 中收集 keys,再批量获取:

related_keys = [ent.rel for ent in entities if ent.rel]
related_entities = ndb.get_multi(related_keys)

related_by_key = {
    entity.key: entity
    for entity in related_entities
    if entity is not None
}

接着组装模板数据:

rows = [
    {
        "entity": ent,
        "related": related_by_key.get(ent.rel),
    }
    for ent in entities
]

模板只负责展示:

{% for row in rows %}
  {% if row.related %}
    {{ row.related.property }}
  {% endif %}
{% endfor %}

get_multi() 可以减少请求往返,但不代表只计算一次 entity read。对于未命中缓存的实体,通常仍会分别按照相应的实体读取规则计费。

注意事项

  • 关联 key 可能是 None,调用 get() 前应先检查。
  • key 存在不代表目标实体仍然存在。实体被删除后,get() 会返回 None。
  • context cache 通常只在当前 NDB context 中有效,不能把它当作跨请求的持久缓存。
  • 查询结果缓存与按 key 获取实体的缓存不是一回事。执行 query 时,即使部分实体已经存在于 context cache,也不能据此认为查询完全不会产生读取。
  • 不同运行环境可能使用旧版 App Engine NDB、google-cloud-ndb 或其他兼容实现,其缓存层和配置方式会有差异。判断实际行为时,应以项目使用的库版本、缓存配置和 Google Cloud 账单指标为准。

备注:内容仅供参考。