neoid 0.0.51 → 0.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.
- data/CHANGELOG.md +35 -0
- data/README.md +148 -47
- data/TODO.md +0 -2
- data/lib/neoid.rb +90 -13
- data/lib/neoid/batch.rb +168 -0
- data/lib/neoid/config.rb +6 -0
- data/lib/neoid/middleware.rb +4 -2
- data/lib/neoid/model_additions.rb +60 -15
- data/lib/neoid/model_config.rb +2 -0
- data/lib/neoid/node.rb +103 -54
- data/lib/neoid/relationship.rb +88 -53
- data/lib/neoid/version.rb +1 -1
- data/spec/neoid/batch_spec.rb +170 -0
- data/spec/neoid/config_spec.rb +13 -0
- data/spec/neoid/model_additions_spec.rb +111 -17
- data/spec/neoid/search_spec.rb +0 -1
- data/spec/neoid_spec.rb +7 -1
- data/spec/spec_helper.rb +11 -9
- data/spec/support/models.rb +12 -3
- data/spec/support/schema.rb +4 -0
- metadata +8 -2
data/lib/neoid/batch.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
module Neoid
|
2
|
+
class Batch
|
3
|
+
def default_options=(value)
|
4
|
+
@default_options = value
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.default_options
|
8
|
+
@default_options ||= { batch_size: 200, individual_promises: true }
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.current_batch
|
12
|
+
Thread.current[:neoid_current_batch]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.current_batch=(batch)
|
16
|
+
Thread.current[:neoid_current_batch] = batch
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.reset_current_batch
|
20
|
+
Thread.current[:neoid_current_batch] = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(options={}, &block)
|
24
|
+
if options.respond_to?(:call) && !block
|
25
|
+
block = options
|
26
|
+
options = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
options.reverse_merge!(self.class.default_options)
|
30
|
+
|
31
|
+
@options = options
|
32
|
+
@block = block
|
33
|
+
end
|
34
|
+
|
35
|
+
def <<(command)
|
36
|
+
commands << command
|
37
|
+
|
38
|
+
if commands.length >= @options[:batch_size]
|
39
|
+
flush_batch
|
40
|
+
end
|
41
|
+
|
42
|
+
if @options[:individual_promises]
|
43
|
+
promise = SingleResultPromiseProxy.new(command)
|
44
|
+
thens << promise
|
45
|
+
promise
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def commands
|
50
|
+
@commands ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
def thens
|
54
|
+
@thens ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
def count
|
58
|
+
@commands ? @commands.count : 0
|
59
|
+
end
|
60
|
+
|
61
|
+
def results
|
62
|
+
@results ||= []
|
63
|
+
end
|
64
|
+
|
65
|
+
def run
|
66
|
+
self.class.current_batch = self
|
67
|
+
|
68
|
+
begin
|
69
|
+
@block.call(self)
|
70
|
+
ensure
|
71
|
+
self.class.reset_current_batch
|
72
|
+
end
|
73
|
+
|
74
|
+
Neoid.logger.info "Neoid batch (#{commands.length} commands)"
|
75
|
+
|
76
|
+
flush_batch
|
77
|
+
|
78
|
+
BatchPromiseProxy.new(results)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def flush_batch
|
83
|
+
return [] if commands.empty?
|
84
|
+
current_results = nil
|
85
|
+
|
86
|
+
# results = Neoid.db.batch(*commands).collect { |result| result['body'] }
|
87
|
+
|
88
|
+
benchmark = Benchmark.measure {
|
89
|
+
current_results = Neoid.db.batch(*commands).collect { |result| result['body'] }
|
90
|
+
}
|
91
|
+
Neoid.logger.info "Neoid batch (#{commands.length} commands) - #{benchmark}"
|
92
|
+
commands.clear
|
93
|
+
|
94
|
+
process_results(current_results)
|
95
|
+
|
96
|
+
thens.zip(current_results).each { |t, result| t.perform(result) }
|
97
|
+
|
98
|
+
thens.clear
|
99
|
+
|
100
|
+
results.concat current_results
|
101
|
+
end
|
102
|
+
|
103
|
+
def process_results(results)
|
104
|
+
results.map! do |result|
|
105
|
+
return result unless result.is_a?(Hash) && result['self'] && result['self'][%r[^https?://.*/(node|relationship)/\d+]]
|
106
|
+
|
107
|
+
type = case $1
|
108
|
+
when 'node' then Neoid::Node
|
109
|
+
when 'relationship' then Neoid::Relationship
|
110
|
+
else return result
|
111
|
+
end
|
112
|
+
|
113
|
+
type.from_hash(result)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# returned from a full batch, after it has been executed,
|
119
|
+
# so a `.then` can be chained after the batch do ... end
|
120
|
+
# it proxies all methods to the result
|
121
|
+
class BatchPromiseProxy
|
122
|
+
def initialize(*results)
|
123
|
+
@results = results
|
124
|
+
end
|
125
|
+
|
126
|
+
def method_missing(method, *args)
|
127
|
+
@results.send(method, *args)
|
128
|
+
end
|
129
|
+
|
130
|
+
def then
|
131
|
+
yield(*@results)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# returned from adding (<<) an item to a batch in a batch block:
|
136
|
+
# Neoid.batch { |batch| (batch << [:neography_command, param]).is_a?(SingleResultPromiseProxy) }
|
137
|
+
# so a `.then` can be chained:
|
138
|
+
# Neoid.batch { |batch| (batch << [:neography_command, param]).then { |result| puts result } }
|
139
|
+
# the `then` is called once the batch is flushed with the result of the single job in the batch
|
140
|
+
# it proxies all methods to the result, so in case it is returned (like in Neoid.execute_script_or_add_to_batch)
|
141
|
+
# the result of the method will be proxied to the result from the batch. See Node#neo_save
|
142
|
+
class SingleResultPromiseProxy
|
143
|
+
def initialize(*args)
|
144
|
+
end
|
145
|
+
|
146
|
+
attr_accessor :result
|
147
|
+
|
148
|
+
def result
|
149
|
+
raise "Accessed result too soon" unless @result
|
150
|
+
@result
|
151
|
+
end
|
152
|
+
|
153
|
+
def method_missing(method, *args)
|
154
|
+
result.send(method, *args)
|
155
|
+
end
|
156
|
+
|
157
|
+
def then(&block)
|
158
|
+
@then = block
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
def perform(result)
|
163
|
+
@result = result
|
164
|
+
return unless @then
|
165
|
+
@then.call(result)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
data/lib/neoid/config.rb
ADDED
data/lib/neoid/middleware.rb
CHANGED
@@ -5,10 +5,12 @@ module Ndoid
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def call(env)
|
8
|
-
|
8
|
+
old_enabled, Thread.current[:neoid_enabled] = Thread.current[:neoid_enabled], true
|
9
|
+
old_batch, Thread.current[:neoid_current_batch] = Thread.current[:neoid_current_batch], nil
|
9
10
|
@app.call(env)
|
10
11
|
ensure
|
11
|
-
Thread.current[:neoid_enabled] =
|
12
|
+
Thread.current[:neoid_enabled] = old_enabled
|
13
|
+
Thread.current[:neoid_current_batch] = old_batch
|
12
14
|
end
|
13
15
|
end
|
14
16
|
end
|
@@ -2,18 +2,26 @@ module Neoid
|
|
2
2
|
module ModelAdditions
|
3
3
|
module ClassMethods
|
4
4
|
attr_reader :neoid_config
|
5
|
-
attr_reader :neoid_options
|
6
5
|
|
7
6
|
def neoid_config
|
8
7
|
@neoid_config ||= Neoid::ModelConfig.new(self)
|
9
8
|
end
|
10
9
|
|
11
10
|
def neoidable(options = {})
|
11
|
+
# defaults
|
12
|
+
neoid_config.auto_index = true
|
13
|
+
neoid_config.enable_model_index = true # but the Neoid.enable_per_model_indexes is false by default. all models will be true only if the primary option is turned on.
|
14
|
+
|
12
15
|
yield(neoid_config) if block_given?
|
13
|
-
|
16
|
+
|
17
|
+
options.each do |key, value|
|
18
|
+
raise "Neoid #{self.name} model options: No such option #{key}" unless neoid_config.respond_to?("#{key}=")
|
19
|
+
neoid_config.send("#{key}=", value)
|
20
|
+
end
|
14
21
|
end
|
15
|
-
|
16
|
-
def
|
22
|
+
|
23
|
+
def neo_model_index_name
|
24
|
+
raise "Per Model index is not enabled. Nodes/Relationships are auto indexed with node_auto_index/relationship_auto_index" unless Neoid.config.enable_per_model_indexes || neoid_config.enable_model_index
|
17
25
|
@index_name ||= "#{self.name.tableize}_index"
|
18
26
|
end
|
19
27
|
end
|
@@ -37,9 +45,51 @@ module Neoid
|
|
37
45
|
end
|
38
46
|
end
|
39
47
|
|
40
|
-
def
|
48
|
+
def neo_save_after_model_save
|
49
|
+
return unless self.class.neoid_config.auto_index
|
50
|
+
neo_save
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def neo_save
|
55
|
+
@_neo_destroyed = false
|
56
|
+
@_neo_representation = _neo_save
|
57
|
+
end
|
58
|
+
|
59
|
+
alias neo_create neo_save
|
60
|
+
alias neo_update neo_save
|
61
|
+
|
62
|
+
def neo_destroy
|
63
|
+
return if @_neo_destroyed
|
64
|
+
@_neo_destroyed = true
|
65
|
+
|
66
|
+
neo_representation = neo_find_by_id
|
67
|
+
return unless neo_representation
|
68
|
+
|
69
|
+
begin
|
70
|
+
neo_representation.del
|
71
|
+
rescue Neography::NodeNotFoundException => e
|
72
|
+
Neoid::logger.info "Neoid#neo_destroy entity not found #{self.class.name} #{self.id}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Not working yet because Neography can't delete a node and all of its realtionships in a batch, and deleting a node with relationships results an error
|
76
|
+
# if Neoid::Batch.current_batch
|
77
|
+
# Neoid::Batch.current_batch << [self.class.delete_command, neo_representation.neo_id]
|
78
|
+
# else
|
79
|
+
# begin
|
80
|
+
# neo_representation.del
|
81
|
+
# rescue Neography::NodeNotFoundException => e
|
82
|
+
# Neoid::logger.info "Neoid#neo_destroy entity not found #{self.class.name} #{self.id}"
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
|
41
86
|
_reset_neo_representation
|
42
|
-
|
87
|
+
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
def neo_unique_id
|
92
|
+
"#{self.class.name}:#{self.id}"
|
43
93
|
end
|
44
94
|
|
45
95
|
protected
|
@@ -52,14 +102,7 @@ module Neoid
|
|
52
102
|
|
53
103
|
private
|
54
104
|
def _neo_representation
|
55
|
-
@_neo_representation ||=
|
56
|
-
results = neo_find_by_id
|
57
|
-
if results
|
58
|
-
neo_load(results.first['self'])
|
59
|
-
else
|
60
|
-
neo_create
|
61
|
-
end
|
62
|
-
end
|
105
|
+
@_neo_representation ||= neo_find_by_id || neo_save
|
63
106
|
end
|
64
107
|
|
65
108
|
def _reset_neo_representation
|
@@ -70,7 +113,9 @@ module Neoid
|
|
70
113
|
def self.included(receiver)
|
71
114
|
receiver.extend ClassMethods
|
72
115
|
receiver.send :include, InstanceMethods
|
73
|
-
|
116
|
+
|
117
|
+
receiver.after_save :neo_save_after_model_save
|
118
|
+
receiver.after_destroy :neo_destroy
|
74
119
|
end
|
75
120
|
end
|
76
121
|
end
|
data/lib/neoid/model_config.rb
CHANGED
data/lib/neoid/node.rb
CHANGED
@@ -1,15 +1,46 @@
|
|
1
1
|
module Neoid
|
2
2
|
module Node
|
3
|
+
def self.from_hash(hash)
|
4
|
+
node = Neography::Node.new(hash)
|
5
|
+
node.neo_server = Neoid.db
|
6
|
+
node
|
7
|
+
end
|
8
|
+
|
3
9
|
module ClassMethods
|
10
|
+
attr_accessor :neo_subref_node
|
11
|
+
|
4
12
|
def neo_subref_rel_type
|
5
13
|
@_neo_subref_rel_type ||= "#{self.name.tableize}_subref"
|
6
14
|
end
|
15
|
+
|
7
16
|
def neo_subref_node_rel_type
|
8
17
|
@_neo_subref_node_rel_type ||= self.name.tableize
|
9
18
|
end
|
10
|
-
|
19
|
+
|
20
|
+
def delete_command
|
21
|
+
:delete_node
|
22
|
+
end
|
23
|
+
|
24
|
+
def neo_model_index
|
25
|
+
return nil unless Neoid.config.enable_per_model_indexes
|
26
|
+
|
27
|
+
Neoid::logger.info "Node#neo_model_index #{neo_subref_rel_type}"
|
28
|
+
|
29
|
+
gremlin_query = <<-GREMLIN
|
30
|
+
g.createManualIndex(neo_model_index_name, Vertex.class);
|
31
|
+
GREMLIN
|
32
|
+
|
33
|
+
# Neoid.logger.info "subref query:\n#{gremlin_query}"
|
34
|
+
|
35
|
+
script_vars = { neo_model_index_name: neo_model_index_name }
|
36
|
+
|
37
|
+
Neoid.execute_script_or_add_to_batch gremlin_query, script_vars
|
38
|
+
end
|
39
|
+
|
11
40
|
def neo_subref_node
|
12
|
-
|
41
|
+
return nil unless Neoid.config.enable_subrefs
|
42
|
+
|
43
|
+
@neo_subref_node ||= begin
|
13
44
|
Neoid::logger.info "Node#neo_subref_node #{neo_subref_rel_type}"
|
14
45
|
|
15
46
|
gremlin_query = <<-GREMLIN
|
@@ -22,19 +53,26 @@ module Neoid
|
|
22
53
|
g.addEdge(g.v(0), subref, neo_subref_rel_type);
|
23
54
|
}
|
24
55
|
|
25
|
-
g.createManualIndex(neo_index_name, Vertex.class);
|
26
|
-
|
27
56
|
subref
|
28
57
|
GREMLIN
|
29
58
|
|
30
|
-
Neoid.logger.info "subref query:\n#{gremlin_query}"
|
59
|
+
# Neoid.logger.info "subref query:\n#{gremlin_query}"
|
31
60
|
|
32
|
-
|
61
|
+
script_vars = {
|
62
|
+
neo_subref_rel_type: neo_subref_rel_type,
|
63
|
+
name: self.name
|
64
|
+
}
|
33
65
|
|
34
|
-
|
66
|
+
Neoid.execute_script_or_add_to_batch gremlin_query, script_vars do |value|
|
67
|
+
Neoid::Node.from_hash(value)
|
68
|
+
end
|
35
69
|
end
|
36
70
|
end
|
37
71
|
|
72
|
+
def reset_neo_subref_node
|
73
|
+
@neo_subref_node = nil
|
74
|
+
end
|
75
|
+
|
38
76
|
def neo_search(term, options = {})
|
39
77
|
Neoid.search(self, term, options)
|
40
78
|
end
|
@@ -42,48 +80,72 @@ module Neoid
|
|
42
80
|
|
43
81
|
module InstanceMethods
|
44
82
|
def neo_find_by_id
|
45
|
-
Neoid::logger.info "Node#neo_find_by_id #{self.class.neo_index_name} #{self.id}"
|
46
|
-
Neoid.db.
|
47
|
-
|
48
|
-
nil
|
83
|
+
# Neoid::logger.info "Node#neo_find_by_id #{self.class.neo_index_name} #{self.id}"
|
84
|
+
node = Neoid.db.get_node_auto_index(Neoid::UNIQUE_ID_KEY, self.neo_unique_id)
|
85
|
+
node.present? ? Neoid::Node.from_hash(node[0]) : nil
|
49
86
|
end
|
50
87
|
|
51
|
-
def
|
88
|
+
def _neo_save
|
52
89
|
return unless Neoid.enabled?
|
53
|
-
|
54
|
-
data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id)
|
90
|
+
|
91
|
+
data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id, Neoid::UNIQUE_ID_KEY => self.neo_unique_id)
|
55
92
|
data.reject! { |k, v| v.nil? }
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
node
|
93
|
+
|
94
|
+
gremlin_query = <<-GREMLIN
|
95
|
+
idx = g.idx('node_auto_index');
|
96
|
+
q = null;
|
97
|
+
if (idx) q = idx.get(unique_id_key, unique_id);
|
98
|
+
|
99
|
+
node = null;
|
100
|
+
if (q && q.hasNext()) {
|
101
|
+
node = q.next();
|
102
|
+
node_data.each {
|
103
|
+
if (node.getProperty(it.key) != it.value) {
|
104
|
+
node.setProperty(it.key, it.value);
|
105
|
+
}
|
106
|
+
}
|
107
|
+
} else {
|
108
|
+
node = g.addVertex(node_data);
|
109
|
+
if (enable_subrefs) g.addEdge(g.v(subref_id), node, neo_subref_rel_type);
|
110
|
+
|
111
|
+
if (enable_model_index) g.idx(neo_model_index_name).put('ar_id', node.ar_id, node);
|
112
|
+
}
|
113
|
+
|
114
|
+
node
|
115
|
+
GREMLIN
|
116
|
+
|
117
|
+
script_vars = {
|
118
|
+
unique_id_key: Neoid::UNIQUE_ID_KEY,
|
119
|
+
node_data: data,
|
120
|
+
unique_id: self.neo_unique_id,
|
121
|
+
enable_subrefs: Neoid.config.enable_subrefs,
|
122
|
+
enable_model_index: Neoid.config.enable_per_model_indexes && self.class.neoid_config.enable_model_index
|
123
|
+
}
|
124
|
+
|
125
|
+
if Neoid.config.enable_subrefs
|
126
|
+
script_vars.update(
|
127
|
+
subref_id: self.class.neo_subref_node.neo_id,
|
128
|
+
neo_subref_rel_type: self.class.neo_subref_rel_type
|
65
129
|
)
|
66
|
-
rescue
|
67
|
-
# something must've happened to the cached subref node, reset and retry
|
68
|
-
@_neo_subref_node = nil
|
69
|
-
retires -= 1
|
70
|
-
retry if retires > 0
|
71
130
|
end
|
72
131
|
|
73
|
-
Neoid.
|
132
|
+
if Neoid.config.enable_per_model_indexes && self.class.neoid_config.enable_model_index
|
133
|
+
script_vars.update(
|
134
|
+
neo_model_index_name: self.class.neo_model_index_name
|
135
|
+
)
|
136
|
+
end
|
74
137
|
|
75
|
-
Neoid::logger.info "Node#
|
76
|
-
|
77
|
-
|
138
|
+
Neoid::logger.info "Node#neo_save #{self.class.name} #{self.id}"
|
139
|
+
|
140
|
+
node = Neoid.execute_script_or_add_to_batch(gremlin_query, script_vars) do |value|
|
141
|
+
@_neo_representation = Neoid::Node.from_hash(value)
|
142
|
+
end.then do |result|
|
143
|
+
neo_search_index
|
144
|
+
end
|
78
145
|
|
79
146
|
node
|
80
147
|
end
|
81
|
-
|
82
|
-
def neo_update
|
83
|
-
Neoid.db.set_node_properties(neo_node, self.to_neo)
|
84
|
-
neo_search_index
|
85
|
-
end
|
86
|
-
|
148
|
+
|
87
149
|
def neo_search_index
|
88
150
|
return if self.class.neoid_config.search_options.blank? || (
|
89
151
|
self.class.neoid_config.search_options.index_fields.blank? &&
|
@@ -113,29 +175,20 @@ module Neoid
|
|
113
175
|
end
|
114
176
|
end
|
115
177
|
|
116
|
-
def neo_load(
|
117
|
-
|
178
|
+
def neo_load(hash)
|
179
|
+
Neoid::Node.from_hash(hash)
|
118
180
|
end
|
119
181
|
|
120
182
|
def neo_node
|
121
183
|
_neo_representation
|
122
184
|
end
|
123
|
-
|
124
|
-
def neo_destroy
|
125
|
-
return unless neo_node
|
126
|
-
Neoid.db.remove_node_from_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, neo_node)
|
127
|
-
|
128
|
-
Neoid.db.remove_node_from_index(self.class.neo_index_name, neo_node)
|
129
|
-
neo_node.del
|
130
|
-
_reset_neo_representation
|
131
|
-
end
|
132
185
|
|
133
186
|
def neo_after_relationship_remove(relationship)
|
134
187
|
relationship.neo_destroy
|
135
188
|
end
|
136
189
|
|
137
190
|
def neo_before_relationship_through_remove(record)
|
138
|
-
rel_model, foreign_key_of_owner, foreign_key_of_record = Neoid.
|
191
|
+
rel_model, foreign_key_of_owner, foreign_key_of_record = Neoid::Relationship.meta_data[self.class.name.to_s][record.class.name.to_s]
|
139
192
|
rel_model = rel_model.to_s.constantize
|
140
193
|
@__neo_temp_rels ||= {}
|
141
194
|
@__neo_temp_rels[record] = rel_model.where(foreign_key_of_owner => self.id, foreign_key_of_record => record.id).first
|
@@ -152,10 +205,6 @@ module Neoid
|
|
152
205
|
receiver.extend ClassMethods
|
153
206
|
receiver.send :include, InstanceMethods
|
154
207
|
Neoid.node_models << receiver
|
155
|
-
|
156
|
-
receiver.after_create :neo_create
|
157
|
-
receiver.after_update :neo_update
|
158
|
-
receiver.after_destroy :neo_destroy
|
159
208
|
end
|
160
209
|
end
|
161
210
|
end
|