neo4j 1.2.1-java → 1.2.2-java
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 +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
|