You may hear the term "Unit of Work" tossed around with SQLAlchemy, often in contrast with Django. Basically, with Django (which you'll hear has the Active Record pattern), you grab an object from the database, modify it a bit, then call its save() method to commit your changes to the database. The SQLAlchemy model, which I won't disguise my love of, works a bit differently: you grab an object from the database using a Session object, which keeps track of that object. You modify the object, then call the session's commit() method. The session then goes over all the objects it's keeping track of, recognizes what's changed, then develops a set of distinct tasks to carry out our modifications (the units of work).
I love this, because it allows for some pretty great optimizations. With Django, if you create a bunch of objects, you can either call save() on them all, which means a query for every object, or you call a specialized batch method to commit them all at once. With SQLAlchemy, you create instances for all the objects and add them to the session. When you call session.commit(), it sees there are a bunch of new objects, and knows it can use that specialized batch method.
This is going to be really handy with Neo4j, because it doesn't have an optimized communications protocol like a relational DB, only its REST interface. So, reducing interactions has a real impact on performance. Currently, the only ORM for Neo4j (neomodel) uses Django's Active Record pattern, which is poorly suited for intensive graph DB usage -- lots of nodes, lots of relationships, LOTS of DB communication. It pays to optimize. Plus, this:
ingredients = Ingredient.query.filter(Ingredient.term.startswith('search query'))is a lot cooler than
results, _ = db.cypher_query('''
START ingredient=node:node_auto_index("term:*search* AND term:*query*")
MATCH (ingredient:INGREDIENT)
WHERE ingredient.term =~ "search query.*"
RETURN ingredient
''')
ingredients = [Ingredient.inflate(r[0]) for r in results]