Advanced Usage

Managing configuration with a .env file

It is generally good practice to avoid storing connection details (and especially passwords) in your source code (and version control). Therefore, Neontology supports the use of .env files (or just normal environment variables) for:

  • NEO4J_URI
  • NEO4J_USERNAME
  • NEO4J_PASSWORD

For example:

# .env
NEO4J_URI=neo4j+s://myneo4j.example.com
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=<PASSWORD>

Automatically apply constraints

With neo4j, we can constrain label/property pairs to be unique and indexed.

Neontology can automatically apply neo4j constraints for all defined nodes using the auto_constrain method.

Simply use auto_constrain() after defining your models and initialising your connection.

Use multiple labels

Sometimes you may want to apply additional labels to nodes, beyond just the primary label. Where this is the case, you can add those labels as a list using the class variable __secondarylabels__.

class ElephantNode(BaseNode):
    __primaryproperty__: ClassVar[str] = "name"
    __primarylabel__: ClassVar[Optional[str]] = "Elephant"
    __secondarylabels__: ClassVar[Optional[list]] = ["Animal"]
    name: str

ellie = ElephantNode(name="Ellie")

Note that methods such as .match and auto_constrain use only the primary label.

Set neo4j properties on match or on create

When we run MERGE operations with neo4j, sometimes we want to only alter properties under certain circumstances.

Note

From v1.0.0, changes in v2 of Pydantic mean that these properties are now defined in a dict called 'json_schema_extra' rather than directly on the field.

You can control this behaviour in Neontology by passing certain parameters in the 'json_schema_extra' dictionary when you define fields:

from typing import ClassVar, Optional
from pydantic import Field
from neontology import BaseNode

class MyNode(BaseNode):
        __primaryproperty__: ClassVar[str] = "my_id"
        __primarylabel__: ClassVar[Optional[str]] = "MyNode"

        my_id: str = "test_node"
        only_set_on_match: Optional[str] = Field(json_schema_extra={"set_on_match": True})
        only_set_on_create: Optional[str] = Field(json_schema_extra={"set_on_create": True})
        normal_field: str

Note

Fields which are 'set_on_match' must be optional as they will be None/null when the node is first created.

Controlling merge relationships

When merging relationships, we might want to merge on certain properties to avoid creating an excessive number of relationships.

Note

From v1.0.0, changes in v2 of Pydantic mean that this property is now defined in a dict called 'json_schema_extra' rather than directly on the field.

To do this use the 'merge_on' key in the 'json_schema_extra' parameter when defining a field.

from typing import ClassVar, Optional
from pydantic import Field
from neontology import BaseRelationship

class MyRel(BaseRelationship):
        __relationshiptype__: ClassVar[Optional[str]] = "MY_RELATIONSHIP_TO"

        source: MyNode
        target: MyNode

        prop_to_merge_on: str = Field(json_schema_extra={"merge_on": True})

In this example, where a relationship with a given source and target exists with the same value for 'prop_to_merge_on', the relationship will be overwritten. If a new 'prop_to_merge_on' value is given then a new relationship will be created with that value.

Type Conversion/Coersion

Neo4j doesn't support the same range of types as Python/Pydantic. Therefore, we do our best to coerce data types to fit into neo4j. However, you may see some data loss when converting between complex data types.

Executing Advanced Cypher Queries with Python

The focus of Neontology is currently ingesting data rather than providing additional ways to query data. Cypher is an incredibly powerful language for doing that already.

If you want to run plain cypher queries, you can do this using the Neo4j driver which can be accessed on Neontology's GraphConnection object.

The Neo4j driver has many features and different ways of executing queries, but the below recipe shows how we can write and execute arbitrary queries with the driver to return data as Python lists / dictionaries. We will use Neo4j's built in support for map projection.

import neo4j
from neontology import init_neontology, GraphConnection

init_neontology()

gc = GraphConnection()

cypher_query = """
MATCH (p:Person)
RETURN COLLECT({name: p.name})
"""

result = gc.driver.execute_query(cypher_query, result_transformer_=neo4j.Result.single)

print(result)

# [{'name': 'Alice'}, {'name': 'Bob'}]