ocean-dynamo 0.3.10 → 0.3.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f6249788253589b08c22ba6f39f134b14ac986b5
4
- data.tar.gz: 1d170f9c05448c3136fe7df37de6f6f43f56666f
3
+ metadata.gz: d5fb4f095350a4a1cc43debd8405c550277ec7c1
4
+ data.tar.gz: e1684c9a9481b00b6ba44657076eeda1930e3628
5
5
  SHA512:
6
- metadata.gz: 84d2847bba2ae68f2c29aa52844081bab0a0e61976b8b9b5fc25cb185f8eb039481418b501ce36db628a8912bfbc95fce488121abe766de45cca39525f5fc433
7
- data.tar.gz: 238e095ec0105f540326430af4e0445c02cdf9ee557780e87c6e10284f53aa6e97aa43be57c29747e343b4051a51c1f3bdfef782abea06e60f8164a5a7a3511b
6
+ metadata.gz: c8673fa49c6af6b931103b669fc1a597e74b68a864354ab26cd5cddf3d1acc509f0d50c171f8deff2c63227b21aaa1eab2875b8684682e6558886d9cf3abc376
7
+ data.tar.gz: 6be8bab8ca04c7a5e3e3cf4364627fef8a0cac2894d480dda2a12cb31ac72c1b2773cc88fb1ac600e51d2ba6ff3db4a5f92aecc820d6b4f0ff7a44b0b678ff1d
data/README.rdoc CHANGED
@@ -8,7 +8,7 @@ OceanDynamo requires Ruby 2.0 and Ruby on Rails 4.0.0 or later.
8
8
  {<img src="https://badge.fury.io/rb/ocean-dynamo.png" alt="Gem Version" />}[http://badge.fury.io/rb/ocean-dynamo]
9
9
 
10
10
 
11
- ==== Features
11
+ === Features
12
12
 
13
13
  As one important use case for OceanDynamo is to facilitate the conversion of SQL based
14
14
  ActiveRecord models to DynamoDB based models, it is important that the syntax and semantics
@@ -19,7 +19,7 @@ is of course based on ActiveModel.
19
19
  The attribute and persistence layer of OceanDynamo is modeled on that of ActiveRecord:
20
20
  there's +save+, +save!+, +create+, +update+, +update!+, +update_attributes+, +find_each+,
21
21
  and all the other methods you're used to. The design goal is always to implement as much
22
- as possible of the ActiveRecord interface, without sacrificing scalability. This makes the
22
+ of the ActiveRecord interface as possible, without compromising scalability. This makes the
23
23
  task of switching from SQL to no-SQL much easier.
24
24
 
25
25
  Thanks to its structural similarity to ActiveRecord, OceanDynamo works with FactoryGirl.
@@ -28,7 +28,7 @@ To facilitate testing, future versions will keep track of and delete instances a
28
28
  OceanDynamo uses primary and secondary indices to retrieve related table items,
29
29
  which means it will scale without limits.
30
30
 
31
- ==== Example
31
+ === Example
32
32
 
33
33
  The following example shows the syntax.
34
34
 
@@ -93,18 +93,54 @@ controllers. Furthermore, OceanDynamo implements much of the infrastructure of A
93
93
  for instance, +read_attribute+, +write_attribute+, and much of the control logic and
94
94
  parameters.
95
95
 
96
- At the moment, Model.find only takes a single id. Model.all and Model.count work as
97
- expected. Model.find_each and Model.find_in_batches are also available.
96
+ +Model.all+ and +.count+ work as expected. +Model.find_each+ is also available.
98
97
 
99
- Work has begun on the +has_many+ / +belongs_to+ association which requires only a primary
100
- index. This association is even more efficient than its ActiveRecord counterpart.
98
+ +belongs_to+ is now operational. The other side, +has_many+, is much simpler and should
99
+ follow very soon (probably today).
100
+
101
+ Restrictions for +belongs_to+ tables:
102
+ * * The hash key must be specified and must not be +:id+.
103
+ * * The range key must not be specified at all.
104
+ * * belongs_to can be specified only once in each class.
105
+
106
+ These restrictions allow OceanDynamo to implement the +has_many+ / +belongs_to+
107
+ relation in a very efficient and massively scalable way.
108
+
109
+ +belongs_to+ claims the range key and uses it to store its own UUID, which normally
110
+ would be stored in the hash key attribute. Instead, the hash key attribute holds the
111
+ UUID of the parent. We have thus reversed the roles of these two fields. As a result,
112
+ all children have their parent UUID as their hash key, and their own UUID in their
113
+ range key.
114
+
115
+ This type of association is even more efficient than its ActiveRecord counterpart as
116
+ it uses only the primary index of the child model for finds and scans in both directions
117
+ of the +has_many+ / +belongs_to+ association.
118
+
119
+ Furthermore, since DynamoDB has powerful primary index searches involving substrings
120
+ and matching, the fact that the range key is a string can be used to implement
121
+ wildcard matching of additional attributes. This gives, amongst other things, the
122
+ equivalent of an SQL GROUP BY request, again without requiring any secondary indices.
123
+
124
+ It's our goal to use a similar technique to implement has_and_belongs_to_many relations,
125
+ which means that secondary indices won't be necessary for the vast majority of
126
+ OceanDynamo use cases. This ultimately means reduced operational costs, as well as
127
+ reduced complexity.
128
+
129
+ +.has_belongs_to?+ returns true if the class has a belongs_to association.
130
+
131
+ +.belongs_to_class returns the class of the belongs_to association, or false if none.
132
+
133
+ +.find+ can now take an array arg.
134
+
135
+ +#hash_key+ and +#range_key+ readers added.
101
136
 
102
137
 
103
138
  === Future milestones
104
139
 
105
140
  After the +has_many+ / +belongs_to+ association, the +has_and_belongs_to_many+ assocation
106
- will be implemented. This will require secondary indices. This, and other associations will
107
- use secondary indices. OceanDynamo will use association proxies to achieve the same kind of
141
+ will be implemented. This will require secondary indices, unlike +has_many+ / +belongs_to+.
142
+
143
+ OceanDynamo will use association proxies to achieve the same kind of
108
144
  interface as ActiveRecord, e.g.: <code>blog_entry.comments.build(body: "Cool!").save!</code>
109
145
 
110
146
 
@@ -115,7 +151,7 @@ e.g. to implement critical job queues. It will be used increasingly as features
115
151
  added to OceanDynamo and will eventually replace all ActiveRecord tables in Ocean.
116
152
 
117
153
 
118
- ==== Installation
154
+ == Installation
119
155
 
120
156
  gem install ocean-dynamo
121
157
 
data/lib/ocean-dynamo.rb CHANGED
@@ -9,11 +9,12 @@ require "ocean-dynamo/exceptions"
9
9
  require "ocean-dynamo/class_variables"
10
10
  require "ocean-dynamo/tables"
11
11
  require "ocean-dynamo/schema"
12
- require "ocean-dynamo/callbacks"
13
12
  require "ocean-dynamo/attributes"
14
- require "ocean-dynamo/queries"
13
+ require "ocean-dynamo/callbacks"
15
14
  require "ocean-dynamo/persistence"
16
- require "ocean-dynamo/associations"
15
+ require "ocean-dynamo/queries"
16
+
17
+ require "ocean-dynamo/associations/belongs_to"
17
18
 
18
19
 
19
20
  module OceanDynamo
@@ -0,0 +1,124 @@
1
+ module OceanDynamo
2
+ class Base
3
+
4
+ def self.belongs_to(target) # :master, "master", Master
5
+ target_attr = target.to_s.underscore # "master"
6
+ target_attr_id = "#{target_attr}_id" # "master_id"
7
+ target_class = target_attr.camelize.constantize # Master
8
+
9
+ assert_only_one_belongs_to!
10
+
11
+ self.table_range_key = table_hash_key # The RANGE KEY is variable
12
+ self.table_hash_key = target_attr_id.to_sym # The HASH KEY is the parent UUID
13
+
14
+ attribute table_hash_key, :string # Define :master_id
15
+ define_attribute_accessors(table_hash_key) # Define master_id, master_id=, master_id?
16
+
17
+ # Make sure there always is a parent
18
+ validates(table_hash_key, presence: true) # Can't save without a parent_id
19
+
20
+ # Define the range attribute (our unique UUID)
21
+ attribute(table_range_key, :string, default: "") # Define :uuid
22
+ define_attribute_accessors(table_range_key) # define uuid, uuid=, uuid?
23
+
24
+
25
+
26
+ # Define the parent id attribute
27
+ attribute target_attr_id, :reference, default: nil, target_class: target_class,
28
+ association: :belongs_to
29
+
30
+ self.class_eval "def #{target_attr}
31
+ read_and_maybe_load_pointer('#{target_attr_id}')
32
+ end"
33
+
34
+ self.class_eval "def #{target_attr}=(value)
35
+ write_attribute('#{target_attr_id}', value)
36
+ @#{target_attr} = value
37
+ end"
38
+
39
+ self.class_eval "def #{target_attr_id}
40
+ get_pointer_id(@#{target_attr})
41
+ end"
42
+
43
+ self.class_eval "def #{target_attr_id}=(value)
44
+ write_attribute('#{target_attr_id}', value)
45
+ @#{target_attr} = value
46
+ end"
47
+ # TODO: "?" methods
48
+ end
49
+
50
+
51
+ #
52
+ # Returns true if the class has a belongs_to association.
53
+ #
54
+ def self.has_belongs_to?
55
+ fields[table_hash_key]['association'] == :belongs_to
56
+ end
57
+
58
+
59
+
60
+ #
61
+ # Returns the class of the belongs_to association, or false if none.
62
+ #
63
+ def self.belongs_to_class
64
+ has_belongs_to? && fields[table_hash_key]['target_class']
65
+ end
66
+
67
+
68
+
69
+ protected
70
+
71
+ #
72
+ # belongs_to can be specified only once in each model, since we use the range key to
73
+ # store its UUID and the hash key to store the UUID of the parent, as in
74
+ # ["parent_uuid", "child_uuid"]. This allows the parent to find all its children
75
+ # extremely efficiently by using only the primary index. It also allows the child
76
+ # to find its parent using only its own hash key. Presto: scalability without any
77
+ # secondary indices in the has_many/belongs_to association.
78
+ #
79
+ # Caveat: the parent must have a simple primary key, not a composite one. It *is*
80
+ # possible to use a composite key, but then the children must use scans to find
81
+ # their parents. We could conditionalise this, of course, so that the lookup
82
+ # strategy is transparent to the user.
83
+ #
84
+ def self.assert_only_one_belongs_to! # :nodoc:
85
+ if has_belongs_to?
86
+ raise OceanDynamo::AssociationMustBeUnique,
87
+ "#{self} already belongs_to #{belongs_to_class}"
88
+ end
89
+ false
90
+ end
91
+
92
+
93
+ #
94
+ # This is run by #initialize and by #assign_attributes to set the
95
+ # association variables (@master, for instance) and its associated attribute
96
+ # (such as master_id) to the value given.
97
+ #
98
+ def assign_associations(attrs) # :nodoc:
99
+ if attrs && attrs.include?(:master)
100
+ @master = attrs[:master]
101
+ write_attribute('master_id', @master)
102
+ end
103
+ end
104
+
105
+
106
+ def read_and_maybe_load_pointer(name) # :nodoc:
107
+ ptr = read_attribute(name)
108
+ return nil if ptr.blank?
109
+ if persisted? && ptr.is_a?(String)
110
+ parent = fields[name][:target_class].find(ptr, consistent: true) # TODO: true?
111
+ write_attribute(name, parent) # Keep the instance we've just read
112
+ else
113
+ ptr
114
+ end
115
+ end
116
+
117
+
118
+ def get_pointer_id(ptr) # :nodoc:
119
+ return nil if ptr.blank?
120
+ ptr.is_a?(String) ? ptr : ptr.id
121
+ end
122
+
123
+ end
124
+ end
@@ -11,6 +11,23 @@ module OceanDynamo
11
11
  attr_reader :dynamo_item # :nodoc:
12
12
 
13
13
 
14
+ #
15
+ # Returns the value of the hash key attribute
16
+ #
17
+ def hash_key
18
+ read_attribute(table_hash_key)
19
+ end
20
+
21
+
22
+ #
23
+ # Returns the value of the range key attribute or false if the
24
+ # table doesn't have a range_key.
25
+ #
26
+ def range_key
27
+ table_range_key && read_attribute(table_range_key)
28
+ end
29
+
30
+
14
31
  def initialize(attrs={})
15
32
  run_callbacks :initialize do
16
33
  @attributes = Hash.new
@@ -22,6 +39,7 @@ module OceanDynamo
22
39
  @new_record = true
23
40
  raise UnknownPrimaryKey unless table_hash_key
24
41
  end
42
+ assign_associations(attrs)
25
43
  attrs && attrs.delete_if { |k, v| !fields.has_key?(k) }
26
44
  super(attrs)
27
45
  end
@@ -89,6 +107,7 @@ module OceanDynamo
89
107
  def assign_attributes(values, without_protection: false)
90
108
  return if values.blank?
91
109
  values = values.stringify_keys
110
+ assign_associations(values)
92
111
  # if values.respond_to?(:permitted?)
93
112
  # unless values.permitted?
94
113
  # raise ActiveModel::ForbiddenAttributesError
@@ -140,7 +159,7 @@ module OceanDynamo
140
159
  if respond_to?("#{k}=")
141
160
  raise
142
161
  else
143
- raise UnknownAttributeError, "unknown attribute: #{k}"
162
+ raise UnknownAttributeError, "unknown attribute: `#{k}'"
144
163
  end
145
164
  end
146
165
 
@@ -23,7 +23,7 @@ module OceanDynamo
23
23
  def initialize(record) # :nodoc:
24
24
  @record = record
25
25
  errors = @record.errors.full_messages.join(", ")
26
- super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
26
+ super(:"#{@record.class}.errors.messages.record_invalid")
27
27
  end
28
28
  end
29
29
 
@@ -38,7 +38,7 @@ module OceanDynamo
38
38
  def initialize(record) # :nodoc:
39
39
  @record = record
40
40
  errors = @record.errors.full_messages.join(", ")
41
- super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
41
+ super(:"#{@record.class}.errors.messages.record_invalid")
42
42
  end
43
43
  end
44
44
 
@@ -54,4 +54,10 @@ module OceanDynamo
54
54
 
55
55
  class AssociationTypeMismatch < DynamoError; end
56
56
 
57
+
58
+ class BelongsToError < DynamoError; end
59
+ class AssociationMustBeUnique < BelongsToError; end
60
+ class RangeKeyMustNotBeSpecified < BelongsToError; end
61
+ class HashKeyMayNotBeNamedId < BelongsToError; end
62
+
57
63
  end
@@ -122,8 +122,13 @@ module OceanDynamo
122
122
  run_callbacks :commit do
123
123
  run_callbacks :save do
124
124
  run_callbacks :create do
125
- k = read_attribute(table_hash_key)
126
- write_attribute(table_hash_key, SecureRandom.uuid) if k == "" || k == nil
125
+ # Default the correct hash key to a UUID
126
+ if self.class.has_belongs_to?
127
+ write_attribute(table_range_key, SecureRandom.uuid) if range_key.blank?
128
+ else
129
+ write_attribute(table_hash_key, SecureRandom.uuid) if hash_key.blank?
130
+ end
131
+
127
132
  set_timestamps
128
133
  dynamo_persist
129
134
  true
@@ -171,8 +176,8 @@ module OceanDynamo
171
176
 
172
177
 
173
178
  def reload(**keywords)
174
- range_key = table_range_key && attributes[table_range_key]
175
- new_instance = self.class.find(id, range_key, **keywords)
179
+ raise "HELLISHNESS" if id == range_key
180
+ new_instance = self.class.find(hash_key, range_key, **keywords)
176
181
  assign_attributes(new_instance.attributes)
177
182
  self
178
183
  end
@@ -295,16 +300,15 @@ module OceanDynamo
295
300
 
296
301
  def serialize_attribute(attribute, value, metadata=fields[attribute],
297
302
  target_class: metadata[:target_class],
298
- type: metadata[:type],
299
- no_save: metadata[:no_save]
303
+ type: metadata[:type]
300
304
  )
301
305
  return nil if value == nil
306
+ #value = value.id if value.kind_of?(target_class)
302
307
  case type
303
308
  when :reference
304
- return nil if no_save
305
309
  raise DynamoError, ":reference must always have a :target_class" unless target_class
306
310
  return value if value.is_a?(String)
307
- return value.id if value.is_a?(target_class)
311
+ return value.id if value.kind_of?(target_class)
308
312
  raise AssociationTypeMismatch, "can't save a #{value.class} in a #{target_class} :reference"
309
313
  when :string
310
314
  return nil if ["", []].include?(value)
@@ -2,9 +2,13 @@ module OceanDynamo
2
2
  class Base
3
3
 
4
4
  def self.find(hash, range=nil, consistent: false)
5
+ return hash.collect {|elem| find elem, range, consistent: consistent } if hash.is_a?(Array)
5
6
  _late_connect?
7
+ hash = hash.id if hash.kind_of?(Base) # TODO: We have (innocuous) leakage, fix!
6
8
  item = dynamo_items[hash, range]
7
- raise RecordNotFound, "can't find a #{self} with primary key ['#{hash}', #{range.inspect}]" unless item.exists?
9
+ unless item.exists?
10
+ raise RecordNotFound, "can't find a #{self} with primary key ['#{hash}', #{range.inspect}]"
11
+ end
8
12
  new._setup_from_dynamo(item, consistent: consistent)
9
13
  end
10
14
 
@@ -41,13 +41,7 @@ module OceanDynamo
41
41
  attribute(lock_attribute, :integer, default: 0) if locking
42
42
  block.call
43
43
  # Define attribute accessors
44
- fields.each do |name, md|
45
- name = name.to_s
46
- # We define accessors even if the name is 'id' (for which we already have methods)
47
- self.class_eval "def #{name}; read_attribute('#{name}'); end"
48
- self.class_eval "def #{name}=(value); write_attribute('#{name}', value); end"
49
- self.class_eval "def #{name}?; read_attribute('#{name}').present?; end"
50
- end
44
+ fields.each { |name, md| define_attribute_accessors(name) }
51
45
  # Connect to AWS
52
46
  establish_db_connection if connect == true
53
47
  # Finally return the full table name
@@ -55,6 +49,14 @@ module OceanDynamo
55
49
  end
56
50
 
57
51
 
52
+ def self.define_attribute_accessors(name)
53
+ name = name.to_s
54
+ self.class_eval "def #{name}; read_attribute('#{name}'); end"
55
+ self.class_eval "def #{name}=(value); write_attribute('#{name}', value); end"
56
+ self.class_eval "def #{name}?; read_attribute('#{name}').present?; end"
57
+ end
58
+
59
+
58
60
  def self.compute_table_name
59
61
  name.pluralize.underscore
60
62
  end
@@ -43,19 +43,30 @@ module OceanDynamo
43
43
 
44
44
 
45
45
  def self.set_dynamo_table_keys
46
- dynamo_table.hash_key = [table_hash_key, fields[table_hash_key][:type]]
46
+ hash_key_type = fields[table_hash_key][:type]
47
+ hash_key_type = :string if hash_key_type == :reference
48
+ dynamo_table.hash_key = [table_hash_key, hash_key_type]
49
+
47
50
  if table_range_key
48
- dynamo_table.range_key = [table_range_key, fields[table_range_key][:type]]
51
+ range_key_type = fields[table_range_key][:type]
52
+ #range_key_type = :string if range_key_type == :reference
53
+ dynamo_table.range_key = [table_range_key, range_key_type]
49
54
  end
50
55
  end
51
56
 
52
57
 
53
58
  def self.create_table
59
+ hash_key_type = fields[table_hash_key][:type]
60
+ hash_key_type = :string if hash_key_type == :reference
61
+
62
+ range_key_type = table_range_key && fields[table_range_key][:type]
63
+ #range_key_type = :string if range_key_type == :reference
64
+
54
65
  self.dynamo_table = dynamo_client.tables.create(table_full_name,
55
66
  table_read_capacity_units, table_write_capacity_units,
56
- hash_key: { table_hash_key => fields[table_hash_key][:type]},
57
- range_key: table_range_key && { table_range_key => fields[table_range_key][:type]}
58
- )
67
+ hash_key: { table_hash_key => hash_key_type},
68
+ range_key: table_range_key && { table_range_key => range_key_type }
69
+ )
59
70
  sleep 1 until dynamo_table.status == :active
60
71
  setup_dynamo
61
72
  true
@@ -1,3 +1,3 @@
1
1
  module OceanDynamo
2
- VERSION = "0.3.10"
2
+ VERSION = "0.3.11"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ocean-dynamo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.10
4
+ version: 0.3.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Bengtson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-09-15 00:00:00.000000000 Z
11
+ date: 2013-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk
@@ -160,12 +160,12 @@ extensions: []
160
160
  extra_rdoc_files: []
161
161
  files:
162
162
  - config/routes.rb
163
- - lib/ocean-dynamo/associations.rb
163
+ - lib/ocean-dynamo/associations/belongs_to.rb
164
+ - lib/ocean-dynamo/associations/collection_proxy.rb
164
165
  - lib/ocean-dynamo/attributes.rb
165
166
  - lib/ocean-dynamo/base.rb
166
167
  - lib/ocean-dynamo/callbacks.rb
167
168
  - lib/ocean-dynamo/class_variables.rb
168
- - lib/ocean-dynamo/collection_proxy.rb
169
169
  - lib/ocean-dynamo/engine.rb
170
170
  - lib/ocean-dynamo/exceptions.rb
171
171
  - lib/ocean-dynamo/persistence.rb
@@ -1,51 +0,0 @@
1
- module OceanDynamo
2
- class Base
3
-
4
- def self.belongs_to(target) # :api_user, "api_user", ApiUser
5
- target_attr = target.to_s.underscore # "api_user"
6
- target_attr_id = "#{target_attr}_id" # "api_user_id"
7
- target_class = target_attr.camelize.constantize # ApiUser
8
- attribute target_attr_id, :reference, default: nil, target_class: target_class
9
- attribute target_attr, :reference, default: nil, target_class: target_class, no_save: true
10
-
11
- self.class_eval "def #{target_attr}
12
- read_and_maybe_load_pointer('#{target_attr_id}')
13
- end"
14
-
15
- self.class_eval "def #{target_attr}=(value)
16
- write_attribute('#{target_attr_id}', value)
17
- write_attribute('#{target_attr}', value)
18
- end"
19
-
20
- self.class_eval "def #{target_attr_id}
21
- read_pointer_id('#{target_attr}')
22
- end"
23
-
24
- self.class_eval "def #{target_attr_id}=(value)
25
- write_attribute('#{target_attr_id}', value)
26
- write_attribute('#{target_attr}', value)
27
- end"
28
- # TODO: "?" methods
29
- end
30
-
31
-
32
- protected
33
-
34
- def read_and_maybe_load_pointer(name) # :nodoc:
35
- ptr = read_attribute(name)
36
- return nil if ptr.blank?
37
- if persisted? && ptr.is_a?(String)
38
- write_attribute(name, fields[name][:target_class].find(ptr)) # Keep the instance we've just read
39
- else
40
- ptr
41
- end
42
- end
43
-
44
- def read_pointer_id(name) # :nodoc:
45
- ptr = read_attribute(name)
46
- return nil if ptr.blank?
47
- ptr.is_a?(String) ? ptr : ptr.id
48
- end
49
-
50
- end
51
- end