Skip to content

Commit

Permalink
Support for Identity Map [#188 state:resolved]
Browse files Browse the repository at this point in the history
See http://martinfowler.com/eaaCatalog/identityMap.html
It is disabled by default. It is enabled when runnning the RSpecs.
The IdentityMap is cache cleared after each request and after each transaction.
When using large traversals (depth > 1) you will not get better performance with identity map
since we probably only will write to the cache and never read.
Notice, Rails/ActiveRecord 3.1 also has support for IdentityMap which is also disabled by default.
  • Loading branch information
andreasronge committed Oct 20, 2011
1 parent e7fc0bc commit 84650f9
Show file tree
Hide file tree
Showing 18 changed files with 462 additions and 46 deletions.
5 changes: 5 additions & 0 deletions config/neo4j/config.yml
Expand Up @@ -7,6 +7,11 @@ storage_path: db
# If disabled all custom rules will also be unavailable.
enable_rules: true

# if identity map should be on or not
# It may impact the performance. Using the identity map will keep all loaded wrapper node/relationship
# object in memory for each thread and transaction - which may speed up or slow down operations.
identity_map: false

# When using the Neo4j::Model you can let neo4j automatically set timestamps when updating/creating nodes.
# If set to true neo4j.rb automatically timestamps create and update operations if the model has properties named created_at/created_on or updated_at/updated_on
# (similar to ActiveRecord).
Expand Down
1 change: 0 additions & 1 deletion example/shared_index/create_contacts.rb
Expand Up @@ -20,7 +20,6 @@ def finish_tx

def new_tx
finish_tx if @tx
puts "NEW TX"
@tx = Neo4j::Transaction.new
end

Expand Down
3 changes: 3 additions & 0 deletions lib/neo4j.rb
Expand Up @@ -104,4 +104,7 @@ module Neo4j

require 'orm_adapter/adapters/neo4j'

require 'neo4j/identity_map'



1 change: 1 addition & 0 deletions lib/neo4j/config.rb
Expand Up @@ -14,6 +14,7 @@ module Neo4j
# <tt>:timestamps</tt>:: default <tt>true</tt> for Rails Neo4j::Model - if timestamps should be used when saving the model
# <tt>:lucene</tt>:: default hash keys: <tt>:fulltext</tt>, <tt>:exact</tt> configuration how the lucene index is stored
# <tt>:enable_rules</tt>:: default true, if false the _all relationship to all instances will not be created and custom rules will not be available.
# <tt>:identity_map</tt>:: default false, See Neo4j::IdentityMap
#
class Config
# This code is copied from merb-core/config.rb.
Expand Down
7 changes: 7 additions & 0 deletions lib/neo4j/event_handler.rb
Expand Up @@ -12,6 +12,7 @@ module Neo4j
# * <tt>on_relationship_deleted</tt>
# * <tt>on_property_changed</tt>
# * <tt>on_rel_property_changed</tt>
# * <tt>on_after_commit</tt>
#
# ==== on_neo4j_started(db)
#
Expand All @@ -28,6 +29,11 @@ module Neo4j
#
# * <tt>db</tt> :: the Neo4j::Database instance
#
# ==== on_after_commit(data, state)
#
# Called after the transaction has successfully committed.
# See http://api.neo4j.org/1.4/org/neo4j/graphdb/event/TransactionEventHandler.html for the data and state parameter.
#
# ==== on_node_created(node)
#
# * <tt>node</tt> :: the node that was created
Expand Down Expand Up @@ -86,6 +92,7 @@ def initialize


def after_commit(data, state)
@listeners.each {|li| li.on_after_commit(data, state) if li.respond_to?(:on_after_commit)}
end

def after_rollback(data, state)
Expand Down
143 changes: 143 additions & 0 deletions lib/neo4j/identity_map.rb
@@ -0,0 +1,143 @@
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/identity_map.rb
# https://github.com/rails/rails/pull/76

module Neo4j

# = Neo4j Identity Map
#
# Ensures that each object gets loaded only once by keeping every loaded
# object in a map. Looks up objects using the map when referring to them.
#
# More information on Identity Map pattern:
# http://www.martinfowler.com/eaaCatalog/identityMap.html
#
# == Configuration
#
# In order to enable IdentityMap, set <tt>config.neo4j.identity_map = true</tt>
# in your <tt>config/application.rb</tt> file. If used outside rails, set Neo4j::Config[:identity_map] = true.
#
# IdentityMap is disabled by default and still in development (i.e. use it with care).
#
module IdentityMap

class << self
def enabled=(flag)
Thread.current[:neo4j_identity_map] = flag
end

def enabled
Thread.current[:neo4j_identity_map]
end

alias enabled? enabled

def node_repository
Thread.current[:node_identity_map] ||= java.util.HashMap.new
end

def rel_repository
Thread.current[:rel_identity_map] ||= java.util.HashMap.new
end

def repository_for(neo_entity)
return nil unless enabled?
if neo_entity.class == Neo4j::Node
node_repository
elsif neo_entity.class == Neo4j::Relationship
rel_repository
else
nil
end
end

def use
old, self.enabled = enabled, true
yield if block_given?
ensure
self.enabled = old
clear
end

def without
old, self.enabled = enabled, false
yield if block_given?
ensure
self.enabled = old
end

def get(neo_entity)
r = repository_for(neo_entity)
r && r.get(neo_entity.neo_id)
end

def add(neo_entity, wrapped_entity)
r = repository_for(neo_entity)
r && r.put(neo_entity.neo_id, wrapped_entity)
end

def remove(neo_entity)
r = repository_for(neo_entity)
r && r.remove(neo_entity.neo_id)
end

def remove_node_by_id(node_id)
node_repository.remove(node_id)
end

def remove_rel_by_id(rel_id)
rel_repository.remove(rel_id)
end

def clear
node_repository.clear
rel_repository.clear
end

def on_after_commit(*)
clear
end

def on_neo4j_started(db)
if not Neo4j::Config[:identity_map]
db.event_handler.remove(self)
end
end

end


class Middleware
class Body #:nodoc:
def initialize(target, original)
@target = target
@original = original
end

def each(&block)
@target.each(&block)
end

def close
@target.close if @target.respond_to?(:close)
ensure
IdentityMap.enabled = @original
IdentityMap.clear
end
end

def initialize(app)
@app = app
end

def call(env)
enabled = IdentityMap.enabled
IdentityMap.enabled = Neo4j::Config[:identity_map]
status, headers, body = @app.call(env)
[status, headers, Body.new(body, enabled)]
end
end
end
end

Neo4j.unstarted_db.event_handler.add(Neo4j::IdentityMap)

14 changes: 9 additions & 5 deletions lib/neo4j/load.rb
Expand Up @@ -3,18 +3,22 @@ module Neo4j
# === Mixin responsible for loading Ruby wrappers for Neo4j Nodes and Relationship.
#
module Load
def wrapper(node) # :nodoc:
return node unless node.property?(:_classname)
to_class(node[:_classname]).load_wrapper(node)
def wrapper(entity) # :nodoc:
return entity unless entity.property?(:_classname)
existing_instance = Neo4j::IdentityMap.get(entity)
return existing_instance if existing_instance
new_instance = to_class(entity[:_classname]).load_wrapper(entity)
Neo4j::IdentityMap.add(entity, new_instance)
new_instance
end

def to_class(class_name) # :nodoc:
class_name.split("::").inject(Kernel) {|container, name| container.const_get(name.to_s) }
end

# Checks if the given entity (node/relationship) or entity id (#neo_id) exists in the database.
def exist?(node_or_node_id, db = Neo4j.started_db)
id = node_or_node_id.kind_of?(Fixnum) ? node_or_node_id : node_or_node_id.id
def exist?(entity_or_entity_id, db = Neo4j.started_db)
id = entity_or_entity_id.kind_of?(Fixnum) ? entity_or_entity_id : entity_or_entity_id.id
_load(id, db) != nil
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/neo4j/node.rb
Expand Up @@ -28,7 +28,7 @@ def exist? #:nodoc:
def wrapped_entity #:nodoc:
self
end

def wrapper #:nodoc:
self.class.wrapper(self)
end
Expand Down
1 change: 1 addition & 0 deletions lib/neo4j/node_mixin/class_methods.rb
Expand Up @@ -39,6 +39,7 @@ def load_wrapper(node)
def new(*args)
node = Neo4j::Node.create
wrapped_node = super()
Neo4j::IdentityMap.add(node, wrapped_node)
wrapped_node.init_on_load(node)
wrapped_node.init_on_create(*args)
wrapped_node
Expand Down
3 changes: 3 additions & 0 deletions lib/neo4j/rails/persistence.rb
Expand Up @@ -154,6 +154,7 @@ def update
def create
node = Neo4j::Node.new
@_java_node = node
Neo4j::IdentityMap.add(node, self)
init_on_create
clear_changes
clear_relationships
Expand All @@ -172,7 +173,9 @@ def reset_attributes
end

def reload_from_database
Neo4j::IdentityMap.remove_node_by_id(id)
if reloaded = self.class.load(id)
clear_relationships
send(:attributes=, reloaded.attributes, false)
end
reloaded
Expand Down
1 change: 1 addition & 0 deletions lib/neo4j/rails/railtie.rb
Expand Up @@ -4,6 +4,7 @@ class Railtie < ::Rails::Railtie

initializer "neo4j.tx" do |app|
app.config.middleware.use Neo4j::Rails::RackMiddleware
app.config.middleware.use Neo4j::IdentityMap::Middleware
end

# Add ActiveModel translations to the I18n load_path
Expand Down
5 changes: 5 additions & 0 deletions lib/neo4j/rails/rel_persistence.rb
Expand Up @@ -158,6 +158,7 @@ def create()
_persist_end_node

@_java_rel = Neo4j::Relationship.new(type, start_node, end_node)
Neo4j::IdentityMap.add(@_java_rel, self)
init_on_create
clear_changes
end unless @end_node.nil?
Expand Down Expand Up @@ -193,6 +194,10 @@ def reset_attributes
end

def reload_from_database
Neo4j::IdentityMap.remove_rel_by_id(id) if persisted?
Neo4j::IdentityMap.remove_node_by_id(@end_node.id) if @end_node && @end_node.persisted?
Neo4j::IdentityMap.remove_node_by_id(@start_node.id) if @start_node && @start_node.persisted?

if reloaded = self.class.load(id)
send(:attributes=, reloaded.attributes, false)
end
Expand Down
9 changes: 5 additions & 4 deletions lib/neo4j/rails/relationships/relationships.rb
Expand Up @@ -4,23 +4,24 @@ module Relationships


def write_changed_relationships #:nodoc:
@relationships.each_value do |storage|
@_relationships.each_value do |storage|
storage.persist
end
end

def clear_relationships #:nodoc:
@relationships = {}
@_relationships && @_relationships.each_value{|storage| storage.remove_from_identity_map}
@_relationships = {}
end


def _create_or_get_storage(rel_type) #:nodoc:
dsl = _decl_rels_for(rel_type.to_sym)
@relationships[rel_type.to_sym] ||= Storage.new(self, rel_type, dsl)
@_relationships[rel_type.to_sym] ||= Storage.new(self, rel_type, dsl)
end

def _create_or_get_storage_for_decl_rels(decl_rels) #:nodoc:
@relationships[decl_rels.rel_type.to_sym] ||= Storage.new(self, decl_rels.rel_type, decl_rels)
@_relationships[decl_rels.rel_type.to_sym] ||= Storage.new(self, decl_rels.rel_type, decl_rels)
end


Expand Down

0 comments on commit 84650f9

Please sign in to comment.