dynamoid 3.2.0 → 3.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +111 -1
- data/README.md +580 -241
- data/lib/dynamoid.rb +2 -0
- data/lib/dynamoid/adapter.rb +15 -15
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +82 -102
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +108 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +29 -16
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +3 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +15 -6
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +15 -5
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +5 -3
- data/lib/dynamoid/application_time_zone.rb +1 -0
- data/lib/dynamoid/associations.rb +182 -19
- data/lib/dynamoid/associations/association.rb +4 -2
- data/lib/dynamoid/associations/belongs_to.rb +2 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
- data/lib/dynamoid/associations/has_many.rb +2 -1
- data/lib/dynamoid/associations/has_one.rb +2 -1
- data/lib/dynamoid/associations/many_association.rb +65 -22
- data/lib/dynamoid/associations/single_association.rb +28 -1
- data/lib/dynamoid/components.rb +8 -3
- data/lib/dynamoid/config.rb +16 -3
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
- data/lib/dynamoid/config/options.rb +1 -0
- data/lib/dynamoid/criteria.rb +2 -1
- data/lib/dynamoid/criteria/chain.rb +418 -46
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
- data/lib/dynamoid/criteria/key_fields_detector.rb +109 -32
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
- data/lib/dynamoid/dirty.rb +239 -32
- data/lib/dynamoid/document.rb +130 -251
- data/lib/dynamoid/dumping.rb +9 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
- data/lib/dynamoid/fields.rb +246 -20
- data/lib/dynamoid/finders.rb +69 -32
- data/lib/dynamoid/identity_map.rb +6 -0
- data/lib/dynamoid/indexes.rb +76 -17
- data/lib/dynamoid/loadable.rb +31 -0
- data/lib/dynamoid/log/formatter.rb +26 -0
- data/lib/dynamoid/middleware/identity_map.rb +1 -0
- data/lib/dynamoid/persistence.rb +592 -122
- data/lib/dynamoid/persistence/import.rb +73 -0
- data/lib/dynamoid/persistence/save.rb +64 -0
- data/lib/dynamoid/persistence/update_fields.rb +63 -0
- data/lib/dynamoid/persistence/upsert.rb +60 -0
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
- data/lib/dynamoid/railtie.rb +1 -0
- data/lib/dynamoid/tasks.rb +3 -1
- data/lib/dynamoid/tasks/database.rb +1 -0
- data/lib/dynamoid/type_casting.rb +12 -2
- data/lib/dynamoid/undumping.rb +8 -0
- data/lib/dynamoid/validations.rb +2 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +49 -71
- data/.coveralls.yml +0 -1
- data/.document +0 -5
- data/.gitignore +0 -74
- data/.rspec +0 -2
- data/.rubocop.yml +0 -71
- data/.rubocop_todo.yml +0 -55
- data/.travis.yml +0 -41
- data/Appraisals +0 -28
- data/Gemfile +0 -8
- data/Rakefile +0 -46
- data/Vagrantfile +0 -29
- data/docker-compose.yml +0 -7
- data/dynamoid.gemspec +0 -57
- data/gemfiles/rails_4_2.gemfile +0 -11
- data/gemfiles/rails_5_0.gemfile +0 -10
- data/gemfiles/rails_5_1.gemfile +0 -10
- data/gemfiles/rails_5_2.gemfile +0 -10
@@ -13,6 +13,7 @@ module Dynamoid
|
|
13
13
|
@identity_map ||= {}
|
14
14
|
end
|
15
15
|
|
16
|
+
# @private
|
16
17
|
def from_database(attrs = {})
|
17
18
|
return super if identity_map_off?
|
18
19
|
|
@@ -29,6 +30,7 @@ module Dynamoid
|
|
29
30
|
document
|
30
31
|
end
|
31
32
|
|
33
|
+
# @private
|
32
34
|
def find_by_id(id, options = {})
|
33
35
|
return super if identity_map_off?
|
34
36
|
|
@@ -41,6 +43,7 @@ module Dynamoid
|
|
41
43
|
identity_map[key] || super
|
42
44
|
end
|
43
45
|
|
46
|
+
# @private
|
44
47
|
def identity_map_key(attrs)
|
45
48
|
key = attrs[hash_key].to_s
|
46
49
|
key += "::#{attrs[range_key]}" if range_key
|
@@ -60,6 +63,7 @@ module Dynamoid
|
|
60
63
|
self.class.identity_map
|
61
64
|
end
|
62
65
|
|
66
|
+
# @private
|
63
67
|
def save(*args)
|
64
68
|
return super if self.class.identity_map_off?
|
65
69
|
|
@@ -69,6 +73,7 @@ module Dynamoid
|
|
69
73
|
result
|
70
74
|
end
|
71
75
|
|
76
|
+
# @private
|
72
77
|
def delete
|
73
78
|
return super if self.class.identity_map_off?
|
74
79
|
|
@@ -76,6 +81,7 @@ module Dynamoid
|
|
76
81
|
super
|
77
82
|
end
|
78
83
|
|
84
|
+
# @private
|
79
85
|
def identity_map_key
|
80
86
|
key = hash_key.to_s
|
81
87
|
key += "::#{range_value}" if self.class.range_key
|
data/lib/dynamoid/indexes.rb
CHANGED
@@ -15,19 +15,43 @@ module Dynamoid
|
|
15
15
|
# Defines a Global Secondary index on a table. Keys can be specified as
|
16
16
|
# hash-only, or hash & range.
|
17
17
|
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
18
|
+
# class Post
|
19
|
+
# include Dynamoid::Document
|
20
|
+
#
|
21
|
+
# field :category
|
22
|
+
#
|
23
|
+
# global_secondary_indexes hash_key: :category
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# The full example with all the options being specified:
|
27
|
+
#
|
28
|
+
# global_secondary_indexes hash_key: :category,
|
29
|
+
# range_key: :created_at,
|
30
|
+
# name: 'posts_category_created_at_index',
|
31
|
+
# projected_attributes: :all,
|
32
|
+
# read_capacity: 100,
|
33
|
+
# write_capacity: 20
|
34
|
+
#
|
35
|
+
# Global secondary index should be declared after fields for mentioned
|
36
|
+
# hash key and optional range key are declared (with method +field+)
|
37
|
+
#
|
38
|
+
# The only mandatory option is +hash_key+. Raises
|
39
|
+
# +Dynamoid::Errors::InvalidIndex+ exception if passed incorrect
|
40
|
+
# options.
|
41
|
+
#
|
42
|
+
# @param [Hash] options the options to pass for this table
|
43
|
+
# @option options [Symbol] name the name for the index; this still gets
|
44
|
+
# namespaced. If not specified, will use a default name.
|
45
|
+
# @option options [Symbol] hash_key the index hash key column.
|
46
|
+
# @option options [Symbol] range_key the index range key column (if
|
23
47
|
# applicable).
|
24
|
-
# @option options [Symbol, Array<Symbol>]
|
25
|
-
# attributes to project for this index. Can be
|
48
|
+
# @option options [Symbol, Array<Symbol>] projected_attributes table
|
49
|
+
# attributes to project for this index. Can be +:keys_only+, +:all+
|
26
50
|
# or an array of included fields. If not specified, defaults to
|
27
|
-
#
|
28
|
-
# @option options [Integer]
|
51
|
+
# +:keys_only+.
|
52
|
+
# @option options [Integer] read_capacity set the read capacity for the
|
29
53
|
# index; does not work on existing indexes.
|
30
|
-
# @option options [Integer]
|
54
|
+
# @option options [Integer] write_capacity set the write capacity for
|
31
55
|
# the index; does not work on existing indexes.
|
32
56
|
def global_secondary_index(options = {})
|
33
57
|
unless options.present?
|
@@ -55,14 +79,38 @@ module Dynamoid
|
|
55
79
|
# Defines a local secondary index on a table. Will use the same primary
|
56
80
|
# hash key as the table.
|
57
81
|
#
|
82
|
+
# class Comment
|
83
|
+
# include Dynamoid::Document
|
84
|
+
#
|
85
|
+
# table hash_key: :post_id
|
86
|
+
# range :created_at, :datetime
|
87
|
+
# field :author_id
|
88
|
+
#
|
89
|
+
# local_secondary_indexes hash_key: :author_id
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# The full example with all the options being specified:
|
93
|
+
#
|
94
|
+
# local_secondary_indexes range_key: :created_at,
|
95
|
+
# name: 'posts_created_at_index',
|
96
|
+
# projected_attributes: :all
|
97
|
+
#
|
98
|
+
# Local secondary index should be declared after fields for mentioned
|
99
|
+
# hash key and optional range key are declared (with method +field+) as
|
100
|
+
# well as after +table+ method call.
|
101
|
+
#
|
102
|
+
# The only mandatory option is +range_key+. Raises
|
103
|
+
# +Dynamoid::Errors::InvalidIndex+ exception if passed incorrect
|
104
|
+
# options.
|
105
|
+
#
|
58
106
|
# @param [Hash] options options to pass for this index.
|
59
|
-
# @option options [Symbol]
|
107
|
+
# @option options [Symbol] name the name for the index; this still gets
|
60
108
|
# namespaced. If not specified, a name is automatically generated.
|
61
|
-
# @option options [Symbol]
|
62
|
-
# @option options [Symbol, Array<Symbol>]
|
63
|
-
# attributes to project for this index. Can be
|
109
|
+
# @option options [Symbol] range_key the range key column for the index.
|
110
|
+
# @option options [Symbol, Array<Symbol>] projected_attributes table
|
111
|
+
# attributes to project for this index. Can be +:keys_only+, +:all+
|
64
112
|
# or an array of included fields. If not specified, defaults to
|
65
|
-
#
|
113
|
+
# +:keys_only+.
|
66
114
|
def local_secondary_index(options = {})
|
67
115
|
unless options.present?
|
68
116
|
raise Dynamoid::Errors::InvalidIndex, 'empty index definition'
|
@@ -94,6 +142,13 @@ module Dynamoid
|
|
94
142
|
self
|
95
143
|
end
|
96
144
|
|
145
|
+
# Returns an index by its hash key and optional range key.
|
146
|
+
#
|
147
|
+
# It works only for indexes without explicit name declared.
|
148
|
+
#
|
149
|
+
# @param hash [scalar] the hash key used to declare an index
|
150
|
+
# @param range [scalar] the range key used to declare an index (optional)
|
151
|
+
# @return [Dynamoid::Indexes::Index, nil] index object or nil if it isn't found
|
97
152
|
def find_index(hash, range = nil)
|
98
153
|
index = indexes[index_key(hash, range)]
|
99
154
|
index
|
@@ -150,6 +205,10 @@ module Dynamoid
|
|
150
205
|
local_secondary_indexes.merge(global_secondary_indexes)
|
151
206
|
end
|
152
207
|
|
208
|
+
# Returns an array of hash keys for all the declared Glocal Secondary
|
209
|
+
# Indexes.
|
210
|
+
#
|
211
|
+
# @return [Array[String]] array of hash keys
|
153
212
|
def indexed_hash_keys
|
154
213
|
global_secondary_indexes.map do |_name, index|
|
155
214
|
index.hash_key.to_s
|
@@ -165,8 +224,8 @@ module Dynamoid
|
|
165
224
|
DEFAULT_PROJECTION_TYPE = :keys_only
|
166
225
|
|
167
226
|
attr_accessor :name, :dynamoid_class, :type, :hash_key, :range_key,
|
168
|
-
|
169
|
-
|
227
|
+
:hash_key_schema, :range_key_schema, :projected_attributes,
|
228
|
+
:read_capacity, :write_capacity
|
170
229
|
|
171
230
|
validate do
|
172
231
|
validate_index_type
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Loadable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def load(attrs)
|
8
|
+
attrs.each do |key, value|
|
9
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
|
14
|
+
# changes to be reflected immediately, you would call this method. This is a consistent read.
|
15
|
+
#
|
16
|
+
# @return [Dynamoid::Document] the document this method was called on
|
17
|
+
#
|
18
|
+
# @since 0.2.0
|
19
|
+
def reload
|
20
|
+
options = { consistent_read: true }
|
21
|
+
|
22
|
+
if self.class.range_key
|
23
|
+
options[:range_key] = range_value
|
24
|
+
end
|
25
|
+
|
26
|
+
self.attributes = self.class.find(hash_key, options).attributes
|
27
|
+
@associations.values.each(&:reset)
|
28
|
+
self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module Log
|
3
|
+
module Formatter
|
4
|
+
|
5
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Log/Formatter.html
|
6
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v2/api/Seahorse/Client/Response.html
|
7
|
+
# https://aws.amazon.com/ru/blogs/developer/logging-requests/
|
8
|
+
class Debug
|
9
|
+
def format(response)
|
10
|
+
bold = "\x1b[1m"
|
11
|
+
color = "\x1b[34m"
|
12
|
+
reset = "\x1b[0m"
|
13
|
+
|
14
|
+
[
|
15
|
+
response.context.operation.name,
|
16
|
+
"#{bold}#{color}\nRequest:\n#{reset}#{bold}",
|
17
|
+
JSON.pretty_generate(JSON.parse(response.context.http_request.body.string)),
|
18
|
+
"#{bold}#{color}\nResponse:\n#{reset}#{bold}",
|
19
|
+
JSON.pretty_generate(JSON.parse(response.context.http_response.body.string)),
|
20
|
+
reset
|
21
|
+
].join("\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/dynamoid/persistence.rb
CHANGED
@@ -4,16 +4,22 @@ require 'bigdecimal'
|
|
4
4
|
require 'securerandom'
|
5
5
|
require 'yaml'
|
6
6
|
|
7
|
+
require 'dynamoid/persistence/import'
|
8
|
+
require 'dynamoid/persistence/update_fields'
|
9
|
+
require 'dynamoid/persistence/upsert'
|
10
|
+
require 'dynamoid/persistence/save'
|
11
|
+
|
7
12
|
# encoding: utf-8
|
8
13
|
module Dynamoid
|
9
|
-
# Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
|
10
|
-
# values to be of the same type as when they were passed in, based on the fields in the class.
|
14
|
+
# # Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
|
15
|
+
# # values to be of the same type as when they were passed in, based on the fields in the class.
|
11
16
|
module Persistence
|
12
17
|
extend ActiveSupport::Concern
|
13
18
|
|
14
19
|
attr_accessor :new_record
|
15
20
|
alias new_record? new_record
|
16
21
|
|
22
|
+
# @private
|
17
23
|
UNIX_EPOCH_DATE = Date.new(1970, 1, 1).freeze
|
18
24
|
|
19
25
|
module ClassMethods
|
@@ -23,15 +29,66 @@ module Dynamoid
|
|
23
29
|
@table_name ||= [Dynamoid::Config.namespace.to_s, table_base_name].reject(&:empty?).join('_')
|
24
30
|
end
|
25
31
|
|
26
|
-
#
|
32
|
+
# Create a table.
|
33
|
+
#
|
34
|
+
# Uses a configuration specified in a model class (with the +table+
|
35
|
+
# method) e.g. table name, schema (hash and range keys), global and local
|
36
|
+
# secondary indexes, billing mode and write/read capacity.
|
37
|
+
#
|
38
|
+
# For instance here
|
39
|
+
#
|
40
|
+
# class User
|
41
|
+
# include Dynamoid::Document
|
42
|
+
#
|
43
|
+
# table key: :uuid
|
44
|
+
# range :last_name
|
45
|
+
#
|
46
|
+
# field :first_name
|
47
|
+
# field :last_name
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# User.create_table
|
51
|
+
#
|
52
|
+
# +create_table+ method call will create a table +dynamoid_users+ with
|
53
|
+
# hash key +uuid+ and range key +name+, DynamoDB default billing mode and
|
54
|
+
# Dynamoid default read/write capacity units (100/20).
|
55
|
+
#
|
56
|
+
# All the configuration can be overridden with +options+ argument.
|
57
|
+
#
|
58
|
+
# User.create_table(table_name: 'users', read_capacity: 200, write_capacity: 40)
|
59
|
+
#
|
60
|
+
# Dynamoid creates a table synchronously by default. DynamoDB table
|
61
|
+
# creation is an asynchronous operation and a client should wait until a
|
62
|
+
# table status changes to +ACTIVE+ and a table becomes available. That's
|
63
|
+
# why Dynamoid is polling a table status and returns results only when a
|
64
|
+
# table becomes available.
|
65
|
+
#
|
66
|
+
# Polling is configured with +Dynamoid::Config.sync_retry_max_times+ and
|
67
|
+
# +Dynamoid::Config.sync_retry_wait_seconds+ configuration options. If
|
68
|
+
# table creation takes more time than configured waiting time then
|
69
|
+
# Dynamoid stops polling and returns +true+.
|
27
70
|
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
71
|
+
# In order to return back asynchronous behaviour and not to wait until a
|
72
|
+
# table is created the +sync: false+ option should be specified.
|
73
|
+
#
|
74
|
+
# User.create_table(sync: false)
|
75
|
+
#
|
76
|
+
# Subsequent method calls for the same table will be ignored.
|
77
|
+
#
|
78
|
+
# @param options [Hash]
|
79
|
+
#
|
80
|
+
# @option options [Symbol] :table_name name of the table
|
81
|
+
# @option options [Symbol] :id hash key name of the table
|
82
|
+
# @option options [Symbol] :hash_key_type Dynamoid type of the hash key - +:string+, +:integer+ or any other scalar type
|
83
|
+
# @option options [Hash] :range_key a Hash with range key name and type in format +{ <name> => <type> }+ e.g. +{ last_name: :string }+
|
84
|
+
# @option options [String] :billing_mode billing mode of a table - either +PROVISIONED+ (default) or +PAY_PER_REQUEST+ (for On-Demand Mode)
|
85
|
+
# @option options [Integer] :read_capacity read capacity units for the table; does not work on existing tables and is ignored when billing mode is +PAY_PER_REQUEST+
|
86
|
+
# @option options [Integer] :write_capacity write capacity units for the table; does not work on existing tables and is ignored when billing mode is +PAY_PER_REQUEST+
|
87
|
+
# @option options [Hash] :local_secondary_indexes
|
88
|
+
# @option options [Hash] :global_secondary_indexes
|
89
|
+
# @option options [true|false] :sync specifies should the method call be synchronous and wait until a table is completely created
|
90
|
+
#
|
91
|
+
# @return [true|false] Whether a table created successfully
|
35
92
|
# @since 0.4.0
|
36
93
|
def create_table(options = {})
|
37
94
|
range_key_hash = if range_key
|
@@ -41,6 +98,7 @@ module Dynamoid
|
|
41
98
|
options = {
|
42
99
|
id: hash_key,
|
43
100
|
table_name: table_name,
|
101
|
+
billing_mode: capacity_mode,
|
44
102
|
write_capacity: write_capacity,
|
45
103
|
read_capacity: read_capacity,
|
46
104
|
range_key: range_key_hash,
|
@@ -49,77 +107,297 @@ module Dynamoid
|
|
49
107
|
global_secondary_indexes: global_secondary_indexes.values
|
50
108
|
}.merge(options)
|
51
109
|
|
52
|
-
Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
|
110
|
+
created_successfuly = Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
|
111
|
+
|
112
|
+
if created_successfuly && self.options[:expires]
|
113
|
+
attribute = self.options[:expires][:field]
|
114
|
+
Dynamoid.adapter.update_time_to_live(table_name: table_name, attribute: attribute)
|
115
|
+
end
|
53
116
|
end
|
54
117
|
|
55
|
-
# Deletes the table for the model
|
118
|
+
# Deletes the table for the model.
|
119
|
+
#
|
120
|
+
# Dynamoid deletes a table asynchronously and doesn't wait until a table
|
121
|
+
# is deleted completely.
|
122
|
+
#
|
123
|
+
# Subsequent method calls for the same table will be ignored.
|
56
124
|
def delete_table
|
57
125
|
Dynamoid.adapter.delete_table(table_name)
|
58
126
|
end
|
59
127
|
|
128
|
+
# @private
|
60
129
|
def from_database(attrs = {})
|
61
|
-
|
62
|
-
attrs_undumped = Undumping.undump_attributes(attrs,
|
63
|
-
|
130
|
+
klass = choose_right_class(attrs)
|
131
|
+
attrs_undumped = Undumping.undump_attributes(attrs, klass.attributes)
|
132
|
+
klass.new(attrs_undumped).tap { |r| r.new_record = false }
|
64
133
|
end
|
65
134
|
|
66
|
-
#
|
67
|
-
# Neither callbacks nor validations run.
|
68
|
-
# It works efficiently because of using BatchWriteItem.
|
135
|
+
# Create several models at once.
|
69
136
|
#
|
70
|
-
#
|
137
|
+
# users = User.import([{ name: 'a' }, { name: 'b' }])
|
71
138
|
#
|
72
|
-
#
|
139
|
+
# +import+ is a relatively low-level method and bypasses some
|
140
|
+
# mechanisms like callbacks and validation.
|
73
141
|
#
|
74
|
-
#
|
142
|
+
# It sets timestamp fields +created_at+ and +updated_at+ if they are
|
143
|
+
# blank. It sets a hash key field as well if it's blank. It expects that
|
144
|
+
# the hash key field is +string+ and sets a random UUID value if the field
|
145
|
+
# value is blank. All the field values are type casted to the declared
|
146
|
+
# types.
|
75
147
|
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
|
148
|
+
# It works efficiently and uses the `BatchWriteItem` operation. In order
|
149
|
+
# to cope with throttling it uses a backoff strategy if it's specified with
|
150
|
+
# `Dynamoid::Config.backoff` configuration option.
|
151
|
+
#
|
152
|
+
# Because of the nature of DynamoDB and its limits only 25 models can be
|
153
|
+
# saved at once. So multiple HTTP requests can be sent to DynamoDB.
|
154
|
+
#
|
155
|
+
# @param array_of_attributes [Array<Hash>]
|
156
|
+
# @return [Array] Created models
|
157
|
+
def import(array_of_attributes)
|
158
|
+
Import.call(self, array_of_attributes)
|
159
|
+
end
|
81
160
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
161
|
+
# Create a model.
|
162
|
+
#
|
163
|
+
# Initializes a new model and immediately saves it to DynamoDB.
|
164
|
+
#
|
165
|
+
# User.create(first_name: 'Mark', last_name: 'Tyler')
|
166
|
+
#
|
167
|
+
# Accepts both Hash and Array of Hashes and can create several models.
|
168
|
+
#
|
169
|
+
# User.create([{ first_name: 'Alice' }, { first_name: 'Bob' }])
|
170
|
+
#
|
171
|
+
# Creates a model and pass it into a block to set other attributes.
|
172
|
+
#
|
173
|
+
# User.create(first_name: 'Mark') do |u|
|
174
|
+
# u.age = 21
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# Validates model and runs callbacks.
|
178
|
+
#
|
179
|
+
# @param attrs [Hash|Array[Hash]] Attributes of the models
|
180
|
+
# @param block [Proc] Block to process a document after initialization
|
181
|
+
# @return [Dynamoid::Document] The created document
|
182
|
+
# @since 0.2.0
|
183
|
+
def create(attrs = {}, &block)
|
184
|
+
if attrs.is_a?(Array)
|
185
|
+
attrs.map { |attr| create(attr, &block) }
|
186
|
+
else
|
187
|
+
build(attrs, &block).tap(&:save)
|
188
|
+
end
|
189
|
+
end
|
87
190
|
|
88
|
-
|
89
|
-
|
90
|
-
|
191
|
+
# Create a model.
|
192
|
+
#
|
193
|
+
# Initializes a new object and immediately saves it to the Dynamoid.
|
194
|
+
# Raises an exception +Dynamoid::Errors::DocumentNotValid+ if validation
|
195
|
+
# failed. Accepts both Hash and Array of Hashes and can create several
|
196
|
+
# models.
|
197
|
+
#
|
198
|
+
# @param attrs [Hash|Array[Hash]] Attributes with which to create the object.
|
199
|
+
# @param block [Proc] Block to process a document after initialization
|
200
|
+
# @return [Dynamoid::Document] The created document
|
201
|
+
# @since 0.2.0
|
202
|
+
def create!(attrs = {}, &block)
|
203
|
+
if attrs.is_a?(Array)
|
204
|
+
attrs.map { |attr| create!(attr, &block) }
|
205
|
+
else
|
206
|
+
build(attrs, &block).tap(&:save!)
|
91
207
|
end
|
208
|
+
end
|
92
209
|
|
93
|
-
|
94
|
-
|
210
|
+
# Update document with provided attributes.
|
211
|
+
#
|
212
|
+
# Instantiates document and saves changes. Runs validations and
|
213
|
+
# callbacks. Don't save changes if validation fails.
|
214
|
+
#
|
215
|
+
# User.update('1', age: 26)
|
216
|
+
#
|
217
|
+
# If range key is declared for a model it should be passed as well:
|
218
|
+
#
|
219
|
+
# User.update('1', 'Tylor', age: 26)
|
220
|
+
#
|
221
|
+
# @param hash_key [Scalar value] hash key
|
222
|
+
# @param range_key_value [Scalar value] range key (optional)
|
223
|
+
# @param attrs [Hash]
|
224
|
+
# @return [Dynamoid::Document] Updated document
|
225
|
+
def update(hash_key, range_key_value = nil, attrs)
|
226
|
+
model = find(hash_key, range_key: range_key_value, consistent_read: true)
|
227
|
+
model.update_attributes(attrs)
|
228
|
+
model
|
229
|
+
end
|
95
230
|
|
96
|
-
|
97
|
-
|
98
|
-
|
231
|
+
# Update document with provided attributes.
|
232
|
+
#
|
233
|
+
# Instantiates document and saves changes. Runs validations and
|
234
|
+
# callbacks.
|
235
|
+
#
|
236
|
+
# User.update!('1', age: 26)
|
237
|
+
#
|
238
|
+
# If range key is declared for a model it should be passed as well:
|
239
|
+
#
|
240
|
+
# User.update('1', 'Tylor', age: 26)
|
241
|
+
#
|
242
|
+
# Raises +Dynamoid::Errors::DocumentNotValid+ exception if validation fails.
|
243
|
+
#
|
244
|
+
# @param hash_key [Scalar value] hash key
|
245
|
+
# @param range_key_value [Scalar value] range key (optional)
|
246
|
+
# @param attrs [Hash]
|
247
|
+
# @return [Dynamoid::Document] Updated document
|
248
|
+
def update!(hash_key, range_key_value = nil, attrs)
|
249
|
+
model = find(hash_key, range_key: range_key_value, consistent_read: true)
|
250
|
+
model.update_attributes!(attrs)
|
251
|
+
model
|
252
|
+
end
|
99
253
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
254
|
+
# Update document.
|
255
|
+
#
|
256
|
+
# Doesn't run validations and callbacks.
|
257
|
+
#
|
258
|
+
# User.update_fields('1', age: 26)
|
259
|
+
#
|
260
|
+
# If range key is declared for a model it should be passed as well:
|
261
|
+
#
|
262
|
+
# User.update_fields('1', 'Tylor', age: 26)
|
263
|
+
#
|
264
|
+
# Can make a conditional update so a document will be updated only if it
|
265
|
+
# meets the specified conditions. Conditions can be specified as a +Hash+
|
266
|
+
# with +:if+ key:
|
267
|
+
#
|
268
|
+
# User.update_fields('1', { age: 26 }, if: { version: 1 })
|
269
|
+
#
|
270
|
+
# Here +User+ model has an integer +version+ field and the document will
|
271
|
+
# be updated only if the +version+ attribute currently has value 1.
|
272
|
+
#
|
273
|
+
# If a document with specified hash and range keys doesn't exist or
|
274
|
+
# conditions were specified and failed the method call returns +nil+.
|
275
|
+
#
|
276
|
+
# +update_fields+ uses the +UpdateItem+ operation so it saves changes and
|
277
|
+
# loads an updated document back with one HTTP request.
|
278
|
+
#
|
279
|
+
# @param hash_key_value [Scalar value] hash key
|
280
|
+
# @param range_key_value [Scalar value] range key (optional)
|
281
|
+
# @param attrs [Hash]
|
282
|
+
# @param conditions [Hash] (optional)
|
283
|
+
# @return [Dynamoid::Document|nil] Updated document
|
284
|
+
def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
285
|
+
optional_params = [range_key_value, attrs, conditions].compact
|
286
|
+
if optional_params.first.is_a?(Hash)
|
287
|
+
range_key_value = nil
|
288
|
+
attrs, conditions = optional_params[0..1]
|
108
289
|
else
|
109
|
-
|
110
|
-
|
111
|
-
|
290
|
+
range_key_value = optional_params.first
|
291
|
+
attrs, conditions = optional_params[1..2]
|
292
|
+
end
|
293
|
+
|
294
|
+
UpdateFields.call(self,
|
295
|
+
partition_key: hash_key_value,
|
296
|
+
sort_key: range_key_value,
|
297
|
+
attributes: attrs,
|
298
|
+
conditions: conditions)
|
299
|
+
end
|
112
300
|
|
113
|
-
|
301
|
+
# Update an existing document or create a new one.
|
302
|
+
#
|
303
|
+
# If a document with specified hash and range keys doesn't exist it
|
304
|
+
# creates a new document with specified attributes. Doesn't run
|
305
|
+
# validations and callbacks.
|
306
|
+
#
|
307
|
+
# User.upsert('1', age: 26)
|
308
|
+
#
|
309
|
+
# If range key is declared for a model it should be passed as well:
|
310
|
+
#
|
311
|
+
# User.upsert('1', 'Tylor', age: 26)
|
312
|
+
#
|
313
|
+
# Can make a conditional update so a document will be updated only if it
|
314
|
+
# meets the specified conditions. Conditions can be specified as a +Hash+
|
315
|
+
# with +:if+ key:
|
316
|
+
#
|
317
|
+
# User.upsert('1', { age: 26 }, if: { version: 1 })
|
318
|
+
#
|
319
|
+
# Here +User+ model has an integer +version+ field and the document will
|
320
|
+
# be updated only if the +version+ attribute currently has value 1.
|
321
|
+
#
|
322
|
+
# If conditions were specified and failed the method call returns +nil+.
|
323
|
+
#
|
324
|
+
# +upsert+ uses the +UpdateItem+ operation so it saves changes and loads
|
325
|
+
# an updated document back with one HTTP request.
|
326
|
+
#
|
327
|
+
# @param hash_key_value [Scalar value] hash key
|
328
|
+
# @param range_key_value [Scalar value] range key (optional)
|
329
|
+
# @param attrs [Hash]
|
330
|
+
# @param conditions [Hash] (optional)
|
331
|
+
# @return [Dynamoid::Document|nil] Updated document
|
332
|
+
def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
333
|
+
optional_params = [range_key_value, attrs, conditions].compact
|
334
|
+
if optional_params.first.is_a?(Hash)
|
335
|
+
range_key_value = nil
|
336
|
+
attrs, conditions = optional_params[0..1]
|
337
|
+
else
|
338
|
+
range_key_value = optional_params.first
|
339
|
+
attrs, conditions = optional_params[1..2]
|
114
340
|
end
|
115
341
|
|
116
|
-
|
117
|
-
|
342
|
+
Upsert.call(self,
|
343
|
+
partition_key: hash_key_value,
|
344
|
+
sort_key: range_key_value,
|
345
|
+
attributes: attrs,
|
346
|
+
conditions: conditions)
|
347
|
+
end
|
348
|
+
|
349
|
+
# Increase a numeric field by specified value.
|
350
|
+
#
|
351
|
+
# User.inc('1', age: 2)
|
352
|
+
#
|
353
|
+
# Can update several fields at once.
|
354
|
+
#
|
355
|
+
# User.inc('1', age: 2, version: 1)
|
356
|
+
#
|
357
|
+
# If range key is declared for a model it should be passed as well:
|
358
|
+
#
|
359
|
+
# User.inc('1', 'Tylor', age: 2)
|
360
|
+
#
|
361
|
+
# Uses efficient low-level +UpdateItem+ operation and does only one HTTP
|
362
|
+
# request.
|
363
|
+
#
|
364
|
+
# Doesn't run validations and callbacks. Doesn't update +created_at+ and
|
365
|
+
# +updated_at+ as well.
|
366
|
+
#
|
367
|
+
# @param hash_key_value [Scalar value] hash key
|
368
|
+
# @param range_key_value [Scalar value] range key (optional)
|
369
|
+
# @param counters [Hash] value to increase by
|
370
|
+
def inc(hash_key_value, range_key_value = nil, counters)
|
371
|
+
options = if range_key
|
372
|
+
value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
|
373
|
+
value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
|
374
|
+
{ range_key: value_dumped }
|
375
|
+
else
|
376
|
+
{}
|
377
|
+
end
|
378
|
+
|
379
|
+
Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
|
380
|
+
counters.each do |k, v|
|
381
|
+
value_casted = TypeCasting.cast_field(v, attributes[k])
|
382
|
+
value_dumped = Dumping.dump_field(value_casted, attributes[k])
|
383
|
+
|
384
|
+
t.add(k => value_dumped)
|
385
|
+
end
|
386
|
+
end
|
118
387
|
end
|
119
388
|
end
|
120
389
|
|
121
|
-
#
|
390
|
+
# Update document timestamps.
|
122
391
|
#
|
392
|
+
# Set +updated_at+ attribute to current DateTime.
|
393
|
+
#
|
394
|
+
# post.touch
|
395
|
+
#
|
396
|
+
# Can update another field in addition with the same timestamp if it's name passed as argument.
|
397
|
+
#
|
398
|
+
# user.touch(:last_login_at)
|
399
|
+
#
|
400
|
+
# @param name [Symbol] attribute name to update (optional)
|
123
401
|
def touch(name = nil)
|
124
402
|
now = DateTime.now
|
125
403
|
self.updated_at = now
|
@@ -127,43 +405,109 @@ module Dynamoid
|
|
127
405
|
save
|
128
406
|
end
|
129
407
|
|
130
|
-
# Is this object persisted in
|
408
|
+
# Is this object persisted in DynamoDB?
|
409
|
+
#
|
410
|
+
# user = User.new
|
411
|
+
# user.persisted? # => false
|
412
|
+
#
|
413
|
+
# user.save
|
414
|
+
# user.persisted? # => true
|
131
415
|
#
|
416
|
+
# @return [true|false]
|
132
417
|
# @since 0.2.0
|
133
418
|
def persisted?
|
134
|
-
!new_record?
|
419
|
+
!(new_record? || @destroyed)
|
135
420
|
end
|
136
421
|
|
137
|
-
#
|
422
|
+
# Create new model or persist changes.
|
423
|
+
#
|
424
|
+
# Run the validation and callbacks. Returns +true+ if saving is successful
|
425
|
+
# and +false+ otherwise.
|
426
|
+
#
|
427
|
+
# user = User.new
|
428
|
+
# user.save # => true
|
429
|
+
#
|
430
|
+
# user.age = 26
|
431
|
+
# user.save # => true
|
432
|
+
#
|
433
|
+
# Validation can be skipped with +validate: false+ option:
|
434
|
+
#
|
435
|
+
# user = User.new(age: -1)
|
436
|
+
# user.save(validate: false) # => true
|
437
|
+
#
|
438
|
+
# +save+ by default sets timestamps attributes - +created_at+ and
|
439
|
+
# +updated_at+ when creates new model and updates +updated_at+ attribute
|
440
|
+
# when update already existing one.
|
138
441
|
#
|
442
|
+
# Changing +updated_at+ attribute at updating a model can be skipped with
|
443
|
+
# +touch: false+ option:
|
444
|
+
#
|
445
|
+
# user.save(touch: false)
|
446
|
+
#
|
447
|
+
# If a model is new and hash key (+id+ by default) is not assigned yet
|
448
|
+
# it was assigned implicitly with random UUID value.
|
449
|
+
#
|
450
|
+
# If +lock_version+ attribute is declared it will be incremented. If it's blank then it will be initialized with 1.
|
451
|
+
#
|
452
|
+
# +save+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
|
453
|
+
# if primary key (hash key + optional range key) already exists in a
|
454
|
+
# table.
|
455
|
+
#
|
456
|
+
# +save+ method call raises +Dynamoid::Errors::StaleObjectError+ exception
|
457
|
+
# if there is +lock_version+ attribute and the document in a table was
|
458
|
+
# already changed concurrently and +lock_version+ was consequently
|
459
|
+
# increased.
|
460
|
+
#
|
461
|
+
# When a table is not created yet the first +save+ method call will create
|
462
|
+
# a table. It's useful in test environment to avoid explicit table
|
463
|
+
# creation.
|
464
|
+
#
|
465
|
+
# @param options [Hash] (optional)
|
466
|
+
# @option options [true|false] :validate validate a model or not - +true+ by default (optional)
|
467
|
+
# @option options [true|false] :touch update tiemstamps fields or not - +true+ by default (optional)
|
468
|
+
# @return [true|false] Whether saving successful or not
|
139
469
|
# @since 0.2.0
|
140
|
-
def save(
|
141
|
-
self.class.create_table
|
470
|
+
def save(options = {})
|
471
|
+
self.class.create_table(sync: true)
|
142
472
|
|
143
|
-
|
144
|
-
conditions = { unless_exists: [self.class.hash_key] }
|
145
|
-
conditions[:unless_exists] << range_key if range_key
|
473
|
+
@_touch_record = options[:touch]
|
146
474
|
|
147
|
-
|
475
|
+
if new_record?
|
476
|
+
run_callbacks(:create) do
|
477
|
+
run_callbacks(:save) do
|
478
|
+
Save.call(self)
|
479
|
+
end
|
480
|
+
end
|
148
481
|
else
|
149
|
-
|
482
|
+
run_callbacks(:save) do
|
483
|
+
Save.call(self)
|
484
|
+
end
|
150
485
|
end
|
151
486
|
end
|
152
487
|
|
153
|
-
#
|
488
|
+
# Update multiple attributes at once, saving the object once the updates
|
489
|
+
# are complete. Returns +true+ if saving is successful and +false+
|
490
|
+
# otherwise.
|
154
491
|
#
|
155
|
-
#
|
492
|
+
# user.update_attributes(age: 27, last_name: 'Tylor')
|
156
493
|
#
|
494
|
+
# @param attributes [Hash] a hash of attributes to update
|
495
|
+
# @return [true|false] Whether updating successful or not
|
157
496
|
# @since 0.2.0
|
158
497
|
def update_attributes(attributes)
|
159
498
|
attributes.each { |attribute, value| write_attribute(attribute, value) }
|
160
499
|
save
|
161
500
|
end
|
162
501
|
|
163
|
-
#
|
164
|
-
#
|
502
|
+
# Update multiple attributes at once, saving the object once the updates
|
503
|
+
# are complete.
|
165
504
|
#
|
166
|
-
#
|
505
|
+
# user.update_attributes!(age: 27, last_name: 'Tylor')
|
506
|
+
#
|
507
|
+
# Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
|
508
|
+
# fails.
|
509
|
+
#
|
510
|
+
# @param attributes [Hash] a hash of attributes to update
|
167
511
|
def update_attributes!(attributes)
|
168
512
|
attributes.each { |attribute, value| write_attribute(attribute, value) }
|
169
513
|
save!
|
@@ -171,20 +515,68 @@ module Dynamoid
|
|
171
515
|
|
172
516
|
# Update a single attribute, saving the object afterwards.
|
173
517
|
#
|
174
|
-
#
|
175
|
-
#
|
518
|
+
# Returns +true+ if saving is successful and +false+ otherwise.
|
519
|
+
#
|
520
|
+
# user.update_attribute(:last_name, 'Tylor')
|
176
521
|
#
|
522
|
+
# @param attribute [Symbol] attribute name to update
|
523
|
+
# @param value [Object] the value to assign it
|
524
|
+
# @return [Dynamoid::Document] self
|
177
525
|
# @since 0.2.0
|
178
526
|
def update_attribute(attribute, value)
|
179
527
|
write_attribute(attribute, value)
|
180
528
|
save
|
181
529
|
end
|
182
530
|
|
531
|
+
# Update a model.
|
532
|
+
#
|
533
|
+
# Runs validation and callbacks. Reloads all attribute values.
|
534
|
+
#
|
535
|
+
# Accepts mandatory block in order to specify operations which will modify
|
536
|
+
# attributes. Supports following operations: +add+, +delete+ and +set+.
|
537
|
+
#
|
538
|
+
# Operation +add+ just adds a value for numeric attributes and join
|
539
|
+
# collections if attribute is a collection (one of +array+, +set+ or
|
540
|
+
# +map+).
|
541
|
+
#
|
542
|
+
# user.update do |t|
|
543
|
+
# t.add(age: 1, followers_count: 5)
|
544
|
+
# t.add(hobbies: ['skying', 'climbing'])
|
545
|
+
# end
|
546
|
+
#
|
547
|
+
# Operation +delete+ is applied to collection attribute types and
|
548
|
+
# substructs one collection from another.
|
549
|
+
#
|
550
|
+
# user.update do |t|
|
551
|
+
# t.delete(hobbies: ['skying'])
|
552
|
+
# end
|
553
|
+
#
|
554
|
+
# Operation +set+ just changes an attribute value:
|
555
|
+
#
|
556
|
+
# user.update do |t|
|
557
|
+
# t.set(age: 21)
|
558
|
+
# end
|
559
|
+
#
|
560
|
+
# All the operations works like +ADD+, +DELETE+ and +PUT+ actions supported
|
561
|
+
# by +AttributeUpdates+
|
562
|
+
# {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
|
563
|
+
# of +UpdateItem+ operation.
|
183
564
|
#
|
184
|
-
# update
|
185
|
-
# never cause an update! to fail, but an update! may cause a concurrent save to fail.
|
565
|
+
# Can update a model conditionaly:
|
186
566
|
#
|
567
|
+
# user.update(if: { age: 20 }) do |t|
|
568
|
+
# t.add(age: 1)
|
569
|
+
# end
|
187
570
|
#
|
571
|
+
# If a document doesn't meet conditions it raises
|
572
|
+
# +Dynamoid::Errors::StaleObjectError+ exception.
|
573
|
+
#
|
574
|
+
# It will increment the +lock_version+ attribute if a table has the column,
|
575
|
+
# but will not check it. Thus, a concurrent +save+ call will never cause an
|
576
|
+
# +update!+ to fail, but an +update!+ may cause a concurrent +save+ to
|
577
|
+
# fail.
|
578
|
+
#
|
579
|
+
# @param conditions [Hash] Conditions on model attributes to make a conditional update (optional)
|
188
580
|
def update!(conditions = {})
|
189
581
|
run_callbacks(:update) do
|
190
582
|
options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
|
@@ -208,6 +600,54 @@ module Dynamoid
|
|
208
600
|
end
|
209
601
|
end
|
210
602
|
|
603
|
+
# Update a model.
|
604
|
+
#
|
605
|
+
# Runs validation and callbacks. Reloads all attribute values.
|
606
|
+
#
|
607
|
+
# Accepts mandatory block in order to specify operations which will modify
|
608
|
+
# attributes. Supports following operations: +add+, +delete+ and +set+.
|
609
|
+
#
|
610
|
+
# Operation +add+ just adds a value for numeric attributes and join
|
611
|
+
# collections if attribute is a collection (one of +array+, +set+ or
|
612
|
+
# +map+).
|
613
|
+
#
|
614
|
+
# user.update do |t|
|
615
|
+
# t.add(age: 1, followers_count: 5)
|
616
|
+
# t.add(hobbies: ['skying', 'climbing'])
|
617
|
+
# end
|
618
|
+
#
|
619
|
+
# Operation +delete+ is applied to collection attribute types and
|
620
|
+
# substructs one collection from another.
|
621
|
+
#
|
622
|
+
# user.update do |t|
|
623
|
+
# t.delete(hobbies: ['skying'])
|
624
|
+
# end
|
625
|
+
#
|
626
|
+
# Operation +set+ just changes an attribute value:
|
627
|
+
#
|
628
|
+
# user.update do |t|
|
629
|
+
# t.set(age: 21)
|
630
|
+
# end
|
631
|
+
#
|
632
|
+
# All the operations works like +ADD+, +DELETE+ and +PUT+ actions supported
|
633
|
+
# by +AttributeUpdates+
|
634
|
+
# {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
|
635
|
+
# of +UpdateItem+ operation.
|
636
|
+
#
|
637
|
+
# Can update a model conditionaly:
|
638
|
+
#
|
639
|
+
# user.update(if: { age: 20 }) do |t|
|
640
|
+
# t.add(age: 1)
|
641
|
+
# end
|
642
|
+
#
|
643
|
+
# If a document doesn't meet conditions it just returns +false+. Otherwise it returns +true+.
|
644
|
+
#
|
645
|
+
# It will increment the +lock_version+ attribute if a table has the column,
|
646
|
+
# but will not check it. Thus, a concurrent +save+ call will never cause an
|
647
|
+
# +update!+ to fail, but an +update!+ may cause a concurrent +save+ to
|
648
|
+
# fail.
|
649
|
+
#
|
650
|
+
# @param conditions [Hash] Conditions on model attributes to make a conditional update (optional)
|
211
651
|
def update(conditions = {}, &block)
|
212
652
|
update!(conditions, &block)
|
213
653
|
true
|
@@ -215,51 +655,117 @@ module Dynamoid
|
|
215
655
|
false
|
216
656
|
end
|
217
657
|
|
218
|
-
#
|
219
|
-
#
|
658
|
+
# Change numeric attribute value.
|
659
|
+
#
|
660
|
+
# Initializes attribute to zero if +nil+ and adds the specified value (by
|
661
|
+
# default is 1). Only makes sense for number-based attributes.
|
662
|
+
#
|
663
|
+
# user.increment(:followers_count)
|
664
|
+
# user.increment(:followers_count, 2)
|
665
|
+
#
|
666
|
+
# @param attribute [Symbol] attribute name
|
667
|
+
# @param by [Numeric] value to add (optional)
|
668
|
+
# @return [Dynamoid::Document] self
|
220
669
|
def increment(attribute, by = 1)
|
221
670
|
self[attribute] ||= 0
|
222
671
|
self[attribute] += by
|
223
672
|
self
|
224
673
|
end
|
225
674
|
|
226
|
-
#
|
227
|
-
#
|
675
|
+
# Change numeric attribute value and save a model.
|
676
|
+
#
|
677
|
+
# Initializes attribute to zero if +nil+ and adds the specified value (by
|
678
|
+
# default is 1). Only makes sense for number-based attributes.
|
679
|
+
#
|
680
|
+
# user.increment!(:followers_count)
|
681
|
+
# user.increment!(:followers_count, 2)
|
682
|
+
#
|
683
|
+
# Returns +true+ if a model was saved and +false+ otherwise.
|
684
|
+
#
|
685
|
+
# @param attribute [Symbol] attribute name
|
686
|
+
# @param by [Numeric] value to add (optional)
|
687
|
+
# @return [true|false] whether saved model successfully
|
228
688
|
def increment!(attribute, by = 1)
|
229
689
|
increment(attribute, by)
|
230
690
|
save
|
231
691
|
end
|
232
692
|
|
233
|
-
#
|
234
|
-
#
|
693
|
+
# Change numeric attribute value.
|
694
|
+
#
|
695
|
+
# Initializes attribute to zero if +nil+ and subtracts the specified value
|
696
|
+
# (by default is 1). Only makes sense for number-based attributes.
|
697
|
+
#
|
698
|
+
# user.decrement(:followers_count)
|
699
|
+
# user.decrement(:followers_count, 2)
|
700
|
+
#
|
701
|
+
# @param attribute [Symbol] attribute name
|
702
|
+
# @param by [Numeric] value to subtract (optional)
|
703
|
+
# @return [Dynamoid::Document] self
|
235
704
|
def decrement(attribute, by = 1)
|
236
705
|
self[attribute] ||= 0
|
237
706
|
self[attribute] -= by
|
238
707
|
self
|
239
708
|
end
|
240
709
|
|
241
|
-
#
|
242
|
-
#
|
710
|
+
# Change numeric attribute value and save a model.
|
711
|
+
#
|
712
|
+
# Initializes attribute to zero if +nil+ and subtracts the specified value
|
713
|
+
# (by default is 1). Only makes sense for number-based attributes.
|
714
|
+
#
|
715
|
+
# user.decrement!(:followers_count)
|
716
|
+
# user.decrement!(:followers_count, 2)
|
717
|
+
#
|
718
|
+
# Returns +true+ if a model was saved and +false+ otherwise.
|
719
|
+
#
|
720
|
+
# @param attribute [Symbol] attribute name
|
721
|
+
# @param by [Numeric] value to subtract (optional)
|
722
|
+
# @return [true|false] whether saved model successfully
|
243
723
|
def decrement!(attribute, by = 1)
|
244
724
|
decrement(attribute, by)
|
245
725
|
save
|
246
726
|
end
|
247
727
|
|
248
|
-
# Delete
|
728
|
+
# Delete a model.
|
729
|
+
#
|
730
|
+
# Runs callbacks.
|
731
|
+
#
|
732
|
+
# Supports optimistic locking with the +lock_version+ attribute and doesn't
|
733
|
+
# delete a model if it's already changed.
|
249
734
|
#
|
735
|
+
# Returns +true+ if deleted successfully and +false+ otherwise.
|
736
|
+
#
|
737
|
+
# @return [true|false] whether deleted successfully
|
250
738
|
# @since 0.2.0
|
251
739
|
def destroy
|
252
740
|
ret = run_callbacks(:destroy) do
|
253
741
|
delete
|
254
742
|
end
|
743
|
+
|
744
|
+
@destroyed = true
|
745
|
+
|
255
746
|
ret == false ? false : self
|
256
747
|
end
|
257
748
|
|
749
|
+
# Delete a model.
|
750
|
+
#
|
751
|
+
# Runs callbacks.
|
752
|
+
#
|
753
|
+
# Supports optimistic locking with the +lock_version+ attribute and doesn't
|
754
|
+
# delete a model if it's already changed.
|
755
|
+
#
|
756
|
+
# Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
|
757
|
+
# failed.
|
258
758
|
def destroy!
|
259
759
|
destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
|
260
760
|
end
|
261
761
|
|
262
|
-
# Delete
|
762
|
+
# Delete a model.
|
763
|
+
#
|
764
|
+
# Supports optimistic locking with the +lock_version+ attribute and doesn't
|
765
|
+
# delete a model if it's already changed.
|
766
|
+
#
|
767
|
+
# Raises +Dynamoid::Errors::StaleObjectError+ exception if cannot delete a
|
768
|
+
# model.
|
263
769
|
#
|
264
770
|
# @since 0.2.0
|
265
771
|
def delete
|
@@ -276,48 +782,12 @@ module Dynamoid
|
|
276
782
|
end
|
277
783
|
options[:conditions] = conditions
|
278
784
|
end
|
785
|
+
|
786
|
+
@destroyed = true
|
787
|
+
|
279
788
|
Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
|
280
789
|
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
281
790
|
raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
|
282
791
|
end
|
283
|
-
|
284
|
-
private
|
285
|
-
|
286
|
-
# Persist the object into the datastore. Assign it an id first if it doesn't have one.
|
287
|
-
#
|
288
|
-
# @since 0.2.0
|
289
|
-
def persist(conditions = nil)
|
290
|
-
run_callbacks(:save) do
|
291
|
-
self.hash_key = SecureRandom.uuid if hash_key.blank?
|
292
|
-
|
293
|
-
# Add an exists check to prevent overwriting existing records with new ones
|
294
|
-
if new_record?
|
295
|
-
conditions ||= {}
|
296
|
-
(conditions[:unless_exists] ||= []) << self.class.hash_key
|
297
|
-
end
|
298
|
-
|
299
|
-
# Add an optimistic locking check if the lock_version column exists
|
300
|
-
if self.class.attributes[:lock_version]
|
301
|
-
conditions ||= {}
|
302
|
-
self.lock_version = (lock_version || 0) + 1
|
303
|
-
# Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually
|
304
|
-
(conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if changes[:lock_version][0]
|
305
|
-
end
|
306
|
-
|
307
|
-
attributes_dumped = Dumping.dump_attributes(attributes, self.class.attributes)
|
308
|
-
|
309
|
-
begin
|
310
|
-
Dynamoid.adapter.write(self.class.table_name, attributes_dumped, conditions)
|
311
|
-
@new_record = false
|
312
|
-
true
|
313
|
-
rescue Dynamoid::Errors::ConditionalCheckFailedException => e
|
314
|
-
if new_record?
|
315
|
-
raise Dynamoid::Errors::RecordNotUnique.new(e, self)
|
316
|
-
else
|
317
|
-
raise Dynamoid::Errors::StaleObjectError.new(self, 'persist')
|
318
|
-
end
|
319
|
-
end
|
320
|
-
end
|
321
|
-
end
|
322
792
|
end
|
323
793
|
end
|