Skip to content

Neo4j.rb v4 Introduction

subvertallchris edited this page Nov 22, 2014 · 10 revisions

Neo4jrb v4: Introduction

As of November 21, 2014, we began merging code for v4 of the gem into the master branch. Unlike the jump from v2 to v3, this is not a complete rebuild. Most of v3's documentation is still valid; however, some subtle but potentially significant API changes have occurred that may be surprising if installed accidentally.

The changes are:

(November 21, 2014)

  • _classname is no longer required for Neo4j 2.1.5+ unless explicitly added to a model
  • Relationships, when loaded, will attempt to look for a camelcased version of the relationship type.
  • Relationship types, when allowed to be set automatically by the gem, will default to all caps with underscores. There is a global config option that will change this.

A guide for this is below. If any of this is unclear, please do not hesitate to create an issue or reach out to Chris or Brian through email. We have a discussion issue here.

Matching Neo4j Objects to Classes

In versions 2 and 3 of this gem, there was a _classname property on each node and relationship created by a model. This was necessary in 2.x because Neo4j 1.9 did not have labels and 3.x because labels required extra work that often resulted in n+1 queries against the database. With Neo4j 2.1.5, released a few weeks after Neo4jrb 3.0 was officially rolled out, node labels and relationship types became part of every node/rel return and we were given a better option.

In Neo4jrb 4.0, the _classname property only exists for legacy support and as an option if you want to override defaults. We call the process of turning a returned Neo4j db object into a usable model as "wrapping." Below, we'll give an overview of how the logic used by the gem to determine which model is responsible for an object. It is far from an exhaustive technical analysis but the code is pretty clear, so please check that or get in touch if you have any questions.

If the database returns...

...a node or rel with _classname property

These would be legacy objects or objects that have their defaults modified. These will always be wrapped by whatever model is given as the property's value.

...a node without _classname

If a node does not have a _classname property, the gem uses the node's labels to determine the model to use. This is handled in node_wrapper.rb and also makes use of labels.rb.

If you want to change the node-to-model mapping...

Use the set_mapped_label_name method within your model to redefine the label used by the model. This is also defined in labels.rb, linked above.

If the database returns...

...a rel without _classname

Relationships are mapped to models in a slightly more complicated process. This is necessary because a relationship can only have one type, so we have to provide extra configuration options to come close to the flexibility offered by multiple labels and subclasses.

The wrapping process is started in rel_wrapper.rb but a lot of the logic lives in types.rb. [Note: If that link stops working, it has probably be merged into master and that branch has been deleted. Please adjust the URL to use blog/master/lib.)

First, it looks at the relationship's type and tries to find a matching key in the WRAPPED_CLASSES constant. What happens next depends on what it finds.

  • If a match is found, it uses uses the model name given as the value.
  • If a match is not found, it attempts to load a model named after the camelized version of the relationship type. HAS_LESSON looks for HasLesson, PLAYING_SHOW becomes PlayingShow, etc,...
  • If no match is found, it returns an unwrapped CypherRelationship or EmbeddedRelationship object based on the database type, as defined in Neo4j::Core.

If you want to change the rel-to-model mapping...

You have two options. The first is to use the type class method within a relationship model.

class StudentLesson
  include Neo4j::ActiveRel
  from_class Student
  to_class Lesson
  type 'ENROLLED_IN'
end

This will create relationships with type ENROLLED_IN and ensure that any relationships returned with the ENROLLED_IN type will be wrapped as StudentLesson.

The second option is to use set_classname to intercept the object during its return and wrap it as a class. This is useful if you want to share one relationship type between multiple models. (Which may be a questionable practice but it has its place!)

class Band
  include Neo4j::ActiveNode
  has_many :out, :shows, rel_class: BandShow
  has_many :out, :tours, rel_class: BandTour
  has_many :out, events: model_class: false, type: 'PERFORMING_ON'
end

class BandShow
  include Neo4j::ActiveRel
  from_class Band
  to_class Show
  type 'PERFORMING_ON'
  set_classname
end

class BandTour
  include Neo4j::ActiveRel
  from_class Band
  to_class Tour
  type `PERFORMING_ON`
  set_classname
end

rel1 = BandShow.create(from_node: band, to_node: show)
rel2 = BandTour.create(from_node: band, to_node: tour)
band.events.count
# 2
band.events.each_rel do |r|
  puts r.class
end
# BandShow(props...)
# BandTour(props...)

A warning on changing default behavior

ActiveNode and ActiveRel models register themselves through type or set_classname when the models are loaded into memory. In Rails's development and test environments, models are loaded lazily, so it's possible to perform a query that results in returned objects that do not have models matching their labels or relationship types.

To get around this, ensure that you call each model once after Rails loads but before you perform your first queries or enable eager_loading of your environment. You'll find this in your environment's config file, config.eager_load.

Automatic Relationship Types

In v3, automatic naming of relationships was downcase with underscores and # prepended. If you had this:

has_many :out, :lessons_taught, model_class: 'Lesson'

...your relationship type would be #lessons_taught. In v4, the relationship type will be LESSONS_TAUGHT.

The reason we decided to do things as they were done in v3 was because we felt that it would be beneficial to tag relationships when they were created by the gem. If a user looked at their graph through the Neo4j web browser and came across a relationship that they didn't remember creating, it would hopefully remind them that they did not create the type.

In v4, we made the decision to go all caps and to not tag with # because we want to encourage best practices. All Neo4j documentation and tutorials demonstrate relationship types with caps. We believe the gem should provide predictable defaults and, with that in mind, felt that the most predictable relationship type would be that which followed this widely used standard.

With all that said, we know that not everybody will want to do things this way. This is especially true if someone has a large database with legacy data that uses #old_format relationship types. To address this, there is a new global config option: config.neo4j.transform_rel_type. Used in application.rb or wherever you normally specify your config info, it accepts three four options:

  • :upcase - :this_class, ThisClass, thiS_claSs (if you don't like yourself) becomes THIS_CLASS
  • :downcase - same as above, only... downcased.
  • :legacy - downcases and prepends #, so ThisClass becomes #this_class
  • :none - uses the string version of whatever is passed with no modifications

If you want to set this from within the app, you can use Neo4j::Config[:transform_rel_type] = #{your_sym}.

These options apply to both associations in ActiveNode models and ActiveRel models. Since ActiveRel models no longer require type, if you have enrolled_in.rb with class EnrolledIn, it will automatically create a relationship type of ENROLLED_IN based on the default settings.

See the section dealing with _classname for more information about overriding types.

Clone this wiki locally