执行多部分事务的最高效方式是将事务封装在 Gremlin 脚本中,并将其作为单个请求执行。
这是一个如何做到这一点的示例——它来自我去年为 Neo4j Heroku Challenge 开发的一个示例应用程序。
项目名为Lightbulb:https://github.com/espeed/lightbulb
自述文件描述了它的作用......
什么是灯泡?
Lightbulb 是一个基于 Git 的、由 Neo4j 支持的 Heroku 博客引擎
用 Python 编写。
您可以在 Emacs(或您最喜欢的文本编辑器)中编写博客条目
并使用 Git 进行版本控制,而不放弃
动态应用。
在 ReStructuredText 中编写博客条目,并使用您的
网站的模板系统。
当您推送到 Heroku 时,条目元数据将自动
保存到 Neo4j,以及从
ReStructuredText 源文件将从磁盘提供。
但是,Neo4j 停止在其免费/测试 Heroku Add On 上提供 Gremlin,因此 Lightbulb 将不适用于 Neo4j/Heroku 新用户。
在明年之内——在TinkerPop book 发布之前——TinkerPop 将发布一个 Rexster Heroku Add On,它完全支持 Gremlin,这样人们就可以在阅读本书的同时在 Heroku 上运行他们的项目。
但现在,您无需担心运行应用程序——所有相关代码都包含在这两个文件中——Lightbulb 应用程序的模型文件及其 Gremlin 脚本文件:
https://github.com/espeed/lightbulb/blob/master/lightbulb/model.py
https://github.com/espeed/lightbulb/blob/master/lightbulb/gremlin.groovy
model.py 提供了一个用于构建自定义灯泡模型和自定义灯泡Graph 类的示例。
gremlin.groovy 包含自定义 Entry 模型执行的自定义 Gremlin 脚本 -- 这个 Gremlin 脚本封装了整个多部分事务,以便它可以作为单个请求执行。
注意在上面的model.py 文件中,我通过重写create() 和update() 方法来自定义EntryProxy,而是定义一个单一的save() 方法来处理创建和更新。
要将自定义 EntryProxy 挂钩到 Entry 模型中,我只需重写 Entry 模型的 get_proxy_class 方法,以便它返回 EntryProxy 类而不是默认的 NodeProxy 类。
Entry 模型中的所有其他内容都是围绕为save_blog_entry Gremlin 脚本(在上面的 gremlin.groovy 文件中定义)构建数据而设计的。
请注意 gremlin.groovy 中的 save_blog_entry() 方法很长并且包含多个闭包。您可以将每个闭包定义为一个独立的方法并使用多个 Python 调用来执行它们,但是您会产生发出多个服务器请求的开销,并且由于请求是分开的,因此无法将它们全部包装在事务中。
通过使用单个 Gremlin 脚本,您可以将所有内容组合到单个事务请求中。这要快得多,而且是事务性的。
可以在 Gremlin 方法的最后一行看到整个脚本是如何执行的:
return transaction(save_blog_entry);
在这里,我只是在内部save_blog_entry 闭包中的所有命令周围包装了一个事务闭包。使事务闭包保持代码隔离,并且比将事务逻辑嵌入到其他闭包中要干净得多。
然后,如果您查看内部 save_blog_entry 闭包中的代码,它只是调用我在上面定义的其他闭包,使用我在 Entry 模型中调用脚本时从 Python 传入的参数:
def _save(self, _data, kwds):
script = self._client.scripts.get('save_blog_entry')
params = self._get_params(_data, kwds)
result = self._client.gremlin(script, params).one()
我传入的参数是在模型的自定义_get_parms()方法中建立起来的:
def _get_params(self, _data, kwds):
params = dict()
# Get the property data, regardless of how it was entered
data = build_data(_data, kwds)
# Author
author = data.pop('author')
params['author_id'] = cache.get("username:%s" % author)
# Topic Tags
tags = (tag.strip() for tag in data.pop('tags').split(','))
topic_bundles = []
for topic_name in tags:
#slug = slugify(topic_name)
bundle = Topic(self._client).get_bundle(name=topic_name)
topic_bundles.append(bundle)
params['topic_bundles'] = topic_bundles
# Entry
# clean off any extra kwds that aren't defined as an Entry Property
desired_keys = self.get_property_keys()
data = extract(desired_keys, data)
params['entry_bundle'] = self.get_bundle(data)
return params
这是_get_params() 在做什么...
buld_data(_data, kwds)是bulbs.element中定义的函数:
https://github.com/espeed/bulbs/blob/master/bulbs/element.py#L959
它只是合并参数,以防用户输入一些作为位置参数和一些作为关键字参数。
我传入_get_params()的第一个参数是author,也就是作者的用户名,但是我没有把用户名传给Gremlin脚本,我传的是author_id。 author_id 已缓存,因此我使用用户名查找 author_id 并将其设置为参数,稍后我将其传递给 Gremlin save_blog_entry 脚本。
然后我为设置的每个博客标签创建TopicModel 对象,并在每个对象上调用get_bundle(),并将它们保存为参数中的topic_bundles 列表。
get_bundle() 方法在bulbs.model 中定义:
https://github.com/espeed/bulbs/blob/master/bulbs/model.py#L363
它只返回一个元组,其中包含模型实例的data、index_name 和索引keys:
def get_bundle(self, _data=None, **kwds):
"""
Returns a tuple containing the property data, index name, and index keys.
:param _data: Data that was passed in via a dict.
:type _data: dict
:param kwds: Data that was passed in via name/value pairs.
:type kwds: dict
:rtype: tuple
"""
self._set_property_defaults()
self._set_keyword_attributes(_data, kwds)
data = self._get_property_data()
index_name = self.get_index_name(self._client.config)
keys = self.get_index_keys()
return data, index_name, keys
我向 Bulbs 添加了 get_bundle() 方法,以提供一种将参数捆绑在一起的漂亮而整洁的方式,这样您的 Gremlin 脚本就不会在其签名中被大量 args 所淹没。
最后,对于Entry,我只需创建一个entry_bundle 并将其存储为参数。
请注意,_get_params() 返回三个参数的 dict:author_id、topic_bundle 和 entry_bundle。
这个params dict 直接传递给Gremlin 脚本:
def _save(self, _data, kwds):
script = self._client.scripts.get('save_blog_entry')
params = self._get_params(_data, kwds)
result = self._client.gremlin(script, params).one()
self._initialize(result)
Gremlin 脚本与params 传入的参数名称相同:
def save_blog_entry(entry_bundle, author_id, topic_bundles) {
// Gremlin code omitted for brevity
}
然后根据需要在 Gremlin 脚本中简单地使用这些参数 - 没什么特别的。
现在我已经创建了我的自定义模型和 Gremlin 脚本,我构建了一个自定义 Graph 对象,它封装了所有代理和相应的模型:
class Graph(Neo4jGraph):
def __init__(self, config=None):
super(Graph, self).__init__(config)
# Node Proxies
self.people = self.build_proxy(Person)
self.entries = self.build_proxy(Entry)
self.topics = self.build_proxy(Topic)
# Relationship Proxies
self.tagged = self.build_proxy(Tagged)
self.author = self.build_proxy(Author)
# Add our custom Gremlin-Groovy scripts
scripts_file = get_file_path(__file__, "gremlin.groovy")
self.scripts.update(scripts_file)
您现在可以直接从应用的model.py 导入Graph 并像往常一样实例化Graph 对象。
>> from lightbulb.model import Graph
>> g = Graph()
>> data = dict(username='espeed',tags=['gremlin','bulbs'],docid='42',title="Test")
>> g.entries.save(data) # execute transaction via Gremlin script
这有帮助吗?