dynamoid 0.4.1 → 0.5.0
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/Dynamoid.gemspec +18 -8
- data/Gemfile +3 -1
- data/Gemfile.lock +48 -24
- data/README.markdown +6 -3
- data/VERSION +1 -1
- data/lib/dynamoid.rb +4 -0
- data/lib/dynamoid/adapter.rb +3 -3
- data/lib/dynamoid/adapter/aws_sdk.rb +19 -13
- data/lib/dynamoid/components.rb +5 -4
- data/lib/dynamoid/config.rb +7 -4
- data/lib/dynamoid/criteria/chain.rb +22 -13
- data/lib/dynamoid/dirty.rb +41 -0
- data/lib/dynamoid/document.rb +55 -26
- data/lib/dynamoid/errors.rb +5 -0
- data/lib/dynamoid/fields.rb +0 -5
- data/lib/dynamoid/finders.rb +60 -12
- data/lib/dynamoid/identity_map.rb +96 -0
- data/lib/dynamoid/indexes/index.rb +1 -1
- data/lib/dynamoid/middleware/identity_map.rb +16 -0
- data/lib/dynamoid/persistence.rb +45 -6
- data/spec/app/models/message.rb +9 -0
- data/spec/app/models/tweet.rb +3 -0
- data/spec/dynamoid/adapter/aws_sdk_spec.rb +34 -34
- data/spec/dynamoid/criteria/chain_spec.rb +17 -9
- data/spec/dynamoid/criteria_spec.rb +8 -2
- data/spec/dynamoid/dirty_spec.rb +49 -0
- data/spec/dynamoid/document_spec.rb +5 -1
- data/spec/dynamoid/fields_spec.rb +47 -17
- data/spec/dynamoid/finders_spec.rb +27 -11
- data/spec/dynamoid/identity_map_spec.rb +45 -0
- data/spec/dynamoid/indexes/index_spec.rb +0 -2
- data/spec/dynamoid/persistence_spec.rb +65 -29
- data/spec/dynamoid_spec.rb +4 -0
- data/spec/spec_helper.rb +21 -25
- metadata +54 -18
- data/lib/dynamoid/adapter/local.rb +0 -196
- data/spec/dynamoid/adapter/local_spec.rb +0 -242
@@ -97,16 +97,7 @@ module Dynamoid #:nodoc:
|
|
97
97
|
#
|
98
98
|
# @since 0.2.0
|
99
99
|
def records_with_index
|
100
|
-
ids =
|
101
|
-
Dynamoid::Adapter.query(index.table_name, index_query).collect{|r| r[:ids]}.inject(Set.new) {|set, result| set + result}
|
102
|
-
else
|
103
|
-
results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
|
104
|
-
if results
|
105
|
-
results[:ids]
|
106
|
-
else
|
107
|
-
[]
|
108
|
-
end
|
109
|
-
end
|
100
|
+
ids = ids_from_index
|
110
101
|
if ids.nil? || ids.empty?
|
111
102
|
[]
|
112
103
|
else
|
@@ -121,8 +112,22 @@ module Dynamoid #:nodoc:
|
|
121
112
|
end
|
122
113
|
end
|
123
114
|
|
115
|
+
# Returns the Set of IDs from the index table.
|
116
|
+
#
|
117
|
+
# @return [Set] a Set containing the IDs from the index.
|
118
|
+
def ids_from_index
|
119
|
+
if index.range_key?
|
120
|
+
Dynamoid::Adapter.query(index.table_name, index_query.merge(consistent_opts)).inject(Set.new) do |all, record|
|
121
|
+
all + Set.new(record[:ids])
|
122
|
+
end
|
123
|
+
else
|
124
|
+
results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
|
125
|
+
results ? results[:ids] : []
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
124
129
|
def records_with_range
|
125
|
-
Dynamoid::Adapter.query(source.table_name, range_query).collect {|hash| source.
|
130
|
+
Dynamoid::Adapter.query(source.table_name, range_query).collect {|hash| source.from_database(hash) }
|
126
131
|
end
|
127
132
|
|
128
133
|
# If the query does not match an index, we'll manually scan the associated table to find results.
|
@@ -136,7 +141,11 @@ module Dynamoid #:nodoc:
|
|
136
141
|
Dynamoid.logger.warn "You can index this query by adding this to #{source.to_s.downcase}.rb: index [#{source.attributes.sort.collect{|attr| ":#{attr}"}.join(', ')}]"
|
137
142
|
end
|
138
143
|
|
139
|
-
|
144
|
+
if @consistent_read
|
145
|
+
raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation'
|
146
|
+
end
|
147
|
+
|
148
|
+
Dynamoid::Adapter.scan(source.table_name, query, query_opts).collect {|hash| source.from_database(hash) }
|
140
149
|
end
|
141
150
|
|
142
151
|
# Format the provided query so that it can be used to query results from DynamoDB.
|
@@ -201,7 +210,7 @@ module Dynamoid #:nodoc:
|
|
201
210
|
|
202
211
|
def range?
|
203
212
|
return false unless source.range_key
|
204
|
-
query_keys == [
|
213
|
+
query_keys == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set)
|
205
214
|
end
|
206
215
|
|
207
216
|
def start_key
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module Dirty
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include ActiveModel::Dirty
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def from_database(*)
|
8
|
+
super.tap { |d| d.changed_attributes.clear }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def save(*)
|
13
|
+
clear_changes { super }
|
14
|
+
end
|
15
|
+
|
16
|
+
def reload
|
17
|
+
super.tap { clear_changes }
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear_changes
|
21
|
+
previous = changes
|
22
|
+
(block_given? ? yield : true).tap do |result|
|
23
|
+
unless result == false #failed validation; nil is OK.
|
24
|
+
@previously_changed = previous
|
25
|
+
changed_attributes.clear
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def write_attribute(name, value)
|
31
|
+
attribute_will_change!(name) unless self.read_attribute(name) == value
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def attribute_method?(attr)
|
38
|
+
super || self.class.attributes.has_key?(attr.to_sym)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/dynamoid/document.rb
CHANGED
@@ -6,18 +6,18 @@ module Dynamoid #:nodoc:
|
|
6
6
|
module Document
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
include Dynamoid::Components
|
9
|
-
|
9
|
+
|
10
10
|
included do
|
11
11
|
class_attribute :options
|
12
12
|
self.options = {}
|
13
|
-
|
13
|
+
|
14
14
|
Dynamoid::Config.included_models << self
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
module ClassMethods
|
18
|
-
# Set up table options, including naming it whatever you want, setting the id key, and manually overriding read and
|
18
|
+
# Set up table options, including naming it whatever you want, setting the id key, and manually overriding read and
|
19
19
|
# write capacity.
|
20
|
-
#
|
20
|
+
#
|
21
21
|
# @param [Hash] options options to pass for this table
|
22
22
|
# @option options [Symbol] :name the name for the table; this still gets namespaced
|
23
23
|
# @option options [Symbol] :id id column for the table
|
@@ -28,21 +28,21 @@ module Dynamoid #:nodoc:
|
|
28
28
|
def table(options = {})
|
29
29
|
self.options = options
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
# Returns the read_capacity for this table.
|
33
33
|
#
|
34
34
|
# @since 0.4.0
|
35
35
|
def read_capacity
|
36
36
|
options[:read_capacity] || Dynamoid::Config.read_capacity
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
# Returns the write_capacity for this table.
|
40
40
|
#
|
41
41
|
# @since 0.4.0
|
42
42
|
def write_capacity
|
43
43
|
options[:write_capacity] || Dynamoid::Config.write_capacity
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
# Returns the id field for this class.
|
47
47
|
#
|
48
48
|
# @since 0.4.0
|
@@ -71,7 +71,7 @@ module Dynamoid #:nodoc:
|
|
71
71
|
def create!(attrs = {})
|
72
72
|
new(attrs).tap(&:save!)
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
# Initialize a new object.
|
76
76
|
#
|
77
77
|
# @param [Hash] attrs Attributes with which to create the object.
|
@@ -101,50 +101,79 @@ module Dynamoid #:nodoc:
|
|
101
101
|
#
|
102
102
|
# @return [Dynamoid::Document] the new document
|
103
103
|
#
|
104
|
-
# @since 0.2.0
|
104
|
+
# @since 0.2.0
|
105
105
|
def initialize(attrs = {})
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
106
|
+
run_callbacks :initialize do
|
107
|
+
self.class.send(:field, self.class.hash_key) unless self.respond_to?(self.class.hash_key)
|
108
|
+
|
109
|
+
@new_record = true
|
110
|
+
@attributes ||= {}
|
111
|
+
@associations ||= {}
|
112
|
+
|
113
|
+
load(attrs)
|
114
|
+
end
|
115
|
+
end
|
111
116
|
|
117
|
+
def load(attrs)
|
112
118
|
self.class.undump(attrs).each {|key, value| send "#{key}=", value }
|
113
119
|
end
|
114
120
|
|
115
121
|
# An object is equal to another object if their ids are equal.
|
116
122
|
#
|
117
|
-
# @since 0.2.0
|
123
|
+
# @since 0.2.0
|
118
124
|
def ==(other)
|
119
|
-
|
120
|
-
|
125
|
+
if self.class.identity_map_on?
|
126
|
+
super
|
127
|
+
else
|
128
|
+
return false if other.nil?
|
129
|
+
other.respond_to?(:hash_key) && other.hash_key == self.hash_key
|
130
|
+
end
|
121
131
|
end
|
122
132
|
|
123
|
-
# Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
|
133
|
+
# Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
|
124
134
|
# changes to be reflected immediately, you would call this method.
|
125
135
|
#
|
126
136
|
# @return [Dynamoid::Document] the document this method was called on
|
127
137
|
#
|
128
|
-
# @since 0.2.0
|
138
|
+
# @since 0.2.0
|
129
139
|
def reload
|
130
|
-
self.attributes = self.class.find(
|
140
|
+
self.attributes = self.class.find(hash_key, :range_key => range_value).attributes
|
131
141
|
@associations.values.each(&:reset)
|
132
142
|
self
|
133
143
|
end
|
134
|
-
|
144
|
+
|
135
145
|
# Return an object's hash key, regardless of what it might be called to the object.
|
136
146
|
#
|
137
147
|
# @since 0.4.0
|
138
148
|
def hash_key
|
139
149
|
self.send(self.class.hash_key)
|
140
150
|
end
|
141
|
-
|
151
|
+
|
142
152
|
# Assign an object's hash key, regardless of what it might be called to the object.
|
143
153
|
#
|
144
154
|
# @since 0.4.0
|
145
|
-
def hash_key=(
|
146
|
-
self.send("#{self.class.hash_key}="
|
155
|
+
def hash_key=(value)
|
156
|
+
self.send("#{self.class.hash_key}=", value)
|
157
|
+
end
|
158
|
+
|
159
|
+
def range_value
|
160
|
+
if range_key = self.class.range_key
|
161
|
+
self.send(range_key)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def range_value=(value)
|
166
|
+
self.send("#{self.class.range_key}=", value)
|
167
|
+
end
|
168
|
+
|
169
|
+
def range_value
|
170
|
+
if range_key = self.class.range_key
|
171
|
+
self.send(range_key)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def range_value=(value)
|
176
|
+
self.send("#{self.class.range_key}=", value)
|
147
177
|
end
|
148
178
|
end
|
149
|
-
|
150
179
|
end
|
data/lib/dynamoid/errors.rb
CHANGED
@@ -13,11 +13,16 @@ module Dynamoid
|
|
13
13
|
# MissingRangeKey is raised when a table that requires a range key is quieried without one.
|
14
14
|
class MissingRangeKey < Error; end
|
15
15
|
|
16
|
+
# raised when the conditional check failed during update operation
|
17
|
+
class ConditionalCheckFailedException < Error; end
|
18
|
+
|
16
19
|
# DocumentNotValid is raised when the document fails validation.
|
17
20
|
class DocumentNotValid < Error
|
18
21
|
def initialize(document)
|
19
22
|
super("Validation failed: #{document.errors.full_messages.join(", ")}")
|
20
23
|
end
|
21
24
|
end
|
25
|
+
|
26
|
+
class InvalidQuery < Error; end
|
22
27
|
end
|
23
28
|
end
|
data/lib/dynamoid/fields.rb
CHANGED
@@ -12,7 +12,6 @@ module Dynamoid #:nodoc:
|
|
12
12
|
class_attribute :range_key
|
13
13
|
|
14
14
|
self.attributes = {}
|
15
|
-
|
16
15
|
field :created_at, :datetime
|
17
16
|
field :updated_at, :datetime
|
18
17
|
end
|
@@ -34,8 +33,6 @@ module Dynamoid #:nodoc:
|
|
34
33
|
define_method(named) { read_attribute(named) }
|
35
34
|
define_method("#{named}?") { !read_attribute(named).nil? }
|
36
35
|
define_method("#{named}=") {|value| write_attribute(named, value) }
|
37
|
-
|
38
|
-
respond_to?(:define_attribute_method) ? define_attribute_method(name) : define_attribute_methods([name])
|
39
36
|
end
|
40
37
|
|
41
38
|
def range(name, type = :string)
|
@@ -59,8 +56,6 @@ module Dynamoid #:nodoc:
|
|
59
56
|
Dynamoid.logger.warn "DynamoDB can't store items larger than #{MAX_ITEM_SIZE} and the #{name} field has a length of #{size}."
|
60
57
|
end
|
61
58
|
|
62
|
-
attribute_will_change!(name) unless self.read_attribute(name) == value
|
63
|
-
|
64
59
|
if association = @associations[name]
|
65
60
|
association.reset
|
66
61
|
end
|
data/lib/dynamoid/finders.rb
CHANGED
@@ -5,9 +5,9 @@ module Dynamoid
|
|
5
5
|
# class level, like find, find_by_id, and the method_missing style finders.
|
6
6
|
module Finders
|
7
7
|
extend ActiveSupport::Concern
|
8
|
-
|
8
|
+
|
9
9
|
module ClassMethods
|
10
|
-
|
10
|
+
|
11
11
|
# Find one or many objects, specified by one id or an array of ids.
|
12
12
|
#
|
13
13
|
# @param [Array/String] *id an array of ids or one single id
|
@@ -27,25 +27,73 @@ module Dynamoid
|
|
27
27
|
if ids.count == 1
|
28
28
|
self.find_by_id(ids.first, options)
|
29
29
|
else
|
30
|
-
|
31
|
-
items[self.table_name].collect{|i| self.build(i).tap { |o| o.new_record = false } }
|
30
|
+
find_all(ids)
|
32
31
|
end
|
33
32
|
end
|
34
33
|
|
34
|
+
# Find all object by hash key or hash and range key
|
35
|
+
#
|
36
|
+
# @param [Array<ID>] ids
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# find all the user with hash key
|
40
|
+
# User.find_all(['1', '2', '3'])
|
41
|
+
#
|
42
|
+
# find all the tweets using hash key and range key
|
43
|
+
# Tweet.find_all([['1', 'red'], ['1', 'green'])
|
44
|
+
def find_all(ids)
|
45
|
+
items = Dynamoid::Adapter.read(self.table_name, ids, options)
|
46
|
+
items[self.table_name].collect{|i| from_database(i) }
|
47
|
+
end
|
48
|
+
|
35
49
|
# Find one object directly by id.
|
36
50
|
#
|
37
51
|
# @param [String] id the id of the object to find
|
38
52
|
#
|
39
53
|
# @return [Dynamoid::Document] the found object, or nil if nothing was found
|
40
54
|
#
|
41
|
-
# @since 0.2.0
|
55
|
+
# @since 0.2.0
|
42
56
|
def find_by_id(id, options = {})
|
43
57
|
if item = Dynamoid::Adapter.read(self.table_name, id, options)
|
44
|
-
|
45
|
-
obj.new_record = false
|
46
|
-
return obj
|
58
|
+
from_database(item)
|
47
59
|
else
|
48
|
-
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Find one object directly by hash and range keys
|
65
|
+
#
|
66
|
+
# @param [String] hash_key of the object to find
|
67
|
+
# @param [String/Integer/Float] range_key of the object to find
|
68
|
+
#
|
69
|
+
def find_by_composite_key(hash_key, range_key, options = {})
|
70
|
+
find_by_id(hash_key, options.merge({:range_key => range_key}))
|
71
|
+
end
|
72
|
+
|
73
|
+
# Find all objects by hash and range keys.
|
74
|
+
#
|
75
|
+
# @example find all ChamberTypes whose level is greater than 1
|
76
|
+
# class ChamberType
|
77
|
+
# include Dynamoid::Document
|
78
|
+
# field :chamber_type, :string
|
79
|
+
# range :level, :integer
|
80
|
+
# table :key => :chamber_type
|
81
|
+
# end
|
82
|
+
# ChamberType.find_all_by_composite_key('DustVault', range_greater_than: 1)
|
83
|
+
#
|
84
|
+
# @param [String] hash_key of the objects to find
|
85
|
+
# @param [Hash] options the options for the range key
|
86
|
+
# @option options [Range] :range_value find the range key within this range
|
87
|
+
# @option options [Number] :range_greater_than find range keys greater than this
|
88
|
+
# @option options [Number] :range_less_than find range keys less than this
|
89
|
+
# @option options [Number] :range_gte find range keys greater than or equal to this
|
90
|
+
# @option options [Number] :range_lte find range keys less than or equal to this
|
91
|
+
#
|
92
|
+
# @return [Array] an array of all matching items
|
93
|
+
#
|
94
|
+
def find_all_by_composite_key(hash_key, options = {})
|
95
|
+
Dynamoid::Adapter.query(self.table_name, options.merge({hash_value: hash_key})).collect do |item|
|
96
|
+
from_database(item)
|
49
97
|
end
|
50
98
|
end
|
51
99
|
|
@@ -59,7 +107,7 @@ module Dynamoid
|
|
59
107
|
#
|
60
108
|
# @return [Dynamoid::Document/Array] the found object, or an array of found objects if all was somewhere in the method
|
61
109
|
#
|
62
|
-
# @since 0.2.0
|
110
|
+
# @since 0.2.0
|
63
111
|
def method_missing(method, *args)
|
64
112
|
if method =~ /find/
|
65
113
|
finder = method.to_s.split('_by_').first
|
@@ -67,7 +115,7 @@ module Dynamoid
|
|
67
115
|
|
68
116
|
chain = Dynamoid::Criteria::Chain.new(self)
|
69
117
|
chain.query = Hash.new.tap {|h| attributes.each_with_index {|attr, index| h[attr.to_sym] = args[index]}}
|
70
|
-
|
118
|
+
|
71
119
|
if finder =~ /all/
|
72
120
|
return chain.all
|
73
121
|
else
|
@@ -79,5 +127,5 @@ module Dynamoid
|
|
79
127
|
end
|
80
128
|
end
|
81
129
|
end
|
82
|
-
|
130
|
+
|
83
131
|
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module IdentityMap
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def self.clear
|
6
|
+
models.each { |m| m.identity_map.clear }
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.models
|
10
|
+
Dynamoid::Config.included_models
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def identity_map
|
15
|
+
@identity_map ||= {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def from_database(attrs = {})
|
19
|
+
return super if identity_map_off?
|
20
|
+
|
21
|
+
key = identity_map_key(attrs)
|
22
|
+
document = identity_map[key]
|
23
|
+
|
24
|
+
if document.nil?
|
25
|
+
document = super
|
26
|
+
identity_map[key] = document
|
27
|
+
else
|
28
|
+
document.load(attrs)
|
29
|
+
end
|
30
|
+
|
31
|
+
document
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_by_id(id, options = {})
|
35
|
+
return super if identity_map_off?
|
36
|
+
|
37
|
+
key = id.to_s
|
38
|
+
|
39
|
+
if range_key = options[:range_key]
|
40
|
+
key += "::#{range_key}"
|
41
|
+
end
|
42
|
+
|
43
|
+
if identity_map[key]
|
44
|
+
identity_map[key]
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def identity_map_key(attrs)
|
51
|
+
key = attrs[hash_key].to_s
|
52
|
+
if range_key
|
53
|
+
key += "::#{attrs[range_key]}"
|
54
|
+
end
|
55
|
+
key
|
56
|
+
end
|
57
|
+
|
58
|
+
def identity_map_on?
|
59
|
+
Dynamoid::Config.identity_map
|
60
|
+
end
|
61
|
+
|
62
|
+
def identity_map_off?
|
63
|
+
!identity_map_on?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def identity_map
|
68
|
+
self.class.identity_map
|
69
|
+
end
|
70
|
+
|
71
|
+
def save(*args)
|
72
|
+
return super if self.class.identity_map_off?
|
73
|
+
|
74
|
+
if result = super
|
75
|
+
identity_map[identity_map_key] = self
|
76
|
+
end
|
77
|
+
result
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete
|
81
|
+
return super if self.class.identity_map_off?
|
82
|
+
|
83
|
+
identity_map.delete(identity_map_key)
|
84
|
+
super
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def identity_map_key
|
89
|
+
key = hash_key.to_s
|
90
|
+
if self.class.range_key
|
91
|
+
key += "::#{range_value}"
|
92
|
+
end
|
93
|
+
key
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|