neo4j 4.1.5 → 5.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +584 -0
  3. data/CONTRIBUTORS +7 -28
  4. data/Gemfile +6 -1
  5. data/README.md +54 -8
  6. data/lib/neo4j.rb +5 -0
  7. data/lib/neo4j/active_node.rb +1 -0
  8. data/lib/neo4j/active_node/dependent/association_methods.rb +35 -17
  9. data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +21 -19
  10. data/lib/neo4j/active_node/has_n.rb +377 -132
  11. data/lib/neo4j/active_node/has_n/association.rb +77 -38
  12. data/lib/neo4j/active_node/id_property.rb +46 -28
  13. data/lib/neo4j/active_node/initialize.rb +18 -6
  14. data/lib/neo4j/active_node/labels.rb +69 -35
  15. data/lib/neo4j/active_node/node_wrapper.rb +37 -30
  16. data/lib/neo4j/active_node/orm_adapter.rb +5 -4
  17. data/lib/neo4j/active_node/persistence.rb +53 -10
  18. data/lib/neo4j/active_node/property.rb +13 -5
  19. data/lib/neo4j/active_node/query.rb +11 -10
  20. data/lib/neo4j/active_node/query/query_proxy.rb +126 -153
  21. data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +15 -25
  22. data/lib/neo4j/active_node/query/query_proxy_link.rb +89 -0
  23. data/lib/neo4j/active_node/query/query_proxy_methods.rb +72 -19
  24. data/lib/neo4j/active_node/query_methods.rb +3 -1
  25. data/lib/neo4j/active_node/scope.rb +17 -21
  26. data/lib/neo4j/active_node/validations.rb +8 -2
  27. data/lib/neo4j/active_rel/initialize.rb +1 -2
  28. data/lib/neo4j/active_rel/persistence.rb +21 -33
  29. data/lib/neo4j/active_rel/property.rb +4 -2
  30. data/lib/neo4j/active_rel/types.rb +20 -8
  31. data/lib/neo4j/config.rb +16 -6
  32. data/lib/neo4j/core/query.rb +2 -2
  33. data/lib/neo4j/errors.rb +10 -0
  34. data/lib/neo4j/migration.rb +57 -46
  35. data/lib/neo4j/paginated.rb +3 -1
  36. data/lib/neo4j/railtie.rb +26 -14
  37. data/lib/neo4j/shared.rb +7 -1
  38. data/lib/neo4j/shared/declared_property.rb +62 -0
  39. data/lib/neo4j/shared/declared_property_manager.rb +150 -0
  40. data/lib/neo4j/shared/persistence.rb +15 -8
  41. data/lib/neo4j/shared/property.rb +64 -49
  42. data/lib/neo4j/shared/rel_type_converters.rb +13 -12
  43. data/lib/neo4j/shared/serialized_properties.rb +0 -15
  44. data/lib/neo4j/shared/type_converters.rb +53 -47
  45. data/lib/neo4j/shared/typecaster.rb +49 -0
  46. data/lib/neo4j/version.rb +1 -1
  47. data/lib/rails/generators/neo4j/model/model_generator.rb +3 -3
  48. data/lib/rails/generators/neo4j_generator.rb +5 -12
  49. data/neo4j.gemspec +4 -3
  50. metadata +30 -11
  51. data/CHANGELOG +0 -545
@@ -1,29 +1,8 @@
1
- Maintainer:
2
- Andreas Ronge <andreas dot ronge at gmail dot com>
1
+ Maintainers:
2
+ Chris Grigg <subvertallchris @ GitHub>
3
+ Brian Underwood <cheerfulstoic @ GitHub>
3
4
 
4
- Contributors:
5
- * Dmytrii Nagirniak
6
- * Marcio Toshio
7
- * Kalyan Akella
8
- * Vivek Prahlad
9
- * Deepak N
10
- * Frédéric Vanclef
11
- * Pere Urbon
12
- * Joe Leaver
13
- * Junegunn Choi
14
- * Jay Adkisson
15
- * Stephan Hagemann
16
- * Bobby Calderwood
17
- * Ben Jackson
18
- * Dwight van Tuyl
19
- * Martin Kleppmann
20
- * Peter Neubauer
21
- * Jan-Felix Wittmann
22
- * Marius Mårnes Mathiesen
23
- * Bert Fitié
24
- * Jan Berkel
25
- * David Beckwith
26
- * Johny Ho
27
- * Carlo Cabanilla
28
- * Anders Janmyr
29
- * Nick Sieger
5
+ Creator:
6
+ Andreas Ronge <andreasronge @ GitHub>
7
+
8
+ See: https://github.com/neo4jrb/neo4j/graphs/contributors
data/Gemfile CHANGED
@@ -2,14 +2,19 @@ source 'http://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'neo4j-core', github: 'neo4jrb/neo4j-core', branch: 'master'
5
+ # gem 'neo4j-core', github: 'neo4jrb/neo4j-core', branch: 'master'
6
6
  # gem 'neo4j-core', path: '../neo4j-core'
7
7
 
8
+ # gem 'active_attr', github: 'neo4jrb/active_attr', branch: 'performance'
9
+ # gem 'active_attr', path: '../active_attr'
10
+
8
11
  group 'test' do
9
12
  gem 'coveralls', require: false
13
+ gem 'codecov', require: false
10
14
  gem 'simplecov', require: false
11
15
  gem 'simplecov-html', require: false
12
16
  gem 'rspec', '~> 2.0'
13
17
  gem 'its'
14
18
  gem 'test-unit'
19
+ gem 'overcommit'
15
20
  end
data/README.md CHANGED
@@ -1,8 +1,36 @@
1
1
  # Welcome to Neo4j.rb
2
+
3
+ ## Code Status
4
+
2
5
  [![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
6
 
7
+ ## Issues
8
+
9
+ [![Next Release](https://badge.waffle.io/neo4jrb/neo4j.png?label=Next%20Release&title=Next%20Release) ![In Progress](https://badge.waffle.io/neo4jrb/neo4j.png?label=In%20Progress&title=In%20Progress) ![In Master](https://badge.waffle.io/neo4jrb/neo4j.png?label=In%20Master&title=In%20Master)](https://waffle.io/neo4jrb/neo4j)
10
+
11
+ [![Post an issue](https://img.shields.io/badge/Bug%3F-Post%20an%20issue!-blue.svg)](https://waffle.io/neo4jrb/neo4j)
12
+
13
+ ## Get Support
14
+
15
+ [![StackOverflow](https://img.shields.io/badge/StackOverflow-Ask%20a%20question!-blue.svg)](http://stackoverflow.com/questions/ask?tags=neo4j.rb+neo4j+ruby) [![Gitter](https://img.shields.io/badge/Gitter-Join%20our%20chat!-blue.svg)](https://gitter.im/neo4jrb/neo4j?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Twitter](https://img.shields.io/badge/Twitter-Tweet%20with%20us!-blue.svg)](https://twitter.com/neo4jrb) [![Mailing list](https://img.shields.io/badge/Mailing%20list-Mail%20us!-blue.svg)](https://groups.google.com/forum/#!forum/neo4jrb)
16
+
17
+ # Introduction
18
+
4
19
  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
20
 
21
+ Neo4j is a transactional, open-source graph database. A graph database manages data in a connected data structure, capable of representing any kind of data in a very accessible way. Information is stored in nodes and relationships connecting them, both of which can have arbitrary properties. To learn more visit [What is a Graph Database?](http://neo4j.com/developer/graph-database/)
22
+
23
+ With this gem you not only do you get a convenient higher level wrapper around Neo4j, but you have access to a powerful high-level query building interface which lets you take advantage of the power of Neo4j like this:
24
+
25
+ ```ruby
26
+ # Break down the top countries where friends' favorite beers come from
27
+ person.friends.favorite_beers.country_of_origin(:country).
28
+ order('count(country) DESC').
29
+ pluck(:country, count: 'count(country)')
30
+ ```
31
+
32
+ It can be installed in your `Gemfile` with a simple `gem 'neo4j'`
33
+
6
34
  For a general overview see our website: http://neo4jrb.io/
7
35
 
8
36
  Winner of a 2014 Graphie for "Best Community Contribution" at Neo4j's [Graph Connect](http://graphconnect.com) conference!
@@ -10,6 +38,25 @@ Winner of a 2014 Graphie for "Best Community Contribution" at Neo4j's [Graph Con
10
38
 
11
39
  Neo4j.rb v4.1.0 was released in January of 2015. Its changes are outlined [here](https://github.com/neo4jrb/neo4j/wiki/Neo4j.rb-v4-Introduction) and in the [announcement message](http://neo4jrb.io/blog/2015/01/09/neo4j-rb_v4-1_released.html). It will take a little time before all documentation is updated to reflect the new release but unless otherwise noted, all 3.X documentation is totally valid for v4.
12
40
 
41
+ ## Neo4j version support
42
+
43
+ | **Neo4j Version** | v2.x | v3.x | v4.x |
44
+ |-------------------|------|------|------|
45
+ | 1.9.x | Yes | No | No |
46
+ | 2.0.x | No | Yes | No |
47
+ | 2.1.x | No | Yes | Yes |
48
+ | 2.2.x | No | No | Yes |
49
+
50
+ ## Neo4j feature support
51
+
52
+ | **Neo4j Feature** | v2.x | v3.x | v4.x |
53
+ |----------------------------|--------|------|------|
54
+ | Auth | No | No | Yes |
55
+ | Remote Cypher | Yes | Yes | Yes |
56
+ | Transactions | Yes | Yes | Yes |
57
+ | High Availability | No | Yes | Yes |
58
+ | Embedded JVM support | Yes | Yes | Yes |
59
+
13
60
  ## Modern (3.x/4.X) Documentation
14
61
 
15
62
  * [Website](http://neo4jrb.io/) (for an introduction)
@@ -20,23 +67,22 @@ Neo4j.rb v4.1.0 was released in January of 2015. Its changes are outlined [here]
20
67
  * [README](https://github.com/neo4jrb/neo4j/tree/2.x)
21
68
  * [Wiki](https://github.com/neo4jrb/neo4j/wiki/Neo4j%3A%3ARails-Introduction)
22
69
 
23
- ## Support
24
-
25
- * Open an [issue](https://github.com/neo4jrb/neo4j/issues), post to [Stack Overflow](http://stackoverflow.com/questions/tagged/neo4j+ruby), or contact one of the developers.
26
- * [Neo4j.rb mailing list](https://groups.google.com/forum/#!forum/neo4jrb)
27
- * Consulting support? Ask any of the developers, info below.
28
-
29
70
  ## Developers
30
71
 
72
+ ### Original Author
73
+
31
74
  * [Andreas Ronge](https://github.com/andreasronge)
75
+
76
+ ### Current Maintainers
77
+
32
78
  * [Brian Underwood](https://github.com/cheerfulstoic)
33
79
  * [Chris Grigg](https://github.com/subvertallchris)
34
80
 
81
+ * Consulting support? Contact [Chris](http://subvertallmedia.com/) and/or [Brian](http://www.brian-underwood.codes/)
35
82
 
36
83
  ## Contributing
37
84
 
38
- Pull request with high test coverage and good [code climate](https://codeclimate.com/github/neo4jrb/neo4j) values will be accepted faster.
39
-
85
+ Always welcome! Please review the [guidelines for contributing](CONTRIBUTING.md) to this repository.
40
86
 
41
87
  ## License
42
88
 
@@ -16,6 +16,7 @@ require 'active_support/concern'
16
16
  require 'active_support/core_ext/class/attribute.rb'
17
17
 
18
18
  require 'active_attr'
19
+ require 'neo4j/errors'
19
20
  require 'neo4j/config'
20
21
  require 'neo4j/wrapper'
21
22
  require 'neo4j/active_rel/rel_wrapper'
@@ -26,11 +27,14 @@ require 'neo4j/type_converters'
26
27
  require 'neo4j/paginated'
27
28
 
28
29
  require 'neo4j/shared/callbacks'
30
+ require 'neo4j/shared/declared_property'
31
+ require 'neo4j/shared/declared_property_manager'
29
32
  require 'neo4j/shared/property'
30
33
  require 'neo4j/shared/persistence'
31
34
  require 'neo4j/shared/validations'
32
35
  require 'neo4j/shared/identity'
33
36
  require 'neo4j/shared/serialized_properties'
37
+ require 'neo4j/shared/typecaster'
34
38
  require 'neo4j/shared'
35
39
 
36
40
  require 'neo4j/active_rel/callbacks'
@@ -50,6 +54,7 @@ require 'neo4j/active_node/query_methods'
50
54
  require 'neo4j/active_node/query/query_proxy_methods'
51
55
  require 'neo4j/active_node/query/query_proxy_enumerable'
52
56
  require 'neo4j/active_node/query/query_proxy_find_in_batches'
57
+ require 'neo4j/active_node/query/query_proxy_link'
53
58
  require 'neo4j/active_node/labels'
54
59
  require 'neo4j/active_node/id_property'
55
60
  require 'neo4j/active_node/callbacks'
@@ -51,6 +51,7 @@ module Neo4j
51
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
55
  super
55
56
  end
56
57
 
@@ -2,26 +2,44 @@ module Neo4j
2
2
  module ActiveNode
3
3
  module Dependent
4
4
  module AssociationMethods
5
+ def validate_dependent(value)
6
+ fail ArgumentError, "Invalid dependent value: #{value.inspect}" if not valid_dependent_value?(value)
7
+ end
8
+
5
9
  def add_destroy_callbacks(model)
6
10
  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
11
+
12
+ model.before_destroy(&method("dependent_#{dependent}_callback"))
13
+ rescue NameError
14
+ raise "Unknown dependent option #{dependent}"
15
+ end
16
+
17
+ private
18
+
19
+ def valid_dependent_value?(value)
20
+ return true if value.nil?
21
+
22
+ self.respond_to?("dependent_#{value}_callback", true)
23
+ end
24
+
25
+ # Callback methods
26
+ def dependent_delete_callback(object)
27
+ object.association_query_proxy(name).delete_all
28
+ end
29
+
30
+ def dependent_delete_orphans_callback(object)
31
+ object.as(:self).unique_nodes(self, :self, :n, :other_rel).query.delete(:n, :other_rel).exec
32
+ end
33
+
34
+ def dependent_destroy_callback(object)
35
+ object.association_query_proxy(name).each_for_destruction(object, &:destroy)
36
+ end
37
+
38
+ def dependent_destroy_orphans_callback(object)
39
+ object.as(:self).unique_nodes(self, :self, :n, :other_rel).each_for_destruction(object, &:destroy)
24
40
  end
41
+
42
+ # End callback methods
25
43
  end
26
44
  end
27
45
  end
@@ -5,13 +5,15 @@ module Neo4j
5
5
  module QueryProxyMethods
6
6
  # Used as part of `dependent: :destroy` and may not have any utility otherwise.
7
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
8
+ # @param [#dependent_children] source_object The node that called this method. Typically, we would use QueryProxy's `source_object` method
9
9
  # but this is not always available, so we require it explicitly.
10
10
  def each_for_destruction(owning_node)
11
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)
12
+ objects = pluck(identity).compact.reject do |obj|
13
+ target.dependent_children.include?(obj)
14
+ end
15
+
16
+ objects.each do |obj|
15
17
  obj.called_by = target
16
18
  target.dependent_children << obj
17
19
  yield obj
@@ -25,22 +27,22 @@ module Neo4j
25
27
  # @param [String, Symbol] other_rel The identifier to use for the relationship in the optional match.
26
28
  # @return [Neo4j::ActiveNode::Query::QueryProxy]
27
29
  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
30
+ fail 'Only supported by in QueryProxy chains started by an instance' unless source_object
31
+
32
+ unique_nodes_query(association, self_identifer, other_node, other_rel)
33
+ .proxy_as(association.target_class, other_node)
34
+ end
35
+
36
+ private
40
37
 
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}())")
38
+ def unique_nodes_query(association, self_identifer, other_node, other_rel)
39
+ query.with(identity).proxy_as_optional(source_object.class, self_identifer)
40
+ .send(association.name, other_node, other_rel)
41
+ .query
42
+ .with(other_node)
43
+ .match("()#{association.arrow_cypher}(#{other_node})")
44
+ .with(other_node, count: 'count(*)')
45
+ .where('count = 1')
44
46
  end
45
47
  end
46
48
  end
@@ -4,54 +4,138 @@ module Neo4j::ActiveNode
4
4
 
5
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_obj
10
- end
7
+ # Return this object from associations
8
+ # It uses a QueryProxy to get results
9
+ # But also caches results and can have results cached on it
10
+ class AssociationProxy
11
+ def initialize(query_proxy, cached_result = nil)
12
+ @query_proxy = query_proxy
13
+ cache_result(cached_result)
11
14
 
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
15
+ # Represents the thing which can be enumerated
16
+ # default to @query_proxy, but will be set to
17
+ # @cached_result if that is set
18
+ @enumerable = @query_proxy
19
+ end
17
20
 
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
21
+ # States:
22
+ # Default
23
+ def inspect
24
+ if @cached_result
25
+ @cached_result.inspect
26
+ else
27
+ "<AssociationProxy @query_proxy=#{@query_proxy.inspect}>"
28
+ end
29
+ end
30
+
31
+ extend Forwardable
32
+ %w(include? empty? count find ==).each do |delegated_method|
33
+ def_delegator :@enumerable, delegated_method
34
+ end
35
+
36
+ include Enumerable
37
+
38
+ def each(&block)
39
+ result.each(&block)
40
+ end
41
+
42
+ def result
43
+ return @cached_result if @cached_result
44
+
45
+ cache_query_proxy_result
46
+
47
+ @cached_result
48
+ end
49
+
50
+ def cache_result(result)
51
+ @cached_result = result
52
+ @enumerable = (@cached_result || @query_proxy)
53
+ end
54
+
55
+ def cache_query_proxy_result
56
+ @query_proxy.to_a.tap do |result|
57
+ result.each do |object|
58
+ object.instance_variable_set('@association_proxy', self)
59
+ end
60
+ cache_result(result)
61
+ end
62
+ end
63
+
64
+ def clear_cache_result
65
+ cache_result(nil)
66
+ end
67
+
68
+ def cached?
69
+ !!@cached_result
70
+ end
71
+
72
+ QUERY_PROXY_METHODS = [:<<, :delete]
73
+ CACHED_RESULT_METHODS = []
74
+
75
+ def method_missing(method_name, *args, &block)
76
+ target = target_for_missing_method(method_name)
77
+
78
+ cache_query_proxy_result if !cached? && !target.is_a?(Neo4j::ActiveNode::Query::QueryProxy)
79
+
80
+ clear_cache_result if target.is_a?(Neo4j::ActiveNode::Query::QueryProxy)
81
+
82
+ return if target.nil?
83
+
84
+ target.public_send(method_name, *args, &block)
85
+ end
86
+
87
+ def with_associations(*spec)
88
+ return_object_clause = '[' + spec.map { |n| "collect(#{n})" }.join(',') + ']'
89
+ query_from_association_spec(spec).pluck(:previous, return_object_clause).map do |record, eager_data|
90
+ eager_data.each_with_index do |eager_records, index|
91
+ record.send(spec[index]).cache_result(eager_records)
92
+ end
93
+
94
+ record
95
+ end
96
+ end
97
+
98
+ private
28
99
 
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]
100
+ def query_from_association_spec(spec)
101
+ spec.inject(@query_proxy.query_as(:previous).return(:previous)) do |query, association_name|
102
+ association = @query_proxy.model.associations[association_name]
103
+ query.optional_match("previous#{association.arrow_cypher}#{association_name}")
104
+ end
105
+ end
106
+
107
+ def target_for_missing_method(method_name)
108
+ case method_name
109
+ when *QUERY_PROXY_METHODS
110
+ @query_proxy
111
+ when *CACHED_RESULT_METHODS
112
+ @cached_result
113
+ else
114
+ if @cached_result && @cached_result.respond_to?(method_name)
115
+ @cached_result
116
+ elsif @query_proxy.respond_to?(method_name)
117
+ @query_proxy
118
+ end
119
+ end
120
+ end
32
121
  end
33
122
 
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
123
+ # Returns the current AssociationProxy cache for the association cache. It is in the format
124
+ # { :association_name => AssociationProxy}
125
+ # This is so that we
126
+ # * don't need to re-build the QueryProxy objects
127
+ # * also because the QueryProxy object caches it's results
128
+ # * so we don't need to query again
129
+ # * so that we can cache results from association calls or eager loading
130
+ def association_proxy_cache
131
+ @association_proxy_cache ||= {}
51
132
  end
52
133
 
53
- def association_reflection(association_obj)
54
- self.class.reflect_on_association(association_obj.name)
134
+ def association_proxy_cache_fetch(key)
135
+ association_proxy_cache.fetch(key) do
136
+ value = yield
137
+ association_proxy_cache[key] = value
138
+ end
55
139
  end
56
140
 
57
141
  # Uses the cypher generated by a QueryProxy object, complete with params, to generate a basic non-cryptographic hash
@@ -62,6 +146,54 @@ module Neo4j::ActiveNode
62
146
  cypher_string.hash.abs
63
147
  end
64
148
 
149
+ def association_query_proxy(name, options = {})
150
+ self.class.send(:association_query_proxy, name, {start_object: self}.merge(options))
151
+ end
152
+
153
+ def association_proxy(name, options = {})
154
+ name = name.to_sym
155
+ hash = [name, options.values_at(:node, :rel)].hash
156
+
157
+ association_proxy_cache_fetch(hash) do
158
+ if previous_association_proxy = self.instance_variable_get('@association_proxy')
159
+ result_by_previous_id = previous_association_proxy_results_by_previous_id(previous_association_proxy, name)
160
+
161
+ previous_association_proxy.result.inject(nil) do |proxy_to_return, object|
162
+ proxy = fresh_association_proxy(name, options, result_by_previous_id[object.neo_id])
163
+
164
+ object.association_proxy_cache[hash] = proxy
165
+
166
+ (self == object ? proxy : proxy_to_return)
167
+ end
168
+ else
169
+ fresh_association_proxy(name, options)
170
+ end
171
+ end
172
+ end
173
+
174
+ private
175
+
176
+ def fresh_association_proxy(name, options = {}, cached_result = nil)
177
+ AssociationProxy.new(association_query_proxy(name, options), cached_result)
178
+ end
179
+
180
+ def previous_association_proxy_results_by_previous_id(association_proxy, association_name)
181
+ query_proxy = self.class.as(:previous).where(neo_id: association_proxy.result.map(&:neo_id))
182
+ query_proxy = self.class.send(:association_query_proxy, association_name, previous_query_proxy: query_proxy, node: :next)
183
+
184
+ Hash[*query_proxy.pluck('ID(previous)', 'collect(next)').flatten(1)]
185
+ end
186
+
187
+ def handle_non_persisted_node(other_node)
188
+ return unless Neo4j::Config[:autosave_on_assignment]
189
+ other_node.try(:save)
190
+ save
191
+ end
192
+
193
+ def validate_persisted_for_association!
194
+ fail(Neo4j::ActiveNode::HasN::NonPersistedNodeError, 'Unable to create relationship with non-persisted nodes') unless self._persisted_obj
195
+ end
196
+
65
197
  module ClassMethods
66
198
  # :nocov:
67
199
  # rubocop:disable Style/PredicateName
@@ -78,119 +210,232 @@ module Neo4j::ActiveNode
78
210
  end
79
211
 
80
212
  def associations
81
- @associations || {}
213
+ @associations ||= {}
214
+ end
215
+
216
+ def associations_keys
217
+ @associations_keys ||= associations.keys
82
218
  end
83
219
 
84
220
  # make sure the inherited classes inherit the <tt>_decl_rels</tt> hash
85
221
  def inherited(klass)
86
222
  klass.instance_variable_set(:@associations, associations.clone)
223
+ @associations_keys = klass.associations_keys.clone
87
224
  super
88
225
  end
89
226
 
90
- # rubocop:disable Style/PredicateName
91
- def has_many(direction, name, options = {})
227
+ # For defining an "has many" association on a model. This defines a set of methods on
228
+ # your model instances. For instance, if you define the association on a Person model:
229
+ #
230
+ # has_many :out, :vehicles, type: :has_vehicle
231
+ #
232
+ # This would define the following methods:
233
+ #
234
+ # **#vehicles**
235
+ # Returns a QueryProxy object. This is an Enumerable object and thus can be iterated
236
+ # over. It also has the ability to accept class-level methods from the Vehicle model
237
+ # (including calls to association methods)
238
+ #
239
+ # **#vehicles=**
240
+ # Takes an array of Vehicle objects and replaces all current ``:HAS_VEHICLE`` relationships
241
+ # with new relationships refering to the specified objects
242
+ #
243
+ # **.vehicles**
244
+ # Returns a QueryProxy object. This would represent all ``Vehicle`` objects associated with
245
+ # either all ``Person`` nodes (if ``Person.vehicles`` is called), or all ``Vehicle`` objects
246
+ # associated with the ``Person`` nodes thus far represented in the QueryProxy chain.
247
+ # For example:
248
+ # ``company.people.where(age: 40).vehicles``
249
+ #
250
+ # Arguments:
251
+ # **direction:**
252
+ # **Available values:** ``:in``, ``:out``, or ``:both``.
253
+ #
254
+ # Refers to the relative to the model on which the association is being defined.
255
+ #
256
+ # Example:
257
+ # ``Person.has_many :out, :posts, type: :wrote``
258
+ #
259
+ # means that a `WROTE` relationship goes from a `Person` node to a `Post` node
260
+ #
261
+ # **name:**
262
+ # The name of the association. The affects the methods which are created (see above).
263
+ # The name is also used to form default assumptions about the model which is being referred to
264
+ #
265
+ # Example:
266
+ # ``Person.has_many :out, :posts, type: :wrote``
267
+ #
268
+ # will assume a `model_class` option of ``'Post'`` unless otherwise specified
269
+ #
270
+ # **options:** A ``Hash`` of options. Allowed keys are:
271
+ # *type*: The Neo4j relationship type. This option is required unless either the
272
+ # `origin` or `rel_class` options are specified
273
+ #
274
+ # *origin*: The name of the association from another model which the `type` and `model_class`
275
+ # can be gathered.
276
+ #
277
+ # Example:
278
+ # ``Person.has_many :out, :posts, origin: :author`` (`model_class` of `Post` is assumed here)
279
+ #
280
+ # ``Post.has_one :in, :author, type: :has_author, model_class: 'Person'``
281
+ #
282
+ # *model_class*: The model class to which the association is referring. Can be either a
283
+ # model object ``include`` ing ``ActiveNode`` or a string (or an ``Array`` of same).
284
+ # **A string is recommended** to avoid load-time issues
285
+ #
286
+ # *rel_class*: The ``ActiveRel`` class to use for this association. Can be either a
287
+ # model object ``include`` ing ``ActiveRel`` or a string (or an ``Array`` of same).
288
+ # **A string is recommended** to avoid load-time issues
289
+ #
290
+ # *dependent*: Enables deletion cascading.
291
+ # **Available values:** ``:delete``, ``:delete_orphans``, ``:destroy``, ``:destroy_orphans``
292
+ # (note that the ``:destroy_orphans`` option is known to be "very metal". Caution advised)
293
+ #
294
+ def has_many(direction, name, options = {}) # rubocop:disable Style/PredicateName
295
+ validate_association_options!(name, options)
92
296
  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
297
+ build_association(:has_many, direction, name, options)
100
298
 
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
- optional: options[:optional],
110
- context: '#{self.name}##{name}',
111
- caller: self
112
- })
113
- end
299
+ define_has_many_methods(name)
300
+ end
114
301
 
115
- def #{name}=(other_nodes)
116
- #{name}(nil, :r).query_as(:n).delete(:r).exec
117
- clear_association_cache
118
- other_nodes.each { |node| #{name} << node }
119
- end
302
+ # For defining an "has one" association on a model. This defines a set of methods on
303
+ # your model instances. For instance, if you define the association on a Person model:
304
+ #
305
+ # has_one :out, :vehicle, type: :has_vehicle
306
+ #
307
+ # This would define the methods: ``#vehicle``, ``#vehicle=``, and ``.vehicle``.
308
+ #
309
+ # See :ref:`#has_many <Neo4j/ActiveNode/HasN/ClassMethods#has_many>` for anything
310
+ # not specified here
311
+ #
312
+ def has_one(direction, name, options = {}) # rubocop:disable Style/PredicateName
313
+ validate_association_options!(name, options)
314
+ name = name.to_sym
315
+ build_association(:has_one, direction, name, options)
120
316
 
121
- def #{name}_rels
122
- #{name}(nil, :r).pluck(:r)
123
- end}, __FILE__, __LINE__)
317
+ define_has_one_methods(name)
318
+ end
124
319
 
125
- instance_eval(%{
126
- def #{name}(node = nil, rel = nil, proxy_obj = nil, options = {})
127
- #{name}_query_proxy({node: node, rel: rel, proxy_obj: proxy_obj}.merge(options))
128
- end
320
+ private
129
321
 
130
- def #{name}_query_proxy(options = {})
131
- query_proxy = options[:proxy_obj] || Neo4j::ActiveNode::Query::QueryProxy.new(::#{self.name}, nil, {
132
- session: self.neo4j_session, query_proxy: nil, context: '#{self.name}' + '##{name}'
133
- })
134
- context = (query_proxy && query_proxy.context ? query_proxy.context : '#{self.name}') + '##{name}'
135
- Neo4j::ActiveNode::Query::QueryProxy.new(#{association.target_class_name_or_nil},
136
- associations[#{name.inspect}],
137
- {
138
- session: self.neo4j_session,
139
- query_proxy: query_proxy,
140
- node: options[:node],
141
- rel: options[:rel],
142
- context: context,
143
- optional: options[:optional] || query_proxy.optional?,
144
- caller: query_proxy.caller
145
- })
146
- end}, __FILE__, __LINE__)
147
- end
148
-
149
- def has_one(direction, name, options = {})
150
- name = name.to_sym
151
- association = build_association(:has_one, direction, name, options)
152
-
153
- module_eval(%{
154
- def #{name}=(other_node)
155
- raise(Neo4j::ActiveNode::HasN::NonPersistedNodeError, 'Unable to create relationship with non-persisted nodes') unless self._persisted_obj
156
- clear_association_cache
157
- #{name}_query_proxy(rel: :r).query_as(:n).delete(:r).exec
158
- #{name}_query_proxy << other_node
159
- end
322
+ VALID_ASSOCIATION_OPTION_KEYS = [:type, :origin, :model_class, :rel_class, :dependent, :before, :after]
160
323
 
161
- def #{name}_query_proxy(options = {})
162
- self.class.#{name}_query_proxy({start_object: self}.merge(options))
163
- end
324
+ def validate_association_options!(association_name, options)
325
+ type_keys = (options.keys & [:type, :origin, :rel_class])
326
+ message = case
327
+ when type_keys.size > 1
328
+ "Only one of 'type', 'origin', or 'rel_class' options are allowed for associations (#{self.class}##{association_name})"
329
+ when type_keys.empty?
330
+ "The 'type' option must be specified( even if it is `nil`) or `origin`/`rel_class` must be specified (#{self.class}##{association_name})"
331
+ when (unknown_keys = options.keys - VALID_ASSOCIATION_OPTION_KEYS).size > 0
332
+ "Unknown option(s) specified: #{unknown_keys.join(', ')} (#{self.class}##{association_name})"
333
+ end
164
334
 
165
- def #{name}_rel
166
- #{name}_query_proxy(rel: :r).pluck(:r).first
167
- end
335
+ fail ArgumentError, message if message
336
+ end
168
337
 
169
- def #{name}(node = nil, rel = nil)
170
- return nil unless self._persisted_obj
171
- result = #{name}_query_proxy(node: node, rel: rel, context: '#{self.name}##{name}')
172
- association = self.class.reflect_on_association(__method__)
173
- query_return = association_instance_get(result.to_cypher_with_params, association)
174
- query_return || association_instance_set(result.to_cypher_with_params, result.first, association)
175
- end}, __FILE__, __LINE__)
176
-
177
- instance_eval(%{
178
- def #{name}_query_proxy(options = {})
179
- Neo4j::ActiveNode::Query::QueryProxy.new(#{association.target_class_name_or_nil},
180
- associations[#{name.inspect}],
181
- {session: self.neo4j_session}.merge(options))
182
- end
338
+ def define_has_many_methods(name)
339
+ define_method(name) do |node = nil, rel = nil, options = {}|
340
+ return [].freeze unless self._persisted_obj
341
+
342
+ association_proxy(name, {node: node, rel: rel, source_object: self}.merge(options))
343
+ end
183
344
 
184
- def #{name}(node = nil, rel = nil, query_proxy = nil, options = {})
185
- context = (query_proxy && query_proxy.context ? query_proxy.context : '#{self.name}') + '##{name}'
186
- #{name}_query_proxy({query_proxy: query_proxy, node: node, rel: rel, context: context}.merge(options))
187
- end}, __FILE__, __LINE__)
345
+ define_has_many_setter(name)
346
+
347
+ define_class_method(name) do |node = nil, rel = nil, options = {}|
348
+ association_proxy(name, {node: node, rel: rel}.merge(options))
349
+ end
188
350
  end
189
- # rubocop:enable Style/PredicateName
190
351
 
191
- private
352
+ def define_has_many_setter(name)
353
+ define_method("#{name}=") do |other_nodes|
354
+ association_proxy_cache.clear
355
+
356
+ Neo4j::Transaction.run { association_proxy(name).replace_with(other_nodes) }
357
+ end
358
+ end
359
+
360
+ def define_has_one_methods(name)
361
+ define_method(name) do |node = nil, rel = nil|
362
+ return nil unless self._persisted_obj
363
+
364
+ association_proxy(name, node: node, rel: rel).first
365
+ end
366
+
367
+ define_has_one_setter(name)
368
+
369
+ define_class_method(name) do |node = nil, rel = nil, options = {}|
370
+ association_proxy(name, {node: node, rel: rel}.merge(options))
371
+ end
372
+ end
373
+
374
+ def define_has_one_setter(name)
375
+ define_method("#{name}=") do |other_node|
376
+ handle_non_persisted_node(other_node)
377
+ validate_persisted_for_association!
378
+ association_proxy_cache.clear # TODO: Should probably just clear for this association...
379
+
380
+ Neo4j::Transaction.run { association_proxy(name).replace_with(other_node) }
381
+ end
382
+ end
383
+
384
+ def define_class_method(*args, &block)
385
+ klass = class << self; self; end
386
+ klass.instance_eval do
387
+ define_method(*args, &block)
388
+ end
389
+ end
390
+
391
+ def association_query_proxy(name, options = {})
392
+ previous_query_proxy = options[:previous_query_proxy] || current_scope
393
+ query_proxy = previous_query_proxy || default_association_query_proxy(name)
394
+
395
+ Neo4j::ActiveNode::Query::QueryProxy.new(association_target_class(name),
396
+ associations[name],
397
+ {session: neo4j_session,
398
+ query_proxy: query_proxy,
399
+ context: "#{query_proxy.context || self.name}##{name}",
400
+ optional: query_proxy.optional?,
401
+ source_object: query_proxy.source_object}.merge(options)).tap do |query_proxy_result|
402
+ target_classes = association_target_classes(name)
403
+ return query_proxy_result.as_models(target_classes) if target_classes
404
+ end
405
+ end
406
+
407
+ def association_proxy(name, options = {})
408
+ query_proxy = association_query_proxy(name, options)
409
+
410
+ AssociationProxy.new(query_proxy)
411
+ end
412
+
413
+ def association_target_class(name)
414
+ target_classes_or_nil = associations[name].target_classes_or_nil
415
+
416
+ return if !target_classes_or_nil.is_a?(Array) || target_classes_or_nil.size != 1
417
+
418
+ target_classes_or_nil[0]
419
+ end
420
+
421
+ def association_target_classes(name)
422
+ target_classes_or_nil = associations[name].target_classes_or_nil
423
+
424
+ return if !target_classes_or_nil.is_a?(Array) || target_classes_or_nil.size <= 1
425
+
426
+ target_classes_or_nil
427
+ end
428
+
429
+ def default_association_query_proxy(name)
430
+ Neo4j::ActiveNode::Query::QueryProxy.new("::#{self.name}".constantize,
431
+ nil,
432
+ session: neo4j_session,
433
+ query_proxy: nil,
434
+ context: "#{self.name}##{name}")
435
+ end
192
436
 
193
437
  def build_association(macro, direction, name, options)
438
+ associations_keys << name
194
439
  Neo4j::ActiveNode::HasN::Association.new(macro, direction, name, options).tap do |association|
195
440
  @associations ||= {}
196
441
  @associations[name] = association