Skip to content

Neo4j v3 Declared Relationships

Brian Underwood edited this page Mar 29, 2015 · 31 revisions

Declare relationships (associations) in your models to create methods for getting, setting, and traversing relationships.

There are a few configurable options possible in all association declarations. A basic understanding of Neo4j relationships is helpful, bordering on necessary, so see http://docs.neo4j.org/chunked/stable/graphdb-neo4j-relationships.html.

The TL;DR version:

has_many and has_one are class methods made available by including the Neo4j::ActiveNode in a class. The first two arguments are required, the rest are optional.

  • First argument is a symbol describe direction: :in, :out, or :both
  • Second argument is a symbol that will create accessor methods and set relationship type if the type optional argument is not given. See below.

Optional arguments:

  • :type with a string or symbol that overrides the default relationship type (based on association name)
  • :model_class with a constant, symbol, or string (recommended) representing the model on the other end of this association, use false to disable
  • :rel_class with a constant, symbol, or string (recommended) representing the ActiveRel model responsible for all relationship configuration. Cannot be used with type and origin.
  • :origin with a symbol, the name of the association in a reciprocal model
  • :before and :after with a symbol of the callback method to use during relationship creation
  • unique: true, which makes Cypher use CREATE UNIQUE instead of CREATE during relationship creation to permit no more than one relationship of a type between two nodes
  • :dependent with a number of symbols to describe what should have when a node is destroyed

See the detailed documentation below for information about all of these options.

All has_many/has_one method calls begin with declaration of direction

Those can be:

  • :in
  • :out
  • :both

:both comes with the caveat that the relationship created will be outgoing but matches will omit the direction.

class Show
  include Neo4j::ActiveNode
  has_many :in, :bands
end

Second argument is a symbol that sets the method and defaults: relationship type and expected object class

class Show
  include Neo4j::ActiveNode
  has_many :in, :bands
end
  • Method is "bands"
  • Relationship type is "#bands"
  • Looks for constant Band These are only DEFAULTS, though. All can be overridden.

In v3, Declared (auto-generated) relationship types are prefixed with "#"

In the above example, the relationship type is "#bands". The reason it is that and not just "bands" is because it acts as a reminder that this relationship type was created by ActiveNode. In practice, the idea is that if you come across this relationship through a match, you don't have to wonder what it is or where it came from, since you will not have specified it explicitly anywhere. It signifies that it was automatically generated.

It is considered a best practice to specify a relationship type and not rely on automatic naming. Automatic naming, while helpful, runs the risk of reusing types and just generally making Cypher queries a bit cumbersome (and let's be honest, ugly) should you ever need to use them.

In v4, declared (auto-generated) relationship types are all caps

Similar to ActiveRecord's naming conventions for file names and tables, the association enrolled_in would become ENROLLED_IN. The # prefix is no longer used.

This behavior can be controlled with the config.neo4j.transform_rel_type option. Permitted values are:

  • :upcase, the current default
  • :downcase
  • :legacy, downcases + leading #
  • :none, takes the association name exactly as it is

Note that this only pertains to automatic relationship types. If you explicitly call type, it will use whatever you give it.

Association names, polymorphic matches and overriding the model

By default, the second argument defines the methods to be created and tells the association to look for a class of that name. If you choose to use an association name that does not match the singular or pluralized version of the model name, use the model_class option to specify a target class.

model_class can be a constant or a string. A string is recommended if you have model subclasses due to possible circular dependency issues.

class Show
  include Neo4j::ActiveNode
  has_many :in, :bands
  # looks for Band

  has_many :in, :people_playing_music
  # Uninitialized Constant PeoplePlayingMusic

  has_many :in, :people_playing_music, model_class: Band
  # overrides default
end

To define a polymorphic association, pass model_class boolean false.

class User
  include Neo4j::ActiveNode
  has_many :out, :managed_objects, type: 'manages', model_class: false
end

class Show
  include Neo4j::ActiveNode
  has_many :in, :admins, model_class: User, type: 'manages'
  # see the notes on "origin" below for a better way of doing this
end

class Band
  include Neo4j::ActiveNode
  has_many :in, :admins, model_class: User, type: 'manages'
end

Self referencing the model (e.g., parent child relations) is also possible. For example, here is how you would declare the relationship "Band inspired by Band":

class Band
  include Neo4j::ActiveNode
  has_many :out, :inspirations, model_class: Band
end

Declaring relationship type

By default, it bases relation type off of the symbol passed. Use type to declare a specific type. Doing this is considered a best practice.

class Show
  include Neo4j::ActiveNode
  has_many :out, :people_playing_instruments, type: 'performing_bands', model_class: Band
end

Declaring Origin

Rather than declaring the full relationship on two models, you can use origin to reference a "master" association.

class Show
  include Neo4j::ActiveNode
  has_many :out, :bands, type: 'performing_bands'
  has_one :out, :headliner, model_class: Band, type: 'headlined_by'
end

class Band
  include Neo4j::ActiveNode
  has_many :in, :shows, origin: :bands
  has_many :in, :headlining_shows, model_class: Show, origin: :headliner
end

The benefit of this is that changing the relationship types declared on Show will not require updates to Band. There is still a dependency -- changing the method on Show will break Band -- but this is testable and very easy to catch.

Relationship callbacks

You can specify before and after callback methods on all declared relationships. There are some simple guidelines:

  • Your callback methods must accept one argument that represents the other node in the relationship. Use self for the node where the method is defined.
  • If your before callback explicitly returns false, the relationship will not be created.
  • has_many relationship setters will return false if a callback explicitly returns false, has_one will always return the object passed into the setter, so check for failure if you need to take action in your app
class Topic
  include Neo4j::ActiveNode
  property :last_post, type: DateTime

  has_many :in, :posts, before: :check_poster, after: :set_topic_stats
  has_one :out, :poster, model_class: User

  private

  def check_poster(other)
    return false if other.poster.nil?
  end

  def set_topic_stats(other)
    self.poster = other.poster
    self.last_post = DateTime.now
    self.save
  end
end

class Post
  include Neo4j::ActiveNode

  has_one :in, :poster, model_class: User, origin: :posts, before: :check_post_privileges, after: :notify_friends

  def check_post_privileges(other)
    return false if other.allowed_to_post == false
  end

  def notify_friends(from, to)
    # call a method to notify the poster's friends of a new post
    other.notify_friends(self)
  end
end

# elsewhere in the app...

if !(@topic.posts << post)
  #raise an error, notify someone, do something...
end

# but what if...
@post.poster = @user
# has_one callbacks do not return false, so check after setting
if @topic.poster.nil?
  notify_admins_banned_user_is_being_shady
end

Creating and querying undeclared relationships

You can create an undeclared relationship between two Neo4j::ActiveNode models.

from_node.create_rel("FRIENDS", to_node)

...and add properties to the relationship.

from_node.create_rel("FRIENDS", to_node, :weight=>1)

Querying an undeclared relationship is same as querying any declared relationship.

from_node.rels(dir: :outgoing, type: "FRIENDS")
to_node.rels(dir: :incoming, type: "FRIENDS")

from_node.rels(dir: :outgoing, type: "FRIENDS").each do |rel|
   puts rel.start_node # from_node itself
   puts rel.end_node # to_node
end

Get rel type of all relationships for a given node:

node.rels.each {|rel| puts rel.rel_type }

Dependent Options (v4)

Similar to ActiveRecord, you can specify four dependent options when declaring an association.

class Route
  include Neo4j::ActiveNode
  has_many :out, :stops, type: 'STOPPING_AT', dependent: :delete_orphans
end

The available options are:

  • :delete, which will delete all associated records in Cypher. Callbacks will not be called. This is the fastest method.
  • :destroy, which will call each on the association and then destroy on each related object. Callbacks will be called. Since this happens in Ruby, it can be a very expensive procedure, so use it carefully.
  • :delete_orphans, which will delete only the associated records that have no other relationships of the same type.
  • :destroy_orphans, same as above, but it takes place in Ruby.

The two orphan-destruction options are unique to Neo4j.rb. As an example of when you'd use them, imagine you are modeling tours, routes, and stops along those routes. A tour can have multiple routes, a route can have multiple stops, a stop can be in multiple routes but must have at least one. When a route is destroyed, :delete_orphans would delete only those related stops that have no other routes.

Forcing unique relationships

Cypher has a CREATE UNIQUE function that can be used instead of CREATE. It ensures that any two nodes cannot have more than one relationship of a specific type between them. To use this, pass unique: true as an option in your has_many call. Be aware that if you assign additional properties to the relationship on a second creation attempt, they will overwrite existing props.

Returning relationships

There are a few different ways to return a relationship object. If you traversing a relationship between two nodes and want to work with the relationship between them at the same time, an easy way to do that is with each_with_rel.

student = Student.first
student.lessons(:node, :rel).each_with_rel do |node, rel|
  puts node.name
  puts rel[:name] #at the moment, rel props are only accessible by keys, not accessor methods
end

Note that you must assign node and relationship identifiers in your association method call.

The result is one query and one method call instead of two.

Query a certain relationship with a certain direction between two nodes at hand:

node.rels(dir: :outgoing, between: another_node).each do |rel|
   puts rel[:name]
end

Advanced Functionality

See Neo4j::ActiveRel for notes about using ActiveRel models for advanced options.

Clone this wiki locally