dynamoid 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -97,16 +97,7 @@ module Dynamoid #:nodoc:
97
97
  #
98
98
  # @since 0.2.0
99
99
  def records_with_index
100
- ids = if index.range_key?
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.new(hash).tap { |r| r.new_record = false } }
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
- Dynamoid::Adapter.scan(source.table_name, query, query_opts).collect {|hash| source.new(hash).tap { |r| r.new_record = false } }
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 == ['id'] || (query_keys.to_set == ['id', source.range_key.to_s].to_set)
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
@@ -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
- self.class.send(:field, self.class.hash_key) unless self.respond_to?(self.class.hash_key)
107
-
108
- @new_record = true
109
- @attributes ||= {}
110
- @associations ||= {}
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
- return false if other.nil?
120
- other.respond_to?(:hash_key) && other.hash_key == self.hash_key
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(self.hash_key).attributes
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=(key)
146
- self.send("#{self.class.hash_key}=".to_sym, 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
@@ -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
@@ -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
@@ -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
- items = Dynamoid::Adapter.read(self.table_name, ids, options)
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
- obj = self.new(item)
45
- obj.new_record = false
46
- return obj
58
+ from_database(item)
47
59
  else
48
- return nil
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