neo4j 1.2.1-java → 1.2.2-java
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +18 -6
- data/CONTRIBUTORS +1 -0
- data/Gemfile +4 -1
- data/lib/neo4j/node.rb +11 -3
- data/lib/neo4j/rails/callbacks.rb +9 -1
- data/lib/neo4j/rails/compositions.rb +256 -0
- data/lib/neo4j/rails/finders.rb +56 -0
- data/lib/neo4j/rails/model.rb +11 -1
- data/lib/neo4j/rails/observer.rb +161 -0
- data/lib/neo4j/rails/persistence.rb +23 -22
- data/lib/neo4j/rails/rails.rb +2 -0
- data/lib/neo4j/rails/railtie.rb +14 -1
- data/lib/neo4j/rails/rel_persistence.rb +12 -10
- data/lib/neo4j/rails/relationships/storage.rb +1 -1
- data/lib/neo4j/rails/transaction.rb +18 -14
- data/lib/neo4j/rails/tx_methods.rb +1 -1
- data/lib/neo4j/rails/validations.rb +5 -1
- data/lib/neo4j/version.rb +1 -1
- metadata +4 -2
data/CHANGELOG
CHANGED
@@ -1,10 +1,22 @@
|
|
1
|
-
|
1
|
+
== 1.2.2 / 2011-09-15
|
2
|
+
* Added compositions support for rails mode (Deepak N)
|
3
|
+
* Added support for nested transactions at the Rails model level (Vivek Prahlad)
|
4
|
+
* Fixing issue where save for invalid entities puts them into an inconsistent state (Vivek Prahlad)
|
5
|
+
* Fix for issue with save when validation fails (Vivek Prahlad)
|
6
|
+
* Fix for accepts_nested_attributes_for when the associated entities are created before a new node (Vivek Prahlad)
|
7
|
+
* Fix to allow has_one relationships to handle nil assignments in models (Vivek Prahlad)
|
8
|
+
* Observers support for neo4j rails model using active model (Deepak N)
|
9
|
+
* Override ActiveModel i18n_scope for neo4j (Deepak N)
|
10
|
+
* Added finders similar to active record and mongoid (Deepak N)
|
11
|
+
* Added find!, find_or_create_by and find_or_initialize_by methods, similar to active record finders (Deepak N)
|
12
|
+
|
13
|
+
== 1.2.1 / 2011-08-29
|
2
14
|
* Fixed failing RSpecs for devise-neo4j gem - column_names method on neo4j orm adapter throws NoMethodError (thanks Deepak N)
|
3
15
|
|
4
|
-
|
16
|
+
== 1.2.0 / 2011-08-16
|
5
17
|
* Upgrade to java library neo4j 1.4.1, see http://neo4j.rubyforge.org/guides/configuration.html
|
6
18
|
|
7
|
-
|
19
|
+
== 1.1.4 / 2011-08-10
|
8
20
|
* Fixed dependency to will_paginate, locked to 3.0.pre4 (newly released 3.0.0 does not work yet with neo4j.rb)
|
9
21
|
|
10
22
|
== 1.1.3 / 2011-08-09
|
@@ -12,12 +24,12 @@
|
|
12
24
|
* BUG: not able to create array properties on relationships (Pere Urbon)
|
13
25
|
* BUG: lucene did not work if starting up neo4j in read only mode (like rails console when the rails is already running)
|
14
26
|
|
15
|
-
|
27
|
+
== 1.1.2 / 2011-06-08
|
16
28
|
* Added configuration option 'enable_rules' to disable the _all relationships and custom rules [#176]
|
17
29
|
* Added a #node method on the Neo4j::Node and Neo4j::NodeMixin. Works like the #rel method but returns the node instead. [#174]
|
18
30
|
* Simplify creating relationship between two existing nodes [#175]
|
19
31
|
|
20
|
-
== 1.1.1 / 2011-05-26
|
32
|
+
== 1.1.1 / 2011-05-26
|
21
33
|
* Made neo4j compatible with rails 3.1.0.rc1 [#170]
|
22
34
|
* Fix for neo4j-devise [#171]
|
23
35
|
* BUG: Neo4j::GraphAlgo shortest path does raise exception if two nodes are not connected [#172]
|
@@ -45,7 +57,7 @@
|
|
45
57
|
* Lots of improvements of the API
|
46
58
|
* Better ActiveModel/Rails integration
|
47
59
|
|
48
|
-
|
60
|
+
== 0.4.4 / 2010-08-01
|
49
61
|
* Fixed bug on traversing when using the RelationshipMixin (#121)
|
50
62
|
* BatchInserter and JRuby 1.6 - Fix iteration error with trying to modify in-place hash
|
51
63
|
|
data/CONTRIBUTORS
CHANGED
data/Gemfile
CHANGED
@@ -7,7 +7,10 @@ group 'test' do
|
|
7
7
|
gem "rdoc", ">= 2.5.10"
|
8
8
|
gem "horo", ">= 1.0.2"
|
9
9
|
gem "rspec", ">= 2.0.0"
|
10
|
-
|
10
|
+
|
11
|
+
# use this version for rspec-rails-matchers which work with latest RSpec (Rspec => RSpec)
|
12
|
+
gem "rspec-rails-matchers", :git => 'git://github.com/afcapel/rspec-rails-matchers.git'
|
13
|
+
|
11
14
|
gem "test-unit"
|
12
15
|
gem 'rcov'
|
13
16
|
end
|
data/lib/neo4j/node.rb
CHANGED
@@ -49,17 +49,25 @@ module Neo4j
|
|
49
49
|
# The property operations give access to the key-value property pairs.
|
50
50
|
# Property keys are always strings. Valid property value types are the primitives(<tt>String</tt>, <tt>Fixnum</tt>, <tt>Float</tt>, <tt>Boolean</tt>), and arrays of those primitives.
|
51
51
|
#
|
52
|
-
# === Instance Methods
|
52
|
+
# === Instance Methods from Included Mixins
|
53
53
|
# * Neo4j::Property - methods that deal with properties
|
54
|
-
# * Neo4j::
|
54
|
+
# * Neo4j::Rels methods for accessing incoming and outgoing relationship and nodes of depth one.
|
55
55
|
# * Neo4j::Equal equality operators: <tt>eql?</tt>, <tt>equal</tt>, <tt>==</tt>
|
56
56
|
# * Neo4j::Index lucene index methods, like indexing a node
|
57
|
+
# * Neo4j::Traversal - provides an API for accessing outgoing and incoming nodes by traversing from this node of any depth.
|
57
58
|
#
|
58
59
|
# === Class Methods from Included Mixins
|
59
60
|
# * Neo4j::Index::ClassMethods lucene index class methods, like find
|
60
61
|
# * Neo4j::Load - methods for loading a node
|
61
62
|
#
|
62
|
-
#
|
63
|
+
# === Neo4j::Node#new and Wrappers
|
64
|
+
#
|
65
|
+
# The Neo4j::Node#new method does not return a new Ruby instance (!). Instead it will call the Neo4j Java API which will return a
|
66
|
+
# *org.neo4j.kernel.impl.core.NodeProxy* object. This java object includes those mixins, see above. The #class method on the java object
|
67
|
+
# returns Neo4j::Node in order to make it feel like an ordnary Ruby object.
|
68
|
+
#
|
69
|
+
# If you want to map your own class to a neo4j node you can use the Neo4j::NodeMixin or the Neo4j::Rails::Model.
|
70
|
+
# The Neo4j::NodeMixin and Neo4j::Rails::Model wraps the Neo4j::Node object. The raw java node/Neo4j::Node object can be access with the Neo4j::NodeMixin#java_node method.
|
63
71
|
#
|
64
72
|
class Node
|
65
73
|
extend Neo4j::Index::ClassMethods
|
@@ -2,7 +2,15 @@ module Neo4j
|
|
2
2
|
module Rails
|
3
3
|
module Callbacks #:nodoc:
|
4
4
|
extend ActiveSupport::Concern
|
5
|
-
|
5
|
+
|
6
|
+
CALLBACKS = [
|
7
|
+
:before_validation, :after_validation,
|
8
|
+
:before_create, :around_create, :after_create,
|
9
|
+
:before_destroy, :around_destroy, :after_destroy,
|
10
|
+
:before_save, :around_save, :after_save,
|
11
|
+
:before_update, :around_update, :after_update,
|
12
|
+
].freeze
|
13
|
+
|
6
14
|
included do
|
7
15
|
[:valid?, :create_or_update, :create, :update, :destroy].each do |method|
|
8
16
|
alias_method_chain method, :callbacks
|
@@ -0,0 +1,256 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Rails
|
3
|
+
# = Neo4j Rails Model Compositions
|
4
|
+
module Compositions # :nodoc:
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def clear_composition_cache #:nodoc:
|
8
|
+
composition_cache.clear if persisted?
|
9
|
+
end
|
10
|
+
|
11
|
+
def composition_cache
|
12
|
+
@composition_cache ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Neo4j Rails Model implements composition through a macro-like class method called +composed_of+
|
16
|
+
# for representing attributes as value objects. It expresses relationships like "Account [is]
|
17
|
+
# composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
|
18
|
+
# to the macro adds a description of how the value objects are created from the attributes of
|
19
|
+
# the entity object (when the entity is initialized either as a new object or from finding an
|
20
|
+
# existing object) and how it can be turned back into attributes (when the entity is saved to
|
21
|
+
# the database).
|
22
|
+
#
|
23
|
+
# class Customer < Neo4j::Rails::Model
|
24
|
+
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
|
25
|
+
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# The customer class now has the following methods to manipulate the value objects:
|
29
|
+
# * <tt>Customer#balance, Customer#balance=(money)</tt>
|
30
|
+
# * <tt>Customer#address, Customer#address=(address)</tt>
|
31
|
+
#
|
32
|
+
# These methods will operate with value objects like the ones described below:
|
33
|
+
#
|
34
|
+
# class Money
|
35
|
+
# include Comparable
|
36
|
+
# attr_reader :amount, :currency
|
37
|
+
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
|
38
|
+
#
|
39
|
+
# def initialize(amount, currency = "USD")
|
40
|
+
# @amount, @currency = amount, currency
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# def exchange_to(other_currency)
|
44
|
+
# exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
|
45
|
+
# Money.new(exchanged_amount, other_currency)
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# def ==(other_money)
|
49
|
+
# amount == other_money.amount && currency == other_money.currency
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# def <=>(other_money)
|
53
|
+
# if currency == other_money.currency
|
54
|
+
# amount <=> amount
|
55
|
+
# else
|
56
|
+
# amount <=> other_money.exchange_to(currency).amount
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# class Address
|
62
|
+
# attr_reader :street, :city
|
63
|
+
# def initialize(street, city)
|
64
|
+
# @street, @city = street, city
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# def close_to?(other_address)
|
68
|
+
# city == other_address.city
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# def ==(other_address)
|
72
|
+
# city == other_address.city && street == other_address.street
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# Now it's possible to access attributes from the database through the value objects instead. If
|
77
|
+
# you choose to name the composition the same as the attribute's name, it will be the only way to
|
78
|
+
# access that attribute. That's the case with our +balance+ attribute. You interact with the value
|
79
|
+
# objects just like you would any other attribute, though:
|
80
|
+
#
|
81
|
+
# customer.balance = Money.new(20) # sets the Money value object and the attribute
|
82
|
+
# customer.balance # => Money value object
|
83
|
+
# customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
|
84
|
+
# customer.balance > Money.new(10) # => true
|
85
|
+
# customer.balance == Money.new(20) # => true
|
86
|
+
# customer.balance < Money.new(5) # => false
|
87
|
+
#
|
88
|
+
# Value objects can also be composed of multiple attributes, such as the case of Address. The order
|
89
|
+
# of the mappings will determine the order of the parameters.
|
90
|
+
#
|
91
|
+
# customer.address_street = "Hyancintvej"
|
92
|
+
# customer.address_city = "Copenhagen"
|
93
|
+
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
|
94
|
+
# customer.address = Address.new("May Street", "Chicago")
|
95
|
+
# customer.address_street # => "May Street"
|
96
|
+
# customer.address_city # => "Chicago"
|
97
|
+
#
|
98
|
+
# == Writing value objects
|
99
|
+
#
|
100
|
+
# Value objects are immutable and interchangeable objects that represent a given value, such as
|
101
|
+
# a Money object representing $5. Two Money objects both representing $5 should be equal (through
|
102
|
+
# methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
|
103
|
+
# unlike entity objects where equality is determined by identity. An entity class such as Customer can
|
104
|
+
# easily have two different objects that both have an address on Hyancintvej. Entity identity is
|
105
|
+
# determined by object or relational unique identifiers (such as primary keys). Normal
|
106
|
+
# Neo4j::Rails::Model classes are entity objects.
|
107
|
+
#
|
108
|
+
# It's also important to treat the value objects as immutable. Don't allow the Money object to have
|
109
|
+
# its amount changed after creation. Create a new Money object with the new value instead. This
|
110
|
+
# is exemplified by the Money#exchange_to method that returns a new value object instead of changing
|
111
|
+
# its own values. Neo4j Rails Model won't persist value objects that have been changed through means
|
112
|
+
# other than the writer method.
|
113
|
+
#
|
114
|
+
# The immutable requirement is enforced by Neo4j Rails Model by freezing any object assigned as a value
|
115
|
+
# object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError.
|
116
|
+
#
|
117
|
+
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
|
118
|
+
# keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
|
119
|
+
#
|
120
|
+
# == Custom constructors and converters
|
121
|
+
#
|
122
|
+
# By default value objects are initialized by calling the <tt>new</tt> constructor of the value
|
123
|
+
# class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
|
124
|
+
# option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
|
125
|
+
# a custom constructor to be specified.
|
126
|
+
#
|
127
|
+
# When a new value is assigned to the value object the default assumption is that the new value
|
128
|
+
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
|
129
|
+
# converted to an instance of value class if necessary.
|
130
|
+
#
|
131
|
+
# For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
|
132
|
+
# should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
|
133
|
+
# for the value class is called +create+ and it expects a CIDR address string as a parameter. New
|
134
|
+
# values can be assigned to the value object using either another NetAddr::CIDR object, a string
|
135
|
+
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
|
136
|
+
# these requirements:
|
137
|
+
#
|
138
|
+
# class NetworkResource < Neo4j::Rails::Model
|
139
|
+
# composed_of :cidr,
|
140
|
+
# :class_name => 'NetAddr::CIDR',
|
141
|
+
# :mapping => [ %w(network_address network), %w(cidr_range bits) ],
|
142
|
+
# :allow_nil => true,
|
143
|
+
# :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
|
144
|
+
# :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
|
145
|
+
# end
|
146
|
+
#
|
147
|
+
# # This calls the :constructor
|
148
|
+
# network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
|
149
|
+
#
|
150
|
+
# # These assignments will both use the :converter
|
151
|
+
# network_resource.cidr = [ '192.168.2.1', 8 ]
|
152
|
+
# network_resource.cidr = '192.168.0.1/24'
|
153
|
+
#
|
154
|
+
# # This assignment won't use the :converter as the value is already an instance of the value class
|
155
|
+
# network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
|
156
|
+
#
|
157
|
+
# # Saving and then reloading will use the :constructor on reload
|
158
|
+
# network_resource.save
|
159
|
+
# network_resource.reload
|
160
|
+
#
|
161
|
+
# == [TBD] Finding records by a value object [TBD]
|
162
|
+
#
|
163
|
+
# Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
|
164
|
+
# by specifying an instance of the value object in the conditions hash. The following example
|
165
|
+
# finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
|
166
|
+
#
|
167
|
+
# Customer.where(:balance => Money.new(20, "USD")).all
|
168
|
+
#
|
169
|
+
module ClassMethods
|
170
|
+
# Adds reader and writer methods for manipulating a value object:
|
171
|
+
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
|
172
|
+
#
|
173
|
+
# Options are:
|
174
|
+
# * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
|
175
|
+
# can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
|
176
|
+
# to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
|
177
|
+
# with this option.
|
178
|
+
# * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
|
179
|
+
# object. Each mapping is represented as an array where the first item is the name of the
|
180
|
+
# entity attribute and the second item is the name the attribute in the value object. The
|
181
|
+
# order in which mappings are defined determine the order in which attributes are sent to the
|
182
|
+
# value class constructor.
|
183
|
+
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
|
184
|
+
# attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
|
185
|
+
# mapped attributes.
|
186
|
+
# This defaults to +false+.
|
187
|
+
# * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
|
188
|
+
# is called to initialize the value object. The constructor is passed all of the mapped attributes,
|
189
|
+
# in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
|
190
|
+
# to instantiate a <tt>:class_name</tt> object.
|
191
|
+
# The default is <tt>:new</tt>.
|
192
|
+
# * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
|
193
|
+
# or a Proc that is called when a new value is assigned to the value object. The converter is
|
194
|
+
# passed the single value that is used in the assignment and is only called if the new value is
|
195
|
+
# not an instance of <tt>:class_name</tt>.
|
196
|
+
#
|
197
|
+
# Option examples:
|
198
|
+
# composed_of :temperature, :mapping => %w(reading celsius)
|
199
|
+
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
|
200
|
+
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
|
201
|
+
# composed_of :gps_location
|
202
|
+
# composed_of :gps_location, :allow_nil => true
|
203
|
+
# composed_of :ip_address,
|
204
|
+
# :class_name => 'IPAddr',
|
205
|
+
# :mapping => %w(ip to_i),
|
206
|
+
# :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
|
207
|
+
# :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
|
208
|
+
#
|
209
|
+
def composed_of(part_id, options = {})
|
210
|
+
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
|
211
|
+
|
212
|
+
name = part_id.id2name
|
213
|
+
class_name = options[:class_name] || name.camelize
|
214
|
+
mapping = options[:mapping] || [ name, name ]
|
215
|
+
mapping = [ mapping ] unless mapping.first.is_a?(Array)
|
216
|
+
allow_nil = options[:allow_nil] || false
|
217
|
+
constructor = options[:constructor] || :new
|
218
|
+
converter = options[:converter]
|
219
|
+
|
220
|
+
reader_method(name, class_name, mapping, allow_nil, constructor)
|
221
|
+
writer_method(name, class_name, mapping, allow_nil, converter)
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
def reader_method(name, class_name, mapping, allow_nil, constructor)
|
226
|
+
define_method(name) do
|
227
|
+
if composition_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !self[pair.first].nil? })
|
228
|
+
attrs = mapping.collect { |pair| self[pair.first] }
|
229
|
+
object = constructor.respond_to?(:call) ? constructor.call(*attrs) : class_name.constantize.send(constructor, *attrs)
|
230
|
+
composition_cache[name] = object
|
231
|
+
end
|
232
|
+
composition_cache[name]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def writer_method(name, class_name, mapping, allow_nil, converter)
|
237
|
+
define_method("#{name}=") do |part|
|
238
|
+
if part.nil? && allow_nil
|
239
|
+
mapping.each { |pair| self[pair.first] = nil }
|
240
|
+
composition_cache[name] = nil
|
241
|
+
else
|
242
|
+
unless part.is_a?(class_name.constantize) || converter.nil?
|
243
|
+
part = converter.respond_to?(:call) ?
|
244
|
+
converter.call(part) :
|
245
|
+
class_name.constantize.send(converter, part)
|
246
|
+
end
|
247
|
+
|
248
|
+
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
|
249
|
+
composition_cache[name] = part.freeze
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
data/lib/neo4j/rails/finders.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
module Neo4j
|
2
2
|
module Rails
|
3
|
+
class RecordNotFoundError < StandardError
|
4
|
+
end
|
5
|
+
|
3
6
|
module Finders
|
4
7
|
extend ActiveSupport::Concern
|
5
8
|
|
@@ -81,6 +84,46 @@ module Neo4j
|
|
81
84
|
end
|
82
85
|
end
|
83
86
|
|
87
|
+
# Finds a model by given id or matching given criteria.
|
88
|
+
# When node not found, raises RecordNotFoundError
|
89
|
+
def find!(*args)
|
90
|
+
self.find(*args).tap do |result|
|
91
|
+
raise Neo4j::Rails::RecordNotFoundError if result.nil?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Find the first Node given the conditions, or creates a new node
|
96
|
+
# with the conditions that were supplied.
|
97
|
+
#
|
98
|
+
# @example Find or create the node.
|
99
|
+
# Person.find_or_create_by(:name => "test")
|
100
|
+
#
|
101
|
+
# @param [ Hash ] attrs The attributes to check.
|
102
|
+
#
|
103
|
+
# @return [ Node ] A matching or newly created node.
|
104
|
+
def find_or_create_by(attrs = {}, &block)
|
105
|
+
find_or(:create, attrs, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Similar to find_or_create_by,calls create! instead of create
|
109
|
+
# Raises RecordInvalidError if model is invalid.
|
110
|
+
def find_or_create_by!(attrs = {}, &block)
|
111
|
+
find_or(:create!, attrs, &block)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Find the first Node given the conditions, or initializes a new node
|
115
|
+
# with the conditions that were supplied.
|
116
|
+
#
|
117
|
+
# @example Find or initialize the node.
|
118
|
+
# Person.find_or_initialize_by(:name => "test")
|
119
|
+
#
|
120
|
+
# @param [ Hash ] attrs The attributes to check.
|
121
|
+
#
|
122
|
+
# @return [ Node ] A matching or newly initialized node.
|
123
|
+
def find_or_initialize_by(attrs = {}, &block)
|
124
|
+
find_or(:new, attrs, &block)
|
125
|
+
end
|
126
|
+
|
84
127
|
def all(*args)
|
85
128
|
if !conditions_in?(*args)
|
86
129
|
# use the _all rule to recover all the stored instances of this node
|
@@ -160,6 +203,19 @@ module Neo4j
|
|
160
203
|
Thread.current[:neo4j_lucene_connection] << hits
|
161
204
|
hits
|
162
205
|
end
|
206
|
+
|
207
|
+
# Find the first object or create/initialize it.
|
208
|
+
#
|
209
|
+
# @example Find or perform an action.
|
210
|
+
# Person.find_or(:create, :name => "Dev")
|
211
|
+
#
|
212
|
+
# @param [ Symbol ] method The method to invoke.
|
213
|
+
# @param [ Hash ] attrs The attributes to query or set.
|
214
|
+
#
|
215
|
+
# @return [ Node ] The first or new node.
|
216
|
+
def find_or(method, attrs = {}, &block)
|
217
|
+
first(:conditions => attrs) || send(method, attrs, &block)
|
218
|
+
end
|
163
219
|
end
|
164
220
|
end
|
165
221
|
end
|
data/lib/neo4j/rails/model.rb
CHANGED
@@ -29,6 +29,7 @@ module Neo4j
|
|
29
29
|
reset_attributes
|
30
30
|
clear_relationships
|
31
31
|
self.attributes = attributes if attributes.is_a?(Hash)
|
32
|
+
yield self if block_given?
|
32
33
|
end
|
33
34
|
|
34
35
|
def id
|
@@ -86,7 +87,7 @@ module Neo4j
|
|
86
87
|
def entity_load(id)
|
87
88
|
Neo4j::Node.load(id)
|
88
89
|
end
|
89
|
-
|
90
|
+
|
90
91
|
##
|
91
92
|
# Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling
|
92
93
|
# dates and times from the database. This is set to :local by default.
|
@@ -127,6 +128,13 @@ module Neo4j
|
|
127
128
|
|
128
129
|
end
|
129
130
|
end
|
131
|
+
|
132
|
+
# Set the i18n scope to overwrite ActiveModel.
|
133
|
+
#
|
134
|
+
# @return [ Symbol ] :neo4j
|
135
|
+
def i18n_scope
|
136
|
+
:neo4j
|
137
|
+
end
|
130
138
|
end
|
131
139
|
end
|
132
140
|
|
@@ -140,8 +148,10 @@ module Neo4j
|
|
140
148
|
include Timestamps # handle created_at, updated_at timestamp properties
|
141
149
|
include Validations # enable validations
|
142
150
|
include Callbacks # enable callbacks
|
151
|
+
include ActiveModel::Observing # enable observers
|
143
152
|
include Finders # ActiveRecord style find
|
144
153
|
include Relationships # for none persisted relationships
|
154
|
+
include Compositions
|
145
155
|
end
|
146
156
|
end
|
147
157
|
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Rails
|
3
|
+
# Observer classes respond to life cycle callbacks to implement trigger-like
|
4
|
+
# behavior outside the original class. This is a great way to reduce the
|
5
|
+
# clutter that normally comes when the model class is burdened with
|
6
|
+
# functionality that doesn't pertain to the core responsibility of the
|
7
|
+
# class. Neo4j's observers work similar to ActiveRecord's. Example:
|
8
|
+
#
|
9
|
+
# class CommentObserver < Neo4j::Rails::Observer
|
10
|
+
# def after_save(comment)
|
11
|
+
# Notifications.comment(
|
12
|
+
# "admin@do.com", "New comment was posted", comment
|
13
|
+
# ).deliver
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# This Observer sends an email when a Comment#save is finished.
|
18
|
+
#
|
19
|
+
# class ContactObserver < Neo4j::Rails::Observer
|
20
|
+
# def after_create(contact)
|
21
|
+
# contact.logger.info('New contact added!')
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def after_destroy(contact)
|
25
|
+
# contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# This Observer uses logger to log when specific callbacks are triggered.
|
30
|
+
#
|
31
|
+
# == Observing a class that can't be inferred
|
32
|
+
#
|
33
|
+
# Observers will by default be mapped to the class with which they share a
|
34
|
+
# name. So CommentObserver will be tied to observing Comment,
|
35
|
+
# ProductManagerObserver to ProductManager, and so on. If you want to
|
36
|
+
# name your observer differently than the class you're interested in
|
37
|
+
# observing, you can use the Observer.observe class method which takes
|
38
|
+
# either the concrete class (Product) or a symbol for that class (:product):
|
39
|
+
#
|
40
|
+
# class AuditObserver < Neo4j::Rails::Observer
|
41
|
+
# observe :account
|
42
|
+
#
|
43
|
+
# def after_update(account)
|
44
|
+
# AuditTrail.new(account, "UPDATED")
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# If the audit observer needs to watch more than one kind of object,
|
49
|
+
# this can be specified with multiple arguments:
|
50
|
+
#
|
51
|
+
# class AuditObserver < Neo4j::Rails::Observer
|
52
|
+
# observe :account, :balance
|
53
|
+
#
|
54
|
+
# def after_update(record)
|
55
|
+
# AuditTrail.new(record, "UPDATED")
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# The AuditObserver will now act on both updates to Account and Balance
|
60
|
+
# by treating them both as records.
|
61
|
+
#
|
62
|
+
# == Available callback methods
|
63
|
+
#
|
64
|
+
# * before_validation
|
65
|
+
# * after_validation
|
66
|
+
# * before_create
|
67
|
+
# * around_create
|
68
|
+
# * after_create
|
69
|
+
# * before_update
|
70
|
+
# * around_update
|
71
|
+
# * after_update
|
72
|
+
# * before_save
|
73
|
+
# * around_save
|
74
|
+
# * after_save
|
75
|
+
# * before_destroy
|
76
|
+
# * around_destroy
|
77
|
+
# * after_destroy
|
78
|
+
#
|
79
|
+
# == Storing Observers in Rails
|
80
|
+
#
|
81
|
+
# If you're using Neo4j within Rails, observer classes are usually stored
|
82
|
+
# in +app/models+ with the naming convention of +app/models/audit_observer.rb+.
|
83
|
+
#
|
84
|
+
# == Configuration
|
85
|
+
#
|
86
|
+
# In order to activate an observer, list it in the +config.neo4j.observers+
|
87
|
+
# configuration setting in your +config/application.rb+ file.
|
88
|
+
#
|
89
|
+
# config.neo4j.observers = :comment_observer, :signup_observer
|
90
|
+
#
|
91
|
+
# Observers will not be invoked unless you define them in your
|
92
|
+
# application configuration.
|
93
|
+
#
|
94
|
+
# == Loading
|
95
|
+
#
|
96
|
+
# Observers register themselves with the model class that they observe,
|
97
|
+
# since it is the class that notifies them of events when they occur.
|
98
|
+
# As a side-effect, when an observer is loaded, its corresponding model
|
99
|
+
# class is loaded.
|
100
|
+
#
|
101
|
+
# Observers are loaded after the application initializers, so that
|
102
|
+
# observed models can make use of extensions. If by any chance you are
|
103
|
+
# using observed models in the initialization, you can
|
104
|
+
# still load their observers by calling +ModelObserver.instance+ before.
|
105
|
+
# Observers are singletons and that call instantiates and registers them.
|
106
|
+
class Observer < ActiveModel::Observer
|
107
|
+
|
108
|
+
# Instantiate the new observer. Will add all child observers as well.
|
109
|
+
#
|
110
|
+
# @example Instantiate the observer.
|
111
|
+
# Neo4j::Rails::Observer.new
|
112
|
+
def initialize
|
113
|
+
super and observed_descendants.each { |klass| add_observer!(klass) }
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
# Get all the child observers.
|
119
|
+
#
|
120
|
+
# @example Get the children.
|
121
|
+
# observer.observed_descendants
|
122
|
+
#
|
123
|
+
# @return [ Array<Class> ] The children.
|
124
|
+
def observed_descendants
|
125
|
+
observed_classes.inject([]) { |all, klass| all += klass.descendants }
|
126
|
+
end
|
127
|
+
|
128
|
+
# Adds the specified observer to the class.
|
129
|
+
#
|
130
|
+
# @example Add the observer.
|
131
|
+
# observer.add_observer!(Document)
|
132
|
+
#
|
133
|
+
# @param [ Class ] klass The child observer to add.
|
134
|
+
def add_observer!(klass)
|
135
|
+
super and define_callbacks(klass)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Defines all the callbacks for each observer of the model.
|
139
|
+
#
|
140
|
+
# @example Define all the callbacks.
|
141
|
+
# observer.define_callbacks(Document)
|
142
|
+
#
|
143
|
+
# @param [ Class ] klass The model to define them on.
|
144
|
+
def define_callbacks(klass)
|
145
|
+
tap do |observer|
|
146
|
+
observer_name = observer.class.name.underscore.gsub('/', '__')
|
147
|
+
Neo4j::Rails::Callbacks::CALLBACKS.each do |callback|
|
148
|
+
next unless respond_to?(callback)
|
149
|
+
callback_meth = :"_notify_#{observer_name}_for_#{callback}"
|
150
|
+
unless klass.respond_to?(callback_meth)
|
151
|
+
klass.send(:define_method, callback_meth) do |&block|
|
152
|
+
observer.send(callback, self, &block)
|
153
|
+
end
|
154
|
+
klass.send(callback, callback_meth)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -73,6 +73,7 @@ module Neo4j
|
|
73
73
|
def reload(options = nil)
|
74
74
|
clear_changes
|
75
75
|
clear_relationships
|
76
|
+
clear_composition_cache
|
76
77
|
reset_attributes
|
77
78
|
unless reload_from_database
|
78
79
|
set_deleted_properties
|
@@ -196,10 +197,13 @@ module Neo4j
|
|
196
197
|
write_attribute(attribute, value) if changed_attributes.has_key?(attribute)
|
197
198
|
end
|
198
199
|
end
|
199
|
-
|
200
|
-
def
|
200
|
+
|
201
|
+
def _create_entity(rel_type, attr)
|
201
202
|
clazz = self.class._decl_rels[rel_type.to_sym].target_class
|
202
|
-
|
203
|
+
_add_relationship(rel_type, clazz.new(attr))
|
204
|
+
end
|
205
|
+
|
206
|
+
def _add_relationship(rel_type, node)
|
203
207
|
if respond_to?("#{rel_type}=")
|
204
208
|
send("#{rel_type}=", node)
|
205
209
|
elsif respond_to?("#{rel_type}")
|
@@ -220,32 +224,29 @@ module Neo4j
|
|
220
224
|
raise "oops #{rel_type}"
|
221
225
|
end
|
222
226
|
end
|
227
|
+
|
228
|
+
def _has_relationship(rel_type, id)
|
229
|
+
!_find_node(rel_type,id).nil?
|
230
|
+
end
|
223
231
|
|
224
232
|
def update_nested_attributes(rel_type, attr, options)
|
225
233
|
allow_destroy, reject_if = [options[:allow_destroy], options[:reject_if]] if options
|
226
|
-
|
227
|
-
if new?
|
228
|
-
# We are updating a node that was created with the 'new' method.
|
229
|
-
# The relationship will only be kept in the Value object.
|
230
|
-
_add_relationship(rel_type, attr) unless reject_if?(reject_if, attr) || (allow_destroy && attr[:_destroy] && attr[:_destroy] != '0')
|
231
|
-
else
|
232
|
-
# We have a node that was created with the #create method - has real Neo4j relationships
|
233
|
-
# does it exist ?
|
234
|
-
found = _find_node(rel_type, attr[:id])
|
235
|
-
|
234
|
+
begin
|
236
235
|
# Check if we want to destroy not found nodes (e.g. {..., :_destroy => '1' } ?
|
237
|
-
destroy = attr[:_destroy] && attr[:_destroy] != '0'
|
238
|
-
|
239
|
-
if
|
240
|
-
if
|
241
|
-
|
236
|
+
destroy = allow_destroy && attr[:_destroy] && attr[:_destroy] != '0'
|
237
|
+
found = Neo4j::Node.load(attr[:id])
|
238
|
+
if destroy
|
239
|
+
found.destroy if found
|
240
|
+
else
|
241
|
+
if not found
|
242
|
+
_create_entity(rel_type, attr) #Create new node from scratch
|
242
243
|
else
|
243
|
-
|
244
|
+
#Create relationship to existing node in case it doesn't exist already
|
245
|
+
_add_relationship(rel_type, found) if (not _has_relationship(rel_type,attr[:id]))
|
246
|
+
found.update_attributes(attr)
|
244
247
|
end
|
245
|
-
elsif !destroy && !reject_if?(reject_if, attr)
|
246
|
-
_add_relationship(rel_type, attr)
|
247
248
|
end
|
248
|
-
end
|
249
|
+
end unless reject_if?(reject_if, attr)
|
249
250
|
end
|
250
251
|
|
251
252
|
public
|
data/lib/neo4j/rails/rails.rb
CHANGED
@@ -9,6 +9,8 @@ require 'neo4j/rails/finders'
|
|
9
9
|
require 'neo4j/rails/mapping/property'
|
10
10
|
require 'neo4j/rails/validations'
|
11
11
|
require 'neo4j/rails/callbacks'
|
12
|
+
require 'neo4j/rails/observer'
|
13
|
+
require 'neo4j/rails/compositions'
|
12
14
|
require 'neo4j/rails/timestamps'
|
13
15
|
require 'neo4j/rails/serialization'
|
14
16
|
require 'neo4j/rails/attributes'
|
data/lib/neo4j/rails/railtie.rb
CHANGED
@@ -5,7 +5,7 @@ module Neo4j
|
|
5
5
|
initializer "neo4j.tx" do |app|
|
6
6
|
app.config.middleware.use Neo4j::Rails::LuceneConnectionCloser
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
# Add ActiveModel translations to the I18n load_path
|
10
10
|
initializer "i18n" do |app|
|
11
11
|
config.i18n.load_path += Dir[File.join(File.dirname(__FILE__), '..', '..', '..', 'config', 'locales', '*.{rb,yml}')]
|
@@ -16,5 +16,18 @@ module Neo4j
|
|
16
16
|
initializer "neo4j.start", :after => :load_config_initializers do |app|
|
17
17
|
Neo4j::Config.setup.merge!(app.config.neo4j.to_hash)
|
18
18
|
end
|
19
|
+
|
20
|
+
# Instantitate any registered observers after Rails initialization and
|
21
|
+
# instantiate them after being reloaded in the development environment
|
22
|
+
initializer "instantiate.observers" do
|
23
|
+
config.after_initialize do
|
24
|
+
::Neo4j::Rails::Model.observers = config.neo4j.observers || []
|
25
|
+
::Neo4j::Rails::Model.instantiate_observers
|
26
|
+
|
27
|
+
ActionDispatch::Callbacks.to_prepare do
|
28
|
+
::Neo4j::Rails::Model.instantiate_observers
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
19
32
|
end
|
20
33
|
end
|
@@ -149,16 +149,18 @@ module Neo4j
|
|
149
149
|
end
|
150
150
|
|
151
151
|
def create()
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
152
|
+
begin
|
153
|
+
# prevent calling create twice
|
154
|
+
@start_node.rm_outgoing_rel(type, self)
|
155
|
+
@end_node.rm_incoming_rel(type, self)
|
156
|
+
|
157
|
+
_persist_start_node
|
158
|
+
_persist_end_node
|
159
|
+
|
160
|
+
@_java_rel = Neo4j::Relationship.new(type, start_node, end_node)
|
161
|
+
init_on_create
|
162
|
+
clear_changes
|
163
|
+
end unless @end_node.nil?
|
162
164
|
true
|
163
165
|
end
|
164
166
|
|
@@ -141,7 +141,7 @@ module Neo4j
|
|
141
141
|
@incoming_rels.clear
|
142
142
|
|
143
143
|
out_rels.each do |rel|
|
144
|
-
rel.end_node.rm_incoming_rel(@rel_type.to_sym, rel)
|
144
|
+
rel.end_node.rm_incoming_rel(@rel_type.to_sym, rel) if rel.end_node
|
145
145
|
success = rel.persisted? || rel.save
|
146
146
|
# don't think this can happen - just in case, TODO
|
147
147
|
raise "Can't save outgoing #{rel}, validation errors ? #{rel.errors.inspect}" unless success
|
@@ -12,11 +12,6 @@ module Neo4j
|
|
12
12
|
# end
|
13
13
|
class Transaction
|
14
14
|
class << self
|
15
|
-
def new
|
16
|
-
finish if Thread.current[:neo4j_transaction]
|
17
|
-
Thread.current[:neo4j_transaction] = Neo4j::Transaction.new
|
18
|
-
end
|
19
|
-
|
20
15
|
def current
|
21
16
|
Thread.current[:neo4j_transaction]
|
22
17
|
end
|
@@ -50,16 +45,25 @@ module Neo4j
|
|
50
45
|
end
|
51
46
|
|
52
47
|
def run
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
48
|
+
if running?
|
49
|
+
yield self
|
50
|
+
else
|
51
|
+
begin
|
52
|
+
new
|
53
|
+
ret = yield self
|
54
|
+
rescue
|
55
|
+
fail
|
56
|
+
raise
|
57
|
+
ensure
|
58
|
+
finish
|
59
|
+
end
|
60
|
+
ret
|
61
61
|
end
|
62
|
-
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def new
|
66
|
+
Thread.current[:neo4j_transaction] = Neo4j::Transaction.new
|
63
67
|
end
|
64
68
|
end
|
65
69
|
end
|
@@ -6,7 +6,7 @@ module Neo4j
|
|
6
6
|
tx_method = "#{method}_in_tx"
|
7
7
|
send(:alias_method, tx_method, method)
|
8
8
|
send(:define_method, method) do |*args|
|
9
|
-
Neo4j::Rails::Transaction.
|
9
|
+
Neo4j::Rails::Transaction.run { send(tx_method, *args) }
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -10,7 +10,11 @@ module Neo4j
|
|
10
10
|
# The validation process on save can be skipped by passing false. The regular Model#save method is
|
11
11
|
# replaced with this when the validations module is mixed in, which it is by default.
|
12
12
|
def save(options={})
|
13
|
-
perform_validations(options) ? super : false
|
13
|
+
result = perform_validations(options) ? super : false
|
14
|
+
if !result
|
15
|
+
Neo4j::Rails::Transaction.fail if Neo4j::Rails::Transaction.running?
|
16
|
+
end
|
17
|
+
result
|
14
18
|
end
|
15
19
|
|
16
20
|
def valid?(context = nil)
|
data/lib/neo4j/version.rb
CHANGED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: neo4j
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 1.2.
|
5
|
+
version: 1.2.2
|
6
6
|
platform: java
|
7
7
|
authors:
|
8
8
|
- Andreas Ronge
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-
|
13
|
+
date: 2011-09-15 00:00:00 +02:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -128,6 +128,7 @@ files:
|
|
128
128
|
- lib/neo4j/rails/railtie.rb
|
129
129
|
- lib/neo4j/rails/rails.rb
|
130
130
|
- lib/neo4j/rails/lucene_connection_closer.rb
|
131
|
+
- lib/neo4j/rails/observer.rb
|
131
132
|
- lib/neo4j/rails/transaction.rb
|
132
133
|
- lib/neo4j/rails/timestamps.rb
|
133
134
|
- lib/neo4j/rails/callbacks.rb
|
@@ -135,6 +136,7 @@ files:
|
|
135
136
|
- lib/neo4j/rails/rel_persistence.rb
|
136
137
|
- lib/neo4j/rails/finders.rb
|
137
138
|
- lib/neo4j/rails/relationship.rb
|
139
|
+
- lib/neo4j/rails/compositions.rb
|
138
140
|
- lib/neo4j/rails/relationships/rels_dsl.rb
|
139
141
|
- lib/neo4j/rails/relationships/node_dsl.rb
|
140
142
|
- lib/neo4j/rails/relationships/relationships.rb
|