I'm interested in building complex Python app based on Neo4j.
As I'm also influenced by software architecture, TDD and testability, I recently found an amazing online book "Architecture Patterns with Python"
Among the architectural patterns presented, the repository pattern struck me as essential in order to write my Python app with testability in mind
The intent of the pattern is to add an abstraction between the core parts of our application and the database to increase flexibility and testability, through the dependency inversion principle.
Example from the book with SQLAlchemy
- decouples the Domain Model from Infrastructure concerns
- dependency inversion: the ORM should import our model, not the other way around
- switching between Neo4j drivers is now easy and doesn't involve a refactoring of our model
- building a fake repository for unit test is now a trivial task
This is my attempt at building this pattern for my application with
First I defined my model:
Even though it should be clear from infrastructure definitions, I still had to implement the idea of a graph database here, by specifying 2 classes
Property = # TODO class GraphNode: pass class GraphRelationship: pass # represents our users class User(GraphNode): name = Property()
Then I created an abstract repository interface, with a single
match method for now:
from model import GraphNode, GraphRelationship from abc import ABC, abstractmethod class AbstractGraphRepository(ABC): @abstractmethod def match(object: Union[GraphNode, GraphRelationship]): raise NotImplementedError()
And implemented a concrete class based on py2neo
from repo import AbstractGraphRepository from py2neo import Graph class Py2NeoRepository(AbstractGraphRepository): def __init__(url: str): self._graph = Graph(url) def match(object: Union[GraphNode, GraphRelationship]): # how to map my domain model to py2neo OGM ?
I'm stuck at this step above , figuring out what's the best way to define a mapping between my domain model, and py2neo's OGM.
If we take a look how they implemented this pattern in book, based on SQLAlchemy, we can see the following definitions:
from sqlalchemy.orm import mapper, relationship import model # dependency-inversion: the ORM depends on the model metadata = MetaData() order_lines = Table( #(2) "order_lines", metadata, Column("id", Integer, primary_key=True, autoincrement=True), Column("sku", String(255)), Column("qty", Integer, nullable=False), Column("orderid", String(255)), ) ... def start_mappers(): # mapping is defined between the OrderLine model object and the order_lines table lines_mapper = mapper(model.OrderLine, order_lines)
And this allows them to implement a concrete
SQLAlchemyRepository really easily:
from model import Batch class SqlAlchemyRepository(AbstractRepository): def __init__(self, session): self.session = session def add(self, batch: model.Batch): self.session.add(batch) # the mapping and translation from the Domain Model to the ORM is done transparently, thanks to the mapping ! def get(self, reference): return self.session.query(model.Batch).filter_by(reference=reference).one() def list(self): return self.session.query(model.Batch).all()
how to implement the same pattern with
neomodel as concrete repositories ?
how to translate from a domain model to the OGM while keeping the overall code simple and efficient by design ?
Thank you in advance !
pinging @technige and and maybe Robin Edwards from neomodel