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.
- 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
|
[![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
|
-
|
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
|