dynamoid 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "dynamoid"
8
- s.version = "0.1.1"
8
+ s.version = "0.1.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Josh Symonds"]
12
- s.date = "2012-02-27"
12
+ s.date = "2012-03-05"
13
13
  s.description = "Dynamoid is an ORM for Amazon's DynamoDB that supports offline development, associations, querying, and everything else you'd expect from an ActiveRecord-style replacement."
14
14
  s.email = "josh@joshsymonds.com"
15
15
  s.extra_rdoc_files = [
@@ -36,7 +36,6 @@ Gem::Specification.new do |s|
36
36
  "lib/dynamoid/associations/has_and_belongs_to_many.rb",
37
37
  "lib/dynamoid/associations/has_many.rb",
38
38
  "lib/dynamoid/associations/has_one.rb",
39
- "lib/dynamoid/attributes.rb",
40
39
  "lib/dynamoid/components.rb",
41
40
  "lib/dynamoid/config.rb",
42
41
  "lib/dynamoid/config/options.rb",
@@ -48,7 +47,6 @@ Gem::Specification.new do |s|
48
47
  "lib/dynamoid/finders.rb",
49
48
  "lib/dynamoid/indexes.rb",
50
49
  "lib/dynamoid/persistence.rb",
51
- "lib/dynamoid/relations.rb",
52
50
  "spec/app/models/address.rb",
53
51
  "spec/app/models/magazine.rb",
54
52
  "spec/app/models/sponsor.rb",
@@ -63,7 +61,6 @@ Gem::Specification.new do |s|
63
61
  "spec/dynamoid/associations/has_many_spec.rb",
64
62
  "spec/dynamoid/associations/has_one_spec.rb",
65
63
  "spec/dynamoid/associations_spec.rb",
66
- "spec/dynamoid/attributes_spec.rb",
67
64
  "spec/dynamoid/config_spec.rb",
68
65
  "spec/dynamoid/criteria/chain_spec.rb",
69
66
  "spec/dynamoid/criteria_spec.rb",
@@ -72,7 +69,6 @@ Gem::Specification.new do |s|
72
69
  "spec/dynamoid/finders_spec.rb",
73
70
  "spec/dynamoid/indexes_spec.rb",
74
71
  "spec/dynamoid/persistence_spec.rb",
75
- "spec/dynamoid/relations_spec.rb",
76
72
  "spec/dynamoid_spec.rb",
77
73
  "spec/spec_helper.rb"
78
74
  ]
@@ -2,6 +2,10 @@
2
2
 
3
3
  Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It provides similar functionality to ActiveRecord and improves on Amazon's existing [HashModel](http://docs.amazonwebservices.com/AWSRubySDK/latest/AWS/Record/HashModel.html) by providing better searching tools, native association support, and a local adapter for offline development.
4
4
 
5
+ DynamoDB is not like other document-based databases you might know, and is very different indeed from relational databases. It sacrifices anything beyond the simplest relational queries and transactional support to provide a fast, cost-efficient, and highly durable storage solution. If your database requires complicated relational queries and transaction support, then this modest Gem cannot provide them for you, and neither can DynamoDB. In those cases you would do better to look elsewhere for your database needs.
6
+
7
+ But if you want a fast, scalable, simple, easy-to-use database (and a Gem that supports it) then look no further!
8
+
5
9
  ## Warning!
6
10
 
7
11
  I'm still working on this gem a lot. It only provides .where(arguments) in its criteria chaining so far. More is coming though!
@@ -18,12 +22,14 @@ Then you need to initialize it to get it going. Put code similar to this somewhe
18
22
 
19
23
  ```ruby
20
24
  Dynamoid.configure do |config|
21
- config.adapter = 'local' # This adapter allows offline development without connecting to the DynamoDB servers. Data is NOT persisted.
22
- # config.adapter = 'aws_sdk' # This adapter establishes a connection to the DynamoDB servers using's Amazon's own awful AWS gem.
25
+ config.adapter = 'local' # This adapter allows offline development without connecting to the DynamoDB servers. Data is *NOT* persisted.
26
+ # config.adapter = 'aws_sdk' # This adapter establishes a connection to the DynamoDB servers using Amazon's own AWS gem.
23
27
  # config.access_key = 'access_key' # If connecting to DynamoDB, your access key is required.
24
28
  # config.secret_key = 'secret_key' # So is your secret key.
25
29
  config.namespace = "dynamoid_#{Rails.application.class.parent_name}_#{Rails.env}" # To namespace tables created by Dynamoid from other tables you might have.
26
- config.warn_on_scan = true # Output a warning to stdout when you perform a scan rather than a query on a table
30
+ config.warn_on_scan = true # Output a warning to the logger when you perform a scan rather than a query on a table.
31
+ config.partitioning = true # Spread writes randomly across the database. See "partitioning" below for more.
32
+ config.partition_size = 200 # Determine the key space size that writes are randomly spread across.
27
33
  end
28
34
 
29
35
  ```
@@ -36,9 +42,11 @@ class User
36
42
 
37
43
  field :name # Every field you have on the object must be specified here.
38
44
  field :email # If you have fields that aren't specified they won't be attached to the object as methods.
45
+ field :rank, :integer # Every field is assumed to be a string unless otherwise specified.
46
+ # created_at and updated_at with a type of :datetime are automatically added.
39
47
 
40
48
  index :name # Only specify indexes if you intend to perform queries on the specified fields.
41
- index :email # Fields without indexes enjoy extremely poor performance as they must use
49
+ index :email # Fields without indexes suffer extremely poor performance as they must use
42
50
  index [:name, :email] # scan rather than query.
43
51
 
44
52
  has_many :addresses # Associations do not accept any options presently. The referenced
@@ -84,6 +92,22 @@ Address.where(:city => 'Chicago').all # Find by any number of matching criteria.
84
92
  Address.find_by_city('Chicago') # The same as above, but using ActiveRecord's older syntax.
85
93
  ```
86
94
 
95
+ And you can also query on associations:
96
+
97
+ ```ruby
98
+ u.addresses.where(:city => 'Chicago').all
99
+ ```
100
+
101
+ But keep in mind Dynamoid -- and document-based storage systems in general -- are not drop-in replacements for existing relational databases. The above query does not efficiently perform a conditional join, but instead finds all the user's addresses and naively filters them in Ruby. For large associations this is a performance hit compared to relational database engines.
102
+
103
+ ## Partitioning, Provisioning, and Performance
104
+
105
+ DynamoDB achieves much of its speed by relying on a random pattern of writes and reads: internally, hash keys are distributed across servers, and reading from two consecutive servers is much faster than reading from the same server twice. Of course, many of our applications request one key (like a commonly used role, a superuser, or a very popular product) much more frequently than other keys. In DynamoDB, this will result in lowered throughput and slower response times, and is a design pattern we should try to avoid.
106
+
107
+ Dynamoid attempts to obviate this problem transparently by employing a partitioning strategy to divide up keys randomly across DynamoDB's servers. Each ID is assigned an additional number (by default 0 to 199, but you can increase the partition size in Dynamoid's configuration) upon save; when read, all 200 hashes are retrieved simultaneously and the most recently updated one is returned to the application. This results in a significant net performance increase, and is usually invisible to the application itself. It does, however, bring up the important issue of provisioning your DynamoDB tables correctly.
108
+
109
+ When your read or write provisioning exceed your table's allowed throughput, DynamoDB will wait on connections until throughput is available again. This will appear as very, very slow requests and can be somewhat frustrating. Partitioning significantly increases the amount of throughput tables will experience; though DynamoDB will ignore keys that don't exist, if you have 20 partitioned keys representing one object, all will be retrieved every time the object is requested. Ensure that your tables are set up for this kind of throughput, or turn provisioning off, to make sure that DynamoDB doesn't throttle your requests.
110
+
87
111
  ## Credits
88
112
 
89
113
  Dynamoid borrows code, structure, and even its name very liberally from the truly amazing [Mongoid](https://github.com/mongoid/mongoid). Without Mongoid to crib from none of this would have been possible, and I hope they don't mind me reusing their very awesome ideas to make DynamoDB just as accessible to the Ruby world as MongoDB.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.2
@@ -9,7 +9,6 @@ require "active_support/time_with_zone"
9
9
  require "active_model"
10
10
 
11
11
  require 'dynamoid/errors'
12
- require 'dynamoid/attributes'
13
12
  require 'dynamoid/fields'
14
13
  require 'dynamoid/indexes'
15
14
  require 'dynamoid/associations'
@@ -34,4 +33,8 @@ module Dynamoid
34
33
  Dynamoid::Config.logger
35
34
  end
36
35
 
36
+ def included_models
37
+ @included_models ||= []
38
+ end
39
+
37
40
  end
@@ -3,6 +3,7 @@ module Dynamoid #:nodoc:
3
3
 
4
4
  module Adapter
5
5
  extend self
6
+ attr_accessor :tables
6
7
 
7
8
  def adapter
8
9
  reconnect! unless @adapter
@@ -13,17 +14,89 @@ module Dynamoid #:nodoc:
13
14
  require "dynamoid/adapter/#{Dynamoid::Config.adapter}" unless Dynamoid::Adapter.const_defined?(Dynamoid::Config.adapter.camelcase)
14
15
  @adapter = Dynamoid::Adapter.const_get(Dynamoid::Config.adapter.camelcase)
15
16
  @adapter.connect! if @adapter.respond_to?(:connect!)
17
+ self.tables = benchmark('Cache Tables') {list_tables}
16
18
  end
17
19
 
18
- def method_missing(method, *args)
19
- if @adapter.respond_to?(method)
20
- start = Time.now
21
- result = @adapter.send(method, *args)
22
- Dynamoid.logger.info "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.join(',')}" unless args.empty? }"
23
- return result
20
+ def benchmark(method, *args)
21
+ start = Time.now
22
+ result = yield
23
+ 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? }"
24
+ return result
25
+ end
26
+
27
+ def write(table, object)
28
+ if Dynamoid::Config.partitioning? && object[:id]
29
+ object[:id] = "#{object[:id]}.#{Random.rand(Dynamoid::Config.partition_size)}"
30
+ object[:updated_at] = Time.now.to_f
31
+ end
32
+ benchmark('Put Item', object) {put_item(table, object)}
33
+ end
34
+
35
+ def read(table, ids)
36
+ if ids.respond_to?(:each)
37
+ if Dynamoid::Config.partitioning?
38
+ results = benchmark('Partitioned Batch Get Item', ids) {batch_get_item(table => id_with_partitions(ids))}
39
+ {table => result_for_partition(results[table])}
40
+ else
41
+ benchmark('Batch Get Item', ids) {batch_get_item(table => ids)}
42
+ end
43
+ else
44
+ if Dynamoid::Config.partitioning?
45
+ results = benchmark('Partitioned Get Item', ids) {batch_get_item(table => id_with_partitions(ids))}
46
+ result_for_partition(results[table]).first
47
+ else
48
+ benchmark('Get Item', ids) {get_item(table, ids)}
49
+ end
50
+ end
51
+ end
52
+
53
+ def delete(table, id)
54
+ if Dynamoid::Config.partitioning?
55
+ benchmark('Delete Item', id) do
56
+ id_with_partitions(id).each {|i| delete_item(table, i)}
57
+ end
58
+ else
59
+ benchmark('Delete Item', id) {delete_item(table, id)}
60
+ end
61
+ end
62
+
63
+ def scan(table, query)
64
+ if Dynamoid::Config.partitioning?
65
+ results = benchmark('Scan', table, query) {adapter.scan(table, query)}
66
+ result_for_partition(results)
67
+ else
68
+ adapter.scan(table, query)
24
69
  end
70
+ end
71
+
72
+ [:batch_get_item, :create_table, :delete_item, :delete_table, :get_item, :list_tables, :put_item, :query].each do |m|
73
+ define_method(m) do |*args|
74
+ benchmark("#{m.to_s}", args) {adapter.send(m, *args)}
75
+ end
76
+ end
77
+
78
+ def id_with_partitions(ids)
79
+ Array(ids).collect {|id| (0...Dynamoid::Config.partition_size).collect{|n| "#{id}.#{n}"}}.flatten
80
+ end
81
+
82
+ def result_for_partition(results)
83
+ Hash.new.tap do |hash|
84
+ Array(results).each do |result|
85
+ next if result.nil?
86
+ id = result[:id].split('.').first
87
+ if !hash[id] || (result[:updated_at] > hash[id][:updated_at])
88
+ result[:id] = id
89
+ hash[id] = result
90
+ end
91
+ end
92
+ end.values
93
+ end
94
+
95
+ def method_missing(method, *args)
96
+ return benchmark(method, *args) {adapter.send(method, *args)} if @adapter.respond_to?(method)
25
97
  super
26
98
  end
99
+
27
100
  end
28
101
 
29
102
  end
@@ -16,21 +16,23 @@ module Dynamoid
16
16
 
17
17
  # BatchGetItem
18
18
  def batch_get_item(options)
19
- batch = AWS::DynamoDB::BatchGet.new(:config => @@connection.config)
20
19
  hash = Hash.new{|h, k| h[k] = []}
21
20
  return hash if options.all?{|k, v| v.empty?}
22
21
  options.each do |t, ids|
23
- batch.table(t, :all, Array(ids)) unless ids.nil? || ids.empty?
24
- end
25
- batch.each do |table_name, attributes|
26
- hash[table_name] << attributes.symbolize_keys!
22
+ Array(ids).in_groups_of(100, false) do |group|
23
+ batch = AWS::DynamoDB::BatchGet.new(:config => @@connection.config)
24
+ batch.table(t, :all, Array(group)) unless group.nil? || group.empty?
25
+ batch.each do |table_name, attributes|
26
+ hash[table_name] << attributes.symbolize_keys!
27
+ end
28
+ end
27
29
  end
28
30
  hash
29
31
  end
30
32
 
31
33
  # CreateTable
32
34
  def create_table(table_name, key)
33
- table = @@connection.tables.create(table_name, 10, 5, :hash_key => {key.to_sym => :string})
35
+ table = @@connection.tables.create(table_name, 100, 20, :hash_key => {key.to_sym => :string})
34
36
  sleep 0.5 while table.status == :creating
35
37
  return table
36
38
  end
@@ -72,7 +74,7 @@ module Dynamoid
72
74
  def put_item(table_name, object)
73
75
  table = @@connection.tables[table_name]
74
76
  table.load_schema
75
- table.items.create(object.delete_if{|k, v| v.nil? || v.empty?})
77
+ table.items.create(object.delete_if{|k, v| v.nil? || (v.respond_to?(:empty?) && v.empty?)})
76
78
  end
77
79
 
78
80
  # Query
@@ -54,7 +54,7 @@ module Dynamoid
54
54
  # PutItem
55
55
  def put_item(table_name, object)
56
56
  table = data[table_name]
57
- table[:data][object[table[:id]]] = object
57
+ table[:data][object[table[:id]]] = object.delete_if{|k, v| v.nil? || (v.respond_to?(:empty?) && v.empty?)}
58
58
  end
59
59
 
60
60
  # Query
@@ -37,7 +37,7 @@ module Dynamoid #:nodoc:
37
37
  private
38
38
 
39
39
  def association(type, name, options = {})
40
- field "#{name}_ids".to_sym
40
+ field "#{name}_ids".to_sym, :set
41
41
  self.associations[name] = options.merge(:type => type)
42
42
  define_method(name) do
43
43
  @associations ||= {}
@@ -4,14 +4,24 @@ module Dynamoid #:nodoc:
4
4
  # The base association module.
5
5
  module Associations
6
6
  module Association
7
- attr_accessor :name, :options, :source
7
+ attr_accessor :name, :options, :source, :query
8
+ include Enumerable
8
9
 
9
10
  def initialize(source, name, options)
10
11
  @name = name
11
12
  @options = options
12
13
  @source = source
14
+ @query = {}
13
15
  end
14
16
 
17
+ def records
18
+ results = target_class.find(source_ids.to_a)
19
+ results = results.nil? ? [] : Array(results)
20
+ return results if query.empty?
21
+ results_with_query(results)
22
+ end
23
+ alias :all :records
24
+
15
25
  def empty?
16
26
  records.empty?
17
27
  end
@@ -49,11 +59,23 @@ module Dynamoid #:nodoc:
49
59
  self << object
50
60
  end
51
61
 
62
+ def where(args)
63
+ args.each {|k, v| query[k] = v}
64
+ self
65
+ end
66
+
67
+ def each(&block)
68
+ records.each(&block)
69
+ end
70
+
52
71
  private
53
72
 
54
- def records
55
- results = target_class.find(source_ids.to_a)
56
- results.nil? ? [] : Array(results)
73
+ def results_with_query(results)
74
+ results.find_all do |result|
75
+ query.all? do |attribute, value|
76
+ result.send(attribute) == value
77
+ end
78
+ end
57
79
  end
58
80
 
59
81
  def target_class
@@ -10,6 +10,14 @@ module Dynamoid #:nodoc:
10
10
  target == other
11
11
  end
12
12
 
13
+ def method_missing(method, *args)
14
+ if target.respond_to?(method)
15
+ target.send(method, *args)
16
+ else
17
+ super
18
+ end
19
+ end
20
+
13
21
  private
14
22
 
15
23
  def target
@@ -10,6 +10,9 @@ module Dynamoid #:nodoc
10
10
  extend ActiveModel::Callbacks
11
11
 
12
12
  define_model_callbacks :create, :save, :destroy
13
+
14
+ before_create :set_created_at
15
+ before_save :set_updated_at
13
16
  end
14
17
 
15
18
  include ActiveModel::Conversion
@@ -19,7 +22,6 @@ module Dynamoid #:nodoc
19
22
  include ActiveModel::Validations
20
23
  include ActiveModel::Serializers::JSON
21
24
  include ActiveModel::Serializers::Xml
22
- include Dynamoid::Attributes
23
25
  include Dynamoid::Fields
24
26
  include Dynamoid::Indexes
25
27
  include Dynamoid::Persistence
@@ -15,6 +15,9 @@ module Dynamoid #:nodoc
15
15
  option :access_key
16
16
  option :secret_key
17
17
  option :warn_on_scan, :default => true
18
+ option :partitioning, :default => false
19
+ option :partition_size, :default => 200
20
+ option :included_models, :default => []
18
21
 
19
22
  def default_logger
20
23
  defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
@@ -38,7 +38,7 @@ module Dynamoid #:nodoc:
38
38
  end
39
39
 
40
40
  def records_with_index
41
- ids = Dynamoid::Adapter.get_item(source.index_table_name(index), source.key_for_index(index, values_for_index))
41
+ ids = Dynamoid::Adapter.read(source.index_table_name(index), source.key_for_index(index, values_for_index))
42
42
  if ids.nil? || ids.empty?
43
43
  []
44
44
  else
@@ -6,22 +6,9 @@ module Dynamoid #:nodoc:
6
6
  module Document
7
7
  extend ActiveSupport::Concern
8
8
  include Dynamoid::Components
9
-
10
- attr_accessor :new_record
11
- alias :new_record? :new_record
12
-
13
- def initialize(attrs = {})
14
- @new_record = true
15
- @attributes ||= {}
16
- self.class.attributes.each {|att| write_attribute(att, attrs[att])}
17
- end
18
9
 
19
- def persisted?
20
- !new_record?
21
- end
22
-
23
- def ==(other)
24
- other.respond_to?(:id) && other.id == self.id
10
+ included do
11
+ Dynamoid::Config.included_models << self
25
12
  end
26
13
 
27
14
  module ClassMethods
@@ -37,6 +24,22 @@ module Dynamoid #:nodoc:
37
24
  self.new(attrs)
38
25
  end
39
26
  end
27
+
28
+ def initialize(attrs = {})
29
+ @new_record = true
30
+ @attributes ||= {}
31
+ attrs = self.class.undump(attrs)
32
+ self.class.attributes.keys.each {|att| write_attribute(att, attrs[att])}
33
+ end
34
+
35
+ def ==(other)
36
+ other.respond_to?(:id) && other.id == self.id
37
+ end
38
+
39
+ def reload
40
+ self.attributes = self.class.find(self.id).attributes
41
+ self
42
+ end
40
43
  end
41
44
 
42
45
  end
@@ -5,16 +5,18 @@ module Dynamoid #:nodoc:
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- class_attribute :fields
8
+ class_attribute :attributes
9
9
 
10
- self.fields = []
10
+ self.attributes = {}
11
11
  field :id
12
+ field :created_at, :datetime
13
+ field :updated_at, :datetime
12
14
  end
13
15
 
14
16
  module ClassMethods
15
- def field(name, options = {})
17
+ def field(name, type = :string, options = {})
16
18
  named = name.to_s
17
- self.fields << name
19
+ self.attributes[name] = {:type => type}.merge(options)
18
20
  define_method(named) do
19
21
  read_attribute(named)
20
22
  end
@@ -27,6 +29,39 @@ module Dynamoid #:nodoc:
27
29
  end
28
30
  end
29
31
 
32
+ attr_accessor :attributes
33
+ alias :raw_attributes :attributes
34
+
35
+ def write_attribute(name, value)
36
+ attributes[name.to_sym] = value
37
+ end
38
+ alias :[]= :write_attribute
39
+
40
+ def read_attribute(name)
41
+ attributes[name.to_sym]
42
+ end
43
+ alias :[] :read_attribute
44
+
45
+ def update_attributes(attributes)
46
+ attributes.each {|attribute, value| self.write_attribute(attribute, value)}
47
+ save
48
+ end
49
+
50
+ def update_attribute(attribute, value)
51
+ write_attribute(attribute, value)
52
+ save
53
+ end
54
+
55
+ private
56
+
57
+ def set_created_at
58
+ self.created_at = DateTime.now
59
+ end
60
+
61
+ def set_updated_at
62
+ self.updated_at = DateTime.now
63
+ end
64
+
30
65
  end
31
66
 
32
67
  end