dynamoid-moda 0.7.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 +15 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +118 -0
- data/Gemfile_activemodel4 +24 -0
- data/Gemfile_activemodel4.lock +88 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +360 -0
- data/Rakefile +93 -0
- data/VERSION +1 -0
- data/doc/.nojekyll +0 -0
- data/doc/Dynamoid.html +328 -0
- data/doc/Dynamoid/Adapter.html +1872 -0
- data/doc/Dynamoid/Adapter/AwsSdk.html +2101 -0
- data/doc/Dynamoid/Adapter/Local.html +1574 -0
- data/doc/Dynamoid/Associations.html +138 -0
- data/doc/Dynamoid/Associations/Association.html +847 -0
- data/doc/Dynamoid/Associations/BelongsTo.html +161 -0
- data/doc/Dynamoid/Associations/ClassMethods.html +766 -0
- data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +167 -0
- data/doc/Dynamoid/Associations/HasMany.html +167 -0
- data/doc/Dynamoid/Associations/HasOne.html +161 -0
- data/doc/Dynamoid/Associations/ManyAssociation.html +1684 -0
- data/doc/Dynamoid/Associations/SingleAssociation.html +627 -0
- data/doc/Dynamoid/Components.html +242 -0
- data/doc/Dynamoid/Config.html +412 -0
- data/doc/Dynamoid/Config/Options.html +638 -0
- data/doc/Dynamoid/Criteria.html +138 -0
- data/doc/Dynamoid/Criteria/Chain.html +1471 -0
- data/doc/Dynamoid/Criteria/ClassMethods.html +105 -0
- data/doc/Dynamoid/Dirty.html +424 -0
- data/doc/Dynamoid/Dirty/ClassMethods.html +174 -0
- data/doc/Dynamoid/Document.html +1033 -0
- data/doc/Dynamoid/Document/ClassMethods.html +1116 -0
- data/doc/Dynamoid/Errors.html +125 -0
- data/doc/Dynamoid/Errors/ConditionalCheckFailedException.html +141 -0
- data/doc/Dynamoid/Errors/DocumentNotValid.html +221 -0
- data/doc/Dynamoid/Errors/Error.html +137 -0
- data/doc/Dynamoid/Errors/InvalidField.html +141 -0
- data/doc/Dynamoid/Errors/InvalidQuery.html +131 -0
- data/doc/Dynamoid/Errors/MissingRangeKey.html +141 -0
- data/doc/Dynamoid/Fields.html +686 -0
- data/doc/Dynamoid/Fields/ClassMethods.html +438 -0
- data/doc/Dynamoid/Finders.html +135 -0
- data/doc/Dynamoid/Finders/ClassMethods.html +943 -0
- data/doc/Dynamoid/IdentityMap.html +492 -0
- data/doc/Dynamoid/IdentityMap/ClassMethods.html +534 -0
- data/doc/Dynamoid/Indexes.html +321 -0
- data/doc/Dynamoid/Indexes/ClassMethods.html +369 -0
- data/doc/Dynamoid/Indexes/Index.html +1142 -0
- data/doc/Dynamoid/Middleware.html +115 -0
- data/doc/Dynamoid/Middleware/IdentityMap.html +264 -0
- data/doc/Dynamoid/Persistence.html +892 -0
- data/doc/Dynamoid/Persistence/ClassMethods.html +836 -0
- data/doc/Dynamoid/Validations.html +415 -0
- data/doc/_index.html +506 -0
- data/doc/class_list.html +53 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +338 -0
- data/doc/file.LICENSE.html +73 -0
- data/doc/file.README.html +416 -0
- data/doc/file_list.html +58 -0
- data/doc/frames.html +28 -0
- data/doc/index.html +416 -0
- data/doc/js/app.js +214 -0
- data/doc/js/full_list.js +178 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +1144 -0
- data/doc/top-level-namespace.html +112 -0
- data/dynamoid-moda.gemspec +210 -0
- data/dynamoid.gemspec +208 -0
- data/lib/dynamoid.rb +46 -0
- data/lib/dynamoid/adapter.rb +267 -0
- data/lib/dynamoid/adapter/aws_sdk.rb +309 -0
- data/lib/dynamoid/associations.rb +106 -0
- data/lib/dynamoid/associations/association.rb +105 -0
- data/lib/dynamoid/associations/belongs_to.rb +44 -0
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
- data/lib/dynamoid/associations/has_many.rb +39 -0
- data/lib/dynamoid/associations/has_one.rb +39 -0
- data/lib/dynamoid/associations/many_association.rb +191 -0
- data/lib/dynamoid/associations/single_association.rb +69 -0
- data/lib/dynamoid/components.rb +37 -0
- data/lib/dynamoid/config.rb +57 -0
- data/lib/dynamoid/config/options.rb +78 -0
- data/lib/dynamoid/criteria.rb +29 -0
- data/lib/dynamoid/criteria/chain.rb +326 -0
- data/lib/dynamoid/dirty.rb +47 -0
- data/lib/dynamoid/document.rb +199 -0
- data/lib/dynamoid/errors.rb +28 -0
- data/lib/dynamoid/fields.rb +138 -0
- data/lib/dynamoid/finders.rb +133 -0
- data/lib/dynamoid/identity_map.rb +96 -0
- data/lib/dynamoid/indexes.rb +69 -0
- data/lib/dynamoid/indexes/index.rb +103 -0
- data/lib/dynamoid/middleware/identity_map.rb +16 -0
- data/lib/dynamoid/persistence.rb +292 -0
- data/lib/dynamoid/validations.rb +36 -0
- data/spec/app/models/address.rb +13 -0
- data/spec/app/models/camel_case.rb +34 -0
- data/spec/app/models/car.rb +6 -0
- data/spec/app/models/magazine.rb +11 -0
- data/spec/app/models/message.rb +9 -0
- data/spec/app/models/nuclear_submarine.rb +5 -0
- data/spec/app/models/sponsor.rb +8 -0
- data/spec/app/models/subscription.rb +12 -0
- data/spec/app/models/tweet.rb +12 -0
- data/spec/app/models/user.rb +26 -0
- data/spec/app/models/vehicle.rb +7 -0
- data/spec/dynamoid/adapter/aws_sdk_spec.rb +376 -0
- data/spec/dynamoid/adapter_spec.rb +155 -0
- data/spec/dynamoid/associations/association_spec.rb +194 -0
- data/spec/dynamoid/associations/belongs_to_spec.rb +71 -0
- data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +47 -0
- data/spec/dynamoid/associations/has_many_spec.rb +42 -0
- data/spec/dynamoid/associations/has_one_spec.rb +45 -0
- data/spec/dynamoid/associations_spec.rb +16 -0
- data/spec/dynamoid/config_spec.rb +27 -0
- data/spec/dynamoid/criteria/chain_spec.rb +210 -0
- data/spec/dynamoid/criteria_spec.rb +75 -0
- data/spec/dynamoid/dirty_spec.rb +57 -0
- data/spec/dynamoid/document_spec.rb +180 -0
- data/spec/dynamoid/fields_spec.rb +156 -0
- data/spec/dynamoid/finders_spec.rb +147 -0
- data/spec/dynamoid/identity_map_spec.rb +45 -0
- data/spec/dynamoid/indexes/index_spec.rb +104 -0
- data/spec/dynamoid/indexes_spec.rb +25 -0
- data/spec/dynamoid/persistence_spec.rb +301 -0
- data/spec/dynamoid/validations_spec.rb +36 -0
- data/spec/dynamoid_spec.rb +14 -0
- data/spec/spec_helper.rb +55 -0
- data/spec/support/with_partitioning.rb +15 -0
- metadata +363 -0
data/lib/dynamoid.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require "delegate"
|
|
2
|
+
require "time"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "active_support/core_ext"
|
|
5
|
+
require 'active_support/json'
|
|
6
|
+
require "active_support/inflector"
|
|
7
|
+
require "active_support/lazy_load_hooks"
|
|
8
|
+
require "active_support/time_with_zone"
|
|
9
|
+
require "active_model"
|
|
10
|
+
|
|
11
|
+
require 'dynamoid/errors'
|
|
12
|
+
require 'dynamoid/fields'
|
|
13
|
+
require 'dynamoid/indexes'
|
|
14
|
+
require 'dynamoid/associations'
|
|
15
|
+
require 'dynamoid/persistence'
|
|
16
|
+
require 'dynamoid/dirty'
|
|
17
|
+
require 'dynamoid/validations'
|
|
18
|
+
require 'dynamoid/criteria'
|
|
19
|
+
require 'dynamoid/finders'
|
|
20
|
+
require 'dynamoid/identity_map'
|
|
21
|
+
require 'dynamoid/config'
|
|
22
|
+
require 'dynamoid/components'
|
|
23
|
+
require 'dynamoid/document'
|
|
24
|
+
require 'dynamoid/adapter'
|
|
25
|
+
|
|
26
|
+
require 'dynamoid/middleware/identity_map'
|
|
27
|
+
|
|
28
|
+
module Dynamoid
|
|
29
|
+
extend self
|
|
30
|
+
|
|
31
|
+
MAX_ITEM_SIZE = 65_536
|
|
32
|
+
|
|
33
|
+
def configure
|
|
34
|
+
block_given? ? yield(Dynamoid::Config) : Dynamoid::Config
|
|
35
|
+
end
|
|
36
|
+
alias :config :configure
|
|
37
|
+
|
|
38
|
+
def logger
|
|
39
|
+
Dynamoid::Config.logger
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def included_models
|
|
43
|
+
@included_models ||= []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
module Dynamoid
|
|
3
|
+
|
|
4
|
+
# Adapter provides a generic, write-through class that abstracts variations in the underlying connections to provide a uniform response
|
|
5
|
+
# to Dynamoid.
|
|
6
|
+
module Adapter
|
|
7
|
+
extend self
|
|
8
|
+
attr_accessor :tables
|
|
9
|
+
|
|
10
|
+
# The actual adapter currently in use: presently AwsSdk.
|
|
11
|
+
#
|
|
12
|
+
# @since 0.2.0
|
|
13
|
+
def adapter
|
|
14
|
+
reconnect! unless @adapter
|
|
15
|
+
@adapter
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Establishes a connection to the underyling adapter and caches all its tables for speedier future lookups. Issued when the adapter is first called.
|
|
19
|
+
#
|
|
20
|
+
# @since 0.2.0
|
|
21
|
+
def reconnect!
|
|
22
|
+
require "dynamoid/adapter/#{Dynamoid::Config.adapter}" unless Dynamoid::Adapter.const_defined?(Dynamoid::Config.adapter.camelcase)
|
|
23
|
+
@adapter = Dynamoid::Adapter.const_get(Dynamoid::Config.adapter.camelcase)
|
|
24
|
+
@adapter.connect! if @adapter.respond_to?(:connect!)
|
|
25
|
+
self.tables = benchmark('Cache Tables') {list_tables}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Shows how long it takes a method to run on the adapter. Useful for generating logged output.
|
|
29
|
+
#
|
|
30
|
+
# @param [Symbol] method the name of the method to appear in the log
|
|
31
|
+
# @param [Array] args the arguments to the method to appear in the log
|
|
32
|
+
# @yield the actual code to benchmark
|
|
33
|
+
#
|
|
34
|
+
# @return the result of the yield
|
|
35
|
+
#
|
|
36
|
+
# @since 0.2.0
|
|
37
|
+
def benchmark(method, *args)
|
|
38
|
+
start = Time.now
|
|
39
|
+
result = yield
|
|
40
|
+
Dynamoid.logger.info "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }"
|
|
41
|
+
return result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Write an object to the adapter. Partition it to a randomly selected key first if necessary.
|
|
45
|
+
#
|
|
46
|
+
# @param [String] table the name of the table to write the object to
|
|
47
|
+
# @param [Object] object the object itself
|
|
48
|
+
# @param [Hash] options Options that are passed to the put_item call
|
|
49
|
+
#
|
|
50
|
+
# @return [Object] the persisted object
|
|
51
|
+
#
|
|
52
|
+
# @since 0.2.0
|
|
53
|
+
def write(table, object, options = nil)
|
|
54
|
+
if Dynamoid::Config.partitioning? && object[:id]
|
|
55
|
+
object[:id] = "#{object[:id]}.#{Random.rand(Dynamoid::Config.partition_size)}"
|
|
56
|
+
object[:updated_at] = Time.now.to_f
|
|
57
|
+
end
|
|
58
|
+
put_item(table, object, options)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Read one or many keys from the selected table. This method intelligently calls batch_get or get on the underlying adapter depending on
|
|
62
|
+
# whether ids is a range or a single key: additionally, if partitioning is enabled, it batch_gets all keys in the partition space
|
|
63
|
+
# automatically. Finally, if a range key is present, it will also interpolate that into the ids so that the batch get will acquire the
|
|
64
|
+
# correct record.
|
|
65
|
+
#
|
|
66
|
+
# @param [String] table the name of the table to write the object to
|
|
67
|
+
# @param [Array] ids to fetch, can also be a string of just one id
|
|
68
|
+
# @param [Hash] options: Passed to the underlying query. The :range_key option is required whenever the table has a range key,
|
|
69
|
+
# unless multiple ids are passed in and Dynamoid::Config.partitioning? is turned off.
|
|
70
|
+
#
|
|
71
|
+
# @since 0.2.0
|
|
72
|
+
def read(table, ids, options = {})
|
|
73
|
+
range_key = options.delete(:range_key)
|
|
74
|
+
|
|
75
|
+
if ids.respond_to?(:each)
|
|
76
|
+
ids = ids.collect{|id| range_key ? [id, range_key] : id}
|
|
77
|
+
if Dynamoid::Config.partitioning?
|
|
78
|
+
results = batch_get_item({table => id_with_partitions(ids)}, options)
|
|
79
|
+
{table => result_for_partition(results[table],table)}
|
|
80
|
+
else
|
|
81
|
+
batch_get_item({table => ids}, options)
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
if Dynamoid::Config.partitioning?
|
|
85
|
+
ids = range_key ? [[ids, range_key]] : ids
|
|
86
|
+
results = batch_get_item({table => id_with_partitions(ids)}, options)
|
|
87
|
+
result_for_partition(results[table],table).first
|
|
88
|
+
else
|
|
89
|
+
options[:range_key] = range_key if range_key
|
|
90
|
+
get_item(table, ids, options)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Delete an item from a table. If partitioning is turned on, deletes all partitioned keys as well.
|
|
96
|
+
#
|
|
97
|
+
# @param [String] table the name of the table to write the object to
|
|
98
|
+
# @param [Array] ids to delete, can also be a string of just one id
|
|
99
|
+
# @param [Array] range_key of the record to delete, can also be a string of just one range_key
|
|
100
|
+
#
|
|
101
|
+
def delete(table, ids, options = {})
|
|
102
|
+
range_key = options[:range_key] #array of range keys that matches the ids passed in
|
|
103
|
+
if ids.respond_to?(:each)
|
|
104
|
+
if range_key.respond_to?(:each)
|
|
105
|
+
#turn ids into array of arrays each element being hash_key, range_key
|
|
106
|
+
ids = ids.each_with_index.map{|id,i| [id,range_key[i]]}
|
|
107
|
+
else
|
|
108
|
+
ids = range_key ? [[ids, range_key]] : ids
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if Dynamoid::Config.partitioning?
|
|
112
|
+
batch_delete_item(table => id_with_partitions(ids))
|
|
113
|
+
else
|
|
114
|
+
batch_delete_item(table => ids)
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
if Dynamoid::Config.partitioning?
|
|
118
|
+
ids = range_key ? [[ids, range_key]] : ids
|
|
119
|
+
batch_delete_item(table => id_with_partitions(ids))
|
|
120
|
+
else
|
|
121
|
+
delete_item(table, ids, options)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Scans a table. Generally quite slow; try to avoid using scan if at all possible.
|
|
127
|
+
#
|
|
128
|
+
# @param [String] table the name of the table to write the object to
|
|
129
|
+
# @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
|
|
130
|
+
#
|
|
131
|
+
# @since 0.2.0
|
|
132
|
+
def scan(table, query, opts = {})
|
|
133
|
+
if Dynamoid::Config.partitioning?
|
|
134
|
+
results = benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
|
|
135
|
+
result_for_partition(results,table)
|
|
136
|
+
else
|
|
137
|
+
benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
[:batch_get_item, :create_table, :delete_item, :delete_table, :get_item, :list_tables, :put_item].each do |m|
|
|
142
|
+
# Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing.
|
|
143
|
+
#
|
|
144
|
+
# @since 0.2.0
|
|
145
|
+
define_method(m) do |*args|
|
|
146
|
+
benchmark("#{m.to_s}", args) {adapter.send(m, *args)}
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Takes a list of ids and returns them with partitioning added. If an array of arrays is passed, we assume the second key is the range key
|
|
151
|
+
# and pass it in unchanged.
|
|
152
|
+
#
|
|
153
|
+
# @example Partition id 1
|
|
154
|
+
# Dynamoid::Adapter.id_with_partitions(['1']) # ['1.0', '1.1', '1.2', ..., '1.199']
|
|
155
|
+
# @example Partition id 1 and range_key 1.0
|
|
156
|
+
# Dynamoid::Adapter.id_with_partitions([['1', 1.0]]) # [['1.0', 1.0], ['1.1', 1.0], ['1.2', 1.0], ..., ['1.199', 1.0]]
|
|
157
|
+
#
|
|
158
|
+
# @param [Array] ids array of ids to partition
|
|
159
|
+
#
|
|
160
|
+
# @since 0.2.0
|
|
161
|
+
def id_with_partitions(ids)
|
|
162
|
+
Array(ids).collect {|id| (0...Dynamoid::Config.partition_size).collect{|n| id.is_a?(Array) ? ["#{id.first}.#{n}", id.last] : "#{id}.#{n}"}}.flatten(1)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
#Get original id (hash_key) and partiton number from a hash_key
|
|
166
|
+
#
|
|
167
|
+
# @param [String] id the id or hash_key of a record, ex. xxxxx.13
|
|
168
|
+
#
|
|
169
|
+
# @return [String,String] original_id and the partition number, ex original_id = xxxxx partition = 13
|
|
170
|
+
def get_original_id_and_partition id
|
|
171
|
+
partition = id.split('.').last
|
|
172
|
+
id = id.split(".#{partition}").first
|
|
173
|
+
|
|
174
|
+
return id, partition
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Takes an array of query results that are partitioned, find the most recently updated ones that share an id and range_key, and return only the most recently updated. Compares each result by
|
|
178
|
+
# their id and updated_at attributes; if the updated_at is the greatest, then it must be the correct result.
|
|
179
|
+
#
|
|
180
|
+
# @param [Array] returned partitioned results from a query
|
|
181
|
+
# @param [String] table_name the name of the table
|
|
182
|
+
#
|
|
183
|
+
# @since 0.2.0
|
|
184
|
+
def result_for_partition(results, table_name)
|
|
185
|
+
table = adapter.get_table(table_name)
|
|
186
|
+
|
|
187
|
+
if table.range_key
|
|
188
|
+
range_key_name = table.range_key.name.to_sym
|
|
189
|
+
|
|
190
|
+
final_hash = {}
|
|
191
|
+
|
|
192
|
+
results.each do |record|
|
|
193
|
+
test_record = final_hash[record[range_key_name]]
|
|
194
|
+
|
|
195
|
+
if test_record.nil? || ((record[range_key_name] == test_record[range_key_name]) && (record[:updated_at] > test_record[:updated_at]))
|
|
196
|
+
#get ride of our partition and put it in the array with the range key
|
|
197
|
+
record[:id], partition = get_original_id_and_partition record[:id]
|
|
198
|
+
final_hash[record[range_key_name]] = record
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
return final_hash.values
|
|
203
|
+
else
|
|
204
|
+
{}.tap do |hash|
|
|
205
|
+
Array(results).each do |result|
|
|
206
|
+
next if result.nil?
|
|
207
|
+
#Need to find the value of id with out the . and partition number
|
|
208
|
+
id, partition = get_original_id_and_partition result[:id]
|
|
209
|
+
|
|
210
|
+
if !hash[id] || (result[:updated_at] > hash[id][:updated_at])
|
|
211
|
+
result[:id] = id
|
|
212
|
+
hash[id] = result
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end.values
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Delegate all methods that aren't defind here to the underlying adapter.
|
|
220
|
+
#
|
|
221
|
+
# @since 0.2.0
|
|
222
|
+
def method_missing(method, *args, &block)
|
|
223
|
+
return benchmark(method, *args) {adapter.send(method, *args, &block)} if @adapter.respond_to?(method)
|
|
224
|
+
super
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
|
|
228
|
+
# only really useful for range queries, since it can only find by one hash key at once. Only provide
|
|
229
|
+
# one range key to the hash. If paritioning is on, will run a query for every parition and join the results
|
|
230
|
+
#
|
|
231
|
+
# @param [String] table_name the name of the table
|
|
232
|
+
# @param [Hash] opts the options to query the table with
|
|
233
|
+
# @option opts [String] :hash_value the value of the hash key to find
|
|
234
|
+
# @option opts [Range] :range_value find the range key within this range
|
|
235
|
+
# @option opts [Number] :range_greater_than find range keys greater than this
|
|
236
|
+
# @option opts [Number] :range_less_than find range keys less than this
|
|
237
|
+
# @option opts [Number] :range_gte find range keys greater than or equal to this
|
|
238
|
+
# @option opts [Number] :range_lte find range keys less than or equal to this
|
|
239
|
+
#
|
|
240
|
+
# @return [Array] an array of all matching items
|
|
241
|
+
#
|
|
242
|
+
def query(table_name, opts = {})
|
|
243
|
+
|
|
244
|
+
unless Dynamoid::Config.partitioning?
|
|
245
|
+
#no paritioning? just pass to the standard query method
|
|
246
|
+
adapter.query(table_name, opts)
|
|
247
|
+
else
|
|
248
|
+
#get all the hash_values that could be possible
|
|
249
|
+
ids = id_with_partitions(opts[:hash_value])
|
|
250
|
+
|
|
251
|
+
#lets not overwrite with the original options
|
|
252
|
+
modified_options = opts.clone
|
|
253
|
+
results = []
|
|
254
|
+
|
|
255
|
+
#loop and query on each of the partition ids
|
|
256
|
+
ids.each do |id|
|
|
257
|
+
modified_options[:hash_value] = id
|
|
258
|
+
|
|
259
|
+
query_result = adapter.query(table_name, modified_options)
|
|
260
|
+
results += query_result.inject([]){|array, result| array += [result]} if query_result.any?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
result_for_partition results, table_name
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require 'aws'
|
|
3
|
+
|
|
4
|
+
module Dynamoid
|
|
5
|
+
module Adapter
|
|
6
|
+
|
|
7
|
+
# The AwsSdk adapter provides support for the AWS-SDK for Ruby gem.
|
|
8
|
+
# More information is available at that Gem's Github page:
|
|
9
|
+
# https://github.com/amazonwebservices/aws-sdk-for-ruby
|
|
10
|
+
#
|
|
11
|
+
module AwsSdk
|
|
12
|
+
extend self
|
|
13
|
+
@@connection = nil
|
|
14
|
+
|
|
15
|
+
# Establish the connection to DynamoDB.
|
|
16
|
+
#
|
|
17
|
+
# @return [AWS::DynamoDB::Connection] the raw DynamoDB connection
|
|
18
|
+
# Call DynamoDB new, with no parameters.
|
|
19
|
+
# Make sure the aws.yml file or aws.rb file, refer the link for more details.
|
|
20
|
+
#https://github.com/amazonwebservices/aws-sdk-for-ruby
|
|
21
|
+
# 1. Create config/aws.yml as follows:
|
|
22
|
+
# Fill in your AWS Access Key ID and Secret Access Key
|
|
23
|
+
# http://aws.amazon.com/security-credentials
|
|
24
|
+
#access_key_id: REPLACE_WITH_ACCESS_KEY_ID
|
|
25
|
+
#secret_access_key: REPLACE_WITH_SECRET_ACCESS_KEY
|
|
26
|
+
#(or)
|
|
27
|
+
#2, Create config/initializers/aws.rb as follows:
|
|
28
|
+
# load the libraries
|
|
29
|
+
#require 'aws'
|
|
30
|
+
# log requests using the default rails logger
|
|
31
|
+
#AWS.config(:logger => Rails.logger)
|
|
32
|
+
# load credentials from a file
|
|
33
|
+
#config_path = File.expand_path(File.dirname(__FILE__)+"/../aws.yml")
|
|
34
|
+
#AWS.config(YAML.load(File.read(config_path)))
|
|
35
|
+
#Additionally include any of the dynamodb paramters as needed
|
|
36
|
+
#(eg: if you would like to change the dynamodb endpoint, then add the parameter in
|
|
37
|
+
# the following paramter in the file aws.yml or aws.rb
|
|
38
|
+
# dynamo_db_endpoint : dynamodb.ap-southeast-1.amazonaws.com)
|
|
39
|
+
# @since 0.2.0
|
|
40
|
+
def connect!
|
|
41
|
+
@@connection = AWS::DynamoDB.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Return the established connection.
|
|
45
|
+
#
|
|
46
|
+
# @return [AWS::DynamoDB::Connection] the raw DynamoDB connection
|
|
47
|
+
#
|
|
48
|
+
# @since 0.2.0
|
|
49
|
+
def connection
|
|
50
|
+
@@connection
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get many items at once from DynamoDB. More efficient than getting each item individually.
|
|
54
|
+
#
|
|
55
|
+
# @example Retrieve IDs 1 and 2 from the table testtable
|
|
56
|
+
# Dynamoid::Adapter::AwsSdk.batch_get_item({'table1' => ['1', '2']}, :consistent_read => true)
|
|
57
|
+
#
|
|
58
|
+
# @param [Hash] table_ids the hash of tables and IDs to retrieve
|
|
59
|
+
# @param [Hash] options to be passed to underlying BatchGet call
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] a hash where keys are the table names and the values are the retrieved items
|
|
62
|
+
#
|
|
63
|
+
# @since 0.2.0
|
|
64
|
+
def batch_get_item(table_ids, options = {})
|
|
65
|
+
hash = Hash.new{|h, k| h[k] = []}
|
|
66
|
+
return hash if table_ids.all?{|k, v| v.empty?}
|
|
67
|
+
table_ids.each do |t, ids|
|
|
68
|
+
Array(ids).in_groups_of(100, false) do |group|
|
|
69
|
+
batch = AWS::DynamoDB::BatchGet.new(:config => @@connection.config)
|
|
70
|
+
batch.table(t, :all, Array(group), options) unless group.nil? || group.empty?
|
|
71
|
+
batch.each do |table_name, attributes|
|
|
72
|
+
hash[table_name] << attributes.symbolize_keys!
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
hash
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Delete many items at once from DynamoDB. More efficient than delete each item individually.
|
|
80
|
+
#
|
|
81
|
+
# @example Delete IDs 1 and 2 from the table testtable
|
|
82
|
+
# Dynamoid::Adapter::AwsSdk.batch_delete_item('table1' => ['1', '2'])
|
|
83
|
+
#or
|
|
84
|
+
# Dynamoid::Adapter::AwsSdk.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]]))
|
|
85
|
+
#
|
|
86
|
+
# @param [Hash] options the hash of tables and IDs to delete
|
|
87
|
+
#
|
|
88
|
+
# @return nil
|
|
89
|
+
#
|
|
90
|
+
def batch_delete_item(options)
|
|
91
|
+
return nil if options.all?{|k, v| v.empty?}
|
|
92
|
+
options.each do |t, ids|
|
|
93
|
+
Array(ids).in_groups_of(25, false) do |group|
|
|
94
|
+
batch = AWS::DynamoDB::BatchWrite.new(:config => @@connection.config)
|
|
95
|
+
batch.delete(t,group)
|
|
96
|
+
batch.process!
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Create a table on DynamoDB. This usually takes a long time to complete.
|
|
103
|
+
#
|
|
104
|
+
# @param [String] table_name the name of the table to create
|
|
105
|
+
# @param [Symbol] key the table's primary key (defaults to :id)
|
|
106
|
+
# @param [Hash] options provide a range_key here if you want one for the table
|
|
107
|
+
#
|
|
108
|
+
# @since 0.2.0
|
|
109
|
+
def create_table(table_name, key = :id, options = {})
|
|
110
|
+
Dynamoid.logger.info "Creating #{table_name} table. This could take a while."
|
|
111
|
+
options[:hash_key] ||= {key.to_sym => :string}
|
|
112
|
+
read_capacity = options[:read_capacity] || Dynamoid::Config.read_capacity
|
|
113
|
+
write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity
|
|
114
|
+
table = @@connection.tables.create(table_name, read_capacity, write_capacity, options)
|
|
115
|
+
sleep 0.5 while table.status == :creating
|
|
116
|
+
return table
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Removes an item from DynamoDB.
|
|
120
|
+
#
|
|
121
|
+
# @param [String] table_name the name of the table
|
|
122
|
+
# @param [String] key the hash key of the item to delete
|
|
123
|
+
# @param [Number] range_key the range key of the item to delete, required if the table has a composite key
|
|
124
|
+
#
|
|
125
|
+
# @since 0.2.0
|
|
126
|
+
def delete_item(table_name, key, options = {})
|
|
127
|
+
range_key = options.delete(:range_key)
|
|
128
|
+
table = get_table(table_name)
|
|
129
|
+
result = table.items.at(key, range_key)
|
|
130
|
+
result.delete unless result.attributes.to_h.empty?
|
|
131
|
+
true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Deletes an entire table from DynamoDB. Only 10 tables can be in the deleting state at once,
|
|
135
|
+
# so if you have more this method may raise an exception.
|
|
136
|
+
#
|
|
137
|
+
# @param [String] table_name the name of the table to destroy
|
|
138
|
+
#
|
|
139
|
+
# @since 0.2.0
|
|
140
|
+
def delete_table(table_name)
|
|
141
|
+
Dynamoid.logger.info "Deleting #{table_name} table. This could take a while."
|
|
142
|
+
table = @@connection.tables[table_name]
|
|
143
|
+
table.delete
|
|
144
|
+
sleep 0.5 while table.exists? == true
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @todo Add a DescribeTable method.
|
|
148
|
+
|
|
149
|
+
# Fetches an item from DynamoDB.
|
|
150
|
+
#
|
|
151
|
+
# @param [String] table_name the name of the table
|
|
152
|
+
# @param [String] key the hash key of the item to find
|
|
153
|
+
# @param [Number] range_key the range key of the item to find, required if the table has a composite key
|
|
154
|
+
#
|
|
155
|
+
# @return [Hash] a hash representing the raw item in DynamoDB
|
|
156
|
+
#
|
|
157
|
+
# @since 0.2.0
|
|
158
|
+
def get_item(table_name, key, options = {})
|
|
159
|
+
range_key = options.delete(:range_key)
|
|
160
|
+
table = get_table(table_name)
|
|
161
|
+
|
|
162
|
+
result = table.items.at(key, range_key).attributes.to_h(options)
|
|
163
|
+
|
|
164
|
+
if result.empty?
|
|
165
|
+
nil
|
|
166
|
+
else
|
|
167
|
+
result.symbolize_keys!
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def update_item(table_name, key, options = {}, &block)
|
|
172
|
+
range_key = options.delete(:range_key)
|
|
173
|
+
conditions = options.delete(:conditions) || {}
|
|
174
|
+
table = get_table(table_name)
|
|
175
|
+
item = table.items.at(key, range_key)
|
|
176
|
+
item.attributes.update(conditions.merge(:return => :all_new), &block)
|
|
177
|
+
rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException
|
|
178
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# List all tables on DynamoDB.
|
|
182
|
+
#
|
|
183
|
+
# @since 0.2.0
|
|
184
|
+
def list_tables
|
|
185
|
+
@@connection.tables.collect(&:name)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Persists an item on DynamoDB.
|
|
189
|
+
#
|
|
190
|
+
# @param [String] table_name the name of the table
|
|
191
|
+
# @param [Object] object a hash or Dynamoid object to persist
|
|
192
|
+
#
|
|
193
|
+
# @since 0.2.0
|
|
194
|
+
def put_item(table_name, object, options = nil)
|
|
195
|
+
table = get_table(table_name)
|
|
196
|
+
table.items.create(
|
|
197
|
+
object.delete_if{|k, v| v.nil? || (v.respond_to?(:empty?) && v.empty?)},
|
|
198
|
+
options || {}
|
|
199
|
+
)
|
|
200
|
+
rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException => e
|
|
201
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
|
|
205
|
+
# only really useful for range queries, since it can only find by one hash key at once. Only provide
|
|
206
|
+
# one range key to the hash.
|
|
207
|
+
#
|
|
208
|
+
# @param [String] table_name the name of the table
|
|
209
|
+
# @param [Hash] opts the options to query the table with
|
|
210
|
+
# @option opts [String] :hash_value the value of the hash key to find
|
|
211
|
+
# @option opts [Range] :range_value find the range key within this range
|
|
212
|
+
# @option opts [Number] :range_greater_than find range keys greater than this
|
|
213
|
+
# @option opts [Number] :range_less_than find range keys less than this
|
|
214
|
+
# @option opts [Number] :range_gte find range keys greater than or equal to this
|
|
215
|
+
# @option opts [Number] :range_lte find range keys less than or equal to this
|
|
216
|
+
#
|
|
217
|
+
# @return [Enumerator] an iterator of all matching items
|
|
218
|
+
#
|
|
219
|
+
# @since 0.2.0
|
|
220
|
+
def query(table_name, opts = {})
|
|
221
|
+
table = get_table(table_name)
|
|
222
|
+
|
|
223
|
+
Enumerator.new do |yielder|
|
|
224
|
+
consistent_opts = { :consistent_read => opts[:consistent_read] || false }
|
|
225
|
+
if table.composite_key?
|
|
226
|
+
table.items.query(opts).each do |data|
|
|
227
|
+
if opts.has_key? :select
|
|
228
|
+
yielder.yield data.attributes.symbolize_keys!
|
|
229
|
+
else
|
|
230
|
+
yielder.yield data.attributes.to_h(consistent_opts).symbolize_keys!
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
else
|
|
234
|
+
yielder.yield get_item(table_name, opts[:hash_value])
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Scan the DynamoDB table. This is usually a very slow operation as it naively filters all data on
|
|
240
|
+
# the DynamoDB servers.
|
|
241
|
+
#
|
|
242
|
+
# @param [String] table_name the name of the table
|
|
243
|
+
# @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
|
|
244
|
+
#
|
|
245
|
+
# @return [Enumerator] an iterator of all matching items
|
|
246
|
+
#
|
|
247
|
+
# @since 0.2.0
|
|
248
|
+
def scan(table_name, scan_hash, select_opts)
|
|
249
|
+
table = get_table(table_name)
|
|
250
|
+
Enumerator.new do |yielder|
|
|
251
|
+
# MF: Add support for advanced scan filters
|
|
252
|
+
key = scan_hash.keys[0] if scan_hash && scan_hash.keys
|
|
253
|
+
exp = scan_hash[:expression] if scan_hash.has_key? :expression
|
|
254
|
+
case exp
|
|
255
|
+
when "IN"
|
|
256
|
+
table.items.where(key).in(*scan_hash[key]).select(select_opts).each do |data|
|
|
257
|
+
yielder.yield data.attributes.symbolize_keys!
|
|
258
|
+
end
|
|
259
|
+
when "BETWEEN"
|
|
260
|
+
table.items.where(key).between(scan_hash[key][0], scan_hash[key][1]).select(select_opts).each do |data|
|
|
261
|
+
yielder.yield data.attributes.symbolize_keys!
|
|
262
|
+
end
|
|
263
|
+
when "BEGINS_WITH"
|
|
264
|
+
table.items.where(key).between(scan_hash[key][0], scan_hash[key][1]).select(select_opts).each do |data|
|
|
265
|
+
yielder.yield data.attributes.symbolize_keys!
|
|
266
|
+
end
|
|
267
|
+
when "CONTAINS"
|
|
268
|
+
table.items.where(key).contains(scan_hash[key]).select(select_opts).each do |data|
|
|
269
|
+
yielder.yield data.attributes.symbolize_keys!
|
|
270
|
+
end
|
|
271
|
+
else
|
|
272
|
+
table.items.where(scan_hash).select(select_opts).each do |data|
|
|
273
|
+
yielder.yield data.attributes.symbolize_keys!
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# @todo Add an UpdateItem method.
|
|
280
|
+
|
|
281
|
+
# @todo Add an UpdateTable method.
|
|
282
|
+
|
|
283
|
+
def get_table(table_name)
|
|
284
|
+
unless table = table_cache[table_name]
|
|
285
|
+
table = @@connection.tables[table_name]
|
|
286
|
+
table.load_schema
|
|
287
|
+
table_cache[table_name] = table
|
|
288
|
+
end
|
|
289
|
+
table
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def table_cache
|
|
293
|
+
@table_cache ||= {}
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Number of items from a table
|
|
297
|
+
#
|
|
298
|
+
# @param [String] table_name the name of the table
|
|
299
|
+
#
|
|
300
|
+
# @return [Integer] the number of items from a table
|
|
301
|
+
#
|
|
302
|
+
# @since 0.6.1
|
|
303
|
+
def count(table_name)
|
|
304
|
+
table = get_table(table_name)
|
|
305
|
+
table.items.count
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|