Querying¶
Introduction¶
Using the activegraph
gem provides the entry point is ActiveGraph::Base
. So you could make a simple query with:
ActiveGraph::Base.query('MATCH (n) RETURN n LIMIT $limit', limit: 10)
Most of the time, though, using the activegraph
gem involves using the Node
and Relationship
APIs as described below.
Node¶
Simple Query Methods¶
There are a number of ways to find and return nodes.
.find
¶
Find an object by id_property
.find_by
¶
find_by
and find_by!
behave as they do in ActiveRecord, returning the first object matching the criteria or nil (or an error in the case of find_by!
)
Post.find_by(title: 'Neo4j.rb is awesome')
Proxy Method Chaining¶
Like in ActiveRecord you can build queries via method chaining. This can start in one of three ways:
Model.all
Model.association
model_object.association
In the case of the association calls, the scope becomes a class-level representation of the association’s model so far. So for example if I were to call post.comments
I would end up with a representation of nodes from the Comment
model, but only those which are related to the post
object via the comments
association.
At this point it should be mentioned that what associations return isn’t an Array
but in fact an AssociationProxy
. AssociationProxy
is Enumerable
so you can still iterate over it as a collection. This allows for the method chaining to build queries, but it also enables eager loading of associations
If if you call a method such as where
, you will end up with a QueryProxy
. Similar to an AssociationProxy
, a QueryProxy
represents an enumerable query which hasn’t yet been executed and which you can call filtering and sorting methods on as well as chaining further associations.
From an AssociationProxy
or a QueryProxy
you can filter, sort, and limit to modify the query that will be performed or call a further association.
Querying the proxy¶
Similar to ActiveRecord you can perform various operations on a proxy like so:
lesson.teachers.where(name: /.* smith/i, age: 34).order(:name).limit(2)
The arguments to these methods are translated into Cypher
query statements. For example in the above statement the regular expression is translated into a Cypher =~
operator. Additionally all values are translated into Neo4j query parameters for the best performance and to avoid query injection attacks.
Chaining associations¶
As you’ve seen, it’s possible to chain methods to build a query on one model. In addition it’s possible to also call associations at any point along the chain to transition to another associated model. The simplest example would be:
student.lessons.teachers
This would returns all of the teachers for all of the lessons which the students is taking. Keep in mind that this builds only one Cypher query to be executed when the result is enumerated. Finally you can combine scoping and association chaining to create complex cypher query with simple Ruby method calls.
student.lessons(:l).where(level: 102).teachers(:t).where('t.age > 34').pluck(:l)
Here we get all of the lessons at the 102 level which have a teacher older than 34. The pluck
method will actually perform the query and return an Array
result with the lessons in question. There is also a return
method which returns an Array
of result objects which, in this case, would respond to a call to the #l
method to return the lesson.
Note here that we’re giving an argument to the associaton methods (lessons(:l)
and teachers(:t)
) in order to define Cypher variables which we can refer to. In the same way we can also pass in a second argument to define a variable for the relationship which the association follows:
student.lessons(:l, :r).where("r.start_date < $the_date and r.end_date >= $the_date").params(the_date: '2014-11-22').pluck(:l)
Here we are limiting lessons by the start_date
and end_date
on the relationship between the student and the lessons. We can also use the rel_where
method to filter based on this relationship:
student.lessons.where(subject: 'Math').rel_where(grade: 85)
See also
Branching¶
When making association chains with Node
you can use the branch
method to go down one path before jumping back to continue where you started from. For example:
# Finds all exams for the student's lessons where there is a teacher who's age is greater than 34
student.lessons.branch { teachers.where('t.age > 34') }.exams
# Similar to the Cypher:
# MATCH (s:Student)-[:HAS_LESSON]->(lesson:Lesson)<-[:TEACHES]-(:Teacher), (lesson)<-[:FOR_LESSON]-(exam:Exam)
# RETURN exam
Associations and Unpersisted Nodes¶
There is some special behavior around association creation when nodes are new and unsaved. Below are a few scenarios and their outcomes.
When both nodes are persisted, associations changes using <<
or =
take place immediately – no need to call save.
student = Student.first
Lesson = Lesson.first
student.lessons << lesson
In that case, the relationship would be created immediately.
When the node on which the association is called is unpersisted, no changes are made to the database until save
is called. Once that happens, a cascading save event will occur.
student = Student.new
lesson = Lesson.first || Lesson.new
# This method will not save `student` or change relationships in the database:
student.lessons << lesson
Once we call save
on student
, two or three things will happen:
- Since
student
is unpersisted, it will be saved - If
lesson
is unpersisted, it will be saved - Once both nodes are saved, the relationship will be created
This process occurs within a transaction. If any part fails, an error will be raised, the transaction will fail, and no changes will be made to the database.
Finally, if you try to associate an unpersisted node with a persisted node, the unpersisted node will be saved and the relationship will be created immediately:
student = Student.first
lesson = Lesson.new
student.lessons << lesson
In the above example, lesson
would be saved and the relationship would be created immediately. There is no need to call save
on student
.
Parameters¶
Neo4j supports parameters which have a number of advantages:
- You don’t need to worry about injection attacks when a value is passed as a parameter
- There is no need to worry about escaping values for parameters
- If only the values that you are passing down for a query change, using parameters keeps the query string the same and allows Neo4j to cache the query execution
The Neo4j.rb project gems try as much as possible to use parameters. For example, if you call where
with a Hash:
Student.all.where(age: 20)
A parameter will be automatically created for the value passed in.
Don’t assume that all methods use parameters. Always check the resulting query!
You can also specify parameters yourself with the params
method like so:
Student.all.where("s.age < $age AND s.name = $name AND s.home_town = $home_town")
.params(age: 24, name: 'James', home_town: 'Dublin')
.pluck(:s)
Variable-length relationships¶
Introduced in version 5.1.0
It is possible to specify a variable-length qualifier to apply to relationships when calling association methods.
student.friends(rel_length: 2)
This would find the friends of friends of a student. Note that you can still name matched nodes and relationships and use those names to build your query as seen above:
student.friends(:f, :r, rel_length: 2).where('f.gender = $gender AND r.since >= $date').params(gender: 'M', date: 1.month.ago)
Note
You can either pass a single options Hash or provide both the node and relationship names along with the optional Hash.
There are many ways to provide the length information to generate all the various possibilities Cypher offers:
# As a Integer:
## Cypher: -[:`FRIENDS`*2]->
student.friends(rel_length: 2)
# As a Range:
## Cypher: -[:`FRIENDS`*1..3]->
student.friends(rel_length: 1..3) # Get up to 3rd degree friends
# As a Hash:
## Cypher: -[:`FRIENDS`*1..3]->
student.friends(rel_length: {min: 1, max: 3})
## Cypher: -[:`FRIENDS`*0..]->
student.friends(rel_length: {min: 0})
## Cypher: -[:`FRIENDS`*..3]->
student.friends(rel_length: {max: 3})
# As the :any Symbol:
## Cypher: -[:`FRIENDS`*]->
student.friends(rel_length: :any)
Caution
By default, “*..3” is equivalent to “*1..3” and “*” is equivalent to “*1..”, but this may change depending on your Node4j server configuration. Keep that in mind when using variable-length relationships queries without specifying a minimum value.
Note
When using variable-length relationships queries on has_one associations, be aware that multiple nodes could be returned!
The Query API¶
The activegraph
gem provides a Query
class which can be used for building very specific queries with method chaining. This can be used either by getting a fresh Query
object from a ActiveGraph::Base
or by building a Query
off of a scope such as above.
ActiveGraph::Base.query # Get a new Query object
# Get a Query object based on a scope
Student.query_as(:s) # For a
student.lessons.query_as(:l)
# ... and based on an object:
student.query_as(:s)
The Query
class has a set of methods which map directly to Cypher clauses and which return another Query
object to allow chaining. For example:
student.lessons.query_as(:l) # This gives us our first Query object
.match("l-[:has_category*]->(root_category:Category)").where("NOT(root_category-[:has_category]->()))
.pluck(:root_category)
Here we can make our own MATCH
clauses unlike in model scoping. We have where
, pluck
, and return
here as well in addition to all of the other clause-methods. See this page for more details.
Note that when using the Query
API if you make multiple calls to methods it will try to combine the calls together into one clause and even to re-order them. If you want to avoid this you can use the #break
method:
# Creates a query representing the cypher: MATCH (q:Person), (r:Car) MATCH (p: Person)-->(q)
query_obj.match(q: Person).match('r:Car').break.match('(p: Person)-->(q)')
TODO Duplicate this page and link to it from here (or just duplicate it here): https://github.com/neo4jrb/neo4j-core/wiki/Queries
See also
#proxy_as¶
Sometimes it makes sense to turn a Query
object into (or back into) a proxy object like you would get from an association. In these cases you can use the Query#proxy_as method:
student.query_as(:s)
.match("(s)-[rel:FRIENDS_WITH*1..3]->(s2:Student")
.proxy_as(Student, :s2).lessons
Here we pick up the s2 variable with the scope of the Student model so that we can continue calling associations on it.
match_to
and first_rel_to
¶
There are two methods, match_to and first_rel_to that both make simple patterns easier.
In the most recent release, match_to accepts nodes; in the master branch and in future releases, it will accept a node or an ID. It is essentially shorthand for association.where(neo_id: node.neo_id) and returns a QueryProxy object.
# starting from a student, match them to a lesson based off of submitted params, then return students in their classes
student.lessons.match_to(params[:id]).students
first_rel_to will return the first relationship found between two nodes in a QueryProxy chain.
student.lessons.first_rel_to(lesson)
# or in the master branch, future releases
student.lessons.first_rel_to(lesson.id)
This returns a relationship object.
Finding in Batches¶
Finding in batches will soon be supported in the neo4j gem, but for now is provided in the neo4j-core gem (documentation)
Orm_Adapter¶
You can also use the orm_adapter API, by calling #to_adapter on your class. See the API, https://github.com/ianwhite/orm_adapter
Find or Create By…¶
QueryProxy has a find_or_create_by
method to make the node rel creation process easier. Its usage is simple:
a_node.an_association(params_hash)
The method has branching logic that attempts to match an existing node and relationship. If the pattern is not found, it tries to find a node of the expected class and create the relationship. If that doesn’t work, it creates the node, then creates the relationship. The process is wrapped in a transaction to prevent a failure from leaving the database in an inconsistent state.
There are some mild caveats. First, it will not work on associations of class methods. Second, you should not use it across more than one associations or you will receive an error. For instance, if you did this:
student.friends.lessons.find_or_create_by(subject: 'Math')
Assuming the lessons
association points to a Lesson
model, you would effectively end up with this:
math = Lesson.find_or_create_by(subject: 'Math')
student.friends.lessons << math
…which is invalid and will result in an error.