neoid 0.0.51 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,6 @@
1
+ module Neoid
2
+ class Config
3
+ attr_accessor :enable_subrefs
4
+ attr_accessor :enable_per_model_indexes
5
+ end
6
+ end
@@ -5,10 +5,12 @@ module Ndoid
5
5
  end
6
6
 
7
7
  def call(env)
8
- old, Thread.current[:neoid_enabled] = Thread.current[:neoid_enabled], true
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] = old
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
- @neoid_options = options
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 neo_index_name
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 neo_resave
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
- neo_update
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 ||= begin
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
- Neoid.models << receiver
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
@@ -3,6 +3,8 @@ module Neoid
3
3
  attr_reader :properties
4
4
  attr_reader :search_options
5
5
  attr_reader :relationship_options
6
+ attr_accessor :enable_model_index
7
+ attr_accessor :auto_index
6
8
 
7
9
  def initialize(klass)
8
10
  @klass = klass
@@ -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
- @_neo_subref_node ||= begin
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
- node = Neography::Node.load(Neoid.db.execute_script(gremlin_query, neo_subref_rel_type: neo_subref_rel_type, name: self.name, neo_index_name: self.neo_index_name))
61
+ script_vars = {
62
+ neo_subref_rel_type: neo_subref_rel_type,
63
+ name: self.name
64
+ }
33
65
 
34
- node
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.get_node_index(self.class.neo_index_name, :ar_id, self.id)
47
- rescue Neography::NotFoundException
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 neo_create
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
- node = Neography::Node.create(data)
58
-
59
- retires = 2
60
- begin
61
- Neography::Relationship.create(
62
- self.class.neo_subref_node_rel_type,
63
- self.class.neo_subref_node,
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.db.add_node_to_index(self.class.neo_index_name, :ar_id, self.id, node)
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#neo_create #{self.class.name} #{self.id}, index = #{self.class.neo_index_name}"
76
-
77
- neo_search_index
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(node)
117
- Neography::Node.load(node)
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.config[:relationship_meta_data][self.class.name.to_s][record.class.name.to_s]
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