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!