Neo4j::Rails Persistence

Jason Gilmore edited this page Dec 17, 2013 · 6 revisions
Clone this wiki locally

The Neo4j::Rails::Model and Neo4j::Rails::Relationship implements the Rails ActiveModel interface and a subset of the ActiveRecord API.

Example:

class IceCream < Neo4j::Rails::Model
  property :flavour
  validates_presence_of :flavour
end

IceCream.new.valid?  # => false
IceCream.new(:flavour => "vanilla").valid?  # => true

Neo4j::Rails::Model

A Neo4j::Rails::Model wraps a Neo4j::Node. The Neo4j::Rails::Model and Neo4j::Rails::Relationship are Active Model compliant and does implement some Active Record method.

Callbacks & Observers

The following callbacks are defined: initialize, valid?, create_or_update, create, update, destroy. See the rails documentation when they are called.
There is also support for Active Model observers, see Neo4j::Rails::Observer and neo4j-observers-example

Model.new

The Neo4j::Model.new methods does not require a transaction (unlike the Neo4j::Node.new method)
since it creates a node in memory with no connection to the neo4j database. This makes it easier to create forms that don’t touch the database if validation fails by using the ActiveRecord-style two-step Model.new + Model#save creation. The node will be saved to the database when the save method is called.

Model.save

Saves the node if the validation was successful. It will create a new transaction if neccessarly.
Notice, nested nodes will also be saved and validated (see below – does raise exception !). Validation can be skipped by model.save( :validate => false )

Model.update_attributes

Updates the model with the given attributes and saves the model if the validation is successful. Will create a new transaction if neccessarly.

Declaring Properties

The Neo4j::Rails::Model#property and Neo4j::Rails::Relationship#property method accept additional configuration parameters:, :default, :length and :null and :type and converter, see Neo4j::Rails::Attributes::ClassMethods#property

Example:

class MyModel < Neo4j::Rails::Model
  # gives title "Mr" unless it is set specifically
  # ensures it is a maximum of 3 chars long
  # ensures it is never saved with a nil value
  property :title, :default => "Mr", :limit => 3, :null => false
  property :superuser, :foo, :type => :boolean  # both superuser and foo will be converted to true/false values
end

Validation

All the normal validations work for both Neo4j::Rails::Model and Neo4j::Rails::Relationship

Example:

class Person < Neo4j::Rails::Model
  property :email, :index => :exact
  validates :email, :uniqueness => true, :allow_blank => false
  validates :password, :presence     => true,
                   :confirmation => true,
                   :length       => { :within => 6..40 }
end

In order to get uniquess validation to work you must have an exact index on the property, as shown above (index :email).

Notice If you are saving a node which has changed relationships/nodes an exception will be thrown if they are not valid !
Notice It is not enought to have a unique validation to make sure nodes and relationships are unique, see below.

Unique Entities

If you want to be 100% sure of having unique nodes or relationship it is not enough with a unique validation on the property (since of possible concurrency issues). Instead, you should declare the property with a :unique => true and use the get_or_create method, see below:

class Person < Neo4j::Rails::Model
  property :email, :index => :exact, :unique => true
  property :name
end

# Either create a new person if there is no person with this email or return a person with this email.
node = Person.get_or_create(:name => 'foo', :email => 'email')

Create with Neo4j::Rails::Relationship

The outgoing method from the example above does not return a relationship object since it allows to chain several nodes with the << operator (creating several relationship in one line).
If you want to set properties on the relationship it’s more convinient to create it with the Neo4j::Rails::Relationship

# create a relationship with one property
rel = Neo4j::Rails::Relationship.new(:type, n1, n2, :since => 1942)
# add another property on the relationship
rel[:foo] = "bar"
# Don't forget to save it
rel.save
# or create and save it in one go
rel = Neo4j::Rails::Relationship.create(:type, n1, n2, :since => 1942, :foo => 'bar')

TIP: You can of course also subclass the Neo4j::Rails::Relationship class just like for Neo4j::Rails::Model class to specify domain specific behavour (e.g. validations, callbacks, business logic etc.)

Create with a has_n/has_one accessor method

The has_n and has_one class methods can generate some convenience method for creating and traversing relationships and nodes.
Validation will only be performed on each nested node if the validates_associated is specified, see below.

class Person << Neo4j::Rails::Model
  has_n(:friends)
end

p1 = Person.new  # or Person.create
p2 = Person.new
p1.friends << p2
p1.save # => true

Relationship of same class is assumed by default. For relationships between different classes, see Mapping nodes/relationships to Ruby classes

Notice that you can combine using friends and the outgoing / incoming methods.
These methods may all return both saved and unsaved nodes.

Example:

a.friends << b
a.outgoing(:friends).first # => nil
a.save
a.friends << c
d.incoming(:friends) << a
a.outgoing(:friends).to_a # =>[b,c,d]

On Naming:

Use ModelClass.relationship when specifying relationship name. Instead of the example above write a.outgoing(Person.friends). The reason is that the relationship is prefixed if it is specified with a to node. Example Person.has_n(:friends).to(Person)

class Person << Neo4j::Rails::Model
  property :name, type: String
  has_n(:friends)               # relationship_type = "friends" in Neo4j 
  has_n(:enemies).to(Person)    # relationship_type = "Person.enemies" in Neo4j
end

Not accounting for this difference in the generated relationship types can cause you problems later when writing your own Cypher queries. You can check yourself by calling the .query method from the relationship accessor generated by Neo4j::Rails::Model.

# p,a,b,c,d,e,f are Person models with name=`letter`

p.friends
# => []
p.enemies
# => []
p.friends.query
# => "START v1=node(12610) MATCH (v1)<-[:`friends`]-(v2) RETURN v2"
p.enemies.query
# => "START v1=node(12610) MATCH (v1)<-[:`Person#enemies`]-(v2) RETURN v2"

p.friends << a
p.outgoing(:friends) << b
p.outgoing(Person.friends) << c
p.enemies << d
p.outgoing(:enemies) << e             # <== HERE BE DRAGONS!
p.outgoing(Person.enemies) << f
p.save
p.friends.map(&:name)
# => ["a","b","c"]
p.enemies.map(&:name)
# => ["d","f"]                        # <== UH OH! `e` IS MISSING!

# we can find them with Cypher, but this only demonstrates how you can shoot yourself in the foot.
Neo4j.query(p){|person| person > Person.enemies > node(:enemy).ret }.to_a.map(&:name)
# => ["d","f"]
Neo4j.query(p){|person| person > :enemies > node(:enemy).ret }.to_a.map(&:name)
# => ["e"]

# This all works because:
Person.friends
# => :friends
Person.enemies
# => :"Person#enemies"

These problems are easy to avoid once you know about the relationship naming rules. Using Model.rel_name will save you any trouble later if you have to go back and make your has_* declarations have to clauses. You can use Model.rel_name directly in your CypherDSL queries and also be sure you never make any mistakes.

Create with friends.build and friends.create

If you declare which node class a has_n or has_one is then you can use the build and create method on the relationship accessor. Example

Just like Active Record you can create relationship like this:

class Person < Neo4j::Rails::Model
  has_n(:friends).to("Person")
end

a.friends.build(property_hash)
a.friends.create(property_hash)

For all generated has_n and has_one methods see, Neo4j::Rails::HasN::ClassMethods

Notice You must declare which node class the relationship points to with to, as shown above.

Create with Nested Attributes

Neo4j.rb supports accepts_nested_attributes_for which can be used to create relationship between nodes.

The following configuration option are available

  • :allow_destroy If true, destroys any members from the attributes hash with a _destroy key and a value that evaluates to true (eg. 1, ‘1’, true, or ‘true’). This option is off by default.
  • :reject_if Allows you to specify a Proc or a Symbol pointing to a method that checks whether a record should be built for a certain attribute hash. The hash is passed to the supplied Proc or the method and it should return either true or false. When no :reject_if is specified, a record will be built for all attribute hashes that do not have a destroy value that evaluates to true. Passing :allblank instead of a Proc will create a proc that will reject a record where all the attributes are blank.

When using the accepts_nested_attributes_for class method you must specify which class the relationship correspond to
by using the to method in has_one or has_n.

Example

class Member < Neo4j::Rails::Model
  has_n(:posts).to(Post)
  has_one(:avatar).to(Avatar)

  accepts_nested_attributes_for :avatar, :allow_destroy => true
  accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes[:title].blank? }
  # when creating, pass in: {:avatar_attributes => {:key => 'value'} }
end

Validation

Validation of relationships and nodes for the friends relationship above will always be performed.
If a related nodes or relationship is not valid then an exception will be raised.

destroy and delete

The destroy and delete method works like the Active Record methods.

rel = p1.rels(:outgoing, :friends).find{|rel| rel.end_node == p3}
rel.destroy  # destroys the friend relationship manually

p1.friends.delete(p2)  # destroys the friend relationship

p1.friends.find(p2).delete  # deletes p2 and the friend relationship

delete_all and destroy_all

You can destroy and delete all relationship in one go (just like Active Record).

p1.friends.destroy_all

To destroy all relationships:

p1.rels.destroy_all
p1.rels(:both, :friends).delete_all # no callbacks, and only the friends relationships

actor = Actor.create
actor.acted_in << movie1
actor.outgoing(:acted_in) << movie2
actor.outgoing(:acted_in) # only include movie2
actor.outgoing(Actor.acted_in) # only include movie1

TIP: By not specifying which class the relationship is associated to (using the to method above) gives you more freedom. If you instead just declared Actor.has_n(:acted_in) then an actor can have an acted_in relationship to different classes. Example @an_actor.acted_in << Neo4j::Rails::Model.new() << Thingy.create.

Timestamps

If a Neo4j::Rails::Model or a Neo4j::Rails::Relationship class (or subclass) has the property updated_at or created_at then
it will be timestamped. This is all that is required.

class Role < Neo4j::Rails::Relationship
  property :updated_at
  property :created_at
end

That’s all you need to do. If you want to disable this behaviour check the configuration below.

MultiTenancy

For multitenancy support check this

Neo4j::Rails::Transaction

All write operations requires a transaction. Read operations like find,load or read properties does not require transactions.
The Neo4j::Rails::Model and Neo4j::Rails::Relationship classes does automatically create transaction if needed. If you need to write several operations in one operation use Neo4j::Rails::Transaction. You will also get better performance using a single transaction instead of several small transactions.

Transaction and around_filter

This class can be used as a filter in order to wrap methods in one transactions.

Example:

class UsersController < ApplicationController
  around_filter Neo4j::Rails::Transaction, :only => [:create]

The Neo4j::Rails::Transaction class can also be used to access the current running transaction in order
to signal for a rollback.

Example:

class UsersController < ApplicationController
   around_filter Neo4j::Rails::Transaction, :only => [:create, :update]

   def update
     #.. create, update delete some nodes/relationships
     #.. something when wrong, rollback everyting
     Neo4j::Rails::Transaction.current.fail
   end

Model#transaction

Notice that you can also use the Model.transaction method to wrap a block of code in a transaction.

Example:

class UsersController < ApplicationController
  def update
    Users.transaction do |tx|
      #.. create, update delete some nodes/relationships
      #.. something when wrong, rollback everyting
      tx.fail
    end
  end