lib/neoid/relationship.rb
module Neoid
module Relationship
class << self
def from_hash(hash)
relationship = RelationshipLazyProxy.new(hash)
relationship
end
def included(receiver)
receiver.send :include, Neoid::ModelAdditions
receiver.send :include, InstanceMethods
receiver.extend ClassMethods
initialize_relationship receiver if Neoid.env_loaded
Neoid.relationship_models << receiver
end
def meta_data
@meta_data ||= {}
end
def initialize_relationship(rel_model)
rel_model.reflect_on_all_associations(:belongs_to).each do |belongs_to|
return if belongs_to.options[:polymorphic]
# e.g. all has_many on User class
all_has_many = belongs_to.klass.reflect_on_all_associations(:has_many)
# has_many (without through) on the side of the relationship that removes a relationship. e.g. User has_many :likes
this_has_many = all_has_many.find { |o| o.klass == rel_model }
next unless this_has_many
# e.g. User has_many :likes, after_remove: ...
full_callback_name = "after_remove_for_#{this_has_many.name}"
belongs_to.klass.send(full_callback_name) << :neo_after_relationship_remove if belongs_to.klass.method_defined?(full_callback_name)
# has_many (with through) on the side of the relationship that removes a relationship. e.g. User has_many :movies, through :likes
many_to_many = all_has_many.find { |o| o.options[:through] == this_has_many.name }
next unless many_to_many
return if many_to_many.options[:as] # polymorphic are not supported here yet
# user_id
foreign_key_of_owner = many_to_many.through_reflection.foreign_key
# movie_id
foreign_key_of_record = many_to_many.source_reflection.foreign_key
(Neoid::Relationship.meta_data ||= {}).tap do |data|
(data[belongs_to.klass.name.to_s] ||= {}).tap do |model_data|
model_data[many_to_many.klass.name.to_s] = [rel_model.name.to_s, foreign_key_of_owner, foreign_key_of_record]
end
end
# e.g. User has_many :movies, through: :likes, before_remove: ...
full_callback_name = "before_remove_for_#{many_to_many.name}"
belongs_to.klass.send(full_callback_name) << :neo_before_relationship_through_remove if belongs_to.klass.method_defined?(full_callback_name)
# e.g. User has_many :movies, through: :likes, after_remove: ...
full_callback_name = "after_remove_for_#{many_to_many.name}"
belongs_to.klass.send(full_callback_name) << :neo_after_relationship_through_remove if belongs_to.klass.method_defined?(full_callback_name)
end
end
end
# this is a proxy that delays loading of start_node and end_node from Neo4j until accessed.
# the original Neography Relatioship loaded them on initialization
class RelationshipLazyProxy < ::Neography::Relationship
def start_node
@start_node_from_db ||= @start_node = Neography::Node.load(@start_node, Neoid.db)
end
def end_node
@end_node_from_db ||= @end_node = Neography::Node.load(@end_node, Neoid.db)
end
end
module ClassMethods
def delete_command
:delete_relationship
end
end
module InstanceMethods
def neo_find_by_id
results = Neoid.db.get_relationship_auto_index(Neoid::UNIQUE_ID_KEY, neo_unique_id)
relationship = results.present? ? Neoid::Relationship.from_hash(results[0]) : nil
relationship
end
def _neo_save
return unless Neoid.enabled?
options = self.class.neoid_config.relationship_options
start_item = send(options[:start_node])
end_item = send(options[:end_node])
return unless start_item && end_item
# initialize nodes
start_item.neo_node
end_item.neo_node
data = to_neo.merge(ar_type: self.class.name, ar_id: id, Neoid::UNIQUE_ID_KEY => neo_unique_id)
data.reject! { |k, v| v.nil? }
rel_type = options[:type].is_a?(Proc) ? options[:type].call(self) : options[:type]
gremlin_query = <<-GREMLIN
idx = g.idx('relationship_auto_index');
q = null;
if (idx) q = idx.get(unique_id_key, unique_id);
relationship = null;
if (q && q.hasNext()) {
relationship = q.next();
relationship_data.each {
if (relationship.getProperty(it.key) != it.value) {
relationship.setProperty(it.key, it.value);
}
}
} else {
node_index = g.idx('node_auto_index');
start_node = node_index.get(unique_id_key, start_node_unique_id).next();
end_node = node_index.get(unique_id_key, end_node_unique_id).next();
relationship = g.addEdge(start_node, end_node, rel_type, relationship_data);
}
relationship
GREMLIN
script_vars = {
unique_id_key: Neoid::UNIQUE_ID_KEY,
relationship_data: data,
unique_id: neo_unique_id,
start_node_unique_id: start_item.neo_unique_id,
end_node_unique_id: end_item.neo_unique_id,
rel_type: rel_type
}
Neoid::logger.info "Relationship#neo_save #{self.class.name} #{id}"
relationship = Neoid.execute_script_or_add_to_batch gremlin_query, script_vars do |value|
Neoid::Relationship.from_hash(value)
end
relationship
end
def neo_load(hash)
Neoid::Relationship.from_hash(hash)
end
def neo_relationship
_neo_representation
end
end
end
end