neo4j 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +10 -0
  3. data/Gemfile +6 -16
  4. data/README.md +5 -2
  5. data/bin/neo4j-jars +6 -6
  6. data/lib/neo4j.rb +13 -9
  7. data/lib/neo4j/active_node.rb +9 -9
  8. data/lib/neo4j/active_node/dependent.rb +11 -0
  9. data/lib/neo4j/active_node/dependent/association_methods.rb +28 -0
  10. data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +48 -0
  11. data/lib/neo4j/active_node/has_n.rb +124 -112
  12. data/lib/neo4j/active_node/has_n/association.rb +45 -30
  13. data/lib/neo4j/active_node/id_property.rb +22 -19
  14. data/lib/neo4j/active_node/initialize.rb +2 -4
  15. data/lib/neo4j/active_node/labels.rb +23 -22
  16. data/lib/neo4j/active_node/node_wrapper.rb +5 -8
  17. data/lib/neo4j/active_node/orm_adapter.rb +2 -4
  18. data/lib/neo4j/active_node/persistence.rb +5 -10
  19. data/lib/neo4j/active_node/property.rb +3 -4
  20. data/lib/neo4j/active_node/query.rb +27 -6
  21. data/lib/neo4j/active_node/query/query_proxy.rb +65 -110
  22. data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +67 -0
  23. data/lib/neo4j/active_node/query/query_proxy_find_in_batches.rb +0 -1
  24. data/lib/neo4j/active_node/query/query_proxy_methods.rb +29 -28
  25. data/lib/neo4j/active_node/query_methods.rb +6 -6
  26. data/lib/neo4j/active_node/reflection.rb +3 -2
  27. data/lib/neo4j/active_node/rels.rb +1 -1
  28. data/lib/neo4j/active_node/scope.rb +13 -8
  29. data/lib/neo4j/active_node/validations.rb +5 -6
  30. data/lib/neo4j/active_rel.rb +1 -2
  31. data/lib/neo4j/active_rel/callbacks.rb +3 -3
  32. data/lib/neo4j/active_rel/persistence.rb +9 -7
  33. data/lib/neo4j/active_rel/property.rb +12 -4
  34. data/lib/neo4j/active_rel/query.rb +6 -8
  35. data/lib/neo4j/active_rel/rel_wrapper.rb +0 -2
  36. data/lib/neo4j/active_rel/related_node.rb +4 -5
  37. data/lib/neo4j/active_rel/types.rb +4 -6
  38. data/lib/neo4j/active_rel/validations.rb +0 -1
  39. data/lib/neo4j/config.rb +11 -23
  40. data/lib/neo4j/core/query.rb +1 -1
  41. data/lib/neo4j/migration.rb +17 -18
  42. data/lib/neo4j/paginated.rb +4 -4
  43. data/lib/neo4j/railtie.rb +19 -19
  44. data/lib/neo4j/shared.rb +7 -3
  45. data/lib/neo4j/shared/callbacks.rb +15 -4
  46. data/lib/neo4j/shared/identity.rb +2 -2
  47. data/lib/neo4j/shared/persistence.rb +10 -21
  48. data/lib/neo4j/shared/property.rb +17 -30
  49. data/lib/neo4j/shared/rel_type_converters.rb +1 -3
  50. data/lib/neo4j/shared/type_converters.rb +13 -25
  51. data/lib/neo4j/shared/validations.rb +3 -3
  52. data/lib/neo4j/tasks/migration.rake +7 -7
  53. data/lib/neo4j/type_converters.rb +1 -1
  54. data/lib/neo4j/version.rb +1 -1
  55. data/lib/rails/generators/neo4j/model/model_generator.rb +16 -12
  56. data/lib/rails/generators/neo4j_generator.rb +18 -18
  57. data/neo4j.gemspec +22 -18
  58. metadata +103 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6f576c543973dba1fb67e8c000487496014fdf85
4
- data.tar.gz: 46a1e8b4d8f7fcc2c388f4791fecea6bf6fec6c9
3
+ metadata.gz: d5ecc7877175dbfe04e305498f2c5501818abf22
4
+ data.tar.gz: eec46ea5ca11ab14fbc8844c14264b332ec56de4
5
5
  SHA512:
6
- metadata.gz: 844a3cd13ec87c091166cf8aef59e4ea5876b8a18b6552167ff5f61aed62f8d04abcb2ba33b303d8c9285c6cd1251427af866b48b6ea72bcbaa62ca5dedfca31
7
- data.tar.gz: 3824b4d1fd649f99f2eaf8f6bc93289008388ce578375b865d10372f56f5e35dd5430d2a5f5b0bb7ab80a8b1314a2773515fc16909b7fa1ed4c45c0d42439845
6
+ metadata.gz: a0514bacb5f60da1bba7e25b04a57fb016b0d7bae4b5fa4fb95a22e8ed746f541f63c6898864c34a174449318ed9216953e5faab84b78bdf97bdf25a9a02851f
7
+ data.tar.gz: d489717468051817dfb88e5fa878f15059e798bdf83b387ec0f3156e8cbf47371fefefae77b3d1b7b556d30c5d6e534f5ae2a38983e864d366b8d36bc0ef8adb
data/CHANGELOG CHANGED
@@ -1,3 +1,13 @@
1
+ == 4.1.0
2
+ This release includes many performance fixes and new features. The most notable:
3
+ * Huge stylist cleanup/refactoring by Brian on the entire gem by Brian armed with Rubocop. See http://neo4jrb.io/blog/2014/12/29/stay-out-of-trouble.html.
4
+ * Every node create, update, and destroy is now wrapped in a transaction. See http://neo4jrb.io/blog/2015/01/06/transactions_everywhere.html.
5
+ * New `dependent` options for associations: `:delete`, `:destroy`, `:delete_orphans`, `:destroy_orphans`. See http://neo4jrb.io/blog/2015/01/07/association_dependent_options.html.
6
+ * New `unique: true` option for associations, `creates_unique_rel` class method for ActiveRel. Both of these will result in relationship creation Cypher using "CREATE UNIQUE" instead of "CREATE".
7
+ * Fixed an n+1 query issue during node creation and update.
8
+ * Dieter simplified some code related to frozen attributes. See https://github.com/neo4jrb/neo4j/pull/655.
9
+ We now have a new website online at http://neo4jrb.io! Keep an eye on it for news and blogs related to this and other projects.
10
+
1
11
  == 4.0.0
2
12
  * Change neo4j-core dependency from 3.1.0 to 4.0.0.
3
13
 
data/Gemfile CHANGED
@@ -2,24 +2,14 @@ source 'http://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- #gem 'neo4j-core', github: 'neo4jrb/neo4j-core', branch: 'master'
6
- #gem 'neo4j-core', git: 'https://github.com/neo4jrb/neo4j-core'
7
- #gem 'orm_adapter', :path => '../orm_adapter'
8
-
9
- gem 'coveralls', require: false
10
-
11
-
12
- group 'development' do
13
- gem 'pry'
14
- gem 'os' # for neo4j-server rake task
15
- gem 'rake'
16
- gem 'yard'
17
- end
5
+ gem 'neo4j-core', github: 'neo4jrb/neo4j-core', branch: 'master'
6
+ # gem 'neo4j-core', path: '../neo4j-core'
18
7
 
19
8
  group 'test' do
9
+ gem 'coveralls', require: false
20
10
  gem 'simplecov', require: false
21
11
  gem 'simplecov-html', require: false
22
- gem "rspec", '~> 2.0'
23
- gem "its"
24
- gem "test-unit"
12
+ gem 'rspec', '~> 2.0'
13
+ gem 'its'
14
+ gem 'test-unit'
25
15
  end
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Welcome to Neo4j.rb
2
- [![Build Status](https://secure.travis-ci.org/neo4jrb/neo4j.png?branch=master)](http://travis-ci.org/neo4jrb/neo4j) [![Coverage Status](https://coveralls.io/repos/neo4jrb/neo4j/badge.png?branch=master)](https://coveralls.io/r/neo4jrb/neo4j?branch=master) [![Code Climate](https://codeclimate.com/github/neo4jrb/neo4j.png)](https://codeclimate.com/github/andreasronge/neo4j)
2
+ [![Build Status](https://secure.travis-ci.org/neo4jrb/neo4j.png?branch=master)](http://travis-ci.org/neo4jrb/neo4j) [![Coverage Status](https://coveralls.io/repos/neo4jrb/neo4j/badge.png?branch=master)](https://coveralls.io/r/neo4jrb/neo4j?branch=master) [![Code Climate](https://codeclimate.com/github/neo4jrb/neo4j.png)](https://codeclimate.com/github/neo4jrb/neo4j) [![PullReview stats](https://www.pullreview.com/github/neo4jrb/neo4j/badges/master.svg?)](https://www.pullreview.com/github/neo4jrb/neo4j/reviews/master)
3
3
 
4
4
  Neo4j.rb is an Active Model compliant Ruby/JRuby wrapper for [the Neo4j graph database](http://www.neo4j.org/). It uses the [neo4j-core](https://github.com/neo4jrb/neo4j-core) and [active_attr](https://github.com/cgriego/active_attr) gems.
5
5
 
6
+ For a general overview see our website: http://neo4jrb.io/
7
+
6
8
  Winner of a 2014 Graphie for "Best Community Contribution" at Neo4j's [Graph Connect](http://graphconnect.com) conference!
7
9
  ![2014 Graphie](http://i.imgur.com/CkOoTTYm.jpg)
8
10
 
@@ -12,6 +14,7 @@ The master branch of this repo now holds stable builds of the v4 version of the
12
14
 
13
15
  ## Modern (3.X) Documentation
14
16
 
17
+ * [Website](http://neo4jrb.io/) (for an introduction)
15
18
  * [Wiki](https://github.com/neo4jrb/neo4j/wiki/Neo4j.rb-v3-Introduction)
16
19
 
17
20
  ## Legacy (2.x) Documentation
@@ -34,7 +37,7 @@ The master branch of this repo now holds stable builds of the v4 version of the
34
37
 
35
38
  ## Contributing
36
39
 
37
- Pull request with high test coverage and good [code climate](https://codeclimate.com/github/andreasronge/neo4j) values will be accepted faster.
40
+ Pull request with high test coverage and good [code climate](https://codeclimate.com/github/neo4jrb/neo4j) values will be accepted faster.
38
41
 
39
42
 
40
43
  ## License
@@ -10,7 +10,7 @@ It copies all jar files which has been required (neo4j-community, neo4j-advanced
10
10
  Usage: neo4j-jars <community|advanced|enterprise>
11
11
 
12
12
  TEXT
13
- exit
13
+ exit
14
14
  end
15
15
 
16
16
  if ARGV.include?('community')
@@ -20,14 +20,14 @@ elsif ARGV.include?('advanced')
20
20
  elsif ARGV.include?('enterprise')
21
21
  require 'neo4j-enterprise' # not really needed
22
22
  else
23
- puts "Expected community, advanced, enterprise"
24
- exit -1
23
+ puts 'Expected community, advanced, enterprise'
24
+ exit(-1)
25
25
  end
26
26
 
27
27
  lib_dir = File.join(Dir.pwd, 'lib')
28
- raise "Expected a lib folder where to copy the jars file, mkdir #{lib_dir}? " unless File.exist?(lib_dir)
28
+ fail "Expected a lib folder where to copy the jars file, mkdir #{lib_dir}? " unless File.exist?(lib_dir)
29
29
 
30
- files = $CLASSPATH.find_all{|x| x =~ /\.jar$/}.collect{|y| y.sub('file:', '')}
31
- files.each {|file| FileUtils.cp(file, lib_dir)}
30
+ files = $CLASSPATH.find_all { |x| x =~ /\.jar$/ }.collect { |y| y.sub('file:', '') }
31
+ files.each { |file| FileUtils.cp(file, lib_dir) }
32
32
 
33
33
  puts "copied #{files.size} files to #{lib_dir}"
@@ -1,17 +1,17 @@
1
1
  require 'neo4j/version'
2
2
 
3
- #require "delegate"
4
- #require "time"
5
- #require "set"
3
+ # require "delegate"
4
+ # require "time"
5
+ # require "set"
6
6
  #
7
- #require "active_support/core_ext"
8
- #require "active_support/json"
9
- #require "active_support/inflector"
10
- #require "active_support/time_with_zone"
7
+ # require "active_support/core_ext"
8
+ # require "active_support/json"
9
+ # require "active_support/inflector"
10
+ # require "active_support/time_with_zone"
11
11
 
12
- require "neo4j-core"
12
+ require 'neo4j-core'
13
13
  require 'neo4j/core/query'
14
- require "active_model"
14
+ require 'active_model'
15
15
  require 'active_support/concern'
16
16
  require 'active_support/core_ext/class/attribute.rb'
17
17
 
@@ -43,8 +43,12 @@ require 'neo4j/active_rel/related_node'
43
43
  require 'neo4j/active_rel/types'
44
44
  require 'neo4j/active_rel'
45
45
 
46
+ require 'neo4j/active_node/dependent'
47
+ require 'neo4j/active_node/dependent/query_proxy_methods'
48
+ require 'neo4j/active_node/dependent/association_methods'
46
49
  require 'neo4j/active_node/query_methods'
47
50
  require 'neo4j/active_node/query/query_proxy_methods'
51
+ require 'neo4j/active_node/query/query_proxy_enumerable'
48
52
  require 'neo4j/active_node/query/query_proxy_find_in_batches'
49
53
  require 'neo4j/active_node/labels'
50
54
  require 'neo4j/active_node/id_property'
@@ -1,5 +1,4 @@
1
1
  module Neo4j
2
-
3
2
  # Makes Neo4j nodes and relationships behave like ActiveRecord objects.
4
3
  # By including this module in your class it will create a mapping for the node to your ruby class
5
4
  # by using a Neo4j Label with the same name as the class. When the node is loaded from the database it
@@ -39,24 +38,25 @@ module Neo4j
39
38
  include Neo4j::ActiveNode::Rels
40
39
  include Neo4j::ActiveNode::HasN
41
40
  include Neo4j::ActiveNode::Scope
41
+ include Neo4j::ActiveNode::Dependent
42
42
 
43
43
  def neo4j_obj
44
- _persisted_obj || raise("Tried to access native neo4j object on a non persisted object")
44
+ _persisted_obj || fail('Tried to access native neo4j object on a non persisted object')
45
45
  end
46
46
 
47
47
  included do
48
48
  def self.inherited(other)
49
- inherit_id_property(other) if self.has_id_property?
49
+ inherit_id_property(other) if self.id_property?
50
50
  inherited_indexes(other) if self.respond_to?(:indexed_properties)
51
- attributes.each_pair { |k,v| other.attributes[k] = v }
51
+ attributes.each_pair { |k, v| other.attributes[k] = v }
52
52
  inherit_serialized_properties(other) if self.respond_to?(:serialized_properties)
53
53
  Neo4j::ActiveNode::Labels.add_wrapped_class(other)
54
54
  super
55
55
  end
56
56
 
57
57
  def self.inherited_indexes(other)
58
- return if indexed_properties.nil?
59
- self.indexed_properties.each { |property| other.index property }
58
+ return if indexed_properties.nil?
59
+ self.indexed_properties.each { |property| other.index property }
60
60
  end
61
61
 
62
62
  def self.inherit_serialized_properties(other)
@@ -65,17 +65,17 @@ module Neo4j
65
65
 
66
66
  def self.inherit_id_property(other)
67
67
  id_prop = self.id_property_info
68
- conf = id_prop[:type].empty? ? { auto: :uuid } : id_prop[:type]
68
+ conf = id_prop[:type].empty? ? {auto: :uuid} : id_prop[:type]
69
69
  other.id_property id_prop[:name], conf
70
70
  end
71
71
 
72
72
  Neo4j::Session.on_session_available do |_|
73
- id_property :uuid, auto: :uuid unless self.has_id_property?
73
+ id_property :uuid, auto: :uuid unless self.id_property?
74
74
 
75
75
  name = Neo4j::Config[:id_property]
76
76
  type = Neo4j::Config[:id_property_type]
77
77
  value = Neo4j::Config[:id_property_type_value]
78
- id_property(name, type => value) if (name && type && value)
78
+ id_property(name, type => value) if name && type && value
79
79
  end
80
80
  end
81
81
 
@@ -0,0 +1,11 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Dependent
4
+ def dependent_children
5
+ @dependent_children ||= []
6
+ end
7
+
8
+ attr_writer :called_by
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Dependent
4
+ module AssociationMethods
5
+ def add_destroy_callbacks(model)
6
+ return if dependent.nil?
7
+ # Bound value for procs
8
+ assoc = self
9
+
10
+ fn = case dependent
11
+ when :delete
12
+ proc { |o| o.send("#{assoc.name}_query_proxy").delete_all }
13
+ when :delete_orphans
14
+ proc { |o| o.as(:self).unique_nodes(assoc, :self, :n, :other_rel).query.delete(:n, :other_rel).exec }
15
+ when :destroy
16
+ proc { |o| o.send("#{assoc.name}_query_proxy").each_for_destruction(o) { |node| node.destroy } }
17
+ when :destroy_orphans
18
+ proc { |o| o.as(:self).unique_nodes(assoc, :self, :n, :other_rel).each_for_destruction(o) { |node| node.destroy } }
19
+ else
20
+ fail "Unknown dependent option #{dependent}"
21
+ end
22
+
23
+ model.before_destroy fn
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Dependent
4
+ # methods used to resolve association dependencies
5
+ module QueryProxyMethods
6
+ # Used as part of `dependent: :destroy` and may not have any utility otherwise.
7
+ # It keeps track of the node responsible for a cascading `destroy` process.
8
+ # @param [#dependent_children] caller The node that called this method. Typically, we would use QueryProxy's `caller` method
9
+ # but this is not always available, so we require it explicitly.
10
+ def each_for_destruction(owning_node)
11
+ target = owning_node.called_by || owning_node
12
+ enumerable_query(identity).each do |obj|
13
+ # Cypher can return nil objects, check for empty results
14
+ next if !obj || target.dependent_children.include?(obj)
15
+ obj.called_by = target
16
+ target.dependent_children << obj
17
+ yield obj
18
+ end
19
+ end
20
+
21
+ # This will match nodes who only have a single relationship of a given type.
22
+ # It's used by `dependent: :delete_orphans` and `dependent: :destroy_orphans` and may not have much utility otherwise.
23
+ # @param [Neo4j::ActiveNode::HasN::Association] association The Association object used throughout the match.
24
+ # @param [String, Symbol] other_node The identifier to use for the other end of the chain.
25
+ # @param [String, Symbol] other_rel The identifier to use for the relationship in the optional match.
26
+ # @return [Neo4j::ActiveNode::Query::QueryProxy]
27
+ def unique_nodes(association, self_identifer, other_node, other_rel)
28
+ fail 'Only supported by in QueryProxy chains started by an instance' unless caller
29
+ both_string = "-[:`#{association.relationship_type}`]-"
30
+ in_string = "<#{both_string}"
31
+ out_string = "#{both_string}>"
32
+ primary_rel, inverse_rel = case association.direction
33
+ when :out
34
+ [out_string, in_string]
35
+ when :in
36
+ [in_string, out_string]
37
+ else
38
+ [both_string, both_string]
39
+ end
40
+
41
+ query.with(identity).proxy_as_optional(caller.class, self_identifer)
42
+ .send("#{association.name}", other_node, other_rel)
43
+ .where("NOT EXISTS((#{self_identifer})#{primary_rel}(#{other_node})#{inverse_rel}())")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,135 +1,143 @@
1
1
  module Neo4j::ActiveNode
2
- module HasN
3
- extend ActiveSupport::Concern
2
+ module HasN
3
+ extend ActiveSupport::Concern
4
4
 
5
- class NonPersistedNodeError < StandardError; end
5
+ class NonPersistedNodeError < StandardError; end
6
6
 
7
- # Clears out the association cache.
8
- def clear_association_cache #:nodoc:
9
- association_cache.clear if persisted?
10
- end
11
-
12
- # Returns the current association cache. It is in the format
13
- # { :association_name => { :hash_of_cypher_string => [collection] }}
14
- def association_cache
15
- @association_cache ||= {}
16
- end
17
-
18
- # Returns the specified association instance if it responds to :loaded?, nil otherwise.
19
- # @param [String] cypher_string the cypher, with params, used for lookup
20
- # @param [Enumerable] association_obj the HasN::Association object used to perform this query
21
- def association_instance_get(cypher_string, association_obj)
22
- return if association_cache.nil? || association_cache.empty?
23
- lookup_obj = cypher_hash(cypher_string)
24
- reflection = association_reflection(association_obj)
25
- return if reflection.nil?
26
- association_cache[reflection.name] ? association_cache[reflection.name][lookup_obj] : nil
27
- end
7
+ # Clears out the association cache.
8
+ def clear_association_cache #:nodoc:
9
+ association_cache.clear if _persisted_obj
10
+ end
28
11
 
29
- # @return [Hash] A hash of all queries in @association_cache created from the association owning this reflection
30
- def association_instance_get_by_reflection(reflection_name)
31
- association_cache[reflection_name]
32
- end
12
+ # Returns the current association cache. It is in the format
13
+ # { :association_name => { :hash_of_cypher_string => [collection] }}
14
+ def association_cache
15
+ @association_cache ||= {}
16
+ end
33
17
 
34
- # Caches an association result. Unlike ActiveRecord, which stores results in @association_cache using { :association_name => [collection_result] },
35
- # ActiveNode stores it using { :association_name => { :hash_string_of_cypher => [collection_result] }}.
36
- # This is necessary because an association name by itself does not take into account :where, :limit, :order, etc,... so it's prone to error.
37
- # @param [Neo4j::ActiveNode::Query::QueryProxy] query_proxy The QueryProxy object that resulted in this result
38
- # @param [Enumerable] collection_result The result of the query after calling :each
39
- # @param [Neo4j::ActiveNode::HasN::Association] association_obj The association traversed to create the result
40
- def association_instance_set(cypher_string, collection_result, association_obj)
41
- return collection_result if Neo4j::Transaction.current
42
- cache_key = cypher_hash(cypher_string)
43
- reflection = association_reflection(association_obj)
44
- return if reflection.nil?
45
- if @association_cache[reflection.name]
46
- @association_cache[reflection.name][cache_key] = collection_result
47
- else
48
- @association_cache[reflection.name] = { cache_key => collection_result }
18
+ # Returns the specified association instance if it responds to :loaded?, nil otherwise.
19
+ # @param [String] cypher_string the cypher, with params, used for lookup
20
+ # @param [Enumerable] association_obj the HasN::Association object used to perform this query
21
+ def association_instance_get(cypher_string, association_obj)
22
+ return if association_cache.nil? || association_cache.empty?
23
+ lookup_obj = cypher_hash(cypher_string)
24
+ reflection = association_reflection(association_obj)
25
+ return if reflection.nil?
26
+ association_cache[reflection.name] ? association_cache[reflection.name][lookup_obj] : nil
49
27
  end
50
- collection_result
51
- end
52
28
 
53
- def association_reflection(association_obj)
54
- self.class.reflect_on_association(association_obj.name)
55
- end
29
+ # @return [Hash] A hash of all queries in @association_cache created from the association owning this reflection
30
+ def association_instance_get_by_reflection(reflection_name)
31
+ association_cache[reflection_name]
32
+ end
56
33
 
57
- # Uses the cypher generated by a QueryProxy object, complete with params, to generate a basic non-cryptographic hash
58
- # for use in @association_cache.
59
- # @param [String] the cypher used in the query
60
- # @return [String] A basic hash of the query
61
- def cypher_hash(cypher_string)
62
- cypher_string.hash.abs
63
- end
34
+ # Caches an association result. Unlike ActiveRecord, which stores results in @association_cache using { :association_name => [collection_result] },
35
+ # ActiveNode stores it using { :association_name => { :hash_string_of_cypher => [collection_result] }}.
36
+ # This is necessary because an association name by itself does not take into account :where, :limit, :order, etc,... so it's prone to error.
37
+ # @param [Neo4j::ActiveNode::Query::QueryProxy] query_proxy The QueryProxy object that resulted in this result
38
+ # @param [Enumerable] collection_result The result of the query after calling :each
39
+ # @param [Neo4j::ActiveNode::HasN::Association] association_obj The association traversed to create the result
40
+ def association_instance_set(cypher_string, collection_result, association_obj)
41
+ return collection_result if Neo4j::Transaction.current
42
+ cache_key = cypher_hash(cypher_string)
43
+ reflection = association_reflection(association_obj)
44
+ return if reflection.nil?
45
+ if @association_cache[reflection.name]
46
+ @association_cache[reflection.name][cache_key] = collection_result
47
+ else
48
+ @association_cache[reflection.name] = {cache_key => collection_result}
49
+ end
50
+ collection_result
51
+ end
64
52
 
65
- module ClassMethods
66
- def has_association?(name)
67
- !!associations[name.to_sym]
53
+ def association_reflection(association_obj)
54
+ self.class.reflect_on_association(association_obj.name)
68
55
  end
69
56
 
70
- def associations
71
- @associations || {}
57
+ # Uses the cypher generated by a QueryProxy object, complete with params, to generate a basic non-cryptographic hash
58
+ # for use in @association_cache.
59
+ # @param [String] the cypher used in the query
60
+ # @return [String] A basic hash of the query
61
+ def cypher_hash(cypher_string)
62
+ cypher_string.hash.abs
72
63
  end
73
64
 
74
- # make sure the inherited classes inherit the <tt>_decl_rels</tt> hash
75
- def inherited(klass)
76
- klass.instance_variable_set(:@associations, associations.clone)
65
+ module ClassMethods
66
+ # :nocov:
67
+ # rubocop:disable Style/PredicateName
68
+ def has_association?(name)
69
+ ActiveSupport::Deprecation.warn 'has_association? is deprecated and may be removed from future releases, use association? instead.', caller
77
70
 
78
- super
79
- end
71
+ association?(name)
72
+ end
73
+ # rubocop:enable Style/PredicateName
74
+ # :nocov:
75
+
76
+ def association?(name)
77
+ !!associations[name.to_sym]
78
+ end
80
79
 
81
- def has_many(direction, name, options = {})
82
- name = name.to_sym
83
-
84
- association = Neo4j::ActiveNode::HasN::Association.new(:has_many, direction, name, options)
85
- @associations ||= {}
86
- @associations[name] = association
87
-
88
- target_class_name = association.target_class_name || 'nil'
89
- create_reflection(:has_many, name, association)
90
-
91
- # TODO: Make assignment more efficient? (don't delete nodes when they are being assigned)
92
- module_eval(%Q{
93
- def #{name}(node = nil, rel = nil)
94
- return [].freeze unless self.persisted?
95
- Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name},
96
- self.class.associations[#{name.inspect}],
97
- {
98
- session: self.class.neo4j_session,
99
- start_object: self,
100
- node: node,
101
- rel: rel,
102
- context: '#{self.name}##{name}',
103
- caller: self
104
- })
80
+ def associations
81
+ @associations || {}
82
+ end
105
83
 
84
+ # make sure the inherited classes inherit the <tt>_decl_rels</tt> hash
85
+ def inherited(klass)
86
+ klass.instance_variable_set(:@associations, associations.clone)
87
+ super
88
+ end
89
+
90
+ # rubocop:disable Style/PredicateName
91
+ def has_many(direction, name, options = {})
92
+ name = name.to_sym
93
+ association = build_association(:has_many, direction, name, options)
94
+ # TODO: Make assignment more efficient? (don't delete nodes when they are being assigned)
95
+ module_eval(%{
96
+ def #{name}(node = nil, rel = nil)
97
+ return [].freeze unless self._persisted_obj
98
+ #{name}_query_proxy(node: node, rel: rel)
99
+ end
100
+
101
+ def #{name}_query_proxy(options = {})
102
+ Neo4j::ActiveNode::Query::QueryProxy.new(#{association.target_class_name_or_nil},
103
+ self.class.associations[#{name.inspect}],
104
+ {
105
+ session: self.class.neo4j_session,
106
+ start_object: self,
107
+ node: options[:node],
108
+ rel: options[:rel],
109
+ context: '#{self.name}##{name}',
110
+ caller: self
111
+ })
106
112
  end
107
113
 
108
114
  def #{name}=(other_nodes)
109
115
  #{name}(nil, :r).query_as(:n).delete(:r).exec
110
116
  clear_association_cache
111
- other_nodes.each do |node|
112
- #{name} << node
113
- end
117
+ other_nodes.each { |node| #{name} << node }
114
118
  end
115
119
 
116
120
  def #{name}_rels
117
121
  #{name}(nil, :r).pluck(:r)
118
122
  end}, __FILE__, __LINE__)
119
123
 
120
- instance_eval(%Q{
124
+ instance_eval(%{
121
125
  def #{name}(node = nil, rel = nil, proxy_obj = nil)
122
- query_proxy = proxy_obj || Neo4j::ActiveNode::Query::QueryProxy.new(::#{self.name}, nil, {
126
+ #{name}_query_proxy(node: node, rel: rel, proxy_obj: proxy_obj)
127
+ end
128
+
129
+ def #{name}_query_proxy(options = {})
130
+ query_proxy = options[:proxy_obj] || Neo4j::ActiveNode::Query::QueryProxy.new(::#{self.name}, nil, {
123
131
  session: self.neo4j_session, query_proxy: nil, context: '#{self.name}' + '##{name}'
124
132
  })
125
133
  context = (query_proxy && query_proxy.context ? query_proxy.context : '#{self.name}') + '##{name}'
126
- Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name},
127
- @associations[#{name.inspect}],
134
+ Neo4j::ActiveNode::Query::QueryProxy.new(#{association.target_class_name_or_nil},
135
+ associations[#{name.inspect}],
128
136
  {
129
137
  session: self.neo4j_session,
130
138
  query_proxy: query_proxy,
131
- node: node,
132
- rel: rel,
139
+ node: options[:node],
140
+ rel: options[:rel],
133
141
  context: context,
134
142
  optional: query_proxy.optional?,
135
143
  caller: query_proxy.caller
@@ -139,17 +147,11 @@ module HasN
139
147
 
140
148
  def has_one(direction, name, options = {})
141
149
  name = name.to_sym
150
+ association = build_association(:has_one, direction, name, options)
142
151
 
143
- association = Neo4j::ActiveNode::HasN::Association.new(:has_one, direction, name, options)
144
- @associations ||= {}
145
- @associations[name] = association
146
-
147
- target_class_name = association.target_class_name || 'nil'
148
- create_reflection(:has_one, name, association)
149
-
150
- module_eval(%Q{
152
+ module_eval(%{
151
153
  def #{name}=(other_node)
152
- raise(Neo4j::ActiveNode::HasN::NonPersistedNodeError, 'Unable to create relationship with non-persisted nodes') unless self.persisted?
154
+ raise(Neo4j::ActiveNode::HasN::NonPersistedNodeError, 'Unable to create relationship with non-persisted nodes') unless self._persisted_obj
153
155
  clear_association_cache
154
156
  #{name}_query_proxy(rel: :r).query_as(:n).delete(:r).exec
155
157
  #{name}_query_proxy << other_node
@@ -164,17 +166,17 @@ module HasN
164
166
  end
165
167
 
166
168
  def #{name}(node = nil, rel = nil)
167
- return nil unless self.persisted?
169
+ return nil unless self._persisted_obj
168
170
  result = #{name}_query_proxy(node: node, rel: rel, context: '#{self.name}##{name}')
169
171
  association = self.class.reflect_on_association(__method__)
170
172
  query_return = association_instance_get(result.to_cypher_with_params, association)
171
173
  query_return || association_instance_set(result.to_cypher_with_params, result.first, association)
172
174
  end}, __FILE__, __LINE__)
173
175
 
174
- instance_eval(%Q{
176
+ instance_eval(%{
175
177
  def #{name}_query_proxy(options = {})
176
- Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name},
177
- @associations[#{name.inspect}],
178
+ Neo4j::ActiveNode::Query::QueryProxy.new(#{association.target_class_name_or_nil},
179
+ associations[#{name.inspect}],
178
180
  {session: self.neo4j_session}.merge(options))
179
181
  end
180
182
 
@@ -183,7 +185,17 @@ module HasN
183
185
  #{name}_query_proxy(query_proxy: query_proxy, node: node, rel: rel, context: context)
184
186
  end}, __FILE__, __LINE__)
185
187
  end
188
+ # rubocop:enable Style/PredicateName
189
+
190
+ private
191
+
192
+ def build_association(macro, direction, name, options)
193
+ Neo4j::ActiveNode::HasN::Association.new(macro, direction, name, options).tap do |association|
194
+ @associations ||= {}
195
+ @associations[name] = association
196
+ create_reflection(macro, name, association, self)
197
+ end
198
+ end
186
199
  end
187
200
  end
188
-
189
201
  end