neo4j 4.1.5 → 5.0.0.rc.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +584 -0
- data/CONTRIBUTORS +7 -28
- data/Gemfile +6 -1
- data/README.md +54 -8
- data/lib/neo4j.rb +5 -0
- data/lib/neo4j/active_node.rb +1 -0
- data/lib/neo4j/active_node/dependent/association_methods.rb +35 -17
- data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +21 -19
- data/lib/neo4j/active_node/has_n.rb +377 -132
- data/lib/neo4j/active_node/has_n/association.rb +77 -38
- data/lib/neo4j/active_node/id_property.rb +46 -28
- data/lib/neo4j/active_node/initialize.rb +18 -6
- data/lib/neo4j/active_node/labels.rb +69 -35
- data/lib/neo4j/active_node/node_wrapper.rb +37 -30
- data/lib/neo4j/active_node/orm_adapter.rb +5 -4
- data/lib/neo4j/active_node/persistence.rb +53 -10
- data/lib/neo4j/active_node/property.rb +13 -5
- data/lib/neo4j/active_node/query.rb +11 -10
- data/lib/neo4j/active_node/query/query_proxy.rb +126 -153
- data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +15 -25
- data/lib/neo4j/active_node/query/query_proxy_link.rb +89 -0
- data/lib/neo4j/active_node/query/query_proxy_methods.rb +72 -19
- data/lib/neo4j/active_node/query_methods.rb +3 -1
- data/lib/neo4j/active_node/scope.rb +17 -21
- data/lib/neo4j/active_node/validations.rb +8 -2
- data/lib/neo4j/active_rel/initialize.rb +1 -2
- data/lib/neo4j/active_rel/persistence.rb +21 -33
- data/lib/neo4j/active_rel/property.rb +4 -2
- data/lib/neo4j/active_rel/types.rb +20 -8
- data/lib/neo4j/config.rb +16 -6
- data/lib/neo4j/core/query.rb +2 -2
- data/lib/neo4j/errors.rb +10 -0
- data/lib/neo4j/migration.rb +57 -46
- data/lib/neo4j/paginated.rb +3 -1
- data/lib/neo4j/railtie.rb +26 -14
- data/lib/neo4j/shared.rb +7 -1
- data/lib/neo4j/shared/declared_property.rb +62 -0
- data/lib/neo4j/shared/declared_property_manager.rb +150 -0
- data/lib/neo4j/shared/persistence.rb +15 -8
- data/lib/neo4j/shared/property.rb +64 -49
- data/lib/neo4j/shared/rel_type_converters.rb +13 -12
- data/lib/neo4j/shared/serialized_properties.rb +0 -15
- data/lib/neo4j/shared/type_converters.rb +53 -47
- data/lib/neo4j/shared/typecaster.rb +49 -0
- data/lib/neo4j/version.rb +1 -1
- data/lib/rails/generators/neo4j/model/model_generator.rb +3 -3
- data/lib/rails/generators/neo4j_generator.rb +5 -12
- data/neo4j.gemspec +4 -3
- metadata +30 -11
- data/CHANGELOG +0 -545
data/CONTRIBUTORS
CHANGED
@@ -1,29 +1,8 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
Maintainers:
|
2
|
+
Chris Grigg <subvertallchris @ GitHub>
|
3
|
+
Brian Underwood <cheerfulstoic @ GitHub>
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
[](http://travis-ci.org/neo4jrb/neo4j) [](https://coveralls.io/r/neo4jrb/neo4j?branch=master) [](https://codeclimate.com/github/neo4jrb/neo4j) [](https://www.pullreview.com/github/neo4jrb/neo4j/reviews/master)
|
3
6
|
|
7
|
+
## Issues
|
8
|
+
|
9
|
+
[  ](https://waffle.io/neo4jrb/neo4j)
|
10
|
+
|
11
|
+
[](https://waffle.io/neo4jrb/neo4j)
|
12
|
+
|
13
|
+
## Get Support
|
14
|
+
|
15
|
+
[](http://stackoverflow.com/questions/ask?tags=neo4j.rb+neo4j+ruby) [](https://gitter.im/neo4jrb/neo4j?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://twitter.com/neo4jrb) [](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
|
-
|
39
|
-
|
85
|
+
Always welcome! Please review the [guidelines for contributing](CONTRIBUTING.md) to this repository.
|
40
86
|
|
41
87
|
## License
|
42
88
|
|
data/lib/neo4j.rb
CHANGED
@@ -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'
|
data/lib/neo4j/active_node.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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]
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
42
|
-
|
43
|
-
.
|
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
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
#
|
35
|
-
#
|
36
|
-
# This is
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
54
|
-
|
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
|
-
#
|
91
|
-
|
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
|
-
|
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
|
-
|
102
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
122
|
-
|
123
|
-
end}, __FILE__, __LINE__)
|
317
|
+
define_has_one_methods(name)
|
318
|
+
end
|
124
319
|
|
125
|
-
|
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
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
166
|
-
|
167
|
-
end
|
335
|
+
fail ArgumentError, message if message
|
336
|
+
end
|
168
337
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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
|
-
|
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
|