ocean-dynamo 0.5.0 → 0.5.1
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 +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
|
|