dynamoid 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|