Skip to content

Instantly share code, notes, and snippets.

@cheerfulstoic
Last active August 29, 2015 14:27
Show Gist options
  • Save cheerfulstoic/8fc7990a9b361fdfa9d0 to your computer and use it in GitHub Desktop.
Save cheerfulstoic/8fc7990a9b361fdfa9d0 to your computer and use it in GitHub Desktop.
Script for "Neo4j.rb Screencast #5 - ActiveRel"

Welcome to the fifth episode in a series of short screencasts about the Ruby neo4j gem. In this episode we will discuss the ActiveRel module which allows us to define model classes for Neo4j relationships.

In previous episodes we've seen that it's possible to work with Neo4j relationships via ActiveNode associations. While associations provide a lot of power through the abstraction of relationships as ties between nodes, Neo4j often allows us to think about relationships as entities worthy of consideration by themselves. When we want a richer representation of a relationship it is useful to create a representative model with it's own structure and logic. In the neo4j gem this is done via the ActiveRel module.

Defining an ActiveRel module is only slightly more complicated than defining an ActiveNode module.

class Viewed
  include Neo4j::ActiveRel

  from_class :User
  to_class :Asset

  property :viewed_at
  property :browser_string
end

Here we've included ActiveRel rather than ActiveNode and the properties should be familiar from episode two. Additionally an ActiveRel model requires two things which ActiveNode does not. We need to define the model class which the relationship is coming from, and the model class it is going to. In the case of a Viewed model it makes sense not to limit ourselves to a specific destination, so we can pass the symbol :any to the to_class method:

to_class :any

The same thing can be done with the from_class method. ActiveRel will raise an exception when we try to create a relationship that doesn't match these criteria, so using :any gives us both freedom and responsibility.

By default ActiveRel uses the name of the model class to determine what the relationship type will be. If we wanted to use the noun View to refer to our model but still have our relationship be the verb "VIEWED" we would call the type method:

class View
  include Neo4j::ActiveRel

  from_class :User
  to_class :Asset
  type :VIEWED
end

Now that we know how to define a model we can record a view in the database, say from our controller, by simply calling create on the ActiveRel model like so:

View.create(from_node: current_user, to_node: @asset)

If you call creates_unique in your ActiveRel model, Cyphers CREATE UNIQUE` clause will be used for all relationship creations:

creates_unique

But ActiveRel isn't just for the creation of objects. If we want to retrieve them we can tie them to an association:

class User
  has_many :out, :viewed_assets, rel_class: :View, model_class: :Asset
  has_many :out, :viewed_users,   rel_class: :View, model_class: :User

  has_many :in, :viewers, rel_class: :View, model_class: :User
end

class Asset
  has_many :in, :viewers, rel_class: :View, model_class: :User
end

While it may seems like we should be able to query relationships directly, Neo4j encourages nodes as the primary point of entry into a query. Since the Neo4j.rb gems don't support legacy indexes, Neo4j relationships can't be indexed outside the anchoring definition of a node, so ActiveNode and ActiveRel focus on nodes as a starting point. Also for this reason, ActiveRel doesn't create a unique UUID property on relationships the way ActiveNode does for nodes. To get our relationship object from the associations we can use either the each_rel method to get just the relationships, or the each_with_rel method to get pairs of nodes and relationships:

current_user.viewed_assets.each_rel.to_a

current_user.viewed_assets.each_with_rel.each do |node, rel|
  # ...
end

As our queries get more complicated we will sometimes want to assign variables to the nodes and relationships and pluck them at the end. The result is the same:

current_user.viewed_assets(:asset, :rel).pluck(:asset, :rel).each do |node, rel|
  # ...
end

If no ActiveRel model is specified, these methods (as well as any query that you do to return relationships) will return basic relationship objects from the neo4j-core gem.

If we want to have validations or callbacks this is easy because ActiveNode and ActiveRel are based on ActiveModel. Here we define two validations and use our own custom IP address validator:

validates :browser_string, presence: true

validates :ip_address, ip_address: true

and here we define a callback to ensure that the viewed_at property gets set when the View is created:

before_create :set_viewed_at

def set_viewed_at
  self.viewed_at ||= Time.now
end

ActiveRel objects also have from_node and to_node methods which can be particularly handy in callbacks to examine and update the nodes on either side of the relationship. Here we can maintain a cache counter on the objects which are viewed whenever a view is created:

after_create :increment_destination_view_count

def increment_destination_view_count
  (to_node.view_count ||= 0) += 1
  to_node.save
end

That's all for this episode. Thanks for watching and stay tuned for the next episode where we learn how to create more complicated queries. Happy graphing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment