ocean-dynamo 0.4.1 → 0.4.2
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.
- checksums.yaml +4 -4
- data/README.rdoc +24 -26
- data/lib/ocean-dynamo/{associations → active_record_stuff}/collection_proxy.rb +0 -0
- data/lib/ocean-dynamo/active_record_stuff/reflection.rb +583 -0
- data/lib/ocean-dynamo/active_record_stuff/relation.rb +653 -0
- data/lib/ocean-dynamo/associations/belongs_to.rb +33 -33
- data/lib/ocean-dynamo/associations/has_many.rb +4 -2
- data/lib/ocean-dynamo/exceptions.rb +9 -1
- data/lib/ocean-dynamo/persistence.rb +1 -0
- data/lib/ocean-dynamo/version.rb +1 -1
- metadata +20 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a28d5f280ebcb046fad6b812a8a4394ca2b64dfc
|
4
|
+
data.tar.gz: fc0f313738efd5370163056bf2915b83645f53b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0259caaebfa02274f8103695c741670d6190be669ecd1c0d92d8165ea1a058f628ae96072c946fc77fc6561a0d7bb1ba075c0773201e6d3f641a69bdfec1b70b
|
7
|
+
data.tar.gz: 2b0108bbef441a611ebdaed44b76512b7ae62d75491fe0788d30f2d0ea1d5f0e80fa08a3c6a10547197d2f2640db9f7970f15e3e9e126c10ffbf778e4692b2ea
|
data/README.rdoc
CHANGED
@@ -10,11 +10,11 @@ OceanDynamo requires Ruby 2.0 and Ruby on Rails 4.0.0 or later.
|
|
10
10
|
|
11
11
|
=== Features
|
12
12
|
|
13
|
-
As one important use case for OceanDynamo is to facilitate the conversion of SQL
|
14
|
-
|
15
|
-
of OceanDynamo
|
16
|
-
callbacks, exceptions and
|
17
|
-
is of course based on ActiveModel.
|
13
|
+
As one important use case for OceanDynamo is to facilitate the conversion of SQL
|
14
|
+
databases to no-SQL DynamoDB databases, it is important that the syntax and semantics
|
15
|
+
of OceanDynamo are as close as possible to those of ActiveRecord. This includes
|
16
|
+
callbacks, exceptions and method chaining semantics. OceanDynamo follows this pattern
|
17
|
+
closely and is of course based on ActiveModel.
|
18
18
|
|
19
19
|
The attribute and persistence layer of OceanDynamo is modeled on that of ActiveRecord:
|
20
20
|
there's +save+, +save!+, +create+, +update+, +update!+, +update_attributes+, +find_each+,
|
@@ -23,9 +23,8 @@ is always to implement as much of the ActiveRecord interface as possible, withou
|
|
23
23
|
compromising scalability. This makes the task of switching from SQL to no-SQL much easier.
|
24
24
|
|
25
25
|
Thanks to its structural similarity to ActiveRecord, OceanDynamo works with FactoryGirl.
|
26
|
-
To facilitate testing, future versions will keep track of and delete instances after tests.
|
27
26
|
|
28
|
-
OceanDynamo uses primary indices to retrieve related table items,
|
27
|
+
OceanDynamo uses only primary indices to retrieve related table items and collections,
|
29
28
|
which means it will scale without limits.
|
30
29
|
|
31
30
|
=== Example
|
@@ -38,7 +37,7 @@ The following example shows the basic syntax for declaring a DynamoDB-based sche
|
|
38
37
|
|
39
38
|
dynamo_schema(:uuid) do
|
40
39
|
attribute :credentials, :string
|
41
|
-
attribute :token
|
40
|
+
attribute :token, :string, default: Proc { SecureRandom.uuid }
|
42
41
|
attribute :steps, :serialized, default: []
|
43
42
|
attribute :max_seconds_in_queue, :integer, default: 1.day
|
44
43
|
attribute :default_poison_limit, :integer, default: 5
|
@@ -61,7 +60,7 @@ The following example shows the basic syntax for declaring a DynamoDB-based sche
|
|
61
60
|
Each attribute has a name, a type (+:string+, +:integer+, +:float+, +:datetime+, +:boolean+,
|
62
61
|
or +:serialized+) where +:string+ is the default. Each attribute also optionally has a default
|
63
62
|
value, which can be a Proc. The hash key attribute is by default +:id+ (overridden as +:uuid+ in
|
64
|
-
the example above) and is a +:string+.
|
63
|
+
the example above) and is a +:string+.
|
65
64
|
|
66
65
|
The +:string+, +:integer+, +:float+ and +:datetime+ types can also store sets of their type.
|
67
66
|
Sets are represented as arrays, may not contain duplicates and may not be empty.
|
@@ -84,7 +83,7 @@ value will return the empty string, <tt>""</tt>.
|
|
84
83
|
connect: :late, # true, :late, nil/false
|
85
84
|
create: false, # If true, create the table if nonexistent
|
86
85
|
locking: :lock_version, # The name of the lock attribute or nil/false
|
87
|
-
timestamps: [:created_at, :updated_at] #
|
86
|
+
timestamps: [:created_at, :updated_at] # A two-element array of timestamp columns, or nil/false
|
88
87
|
) do
|
89
88
|
# Attribute definitions
|
90
89
|
...
|
@@ -97,6 +96,8 @@ value will return the empty string, <tt>""</tt>.
|
|
97
96
|
|
98
97
|
The following example shows how to set up a +has_many+ / +belongs_to+ relation:
|
99
98
|
|
99
|
+
class Child < OceanDynamo::Table; end
|
100
|
+
|
100
101
|
class Parent < OceanDynamo::Table
|
101
102
|
dynamo_schema do
|
102
103
|
attribute :this
|
@@ -105,8 +106,8 @@ The following example shows how to set up a +has_many+ / +belongs_to+ relation:
|
|
105
106
|
end
|
106
107
|
has_many :children
|
107
108
|
end
|
108
|
-
|
109
|
-
|
109
|
+
|
110
|
+
|
110
111
|
class Child < OceanDynamo::Table
|
111
112
|
dynamo_schema(:uuid) do
|
112
113
|
attribute :foo
|
@@ -126,6 +127,7 @@ Restrictions for +belongs_to+ tables:
|
|
126
127
|
|
127
128
|
Restrictions for +has_many+ tables:
|
128
129
|
* The +has_many+ table may not have a range key.
|
130
|
+
* +has_many+ must be placed after the +dynamo_schema+ attribute block.
|
129
131
|
|
130
132
|
These restrictions allow OceanDynamo to implement the +has_many+ / +belongs_to+
|
131
133
|
relation in a very efficient and massively scalable way.
|
@@ -147,9 +149,9 @@ and matching, the fact that the range key is a string can be used to implement
|
|
147
149
|
wildcard matching of additional attributes. This gives, amongst other things, the
|
148
150
|
equivalent of an SQL GROUP BY request, again without requiring any secondary indices.
|
149
151
|
|
150
|
-
It's our goal to use a similar technique to implement has_and_belongs_to_many relations,
|
152
|
+
It's our goal to use a similar technique to implement +has_and_belongs_to_many+ relations,
|
151
153
|
which means that secondary indices won't be necessary for the vast majority of
|
152
|
-
|
154
|
+
DynamoDB tables. This ultimately means reduced operational costs, as well as
|
153
155
|
reduced complexity.
|
154
156
|
|
155
157
|
|
@@ -158,22 +160,18 @@ reduced complexity.
|
|
158
160
|
OceanDynamo is fully usable as an ActiveModel and can be used by Rails
|
159
161
|
controllers. OceanDynamo implements much of the infrastructure of ActiveRecord;
|
160
162
|
for instance, +read_attribute+, +write_attribute+, and much of the control logic and
|
161
|
-
|
162
|
-
|
163
|
-
* has_many now works, in a minimalistic fashion. To implement the rest of the interface,
|
164
|
-
we need association proxies.
|
163
|
+
internal organisation.
|
165
164
|
|
166
|
-
*
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
165
|
+
* +belongs_to+ now handles setting and retrieving the parent association correctly in
|
166
|
+
all cases, including direct and mass assignment (+:master+, +:master_id+, etc).
|
167
|
+
* +has_many=+ now destroys rather than deletes.
|
168
|
+
* Got rid of the +fake_dynamo+ purge message.
|
169
|
+
* Relations now take an optional reload arg, as in <tt>parent.children(true)</tt>.
|
171
170
|
|
172
|
-
* #reload clears all relations.
|
173
171
|
|
174
172
|
=== Future milestones
|
175
173
|
|
176
|
-
* Association proxies, to implement
|
174
|
+
* Association proxies, to implement ActiveRecord-style method chaining, e.g.:
|
177
175
|
<code>blog_entry.comments.build(body: "Cool!").save!</code>
|
178
176
|
|
179
177
|
* The +has_and_belongs_to_many+ assocation.
|
@@ -184,7 +182,7 @@ parameters.
|
|
184
182
|
=== Current use
|
185
183
|
|
186
184
|
OceanDynamo is currently used in the Ocean framework (http://wiki.oceanframework.net)
|
187
|
-
e.g. to implement
|
185
|
+
e.g. to implement highly scalable job queues. It will be used increasingly as features are
|
188
186
|
added to OceanDynamo and will eventually replace all ActiveRecord tables in Ocean.
|
189
187
|
|
190
188
|
|
File without changes
|
@@ -0,0 +1,583 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
# = Active Record Reflection
|
3
|
+
module Reflection # :nodoc:
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :reflections
|
8
|
+
self.reflections = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Reflection enables to interrogate Active Record classes and objects
|
12
|
+
# about their associations and aggregations. This information can,
|
13
|
+
# for example, be used in a form builder that takes an Active Record object
|
14
|
+
# and creates input fields for all of the attributes depending on their type
|
15
|
+
# and displays the associations to other objects.
|
16
|
+
#
|
17
|
+
# MacroReflection class has info for AggregateReflection and AssociationReflection
|
18
|
+
# classes.
|
19
|
+
module ClassMethods
|
20
|
+
def create_reflection(macro, name, scope, options, active_record)
|
21
|
+
case macro
|
22
|
+
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
|
23
|
+
klass = options[:through] ? ThroughReflection : AssociationReflection
|
24
|
+
reflection = klass.new(macro, name, scope, options, active_record)
|
25
|
+
when :composed_of
|
26
|
+
reflection = AggregateReflection.new(macro, name, scope, options, active_record)
|
27
|
+
end
|
28
|
+
|
29
|
+
self.reflections = self.reflections.merge(name => reflection)
|
30
|
+
reflection
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
34
|
+
def reflect_on_all_aggregations
|
35
|
+
reflections.values.grep(AggregateReflection)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
|
39
|
+
#
|
40
|
+
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
|
41
|
+
#
|
42
|
+
def reflect_on_aggregation(aggregation)
|
43
|
+
reflection = reflections[aggregation]
|
44
|
+
reflection if reflection.is_a?(AggregateReflection)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns an array of AssociationReflection objects for all the
|
48
|
+
# associations in the class. If you only want to reflect on a certain
|
49
|
+
# association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
|
50
|
+
# <tt>:belongs_to</tt>) as the first parameter.
|
51
|
+
#
|
52
|
+
# Example:
|
53
|
+
#
|
54
|
+
# Account.reflect_on_all_associations # returns an array of all associations
|
55
|
+
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
|
56
|
+
#
|
57
|
+
def reflect_on_all_associations(macro = nil)
|
58
|
+
association_reflections = reflections.values.grep(AssociationReflection)
|
59
|
+
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the AssociationReflection object for the +association+ (use the symbol).
|
63
|
+
#
|
64
|
+
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
|
65
|
+
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
|
66
|
+
#
|
67
|
+
def reflect_on_association(association)
|
68
|
+
reflection = reflections[association]
|
69
|
+
reflection if reflection.is_a?(AssociationReflection)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
|
73
|
+
def reflect_on_all_autosave_associations
|
74
|
+
reflections.values.select { |reflection| reflection.options[:autosave] }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Base class for AggregateReflection and AssociationReflection. Objects of
|
79
|
+
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
|
80
|
+
#
|
81
|
+
# MacroReflection
|
82
|
+
# AggregateReflection
|
83
|
+
# AssociationReflection
|
84
|
+
# ThroughReflection
|
85
|
+
class MacroReflection
|
86
|
+
# Returns the name of the macro.
|
87
|
+
#
|
88
|
+
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
|
89
|
+
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
|
90
|
+
attr_reader :name
|
91
|
+
|
92
|
+
# Returns the macro type.
|
93
|
+
#
|
94
|
+
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt>
|
95
|
+
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
|
96
|
+
attr_reader :macro
|
97
|
+
|
98
|
+
attr_reader :scope
|
99
|
+
|
100
|
+
# Returns the hash of options used for the macro.
|
101
|
+
#
|
102
|
+
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
|
103
|
+
# <tt>has_many :clients</tt> returns +{}+
|
104
|
+
attr_reader :options
|
105
|
+
|
106
|
+
attr_reader :active_record
|
107
|
+
|
108
|
+
attr_reader :plural_name # :nodoc:
|
109
|
+
|
110
|
+
def initialize(macro, name, scope, options, active_record)
|
111
|
+
@macro = macro
|
112
|
+
@name = name
|
113
|
+
@scope = scope
|
114
|
+
@options = options
|
115
|
+
@active_record = active_record
|
116
|
+
@plural_name = active_record.pluralize_table_names ?
|
117
|
+
name.to_s.pluralize : name.to_s
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns the class for the macro.
|
121
|
+
#
|
122
|
+
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
|
123
|
+
# <tt>has_many :clients</tt> returns the Client class
|
124
|
+
def klass
|
125
|
+
@klass ||= class_name.constantize
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns the class name for the macro.
|
129
|
+
#
|
130
|
+
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
|
131
|
+
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
|
132
|
+
def class_name
|
133
|
+
@class_name ||= (options[:class_name] || derive_class_name).to_s
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
|
137
|
+
# and +other_aggregation+ has an options hash assigned to it.
|
138
|
+
def ==(other_aggregation)
|
139
|
+
super ||
|
140
|
+
other_aggregation.kind_of?(self.class) &&
|
141
|
+
name == other_aggregation.name &&
|
142
|
+
other_aggregation.options &&
|
143
|
+
active_record == other_aggregation.active_record
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
def derive_class_name
|
148
|
+
name.to_s.camelize
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
# Holds all the meta-data about an aggregation as it was specified in the
|
154
|
+
# Active Record class.
|
155
|
+
class AggregateReflection < MacroReflection #:nodoc:
|
156
|
+
def mapping
|
157
|
+
mapping = options[:mapping] || [name, name]
|
158
|
+
mapping.first.is_a?(Array) ? mapping : [mapping]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Holds all the meta-data about an association as it was specified in the
|
163
|
+
# Active Record class.
|
164
|
+
class AssociationReflection < MacroReflection #:nodoc:
|
165
|
+
# Returns the target association's class.
|
166
|
+
#
|
167
|
+
# class Author < ActiveRecord::Base
|
168
|
+
# has_many :books
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# Author.reflect_on_association(:books).klass
|
172
|
+
# # => Book
|
173
|
+
#
|
174
|
+
# <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
|
175
|
+
# a new association object. Use +build_association+ or +create_association+
|
176
|
+
# instead. This allows plugins to hook into association object creation.
|
177
|
+
def klass
|
178
|
+
@klass ||= active_record.send(:compute_type, class_name)
|
179
|
+
end
|
180
|
+
|
181
|
+
def initialize(*args)
|
182
|
+
super
|
183
|
+
@collection = [:has_many, :has_and_belongs_to_many].include?(macro)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns a new, unsaved instance of the associated class. +attributes+ will
|
187
|
+
# be passed to the class's constructor.
|
188
|
+
def build_association(attributes, &block)
|
189
|
+
klass.new(attributes, &block)
|
190
|
+
end
|
191
|
+
|
192
|
+
def table_name
|
193
|
+
@table_name ||= klass.table_name
|
194
|
+
end
|
195
|
+
|
196
|
+
def quoted_table_name
|
197
|
+
@quoted_table_name ||= klass.quoted_table_name
|
198
|
+
end
|
199
|
+
|
200
|
+
def join_table
|
201
|
+
@join_table ||= options[:join_table] || derive_join_table
|
202
|
+
end
|
203
|
+
|
204
|
+
def foreign_key
|
205
|
+
@foreign_key ||= options[:foreign_key] || derive_foreign_key
|
206
|
+
end
|
207
|
+
|
208
|
+
def foreign_type
|
209
|
+
@foreign_type ||= options[:foreign_type] || "#{name}_type"
|
210
|
+
end
|
211
|
+
|
212
|
+
def type
|
213
|
+
@type ||= options[:as] && "#{options[:as]}_type"
|
214
|
+
end
|
215
|
+
|
216
|
+
def primary_key_column
|
217
|
+
@primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
|
218
|
+
end
|
219
|
+
|
220
|
+
def association_foreign_key
|
221
|
+
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
|
222
|
+
end
|
223
|
+
|
224
|
+
# klass option is necessary to support loading polymorphic associations
|
225
|
+
def association_primary_key(klass = nil)
|
226
|
+
options[:primary_key] || primary_key(klass || self.klass)
|
227
|
+
end
|
228
|
+
|
229
|
+
def active_record_primary_key
|
230
|
+
@active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
|
231
|
+
end
|
232
|
+
|
233
|
+
def counter_cache_column
|
234
|
+
if options[:counter_cache] == true
|
235
|
+
"#{active_record.name.demodulize.underscore.pluralize}_count"
|
236
|
+
elsif options[:counter_cache]
|
237
|
+
options[:counter_cache].to_s
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def columns(tbl_name)
|
242
|
+
@columns ||= klass.connection.columns(tbl_name)
|
243
|
+
end
|
244
|
+
|
245
|
+
def reset_column_information
|
246
|
+
@columns = nil
|
247
|
+
end
|
248
|
+
|
249
|
+
def check_validity!
|
250
|
+
check_validity_of_inverse!
|
251
|
+
|
252
|
+
if has_and_belongs_to_many? && association_foreign_key == foreign_key
|
253
|
+
raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def check_validity_of_inverse!
|
258
|
+
unless options[:polymorphic]
|
259
|
+
if has_inverse? && inverse_of.nil?
|
260
|
+
raise InverseOfAssociationNotFoundError.new(self)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def through_reflection
|
266
|
+
nil
|
267
|
+
end
|
268
|
+
|
269
|
+
def source_reflection
|
270
|
+
nil
|
271
|
+
end
|
272
|
+
|
273
|
+
# A chain of reflections from this one back to the owner. For more see the explanation in
|
274
|
+
# ThroughReflection.
|
275
|
+
def chain
|
276
|
+
[self]
|
277
|
+
end
|
278
|
+
|
279
|
+
def nested?
|
280
|
+
false
|
281
|
+
end
|
282
|
+
|
283
|
+
# An array of arrays of scopes. Each item in the outside array corresponds to a reflection
|
284
|
+
# in the #chain.
|
285
|
+
def scope_chain
|
286
|
+
scope ? [[scope]] : [[]]
|
287
|
+
end
|
288
|
+
|
289
|
+
alias :source_macro :macro
|
290
|
+
|
291
|
+
def has_inverse?
|
292
|
+
@options[:inverse_of]
|
293
|
+
end
|
294
|
+
|
295
|
+
def inverse_of
|
296
|
+
if has_inverse?
|
297
|
+
@inverse_of ||= klass.reflect_on_association(options[:inverse_of])
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def polymorphic_inverse_of(associated_class)
|
302
|
+
if has_inverse?
|
303
|
+
if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
|
304
|
+
inverse_relationship
|
305
|
+
else
|
306
|
+
raise InverseOfAssociationNotFoundError.new(self, associated_class)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Returns whether or not this association reflection is for a collection
|
312
|
+
# association. Returns +true+ if the +macro+ is either +has_many+ or
|
313
|
+
# +has_and_belongs_to_many+, +false+ otherwise.
|
314
|
+
def collection?
|
315
|
+
@collection
|
316
|
+
end
|
317
|
+
|
318
|
+
# Returns whether or not the association should be validated as part of
|
319
|
+
# the parent's validation.
|
320
|
+
#
|
321
|
+
# Unless you explicitly disable validation with
|
322
|
+
# <tt>validate: false</tt>, validation will take place when:
|
323
|
+
#
|
324
|
+
# * you explicitly enable validation; <tt>validate: true</tt>
|
325
|
+
# * you use autosave; <tt>autosave: true</tt>
|
326
|
+
# * the association is a +has_many+ association
|
327
|
+
def validate?
|
328
|
+
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
|
329
|
+
end
|
330
|
+
|
331
|
+
# Returns +true+ if +self+ is a +belongs_to+ reflection.
|
332
|
+
def belongs_to?
|
333
|
+
macro == :belongs_to
|
334
|
+
end
|
335
|
+
|
336
|
+
def has_and_belongs_to_many?
|
337
|
+
macro == :has_and_belongs_to_many
|
338
|
+
end
|
339
|
+
|
340
|
+
def association_class
|
341
|
+
case macro
|
342
|
+
when :belongs_to
|
343
|
+
if options[:polymorphic]
|
344
|
+
Associations::BelongsToPolymorphicAssociation
|
345
|
+
else
|
346
|
+
Associations::BelongsToAssociation
|
347
|
+
end
|
348
|
+
when :has_and_belongs_to_many
|
349
|
+
Associations::HasAndBelongsToManyAssociation
|
350
|
+
when :has_many
|
351
|
+
if options[:through]
|
352
|
+
Associations::HasManyThroughAssociation
|
353
|
+
else
|
354
|
+
Associations::HasManyAssociation
|
355
|
+
end
|
356
|
+
when :has_one
|
357
|
+
if options[:through]
|
358
|
+
Associations::HasOneThroughAssociation
|
359
|
+
else
|
360
|
+
Associations::HasOneAssociation
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def polymorphic?
|
366
|
+
options.key? :polymorphic
|
367
|
+
end
|
368
|
+
|
369
|
+
private
|
370
|
+
def derive_class_name
|
371
|
+
class_name = name.to_s.camelize
|
372
|
+
class_name = class_name.singularize if collection?
|
373
|
+
class_name
|
374
|
+
end
|
375
|
+
|
376
|
+
def derive_foreign_key
|
377
|
+
if belongs_to?
|
378
|
+
"#{name}_id"
|
379
|
+
elsif options[:as]
|
380
|
+
"#{options[:as]}_id"
|
381
|
+
else
|
382
|
+
active_record.name.foreign_key
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def derive_join_table
|
387
|
+
[active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
|
388
|
+
end
|
389
|
+
|
390
|
+
def primary_key(klass)
|
391
|
+
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Holds all the meta-data about a :through association as it was specified
|
396
|
+
# in the Active Record class.
|
397
|
+
class ThroughReflection < AssociationReflection #:nodoc:
|
398
|
+
delegate :foreign_key, :foreign_type, :association_foreign_key,
|
399
|
+
:active_record_primary_key, :type, :to => :source_reflection
|
400
|
+
|
401
|
+
# Gets the source of the through reflection. It checks both a singularized
|
402
|
+
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
|
403
|
+
#
|
404
|
+
# class Post < ActiveRecord::Base
|
405
|
+
# has_many :taggings
|
406
|
+
# has_many :tags, through: :taggings
|
407
|
+
# end
|
408
|
+
#
|
409
|
+
# class Tagging < ActiveRecord::Base
|
410
|
+
# belongs_to :post
|
411
|
+
# belongs_to :tag
|
412
|
+
# end
|
413
|
+
#
|
414
|
+
# tags_reflection = Post.reflect_on_association(:tags)
|
415
|
+
#
|
416
|
+
# taggings_reflection = tags_reflection.source_reflection
|
417
|
+
# # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags">
|
418
|
+
#
|
419
|
+
def source_reflection
|
420
|
+
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
|
421
|
+
end
|
422
|
+
|
423
|
+
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
|
424
|
+
# of a HasManyThrough or HasOneThrough association.
|
425
|
+
#
|
426
|
+
# class Post < ActiveRecord::Base
|
427
|
+
# has_many :taggings
|
428
|
+
# has_many :tags, through: :taggings
|
429
|
+
# end
|
430
|
+
#
|
431
|
+
# tags_reflection = Post.reflect_on_association(:tags)
|
432
|
+
# taggings_reflection = tags_reflection.through_reflection
|
433
|
+
#
|
434
|
+
def through_reflection
|
435
|
+
@through_reflection ||= active_record.reflect_on_association(options[:through])
|
436
|
+
end
|
437
|
+
|
438
|
+
# Returns an array of reflections which are involved in this association. Each item in the
|
439
|
+
# array corresponds to a table which will be part of the query for this association.
|
440
|
+
#
|
441
|
+
# The chain is built by recursively calling #chain on the source reflection and the through
|
442
|
+
# reflection. The base case for the recursion is a normal association, which just returns
|
443
|
+
# [self] as its #chain.
|
444
|
+
#
|
445
|
+
# class Post < ActiveRecord::Base
|
446
|
+
# has_many :taggings
|
447
|
+
# has_many :tags, through: :taggings
|
448
|
+
# end
|
449
|
+
#
|
450
|
+
# tags_reflection = Post.reflect_on_association(:tags)
|
451
|
+
# tags_reflection.chain
|
452
|
+
# # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>,
|
453
|
+
# <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>]
|
454
|
+
#
|
455
|
+
def chain
|
456
|
+
@chain ||= begin
|
457
|
+
chain = source_reflection.chain + through_reflection.chain
|
458
|
+
chain[0] = self # Use self so we don't lose the information from :source_type
|
459
|
+
chain
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# Consider the following example:
|
464
|
+
#
|
465
|
+
# class Person
|
466
|
+
# has_many :articles
|
467
|
+
# has_many :comment_tags, through: :articles
|
468
|
+
# end
|
469
|
+
#
|
470
|
+
# class Article
|
471
|
+
# has_many :comments
|
472
|
+
# has_many :comment_tags, through: :comments, source: :tags
|
473
|
+
# end
|
474
|
+
#
|
475
|
+
# class Comment
|
476
|
+
# has_many :tags
|
477
|
+
# end
|
478
|
+
#
|
479
|
+
# There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags,
|
480
|
+
# but only Comment.tags will be represented in the #chain. So this method creates an array
|
481
|
+
# of scopes corresponding to the chain.
|
482
|
+
def scope_chain
|
483
|
+
@scope_chain ||= begin
|
484
|
+
scope_chain = source_reflection.scope_chain.map(&:dup)
|
485
|
+
|
486
|
+
# Add to it the scope from this reflection (if any)
|
487
|
+
scope_chain.first << scope if scope
|
488
|
+
|
489
|
+
through_scope_chain = through_reflection.scope_chain
|
490
|
+
|
491
|
+
if options[:source_type]
|
492
|
+
through_scope_chain.first <<
|
493
|
+
through_reflection.klass.where(foreign_type => options[:source_type])
|
494
|
+
end
|
495
|
+
|
496
|
+
# Recursively fill out the rest of the array from the through reflection
|
497
|
+
scope_chain + through_scope_chain
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
# The macro used by the source association
|
502
|
+
def source_macro
|
503
|
+
source_reflection.source_macro
|
504
|
+
end
|
505
|
+
|
506
|
+
# A through association is nested if there would be more than one join table
|
507
|
+
def nested?
|
508
|
+
chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
|
509
|
+
end
|
510
|
+
|
511
|
+
# We want to use the klass from this reflection, rather than just delegate straight to
|
512
|
+
# the source_reflection, because the source_reflection may be polymorphic. We still
|
513
|
+
# need to respect the source_reflection's :primary_key option, though.
|
514
|
+
def association_primary_key(klass = nil)
|
515
|
+
# Get the "actual" source reflection if the immediate source reflection has a
|
516
|
+
# source reflection itself
|
517
|
+
source_reflection = self.source_reflection
|
518
|
+
while source_reflection.source_reflection
|
519
|
+
source_reflection = source_reflection.source_reflection
|
520
|
+
end
|
521
|
+
|
522
|
+
source_reflection.options[:primary_key] || primary_key(klass || self.klass)
|
523
|
+
end
|
524
|
+
|
525
|
+
# Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
|
526
|
+
#
|
527
|
+
# class Post < ActiveRecord::Base
|
528
|
+
# has_many :taggings
|
529
|
+
# has_many :tags, through: :taggings
|
530
|
+
# end
|
531
|
+
#
|
532
|
+
# tags_reflection = Post.reflect_on_association(:tags)
|
533
|
+
# tags_reflection.source_reflection_names
|
534
|
+
# # => [:tag, :tags]
|
535
|
+
#
|
536
|
+
def source_reflection_names
|
537
|
+
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
|
538
|
+
end
|
539
|
+
|
540
|
+
def source_options
|
541
|
+
source_reflection.options
|
542
|
+
end
|
543
|
+
|
544
|
+
def through_options
|
545
|
+
through_reflection.options
|
546
|
+
end
|
547
|
+
|
548
|
+
def check_validity!
|
549
|
+
if through_reflection.nil?
|
550
|
+
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
|
551
|
+
end
|
552
|
+
|
553
|
+
if through_reflection.options[:polymorphic]
|
554
|
+
raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
|
555
|
+
end
|
556
|
+
|
557
|
+
if source_reflection.nil?
|
558
|
+
raise HasManyThroughSourceAssociationNotFoundError.new(self)
|
559
|
+
end
|
560
|
+
|
561
|
+
if options[:source_type] && source_reflection.options[:polymorphic].nil?
|
562
|
+
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
|
563
|
+
end
|
564
|
+
|
565
|
+
if source_reflection.options[:polymorphic] && options[:source_type].nil?
|
566
|
+
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
|
567
|
+
end
|
568
|
+
|
569
|
+
if macro == :has_one && through_reflection.collection?
|
570
|
+
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
|
571
|
+
end
|
572
|
+
|
573
|
+
check_validity_of_inverse!
|
574
|
+
end
|
575
|
+
|
576
|
+
private
|
577
|
+
def derive_class_name
|
578
|
+
# get the class_name of the belongs_to association of the through reflection
|
579
|
+
options[:source_type] || source_reflection.class_name
|
580
|
+
end
|
581
|
+
end
|
582
|
+
end
|
583
|
+
end
|