Neomodel--让Django用上Neo4j

2017年04月26日

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替代RelationshipToRelationshipFrom

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_createcreate_or_update方法进行调用的。

无论节点是否是新的,都调用保存hook。要确定pre_save是否存在节点,检查self上的id属性。

9.1 关系上的Hook

Hook上的pre_savepost_saveStructuredRel模型中可用,当直接调用对象上的保存或通过连接创建新的关系时,它们被执行。

注意,在连接期间的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/


版权声明:本文为博主原创文章,转载请注明出处 本文总阅读量