Skip to content

Commit

Permalink
Merge b637942 into c7b7c46
Browse files Browse the repository at this point in the history
  • Loading branch information
subvertallchris committed Sep 13, 2014
2 parents c7b7c46 + b637942 commit 7a1fdc9
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 57 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Expand Up @@ -3,7 +3,7 @@ source 'http://rubygems.org'
gemspec

#gem 'neo4j-core', path: '../neo4j-core'
#gem 'neo4j-core', :git => 'https://github.com/andreasronge/neo4j-core.git'
gem 'neo4j-core', github: 'neo4jrb/neo4j-core'
#gem 'orm_adapter', :path => '../orm_adapter'

gem 'coveralls', require: false
Expand Down
75 changes: 72 additions & 3 deletions lib/neo4j/active_node/has_n.rb
Expand Up @@ -4,6 +4,64 @@ module HasN

class NonPersistedNodeError < StandardError; end

# Clears out the association cache.
def clear_association_cache #:nodoc:
association_cache.clear if persisted?
end

# Returns the current association cache. It is in the format
# { :association_name => { :hash_of_cypher_string => [collection] }}
def association_cache
@association_cache ||= {}
end

# Returns the specified association instance if it responds to :loaded?, nil otherwise.
# @param [String] cypher_string the cypher, with params, used for lookup
# @param [Enumerable] association_obj the HasN::Association object used to perform this query
def association_instance_get(cypher_string, association_obj)
return if association_cache.nil? || association_cache.empty?
lookup_obj = cypher_hash(cypher_string)
reflection = association_reflection(association_obj)
return if reflection.nil?
association_cache[reflection.name] ? association_cache[reflection.name][lookup_obj] : nil
end

# @return [Hash] A hash of all queries in @association_cache created from the association owning this reflection
def association_instance_get_by_reflection(reflection_name)
association_cache[reflection_name]
end

# Caches an association result. Unlike ActiveRecord, which stores results in @association_cache using { :association_name => [collection_result] },
# ActiveNode stores it using { :association_name => { :hash_string_of_cypher => [collection_result] }}.
# This is necessary because an association name by itself does not take into account :where, :limit, :order, etc,... so it's prone to error.
# @param [Neo4j::ActiveNode::Query::QueryProxy] query_proxy The QueryProxy object that resulted in this result
# @param [Enumerable] collection_result The result of the query after calling :each
# @param [Neo4j::ActiveNode::HasN::Association] association_obj The association traversed to create the result
def association_instance_set(cypher_string, collection_result, association_obj)
return collection_result if Neo4j::Transaction.current
cache_key = cypher_hash(cypher_string)
reflection = association_reflection(association_obj)
return if reflection.nil?
if @association_cache[reflection.name]
@association_cache[reflection.name][cache_key] = collection_result
else
@association_cache[reflection.name] = { cache_key => collection_result }
end
collection_result
end

def association_reflection(association_obj)
self.class.reflect_on_association(association_obj.name)
end

# Uses the cypher generated by a QueryProxy object, complete with params, to generate a basic non-cryptographic hash
# for use in @association_cache.
# @param [String] the cypher used in the query
# @return [String] A basic hash of the query
def cypher_hash(cypher_string)
cypher_string.hash.abs
end

module ClassMethods
def has_association?(name)
!!associations[name.to_sym]
Expand Down Expand Up @@ -41,13 +99,15 @@ def #{name}(node = nil, rel = nil)
start_object: self,
node: node,
rel: rel,
context: '#{self.name}##{name}'
context: '#{self.name}##{name}',
caller: self
})
end
def #{name}=(other_nodes)
#{name}(nil, :r).query_as(:n).delete(:r).exec
clear_association_cache
other_nodes.each do |node|
#{name} << node
end
Expand All @@ -70,7 +130,8 @@ def #{name}(node = nil, rel = nil, proxy_obj = nil)
query_proxy: query_proxy,
node: node,
rel: rel,
context: context
context: context,
caller: query_proxy.caller
})
end}, __FILE__, __LINE__)
end
Expand All @@ -88,6 +149,7 @@ def has_one(direction, name, options = {})
module_eval(%Q{
def #{name}=(other_node)
raise(Neo4j::ActiveNode::HasN::NonPersistedNodeError, 'Unable to create relationship with non-persisted nodes') unless self.persisted?
clear_association_cache
#{name}_query_proxy(rel: :r).query_as(:n).delete(:r).exec
#{name}_query_proxy << other_node
end
Expand All @@ -102,7 +164,14 @@ def #{name}_rel
def #{name}(node = nil, rel = nil)
return nil unless self.persisted?
#{name}_query_proxy(node: node, rel: rel, context: '#{self.name}##{name}').first
result = #{name}_query_proxy(node: node, rel: rel, context: '#{self.name}##{name}')
association = self.class.reflect_on_association(__method__)
query_return = association_instance_get(result.to_cypher_with_params, association)
if query_return.nil?
association_instance_set(result.to_cypher_with_params, result.first, association)
else
query_return
end
end}, __FILE__, __LINE__)

instance_eval(%Q{
Expand Down
1 change: 1 addition & 0 deletions lib/neo4j/active_node/initialize.rb
Expand Up @@ -8,6 +8,7 @@ module Neo4j::ActiveNode::Initialize
def init_on_load(persisted_node, properties)
@_association_attributes = self.class.extract_association_attributes!(properties)
@_persisted_obj = persisted_node
@association_cache = {}
changed_attributes && changed_attributes.clear
@attributes = attributes.merge(properties.stringify_keys)
self.default_properties=properties
Expand Down
40 changes: 39 additions & 1 deletion lib/neo4j/active_node/persistence.rb
@@ -1,8 +1,46 @@
module Neo4j::ActiveNode
module Persistence

class RecordInvalidError < RuntimeError
attr_reader :record

def initialize(record)
@record = record
super(@record.errors.full_messages.join(", "))
end
end

extend ActiveSupport::Concern
include Neo4j::Shared::Persistence

# Saves the model.
#
# If the model is new a record gets created in the database, otherwise the existing record gets updated.
# If perform_validation is true validations run.
# If any of them fail the action is cancelled and save returns false. If the flag is false validations are bypassed altogether. See ActiveRecord::Validations for more information.
# There’s a series of callbacks associated with save. If any of the before_* callbacks return false the action is cancelled and save returns false.
def save(*)
update_magic_properties
clear_association_cache
create_or_update
end

# Persist the object to the database. Validations and Callbacks are included
# by default but validation can be disabled by passing :validate => false
# to #save! Creates a new transaction.
#
# @raise a RecordInvalidError if there is a problem during save.
# @param (see Neo4j::Rails::Validations#save)
# @return nil
# @see #save
# @see Neo4j::Rails::Validations Neo4j::Rails::Validations - for the :validate parameter
# @see Neo4j::Rails::Callbacks Neo4j::Rails::Callbacks - for callbacks
def save!(*args)
unless save(*args)
raise RecordInvalidError.new(self)
end
end

# Creates a model with values matching those of the instance attributes and returns its id.
# @private
# @return true
Expand Down Expand Up @@ -65,4 +103,4 @@ def load_entity(id)
private

end
end
end
34 changes: 28 additions & 6 deletions lib/neo4j/active_node/query/query_proxy.rb
Expand Up @@ -6,6 +6,10 @@ class QueryProxy
include Enumerable
include Neo4j::ActiveNode::Query::QueryProxyMethods

# The most recent node to start a QueryProxy chain.
# Will be nil when using QueryProxy chains on class methods.
attr_reader :caller

def initialize(model, association = nil, options = {})
@model = model
@association = association
Expand All @@ -14,6 +18,7 @@ def initialize(model, association = nil, options = {})
@node_var = options[:node]
@rel_var = options[:rel] || _rel_chain_var
@session = options[:session]
@caller = options[:caller]
@chain = []
@params = options[:query_proxy] ? options[:query_proxy].instance_variable_get('@params') : {}
end
Expand All @@ -22,16 +27,24 @@ def identity
@node_var || :result
end

def enumerable_query(node, rel = nil)
pluck_this = rel.nil? ? [node] : [node, rel]
return self.pluck(*pluck_this) if @association.nil? || caller.nil?
cypher_string = self.to_cypher_with_params(pluck_this)
association_collection = caller.association_instance_get(cypher_string, @association)
if association_collection.nil?
association_collection = self.pluck(*pluck_this)
caller.association_instance_set(cypher_string, association_collection, @association) unless association_collection.empty?
end
association_collection
end

def each(node = true, rel = nil, &block)
if node && rel
self.pluck(identity, @rel_var).each do |obj, rel|
yield obj, rel
end
enumerable_query(identity, @rel_var).each { |obj, rel| yield obj, rel }
else
pluck_this = !rel ? identity : @rel_var
self.pluck(pluck_this).each do |obj|
yield obj
end
enumerable_query(pluck_this).each { |obj| yield obj }
end
end

Expand Down Expand Up @@ -97,6 +110,14 @@ def to_cypher
query.to_cypher
end

# Returns a string of the cypher query with return objects and params
# @param [Array] columns array containing symbols of identifiers used in the query
# @return [String]
def to_cypher_with_params(columns = [:result])
final_query = query.return_query(columns)
"#{final_query.to_cypher} | params: #{final_query.send(:merge_params)}"
end

# To add a relationship for the node for the association on this QueryProxy
def <<(other_node)
create(other_node, {})
Expand Down Expand Up @@ -131,6 +152,7 @@ def create(other_nodes, properties)
return false if @association.perform_callback(@options[:start_object], other_node, :before) == false

start_object = @options[:start_object]
start_object.clear_association_cache
_session.query(context: @options[:context])
.start(start: "node(#{start_object.neo_id})", end: "node(#{other_node.neo_id})")
.create("start#{_association_arrow(properties, true)}end").exec
Expand Down
38 changes: 1 addition & 37 deletions lib/neo4j/shared/persistence.rb
@@ -1,45 +1,9 @@
module Neo4j::Shared
module Persistence

class RecordInvalidError < RuntimeError
attr_reader :record

def initialize(record)
@record = record
super(@record.errors.full_messages.join(", "))
end
end

extend ActiveSupport::Concern
include Neo4j::TypeConverters

# Saves the model.
#
# If the model is new a record gets created in the database, otherwise the existing record gets updated.
# If perform_validation is true validations run.
# If any of them fail the action is cancelled and save returns false. If the flag is false validations are bypassed altogether. See ActiveRecord::Validations for more information.
# There’s a series of callbacks associated with save. If any of the before_* callbacks return false the action is cancelled and save returns false.
def save(*)
update_magic_properties
create_or_update
end

# Persist the object to the database. Validations and Callbacks are included
# by default but validation can be disabled by passing :validate => false
# to #save! Creates a new transaction.
#
# @raise a RecordInvalidError if there is a problem during save.
# @param (see Neo4j::Rails::Validations#save)
# @return nil
# @see #save
# @see Neo4j::Rails::Validations Neo4j::Rails::Validations - for the :validate parameter
# @see Neo4j::Rails::Callbacks Neo4j::Rails::Callbacks - for callbacks
def save!(*args)
unless save(*args)
raise RecordInvalidError.new(self)
end
end

def update_model
if changed_attributes && !changed_attributes.empty?
changed_props = attributes.select{|k,v| changed_attributes.include?(k)}
Expand All @@ -49,7 +13,6 @@ def update_model
end
end


# Convenience method to set attribute and #save at the same time
# @param [Symbol, String] attribute of the attribute to update
# @param [Object] value to set
Expand Down Expand Up @@ -138,6 +101,7 @@ def freeze_if_deleted

def reload
return self if new_record?
clear_association_cache
changed_attributes && changed_attributes.clear
unless reload_from_database
@_deleted = true
Expand Down
2 changes: 1 addition & 1 deletion spec/e2e/id_property_spec.rb
Expand Up @@ -99,7 +99,7 @@
it 'throws exception if the same uuid is generated when saving node' do
clazz.create(myid: 'z')
a = clazz.new(myid: 'z')
expect { clazz.create!(myid: 'z') }.to raise_error(Neo4j::Shared::Persistence::RecordInvalidError)
expect { clazz.create!(myid: 'z') }.to raise_error(Neo4j::ActiveNode::Persistence::RecordInvalidError)
end

describe 'property myid' do
Expand Down

0 comments on commit 7a1fdc9

Please sign in to comment.