1. 项目简介
Neo4j是图形数据库的典型代表,详情请见参考,官方默认支持Python的操作调用。
而推荐的对象图映射器(OGM,Object Graph Mapper)有neo4django,存在支持Django和Neo4j版本过低、支持操作少等问题。通过一段时间的资料查找,找到了Neomodel,相比之下,其优点如下:
-
类似于Django的model格式的定义
-
强大的查询API
-
通过技术限制强制你的模式(Enforce your schema through cardinality restrictions)
-
完整的transaction支持
-
进程安全
-
预先/事后 保存/删除 hooks
-
通过django_neomodel 进行整合
2. Neomodel的安装
2.1 Requirements
-
python 2.7, 3.3+
-
neo4j 3.0+
3. 基本操作
3.1 连接数据库
在执行任何neomodel代码之前,需要在设置连接URL,代码如下:
from neomodel import config
config.DATABASE_URL = 'bolt://neo4j:neo4j@localhost:7687' # default
在Django中,可以在settings.py中设置。
注意修改默认的用户名和密码
同时,可以通过调用set_connection
在任何时间修改连接url,代码如下:
from neomodel import db
db.set_connection('bolt://neo4j:neo4j@localhost:7687')
3.2 节点及关系定义
本小节先介绍基本的节点及关系定义操作,如下:
from neomodel import (config, StructuredNode, StringProperty, IntegerProperty, UniqueIdProperty, RelationshipTo, RelationshipFrom)
config.DATABASE_URL = 'bolt://neo4j:password@localhost:7687'
class Country(StructuredNode):
code = StringProperty(unique_index=True, required=True)
# traverse incoming IS_FROM relation, inflate to Person objects
inhabitant = RelationshipFrom('Person', 'IS_FROM')
class Person(StructuredNode):
uid = UniqueIdProperty()
name = StringProperty(unique_index=True)
age = IntegerProperty(index=True, default=0)
# traverse outgoing IS_FROM relations, inflate to Country objects
country = RelationshipTo(Country, 'IS_FROM')
以上代码定义的一种关系是IS_FROM
,定义了两种不同的关系方法,一种通过Person对象,一种通过Country对象。
如果不想表示关联关系的方向,则可以使用Relationship
替代RelationshipTo
和RelationshipFrom
。
Neomodel会自动的为数据库中的每个StructuredNode
类创建一个标签,具有相应的索引和约束。
3.3 添加到数据库
在Python中创建节点定义以后,任何约束或索引需要被添加到Neo4j。Noemodel提供了一个脚本来自动执行,如下:
$ neomodel_install_labels yourapp.py someapp.models --db bolt://neo4j:neo4j@localhost:7687
修改schema之后执行。
3.4 创建、保存、删除
常规的使用方法如下:
jim = Person(name='Jim', age=3).save()
jim.age = 4
jim.save() # validation happens here
jim.delete()
jim.refresh() # reload properties from neo
jim.id # neo4j internal id
3.5 查找节点
使用.nodes
类属性:
# raises Person.DoesNotExist if no match
jim = Person.nodes.get(name='Jim')
# Will return None unless bob exists
someone = Person.nodes.get_or_none(name='bob')
# Return set of nodes
people = Person.nodes.filter(age__gt=3)
3.6 关系定义
使用关系如下:
germany = Country(code='DE').save()
jim.country.connect(germany)
if jim.country.is_connected(germany):
print("Jim's from Germany")
for p in germany.inhabitant.all():
print(p.name) # Jim
len(germany.inhabitant) # 1
# Find people called 'Jim' in germany
germany.inhabitant.search(name='Jim')
jim.country.disconnect(germany)
4. 关系
无方向的关联关系,第一个参数是类,第二个参数是neo4j的关系:
class Person(StructuredNode):
friends = Relationship('Person', 'FRIEND')
当定义关系,可以参考其它模型中的类,可以避免循环导入:
class Garage(StructuredNode):
cars = RelationshipTo('transport.models.Car', 'CAR')
vans = RelationshipTo('.models.Van', 'VAN')
4.1 基数
在关系中可能强制基数限制,需要记住在两边的定义中都要进行说明。
class Person(StructuredNode):
car = RelationshipTo('Car', 'CAR', cardinality=One)
class Car(StructuredNode):
owner = RelationshipFrom('Person', cardinality=One)
以下基数类是可用的:
ZeroOrMore (默认), OneOrMore, ZeroOrOne, One
如果基数限制被现有数据打破,就会引起一个CardinalityViolation
异常;尝试去打破基数限制,会引起一个AttemptedCardinalityViolation
。
4.2 属性
Neomodel使用关系模型来定义存储在关系中的属性(即关系可以通过模型来表示)。
class FriendRel(StructuredRel):
since = DateTimeProperty(default=lambda: datetime.now(pytz.utc))
met = StringProperty()
class Person(StructuredNode):
name = StringProperty()
friends = RelationshipTo('Person', 'FRIEND', model=FriendRel)
rel = jim.friends.connect(bob)
rel.since # datetime object
当调用连接方法时,这些可以通过:
rel = jim.friends.connect(bob, {'since': yesterday, 'met': 'Paris'})
print(rel.start_node().name) # jim
print(rel.end_node().name) # bob
rel.met = "Amsterdam"
rel.save()
使用“relationship”方法,可以在两个节点之间检索关系,这只在一个定义了关系模型的关系中可用。
rel = jim.friends.relationship(bob)
4.3 关系的独特性
neomodel的默认情况下,两个节点之间只有一种类型的关系,除非在调用连接时定义不同的属性。neomodel在cypher中使用CREATE UNIQUE
来实现。
4.4 显性穿越(Explicit Traversal)【没搞懂】
通过创建一个Traversal对象来具体说明一个节点穿越,这将会使得所有Person
实体直接关联到另一个Person
,通过所有关系:
definition = dict(node_class=Person, direction=OUTGOING, relation_type=None, model=None)
relations_traversal = Traversal(jim, Person.__label__, definition)
all_jims_relations = relations_traversal.all()
-
node类:关系目标的类型
-
direction:OUTGOING/INCOMING/EITHER
-
realtion_type:可以是None(任何方向),”*“用于所有的路径,或关系类型的明确名称
-
model:model对象的类型,没有简单的关系。
5. 属性类型
以下属性在节点和关系中可用:
StringProperty, IntegerProperty, FloatProperty, BooleanProperty, ArrayProperty
DateProperty, DateTimeProperty, JSONProperty, AliasProperty, UniqueIdProperty
5.1 默认
默认值,可以给任何属性添加一个默认值,这可以是一个函数或任何可调用的:
from uuid import uuid4
my_id = StringProperty(unique_index=True, default=uuid4)
可以使用包装函数或lambda函数提供参数:
my_datetime = DateTimeProperty(default=lambda: datetime.now(pytz.utc))
5.2 选择
可以使用选项指定StringProperty
的有效值列表:
class Person(StructuredNode):
SEXES = (
('M', 'Male'),
('F', 'Female')
)
sex = StringProperty(required=True, choices=SEXES)
tim = Person(sex='M').save()
tim.sex # M
tim.get_sex_display() # 'Male'
值会被检查,包括何时从neo4j中保存和加载。
5.3 列表属性
Neo4j支持列表表示属性值,使用ArrayProperty
类表示。你可以随意的提供一个列表元素类型,和另一个属性实例,作为ArrayProperty的第一个参数:
class Person(StructuredNode):
names = ArrayProperty(StringProperty(), required=True)
bob = Person(names=['bob', 'rob', 'robert']).save()
在这个例子中,列表中的每一个元素在持久化之前,都被放大到一个字符串。
5.4 唯一的标识符
neo4j中的所有节点有一个内部id(在neomodel中通过“id”进行访问),但是这些不能被一个应用程序所用。neomodel提供UniqueIdProperty
来产生节点的唯一标识符(和唯一的索引):
class Person(StructuredNode):
uid = UniqueIdProperty()
Person.nodes.get(uid='a12df...')
5.5 日期和时间
“DateTimeProperty”接受如何时间区域的datetime.datetime对象,并将它们保存为一个UTC 历元值。这些UTC历元值受UTC 时间区域的datetime.datetime对象影响。
“DateProperty”接受datetime.date对象,作为“YYYY-MM-DD”字符串属性存储。
可以使用”default_now”参数默认存储当前的时间:
created = DateTimeProperty(default_now=True)
可以通过设置配置NEOMODEL_FORCE_TIMEZONE=1
强制设置时间域。
5.6 其它属性
-
“EmailProperty”——确认邮件(通过正则)
-
“RegexProperty”——通过验证器正则表达式:”RegexProperty”(expression=r’dw’)
-
“NormalProperty”——使用一个(标准化的)方法来“充气”和“放气”【搞不懂】
5.7 别名属性
允许别名到其它属性,可用于提供“魔术”行为。(仅支持StructuredNodes
):
class Person(StructuredNode):
full_name = StringProperty(index=True)
name = AliasProperty(to='full_name')
Person.nodes.filter(name='Jim') # just works
5.8 独立数据库属性名
您可以使用”db_property”指定独立的属性名称,该名称在数据库级别上使用。 它的行为就像Django的”db_column”。 这对于例如在python属性后面隐藏图属性 很有用:
class Person(StructuredNode):
name_ = StringProperty(db_property='name')
@property
def name(self):
return self.name_.lower() if self.name_ else None
@name.setter
def name(self, value):
self.name_ = value
6. 高级查询
Neomodel 包括一个API,用于查询节点集,而不需要写cypher:
class SupplierRel(StructuredRel):
since = DateTimeProperty(default=datetime.now)
class Supplier(StructuredNode):
name = StringProperty()
delivery_cost = IntegerProperty()
coffees = RelationshipTo('Coffee', 'SUPPLIES')
class Coffee(StructuredNode):
name = StringProperty(unique_index=True)
price = IntegerProperty()
suppliers = RelationshipFrom(Supplier, 'SUPPLIES', model=SupplierRel)
6.1 节点设置和过滤
一个类上的nodes
属性是包含该类型的数据库中所有节点的集合。
该集合(节点集合)可以进行迭代和过滤。在hood下使用neo4j 2中引进的标签:
# nodes with label Coffee whose price is greater than 2
Coffee.nodes.filter(price__gt=2)
try:
java = Coffee.nodes.get(name='Java')
except Coffee.DoesNotExist:
print "Couldn't find coffee 'Java'"
过滤方法借助相同的django过滤格式,使用双下划线前缀运算符:
-
lt ——小于
-
gt ——大于
-
lte —— 小于等于
-
gte —— 大于等于
-
ne —— 不等于
-
in —— 項在列表
-
isnull ——True 是NULL,False是非NULL
-
exact —— 字符串等于
-
iexact —— 字符串等于,大小写不敏感
-
contains —— 包含字符串值
-
icontains —— 包含字符串值,大小写不敏感
-
startswith —— 以字符串值开始
-
istartswith —— 以字符串值开始,大小写不敏感
-
endswith —— 以字符串值结束
-
iendswith —— 以字符串值结束,大小写不敏感
-
regex —— 匹配一个正则表达
-
iregex —— 匹配一个正则表达,大小写不敏感
6.2 有一个关联关系
“has”方法检查(一个或多个)关联的存在性,在以下例子中,它返回有supplier的”Coffee”节点集:
Coffee.nodes.has(suppliers=True)
设置suppliers=False
,可以查找没有supplier的Coffee
节点。
6.3 迭代、切片和更多
支持迭代、切片和计算:
# Iterable
for coffee in Coffee.nodes:
print coffee.name
# Sliceable using python slice syntax
coffee = Coffee.nodes.filter(price__gt=2)[2:]
切片语法返回一个节点集对象,可以链接。
Length和Boolean方法不返回节点集,所以不能链接的更远:
# Count with __len__
print len(Coffee.nodes.filter(price__gt=2))
if Coffee.nodes:
print "We have coffee nodes!"
6.4 通过关系属性过滤
使用match
方法过滤关系属性是可能的,注意,关系必须有一个定义:
nescafe = Coffee.nodes.get(name="Nescafe")
for supplier in nescafe.suppliers.match(since_lt=january):
print supplier.name
6.5 通过属性排序
为了通过一个特定的属性得到排序结果,使用order_by
方法:
# Ascending sort
for coffee in Coffee.nodes.order_by('price'):
print coffee, coffee.price
# Descending sort
for supplier in Supplier.nodes.order_by('-delivery_cost'):
print supplier, supplier.delivery_cost
为了从之前定义的查询中移除排序,在order_by
中使用None:
# Sort in descending order
suppliers = Supplier.nodes.order_by('-delivery_cost')
# Don't order; yield nodes in the order neo4j returns them
suppliers = suppliers.order_by(None)
随机排序,在order_by
中使用“?”:
Coffee.nodes.order_by('?')
7. Cypher查询
可能通过cypher进行复杂的查询,每个StructuredNode
提供一个“inflate”类方法,这个inflate节点到他们的类,保证节点作为正确的类型受到影响:
class Person(StructuredNode):
def friends(self):
results, columns = self.cypher("MATCH (a) WHERE id(a)={self} MATCH (a)-[:FRIEND]->(b) RETURN b")
return [self.inflate(row[0]) for row in results]
自主查询参数调用当前节点id预填充,可以将查询参数的传递给cypher方法。
7.1 Stand alone
一个StructuredNode
的外部:
# for standalone queries
from neomodel import db
results, meta = db.cypher_query(query, params)
people = [Person.inflate(row[0]) for row in results]
7.2 Logging
通过设置环境变量NEOMODEL_CYPHER_DEBUG=1
,记录查询和时间的日志。
7.3 Utilities
以下Utilities函数是可用的:
clear_neo4j_database(db) # deletes all nodes and relationships
# Change database password (you will need to call db.set_connection(...) after
change_neo4j_password(db, new_password)
8. 事务
事务可以通过一个上下文管理器来使用:
from neomodel import db
with db.transaction:
Person(name='Bob').save()
或者作为一个函数装饰器:
@db.transaction
def update_user_name(uid, name):
user = Person.nodes.filter(uid=uid)[0]
user.name = name
user.save()
或者手工:
db.begin()
try:
new_user = Person(name=username, email=email).save()
send_email(new_user)
db.commit()
except Exception as e:
db.rollback()
事务就像db对象一样,是线程本地的(查看threading.local
),如果使用celery或另一个任务调度器,建议将每个任务包装在一个事务中。
@task
@db.transaction # comes after the task decorator
def send_email(user):
...
9. Hook
在 StructuredNode
的子类中可以定义以下的hook方法:
pre_save, post_save, pre_delete, post_delete, post_create
所有的方法没有参数,post hook 方法实例如下:
class Person(StructuredNode):
def post_create(self):
email_welcome_message(self)
注意,post_create
不是由get_or_create
和create_or_update
方法进行调用的。
无论节点是否是新的,都调用保存hook。要确定pre_save
是否存在节点,检查self上的id属性。
9.1 关系上的Hook
Hook上的pre_save
和post_save
在StructuredRel
模型中可用,当直接调用对象上的保存或通过连接创建新的关系时,它们被执行。
注意,在连接期间的pre_save调用中,起始和结束节点不可用。
9.2 Django 信号
通过django_model,支持信号。
10. 批量节点操作
所有批处理操作都可以用一个或多个节点执行。
10.1 create()
注意,批处理创建操作是Neo4j REST API的一个功能,考虑到方便和兼容性,neomodel使用Bolt,对于每一个给出的字典发出一个CREATE
查询。
在单个事务中,一次创建多个节点:
with db.transaction:
people = Person.create(
{'name': 'Tim', 'age': 83},
{'name': 'Bob', 'age': 23},
{'name': 'Jill', 'age': 34},
)
10.2 create_or_update()
在单个操作中,原子地创建或更新节点:
people = Person.create_or_update(
{'name': 'Tim', 'age': 83},
{'name': 'Bob', 'age': 23},
{'name': 'Jill', 'age': 34},
)
more_people = Person.create_or_update(
{'name': 'Tim', 'age': 73},
{'name': 'Bob', 'age': 35},
{'name': 'Jane', 'age': 24},
)
这对保证数据是最新的是有用的,每个节点通过需要的 和/或 唯一的属性进行匹配。任何额外的属性将会被设置到一个最新的创建的或存在的节点。
重要的是提供一个唯一的标识符,在已知的情况下,将生成默认值省略的任何字段。
10.3 get_or_create()
在单个操作中,原子地获取或创建节点:
people = Person.get_or_create(
{'name': 'Tim'},
{'name': 'Bob'},
)
people_with_jill = Person.get_or_create(
{'name': 'Tim'},
{'name': 'Bob'},
{'name': 'Jill'},
)
# are same nodes
assert people[0] == people_with_jill[0]
assert people[1] == people_with_jill[1]
这对于确定特定节点存在是很有用的,只有必须指定所有需要的属性,以保证唯一性。在该例子中,“Tim”和“Bob”在第一次调用中被创建,在第二次调用中被索引。
另外,get_or_create()
允许“关系”参数通过,当一个关系被指定,基于关系的匹配已经结束,而不是全局的:
class Dog(StructuredNode):
name = StringProperty(required=True)
owner = RelationshipTo('Person', 'owner')
class Person(StructuredNode):
name = StringProperty(unique_index=True)
pets = RelationshipFrom('Dog', 'owner')
bob = Person.get_or_create({"name": "Bob"})[0]
bobs_gizmo = Dog.get_or_create({"name": "Gizmo"}, relationship=bob.pets)
tim = Person.get_or_create({"name": "Tim"})[0]
tims_gizmo = Dog.get_or_create({"name": "Gizmo"}, relationship=tim.pets)
# not the same gizmo
assert bobs_gizmo[0] != tims_gizmo[0]
如果唯一必须的属性是唯一的,则操作是多余的。然而,对于简单的必须属性,关系成为唯一标识符的一部分。
11. 扩展neomodel
11.1 继承
可创建一个”base node”类,其扩展neomodel提供的函数(如 neomodel.contrib.SemiStructuredNode
)。
或者,只是有你想分享的公共的方法和属性,这可以在你想继承的任何基类中使用__abstract_node__
属性:
class User(StructuredNode):
__abstract_node__ = True
name = StringProperty(unique_index=True)
class Shopper(User):
balance = IntegerProperty(index=True)
def credit_account(self, amount):
self.balance = self.balance + int(amount)
self.save()
11.2 多态
可以在节点类之间使用多态分享功能
class UserMixin(object):
name = StringProperty(unique_index=True)
password = StringProperty()
class CreditMixin(object):
balance = IntegerProperty(index=True)
def credit_account(self, amount):
self.balance = self.balance + int(amount)
self.save()
class Shopper(StructuredNode, User, CreditMixin):
pass
jim = Shopper(name='jimmy', balance=300).save()
jim.credit_account(50)
确保你的多态不从StructuredNode
和你具体的类继承。
11.3 覆盖StructuredNode
构造函数
当定义类有一个通俗的__init__(self, ...)
方法,你必须经常调用super()
方法,用于 neomodel magic工作:
class Item(StructuredNode):
name = StringProperty(unique_index=True)
uid = StringProperty(unique_index=True)
def __init__(self, product, *args, **kwargs):
self.product = product
kwargs["uid"] = 'g.' + str(self.product.pk)
kwargs["name"] = self.product.product_name
super(Item, self).__init__(self, *args, **kwargs)
重点提示,StructuredNode
的构造器将覆盖属性集(在类中定义)。所以必须通过kwargs
传递值(如上所示),可以在调用构造器后设置它们,但是需要跳过确认。
12. 配置
包括neomodel的“config”模型和它的变量。
12.1数据库
设置连接细节:
config.DATABASE_URL = 'bolt://neo4j:neo4j@localhost:7687`
设置加密连接不可用(用于开发):
config.ENCRYPTED_CONNECTION = False
调整连接池大小:
config.MAX_POOL_SIZE = 50 # default
12.2 保证原子索引和约束创建
在StructuredNode定义之后,neomodel可以在编译时安装相应的约束和索引。 但这种方法仅推荐用于测试:
from neomodel import config
# before loading your node definitions
config.AUTO_INSTALL_LABELS = True
Neomodel为任务提供了一个脚本neomodel_install_labels
,但是想手动设置,请看:
对一个简单的类安装索引和约束:
from neomodel import install_labels
install_labels(YourClass)
或对你的全部”模式”:
import yourapp # make sure your app is loaded
from neomodel import install_all_labels
install_all_labels()
# Output:
# Setting up labels and constraints...
# Found yourapp.models.User
# + Creating unique constraint for name on label User for class yourapp.models.User
# ...
12.3 需要在DateTimeProperty中设置时间域
确保所有DateTimes在序列化到UTC时代之前都提供了一个时区:
config.FORCE_TIMEZONE = True # default False
参考
【1】Django Neomodel,https://github.com/robinedwards/django-neomodel
【2】Neomodel documentation,http://neomodel.readthedocs.io/en/latest/