ocean-dynamo 0.5.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.rdoc +2 -2
- data/lib/ocean-dynamo.rb +3 -0
- data/lib/ocean-dynamo/active_record_stuff/association.rb +244 -0
- data/lib/ocean-dynamo/active_record_stuff/collection_association.rb +580 -0
- data/lib/ocean-dynamo/active_record_stuff/collection_proxy.rb +2 -8
- data/lib/ocean-dynamo/active_record_stuff/has_and_belongs_to_many_association.rb +57 -0
- data/lib/ocean-dynamo/active_record_stuff/has_many_association.rb +132 -0
- data/lib/ocean-dynamo/active_record_stuff/reflection.rb +2 -2
- data/lib/ocean-dynamo/active_record_stuff/relation.rb +2 -2
- data/lib/ocean-dynamo/associations/association.rb +158 -0
- data/lib/ocean-dynamo/associations/associations.rb +4 -0
- data/lib/ocean-dynamo/associations/belongs_to.rb +32 -4
- data/lib/ocean-dynamo/associations/collection_association.rb +39 -0
- data/lib/ocean-dynamo/associations/relation.rb +29 -0
- data/lib/ocean-dynamo/attributes.rb +1 -0
- data/lib/ocean-dynamo/version.rb +1 -1
- metadata +9 -2
@@ -1,11 +1,5 @@
|
|
1
|
-
module OceanDynamo
|
2
|
-
module Associations
|
3
|
-
#
|
4
|
-
# This entire file shamelessly lifted from ActiveRecord, for compatibility
|
5
|
-
# reasons. OceanDynamo must implement the same query interface as ActiveRecord.
|
6
|
-
#
|
7
|
-
|
8
|
-
|
1
|
+
module OceanDynamo #:nodoc:
|
2
|
+
module Associations #:nodoc:
|
9
3
|
#
|
10
4
|
# Association proxies in OceanDynamo are middlemen between the object that
|
11
5
|
# holds the association, known as the <tt>@owner</tt>, and the actual associated
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ActiveRecord #:nodoc:
|
2
|
+
# = Active Record Has And Belongs To Many Association
|
3
|
+
module Associations #:nodoc:
|
4
|
+
class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
|
5
|
+
attr_reader :join_table
|
6
|
+
|
7
|
+
def initialize(owner, reflection)
|
8
|
+
@join_table = Arel::Table.new(reflection.join_table)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def insert_record(record, validate = true, raise = false)
|
13
|
+
if record.new_record?
|
14
|
+
if raise
|
15
|
+
record.save!(:validate => validate)
|
16
|
+
else
|
17
|
+
return unless record.save(:validate => validate)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
stmt = join_table.compile_insert(
|
22
|
+
join_table[reflection.foreign_key] => own
|
23
|
+
er.id,
|
24
|
+
join_table[reflection.association_foreign_key] => record.id
|
25
|
+
)
|
26
|
+
|
27
|
+
owner.class.connection.insert stmt
|
28
|
+
|
29
|
+
record
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def count_records
|
35
|
+
load_target.size
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete_records(records, method)
|
39
|
+
relation = join_table
|
40
|
+
condition = relation[reflection.foreign_key].eq(owner.id)
|
41
|
+
|
42
|
+
unless records == :all
|
43
|
+
condition = condition.and(
|
44
|
+
relation[reflection.association_foreign_key]
|
45
|
+
.in(records.map { |x| x.id }.compact)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
owner.class.connection.delete(relation.where(condition).compile_delete)
|
50
|
+
end
|
51
|
+
|
52
|
+
def invertible_for?(record)
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module ActiveRecord #:nodoc:
|
2
|
+
# = Active Record Has Many Association
|
3
|
+
module Associations #:nodoc:
|
4
|
+
# This is the proxy that handles a has many association.
|
5
|
+
#
|
6
|
+
# If the association has a <tt>:through</tt> option further specialization
|
7
|
+
# is provided by its child HasManyThroughAssociation.
|
8
|
+
class HasManyAssociation < CollectionAssociation #:nodoc:
|
9
|
+
|
10
|
+
def handle_dependency
|
11
|
+
case options[:dependent]
|
12
|
+
when :restrict_with_exception
|
13
|
+
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
|
14
|
+
|
15
|
+
when :restrict_with_error
|
16
|
+
unless empty?
|
17
|
+
record = klass.human_attribute_name(reflection.name).downcase
|
18
|
+
owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
else
|
23
|
+
if options[:dependent] == :destroy
|
24
|
+
# No point in executing the counter update since we're going to destroy the parent anyway
|
25
|
+
load_target.each { |t| t.destroyed_by_association = reflection }
|
26
|
+
destroy_all
|
27
|
+
else
|
28
|
+
delete_all
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def insert_record(record, validate = true, raise = false)
|
34
|
+
set_owner_attributes(record)
|
35
|
+
|
36
|
+
if raise
|
37
|
+
record.save!(:validate => validate)
|
38
|
+
else
|
39
|
+
record.save(:validate => validate)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Returns the number of records in this collection.
|
46
|
+
#
|
47
|
+
# If the association has a counter cache it gets that value. Otherwise
|
48
|
+
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
|
49
|
+
# there's one. Some configuration options like :group make it impossible
|
50
|
+
# to do an SQL count, in those cases the array count will be used.
|
51
|
+
#
|
52
|
+
# That does not depend on whether the collection has already been loaded
|
53
|
+
# or not. The +size+ method is the one that takes the loaded flag into
|
54
|
+
# account and delegates to +count_records+ if needed.
|
55
|
+
#
|
56
|
+
# If the collection is empty the target is set to an empty array and
|
57
|
+
# the loaded flag is set to true as well.
|
58
|
+
def count_records
|
59
|
+
count = if has_cached_counter?
|
60
|
+
owner.send(:read_attribute, cached_counter_attribute_name)
|
61
|
+
else
|
62
|
+
scope.count
|
63
|
+
end
|
64
|
+
|
65
|
+
# If there's nothing in the database and @target has no new records
|
66
|
+
# we are certain the current target is an empty array. This is a
|
67
|
+
# documented side-effect of the method that may avoid an extra SELECT.
|
68
|
+
@target ||= [] and loaded! if count == 0
|
69
|
+
|
70
|
+
[association_scope.limit_value, count].compact.min
|
71
|
+
end
|
72
|
+
|
73
|
+
def has_cached_counter?(reflection = reflection)
|
74
|
+
owner.attribute_present?(cached_counter_attribute_name(reflection))
|
75
|
+
end
|
76
|
+
|
77
|
+
def cached_counter_attribute_name(reflection = reflection)
|
78
|
+
options[:counter_cache] || "#{reflection.name}_count"
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_counter(difference, reflection = reflection)
|
82
|
+
if has_cached_counter?(reflection)
|
83
|
+
counter = cached_counter_attribute_name(reflection)
|
84
|
+
owner.class.update_counters(owner.id, counter => difference)
|
85
|
+
owner[counter] += difference
|
86
|
+
owner.changed_attributes.delete(counter) # eww
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# This shit is nasty. We need to avoid the following situation:
|
91
|
+
#
|
92
|
+
# * An associated record is deleted via record.destroy
|
93
|
+
# * Hence the callbacks run, and they find a belongs_to on the record with a
|
94
|
+
# :counter_cache options which points back at our owner. So they update the
|
95
|
+
# counter cache.
|
96
|
+
# * In which case, we must make sure to *not* update the counter cache, or else
|
97
|
+
# it will be decremented twice.
|
98
|
+
#
|
99
|
+
# Hence this method.
|
100
|
+
def inverse_updates_counter_cache?(reflection = reflection)
|
101
|
+
counter_name = cached_counter_attribute_name(reflection)
|
102
|
+
reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
|
103
|
+
inverse_reflection.counter_cache_column == counter_name
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
# Deletes the records according to the <tt>:dependent</tt> option.
|
108
|
+
def delete_records(records, method)
|
109
|
+
if method == :destroy
|
110
|
+
records.each { |r| r.destroy }
|
111
|
+
update_counter(-records.length) unless inverse_updates_counter_cache?
|
112
|
+
else
|
113
|
+
if records == :all
|
114
|
+
scope = self.scope
|
115
|
+
else
|
116
|
+
scope = self.scope.where(reflection.klass.primary_key => records)
|
117
|
+
end
|
118
|
+
|
119
|
+
if method == :delete_all
|
120
|
+
update_counter(-scope.delete_all)
|
121
|
+
else
|
122
|
+
update_counter(-scope.update_all(reflection.foreign_key => nil))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def foreign_key_present?
|
128
|
+
owner.attribute_present?(reflection.association_primary_key)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module ActiveRecord
|
1
|
+
module ActiveRecord #:nodoc:
|
2
2
|
# = Active Record Reflection
|
3
3
|
module Reflection # :nodoc:
|
4
4
|
extend ActiveSupport::Concern
|
@@ -16,7 +16,7 @@ module ActiveRecord
|
|
16
16
|
#
|
17
17
|
# MacroReflection class has info for AggregateReflection and AssociationReflection
|
18
18
|
# classes.
|
19
|
-
module ClassMethods
|
19
|
+
module ClassMethods #:nodoc:
|
20
20
|
def create_reflection(macro, name, scope, options, active_record)
|
21
21
|
case macro
|
22
22
|
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module OceanDynamo
|
2
|
+
module Associations
|
3
|
+
#
|
4
|
+
# This is the root class of all Associations.
|
5
|
+
# The class structure is exactly like in ActiveRecord:
|
6
|
+
#
|
7
|
+
# Association
|
8
|
+
# CollectionAssociation
|
9
|
+
# HasAndBelongsToManyAssociation
|
10
|
+
# HasManyAssociation
|
11
|
+
#
|
12
|
+
# It should be noted, however, that the ActiveRecord documentation
|
13
|
+
# is misleading: belongs_to and has_one no longer are implemented using
|
14
|
+
# proxies, even though the documentation and the source itself says it is.
|
15
|
+
# Furthermore, method_missing is no longer used at all, despite what the
|
16
|
+
# documentation and source comments say.
|
17
|
+
#
|
18
|
+
# In OceanDynamo, we have removed the unused classes and stripped away
|
19
|
+
# the SQL-specific features such as scopes. Neither do we implement counter
|
20
|
+
# caches. We have kept the same module and class structure for compatibility,
|
21
|
+
# though.
|
22
|
+
#
|
23
|
+
class Association #:nodoc:
|
24
|
+
|
25
|
+
attr_reader :owner
|
26
|
+
attr_reader :reflection
|
27
|
+
attr_reader :target
|
28
|
+
|
29
|
+
|
30
|
+
#
|
31
|
+
#
|
32
|
+
#
|
33
|
+
def initialize(owner, reflection)
|
34
|
+
@owner, @reflection = owner, reflection
|
35
|
+
reset
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
|
40
|
+
#
|
41
|
+
def reset
|
42
|
+
@loaded = false
|
43
|
+
@target = nil
|
44
|
+
@stale_state = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Has the \target been already \loaded?
|
49
|
+
#
|
50
|
+
def loaded?
|
51
|
+
@loaded
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Asserts the \target has been loaded setting the \loaded flag to +true+.
|
56
|
+
#
|
57
|
+
def loaded!
|
58
|
+
@loaded = true
|
59
|
+
@stale_state = stale_state
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
|
64
|
+
#
|
65
|
+
def target=(target)
|
66
|
+
@target = target
|
67
|
+
loaded!
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# The target is stale if the target no longer points to the record(s) that the
|
72
|
+
# relevant foreign_key(s) refers to. If stale, the association accessor method
|
73
|
+
# on the owner will reload the target. It's up to subclasses to implement the
|
74
|
+
# stale_state method if relevant.
|
75
|
+
#
|
76
|
+
# Note that if the target has not been loaded, it is not considered stale.
|
77
|
+
#
|
78
|
+
def stale_target?
|
79
|
+
loaded? && @stale_state != stale_state
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Loads the \target if needed and returns it.
|
84
|
+
#
|
85
|
+
# This method is abstract in the sense that it relies on +find_target+,
|
86
|
+
# which is expected to be provided by descendants.
|
87
|
+
#
|
88
|
+
# If the \target is already \loaded it is just returned. Thus, you can call
|
89
|
+
# +load_target+ unconditionally to get the \target.
|
90
|
+
#
|
91
|
+
# ActiveRecord::RecordNotFound is rescued within the method, and it is
|
92
|
+
# not reraised. The proxy is \reset and +nil+ is the return value.
|
93
|
+
#
|
94
|
+
def load_target
|
95
|
+
@target = find_target if (@stale_state && stale_target?) || find_target?
|
96
|
+
loaded! unless loaded?
|
97
|
+
target
|
98
|
+
rescue OceanDynamo::RecordNotFound
|
99
|
+
reset
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Returns the class of the target. belongs_to polymorphic used to override this
|
104
|
+
# to look at the polymorphic_type field on the owner. However, belongs_to is no
|
105
|
+
# longer implemented in AR using an Assocation, so we keep this only for structural
|
106
|
+
# compatibility.
|
107
|
+
#
|
108
|
+
def klass
|
109
|
+
reflection.klass
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Reloads the \target and returns +self+ on success.
|
114
|
+
#
|
115
|
+
def reload
|
116
|
+
reset
|
117
|
+
load_target
|
118
|
+
self unless target.nil?
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def find_target?
|
125
|
+
!loaded? && (!owner.new_record? || foreign_key_present?) && klass
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Should be true if there is a foreign key present on the owner which
|
130
|
+
# references the target. This is used to determine whether we can load
|
131
|
+
# the target if the owner is currently a new record (and therefore
|
132
|
+
# without a key).
|
133
|
+
#
|
134
|
+
# Currently implemented by belongs_to (vanilla and polymorphic) and
|
135
|
+
# has_one/has_many :through associations which go through a belongs_to
|
136
|
+
#
|
137
|
+
# This method is, nowadays, merely an archaeological artifact since
|
138
|
+
# +belongs_to+ no longer uses Associations, meaning this method will
|
139
|
+
# never be overridden.
|
140
|
+
#
|
141
|
+
def foreign_key_present?
|
142
|
+
false
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
# This should be implemented to return the values of the relevant key(s) on the owner,
|
147
|
+
# so that when stale_state is different from the value stored on the last find_target,
|
148
|
+
# the target is stale.
|
149
|
+
#
|
150
|
+
# This is only relevant to certain associations, which is why it returns nil by default.
|
151
|
+
#
|
152
|
+
def stale_state
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -4,6 +4,10 @@ module OceanDynamo
|
|
4
4
|
def self.included(base)
|
5
5
|
base.extend(ClassMethods)
|
6
6
|
end
|
7
|
+
|
8
|
+
# This class really does some of the stuff that in ActiveRecord is
|
9
|
+
# handled by the Reflection class. We could switch to the AR paradigm
|
10
|
+
# at some point.
|
7
11
|
|
8
12
|
|
9
13
|
# ---------------------------------------------------------
|
@@ -24,7 +24,7 @@ module OceanDynamo
|
|
24
24
|
# end
|
25
25
|
# has_many :topics, dependent: :destroy
|
26
26
|
# end
|
27
|
-
#
|
27
|
+
#
|
28
28
|
# class Topic < OceanDynamo::Table
|
29
29
|
# dynamo_schema(:uuid) do
|
30
30
|
# attribute :title
|
@@ -32,14 +32,14 @@ module OceanDynamo
|
|
32
32
|
# belongs_to :forum
|
33
33
|
# has_many :posts, dependent: :destroy
|
34
34
|
# end
|
35
|
-
#
|
35
|
+
#
|
36
36
|
# class Post < OceanDynamo::Table
|
37
37
|
# dynamo_schema(:uuid) do
|
38
38
|
# attribute :body
|
39
39
|
# end
|
40
40
|
# belongs_to :topic, composite_key: true
|
41
41
|
# end
|
42
|
-
#
|
42
|
+
#
|
43
43
|
# The only non-standard aspect of the above is <tt>composite_key: true</tt>,
|
44
44
|
# which is required as the Topic class itself has a +belongs_to+ relation and
|
45
45
|
# thus has a composite key. This must be declared in the child class as it
|
@@ -57,6 +57,8 @@ module OceanDynamo
|
|
57
57
|
target_class = class_name.constantize # Master
|
58
58
|
|
59
59
|
assert_only_one_belongs_to!
|
60
|
+
assert_range_key_not_specified!
|
61
|
+
assert_hash_key_is_not_id!
|
60
62
|
|
61
63
|
self.table_range_key = table_hash_key # The RANGE KEY is variable
|
62
64
|
self.table_hash_key = target_attr_id.to_sym # The HASH KEY is the parent UUID
|
@@ -72,7 +74,6 @@ module OceanDynamo
|
|
72
74
|
define_attribute_accessors(table_range_key) # define uuid, uuid=, uuid?
|
73
75
|
|
74
76
|
|
75
|
-
|
76
77
|
# Define the parent id attribute
|
77
78
|
attribute target_attr_id, :reference, default: nil, target_class: target_class,
|
78
79
|
association: :belongs_to
|
@@ -101,6 +102,15 @@ module OceanDynamo
|
|
101
102
|
@#{target_attr} = nil
|
102
103
|
value
|
103
104
|
end"
|
105
|
+
|
106
|
+
# Define parent builders
|
107
|
+
self.class_eval "def self.build_#{target_attr}(**opts)
|
108
|
+
#{target_class}.new(opts)
|
109
|
+
end"
|
110
|
+
|
111
|
+
self.class_eval "def self.create_#{target_attr}(**opts)
|
112
|
+
#{target_class}.create(opts)
|
113
|
+
end"
|
104
114
|
end
|
105
115
|
|
106
116
|
|
@@ -144,6 +154,24 @@ module OceanDynamo
|
|
144
154
|
end
|
145
155
|
false
|
146
156
|
end
|
157
|
+
|
158
|
+
|
159
|
+
#
|
160
|
+
# Make sure the range key isn't specified.
|
161
|
+
#
|
162
|
+
def assert_range_key_not_specified! # :nodoc:
|
163
|
+
raise RangeKeyMustNotBeSpecified,
|
164
|
+
"Tables with belongs_to relations may not specify the range key" if table_range_key
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
#
|
169
|
+
# Make sure the hash key isn't called :id.
|
170
|
+
#
|
171
|
+
def assert_hash_key_is_not_id! # :nodoc:
|
172
|
+
raise HashKeyMayNotBeNamedId,
|
173
|
+
"Tables with belongs_to relations may not name their hash key :id" if table_hash_key.to_s == "id"
|
174
|
+
end
|
147
175
|
end
|
148
176
|
|
149
177
|
|