class AWS::Record::Base

An ActiveRecord-like interface built ontop of Amazon SimpleDB.

class Book < AWS::Record::Model

  string_attr :title
  string_attr :author
  integer_attr :number_of_pages

  timestamps # adds a :created_at and :updated_at pair of timestamps

end

b = Book.new(:title => 'My Book', :author => 'Me', :pages => 1)
b.save

# Attribute Macros

When extending AWS::Record::Model you should first consider what attributes your class should have. Unlike ActiveRecord, AWS::Record models are not backed by a database table/schema. You must choose what attributes (and what types) you need.

### Usage

Normally you just call these methods inside your model class definition:

class Book < AWS::Record::Model
  string_attr :title
  boolean_attr :has_been_read
  integer_attr :number_of_pages
  float_attr :weight_in_pounds
  datetime_attr :published_at
end

For each attribute macro a pair of setter/getter methods are added # to your class (and a few other useful methods).

b = Book.new
b.title = "My Book"
b.has_been_read = true
b.number_of_pages = 1000
b.weight_in_pounds = 1.1
b.published_at = Time.now
b.save

b.id #=> "0aa894ca-8223-4d34-831e-e5134b2bb71c"
b.attributes
#=> { 'title' => 'My Book', 'has_been_read' => true, ... }

### Default Values

All attribute macros accept the `:default_value` option. This sets a value that is populated onto all new instnaces of the class.

class Book < AWS::Record::Model
  string_attr :author, :default_value => 'Me'
end

Book.new.author #=> 'Me'

### Multi-Valued (Set) Attributes

AWS::Record permits storing multiple values with a single attribute.

class Book < AWS::Record::Model
  string_attr :tags, :set => true
end

b = Book.new
b.tags #=> #<Set: {}>

b.tags = ['fiction', 'fantasy']
b.tags #=> #<Set: {'fiction', 'fantasy'}>

These multi-valued attributes are treated as sets, not arrays. This means:

Please consider these limitations when you choose to use the `:set` option with the attribute macros.

# Validations

It's important to validate models before there are persisted to keep your data clean. AWS::Record supports most of the ActiveRecord style validators.

class Book < AWS::Record::Model
  string_attr :title
  validates_presence_of :title
end

b = Book.new
b.valid? #=> false
b.errors.full_messages #=> ['Title may not be blank']

Validations are checked before saving a record. If any of the validators adds an error, the the save will fail.

For more information about the available validation methods see {Validations}.

# Finder Methods

You can find records by their ID. Each record gets a UUID when it is saved for the first time. You can use this ID to fetch the record at a latter time:

b = Book["0aa894ca-8223-4d34-831e-e5134b2bb71c"]

b = Book.find("0aa894ca-8223-4d34-831e-e5134b2bb71c")

If you try to find a record by ID that has no data an error will be raised.

### All

You can enumerate all of your records using `all`.

Book.all.each do |book|
  puts book.id
end

Book.find(:all) do |book|
  puts book.id
end

Be careful when enumerating all. Depending on the number of records and number of attributes each record has, this can take a while, causing quite a few requests.

### First

If you only want a single record, you should use `first`.

b = Book.first

### Modifiers

Frequently you do not want ALL records or the very first record. You can pass options to `find`, `all` and `first`.

my_books = Book.find(:all, :where => 'owner = "Me"')

book = Book.first(:where => { :has_been_read => false })

You can pass as find options:

# Scopes

More useful than writing query fragments all over the place is to name your most common conditions for reuse.

class Book < AWS::Record::Model

  scope :mine, where(:owner => 'Me')

  scope :unread, where(:has_been_read => false)

  scope :by_popularity, order(:score, :desc)

  scope :top_10, by_popularity.limit(10)

end

# The following expression returns 10 books that belong
# to me, that are unread sorted by popularity.
next_good_reads = Book.mine.unread.top_10

There are 3 standard scope methods:

### Conditions (where)

Where accepts aruments in a number of forms:

  1. As an sql-like fragment. If you need to escape values this form is not suggested.

    Book.where('title = "My Book"')
    
  2. An sql-like fragment, with placeholders. This escapes quoted arguments properly to avoid injection.

    Book.where('title = ?', 'My Book')
    
  3. A hash of key-value pairs. This is the simplest form, but also the least flexible. You can not use this form if you need more complex expressions that use or.

    Book.where(:title => 'My Book')
    

### Order

This orders the records as returned by AWS. Default ordering is ascending. Pass the value :desc as a second argument to sort in reverse ordering.

Book.order(:title)        # alphabetical ordering
Book.order(:title, :desc) # reverse alphabetical ordering

You may only order by a single attribute. If you call order twice in the chain, the last call gets presedence:

Book.order(:title).order(:price)

In this example the books will be ordered by :price and the order(:title) is lost.

### Limit

Just call `limit` with an integer argument. This sets the maximum number of records to retrieve:

Book.limit(2)

### Delayed Execution

It should be noted that all finds are lazy (except `first`). This means the value returned is not an array of records, rather a handle to a {Scope} object that will return records when you enumerate over them.

This allows you to build an expression without making unecessary requests. In the following example no request is made until the call to each_with_index.

all_books = Books.all
ten_books = all_books.limit(10)

ten_books.each_with_index do |book,n|
  puts "#{n + 1} : #{book.title}"
end

Public Class Methods

[](id, options = {})
Alias for: find_by_id
all(options = {}) click to toggle source

Returns an enumerable scope object represents all records.

Book.all.each do |book|
  # ...
end

This method is equivalent to `find(:all)`, and therefore you can also pass aditional options. See {.find} for more information on what options you can pass.

Book.all(:where => { :author' => 'me' }).each do |my_book|
  # ...
end

@return [Scope] Returns an enumerable scope object.

# File lib/aws/record/model/finder_methods.rb, line 118
def all options = {}
  new_scope.find(:all, options)
end
boolean_attr(name, options = {}) click to toggle source

Adds a boolean attribute to this class.

@example

class Book < AWS::Record::Model
  boolean_attr :read
end

b = Book.new
b.read? # => false
b.read = true
b.read? # => true

listing = Listing.new(:score => '123.456'
listing.score # => 123.456

@param [Symbol] name The name of the attribute.

# File lib/aws/record/model/attributes.rb, line 295
def boolean_attr name, options = {}

  attr = add_attribute(Attributes::BooleanAttr.new(name, options))

  # add the boolean question mark method
  define_method("#{attr.name}?") do
    !!__send__(attr.name)
  end

end
count(options = {}) click to toggle source

Counts records in SimpleDB.

With no arguments, counts all records:

People.count

Accepts query options to count a subset of records:

People.count(:where => { :boss => true })

You can also count records on a scope object:

People.find(:all).where(:boss => true).count

See {find} and {Scope#count} for more details.

@param [Hash] options ({}) Options for counting

records.

@option options [Mixed] :where Conditions that determine what

records are counted.

@option options [Integer] :limit The max number of records to count.

# File lib/aws/record/model/finder_methods.rb, line 149
def count(options = {})
  new_scope.count(options)
end
Also aliased as: size
create_domain(shard_name = nil) click to toggle source

Creates the SimpleDB domain that is configured for this class.

class Product < AWS::Record::Model
end

Product.create_table #=> 'Product'

If you share a single AWS account with multiple applications, you can provide a domain prefix for your model classes.

AWS::Record.domain_prefix = 'myapp-'

Product.create_table #=> 'myapp-Product'

If you have set a model shard name, this is used in place of the class name.

AWS::Record.domain_prefix = 'prod-'
class Product < AWS::Record::Model
  set_shard_name 'products'
end

Product.create_table #=> 'prod-products'

If you shard you data across multiple domains, you can specify the shard name:

# create two domains, with the given names
Product.create_domain 'products-1'
Product.create_domain 'products-2'

@param [optional,String] shard_name Defaults to the class name.

@return [SimpleDB::Domain]

# File lib/aws/record/model.rb, line 329
def create_domain shard_name = nil
  sdb.domains.create(sdb_domain_name(shard_name))
end
date_attr(name, options = {}) click to toggle source

Adds a date attribute to this class.

@example A standard date attribute

class Person < AWS::Record::Model
  date_attr :birthdate
end

baby = Person.new
baby.birthdate = Time.now
baby.birthdate #=> <Date: ....>

@param [Symbol] name The name of the attribute.

@param [Hash] options

@option options [Boolean] :set (false) When true this attribute

can have multiple dates.
# File lib/aws/record/model/attributes.rb, line 350
def date_attr name, options = {}
  add_attribute(Record::Attributes::DateAttr.new(name, options))
end
datetime_attr(name, options = {}) click to toggle source

Adds a datetime attribute to this class.

@example A standard datetime attribute

class Recipe < AWS::Record::Model
  datetime_attr :invented
end

recipe = Recipe.new(:invented => Time.now)
recipe.invented #=> <DateTime ...>

If you add a datetime_attr for `:created_at` and/or `:updated_at` those will be automanaged.

@param [Symbol] name The name of the attribute.

@param [Hash] options

@option options [Boolean] :set (false) When true this attribute

can have multiple date times.
# File lib/aws/record/model/attributes.rb, line 327
def datetime_attr name, options = {}
  add_attribute(Record::Attributes::DateTimeAttr.new(name, options))
end
domain(shard_name)
Alias for: shard
each(&block) click to toggle source

Yields once for each record.

# File lib/aws/record/model/finder_methods.rb, line 123
def each &block
  all.each(&block)
end
find(*args) click to toggle source

Finds records in SimpleDB and returns them as objects of the current class.

Finding `:all` returns an enumerable scope object

People.find(:all, :order => [:age, :desc], :limit => 10).each do |person|
  puts person.name
end

Finding `:first` returns a single record (or nil)

boss = People.find(:first, :where => { :boss => true })

Find accepts a hash of find modifiers (`:where`, `:order` and `:limit`). You can also choose to omit these modifiers and chain them on the scope object returned. In the following example only one request is made to SimpleDB (when each is called)

people = People.find(:all)

johns = people.where(:name => 'John Doe')

johns.order(:age, :desc).limit(10).each do |suspects|
  # ...
end

See also {where}, {order} and {limit} for more information and options.

@overload find(id)

@param id The record to find, raises an exception if the record is
  not found.

@overload find(mode, options = {})

@param [:all,:first] mode (:all) When finding `:all` matching records
  and array is returned of records.  When finding `:first` then
  `nil` or a single record will be returned.
@param [Hash] options
@option options [Mixed] :where Conditions that determine what
  records are returned.
@option options [String,Array] :sort The order records should be
  returned in.
@option options [Integer] :limit The max number of records to fetch.
# File lib/aws/record/model/finder_methods.rb, line 85
def find *args
  new_scope.find(*args)
end
find_by_id(id, options = {}) click to toggle source

@param [String] id The id of the record to load. @param [Hash] options @option options [String] :shard Specifies what shard (i.e. domain)

should be searched.

@raise [RecordNotFound] Raises a record not found exception if there

was no data found for the given id.

@return [Record::HashModel] Returns the record with the given id.

# File lib/aws/record/model/finder_methods.rb, line 26
def find_by_id id, options = {}

  domain = sdb_domain(options[:shard] || options[:domain])

  data = domain.items[id].data.attributes

  raise RecordNotFound, "no data found for id: #{id}" if data.empty?

  obj = self.new(:shard => domain)
  obj.send(:hydrate, id, data)
  obj

end
Also aliased as: []
first(options = {}) click to toggle source

@return [Object,nil] Returns the first record found. If there were

no records found, nil is returned.
# File lib/aws/record/model/finder_methods.rb, line 156
def first options = {}
  new_scope.first(options)
end
float_attr(name, options = {}) click to toggle source

Adds a float attribute to this class.

class Listing < AWS::Record::Model
  float_attr :score
end

listing = Listing.new(:score => '123.456')
listing.score # => 123.456

@param [Symbol] name The name of the attribute. @param [Hash] options @option options [Boolean] :set (false) When true this attribute

can have multiple values.
# File lib/aws/record/model/attributes.rb, line 231
def float_attr name, options = {}
  add_attribute(Attributes::FloatAttr.new(name, options))
end
integer_attr(name, options = {}) click to toggle source

Adds an integer attribute to this class.

class Recipe < AWS::Record::Model
  integer_attr :servings
end

recipe = Recipe.new(:servings => '10')
recipe.servings #=> 10

@param [Symbol] name The name of the attribute. @param [Hash] options @option options [Boolean] :set (false) When true this attribute

can have multiple values.
# File lib/aws/record/model/attributes.rb, line 181
def integer_attr name, options = {}
  add_attribute(Attributes::IntegerAttr.new(name, options))
end
limit(limit) click to toggle source

The maximum number of records to return. By default, all records matching the where conditions will be returned from a find.

People.limit(10).each {|person| ... }

Limit can be chained with other scope modifiers:

People.where(:age => 40).limit(10).each {|person| ... }
# File lib/aws/record/model/finder_methods.rb, line 225
def limit limit
  new_scope.limit(limit)
end
order(*args) click to toggle source

Defines the order in which records are returned when performing a find. SimpleDB only allows sorting by one attribute per request.

# oldest to youngest
People.order(:age, :desc).each {|person| ... }

You can chain order with the other scope modifiers:

Pepole.order(:age, :desc).limit(10).each {|person| ... }

@overload order(attribute, direction = :asc)

@param [String,Symbol] attribute The attribute to sort by.
@param [:asc,:desc] direction (:asc) The direction to sort.
# File lib/aws/record/model/finder_methods.rb, line 212
def order *args
  new_scope.order(*args)
end
sdb_domain(shard_name = nil) click to toggle source

@return [AWS::SimpleDB::Domain] @api private

# File lib/aws/record/model.rb, line 335
def sdb_domain shard_name = nil
  sdb.domains[sdb_domain_name(shard_name)]
end
shard(shard_name) click to toggle source

Returns a chainable scope object that restricts further scopes to a particular domain.

Book.domain('books-2').each do |book|
  # ...
end

@param [String] shard_name @return [Scope] Returns a scope for restricting the domain of subsequent

# File lib/aws/record/model/finder_methods.rb, line 98
def shard shard_name
  new_scope.shard(shard_name)
end
Also aliased as: domain
size(options = {})
Alias for: count
sortable_float_attr(name, options = {}) click to toggle source

Adds sortable float attribute to this class.

Persisted values are stored (and sorted) as strings. This makes it more difficult to sort numbers because they don't sort lexicographically unless they have been offset to be positive and then zero padded.

### Postive Floats

To store floats in a sort-friendly manor:

sortable_float_attr :score, :range => (0..10)

This will cause values like 5.5 to persist as a string like '05.5' so that they can be sorted lexicographically.

### Negative Floats

If you need to store negative sortable floats, increase your `:range` to include a negative value.

sortable_float_attr :position, :range => (-10..10)

AWS::Record will add 10 to all values and zero pad them (e.g. -10.0 will be represented as '00.0' and 10 will be represented as '20.0'). This will allow the values to be compared lexicographically.

@note If you change the `:range` after some values have been persisted

you must also manually migrate all of the old values to have the
correct padding & offset or they will be interpreted differently.

@param [Symbol] name The name of the attribute. @param [Hash] options @option options [Range] :range The range of numbers this attribute

should represent.  The min and max values of this range will determine
how many digits of precision are required and how much of an offset
is required to make the numbers sort lexicographically.

@option options [Boolean] :set (false) When true this attribute

can have multiple values.
# File lib/aws/record/model/attributes.rb, line 274
def sortable_float_attr name, options = {}
  add_attribute(Attributes::SortableFloatAttr.new(name, options))
end
sortable_integer_attr(name, options = {}) click to toggle source

Adds a sortable integer attribute to this class.

class Person < AWS::Record::Model
  sortable_integer_attr :age, :range => 0..150
end

person = Person.new(:age => 10)
person.age #=> 10

### Validations

It is recomended to apply a validates_numericality_of with minimum and maximum value constraints. If a value is assigned to a sortable integer that falls outside of the +:range: it will raise a runtime error when the record is saved.

### Difference Between Sortable an Regular Integer Attributes

Because SimpleDB does not support numeric types, all values must be converted to strings. This complicates sorting by numeric values. To accomplish sorting numeric attributes the values must be zero padded and have an offset applied to eliminate negative values.

@param [Symbol] name The name of the attribute. @param [Hash] options @option options [Range] :range A numeric range the represents the

minimum and  maximum values this attribute should accept.

@option options [Boolean] :set (false) When true this attribute

can have multiple values.
# File lib/aws/record/model/attributes.rb, line 214
def sortable_integer_attr name, options = {}
  add_attribute(Attributes::SortableIntegerAttr.new(name, options))
end
string_attr(name, options = {}) click to toggle source

Adds a string attribute to this class.

@example A standard string attribute

class Recipe < AWS::Record::Model
  string_attr :name
end

recipe = Recipe.new(:name => "Buttermilk Pancakes")
recipe.name #=> 'Buttermilk Pancakes'

@example A string attribute with `:set` set to true

class Recipe < AWS::Record::Model
  string_attr :tags, :set => true
end

recipe = Recipe.new(:tags => %w(popular dessert))
recipe.tags #=> #<Set: {"popular", "desert"}>

@param [Symbol] name The name of the attribute. @param [Hash] options @option options [Boolean] :set (false) When true this attribute

can have multiple values.
# File lib/aws/record/model/attributes.rb, line 164
def string_attr name, options = {}
  add_attribute(Record::Attributes::StringAttr.new(name, options))
end
timestamps() click to toggle source

A convenience method for adding the standard two datetime attributes `:created_at` and `:updated_at`.

@example

class Recipe < AWS::Record::Model
  timestamps
end

recipe = Recipe.new
recipe.save
recipe.created_at #=> <DateTime ...>
recipe.updated_at #=> <DateTime ...>
# File lib/aws/record/model/attributes.rb, line 368
def timestamps
  c = datetime_attr :created_at
  u = datetime_attr :updated_at
  [c, u]
end
where(*args) click to toggle source

Limits which records are retried from SimpleDB when performing a find.

Simple string condition

Car.where('color = "red" or color = "blue"').each {|car| ... }

String with placeholders for quoting params

Car.where('color = ?', 'red')

Car.where('color = ? OR style = ?', 'red', 'compact')

# produces a condition using in, like: WHERE color IN ('red', 'blue')
Car.where('color IN ?', ['red','blue'])

Hash arguments

# WHERE age = '40' AND gender = 'male'
People.where(:age => 40, :gender => 'male').each {|person| ... }

# WHERE name IN ('John', 'Jane')
People.where(:name => ['John', 'Jane']).each{|person| ... }

Chaining where with other scope modifiers

# 10 most expensive red cars
Car.where(:color => 'red').order(:price, :desc).limit(10)

@overload where(conditions_hash)

@param [Hash] conditions_hash A hash of attributes to values.  Each
  key/value pair from the hash becomes a find condition.  All
  conditions are joined by AND.

@overload where(sql_fragment[, quote_params, …])

# File lib/aws/record/model/finder_methods.rb, line 195
def where *args
  new_scope.where(*args)
end

Protected Class Methods

sdb() click to toggle source
# File lib/aws/record/model.rb, line 345
def sdb
  AWS::SimpleDB.new
end
sdb_domain_name(shard_name = nil) click to toggle source
# File lib/aws/record/model.rb, line 340
def sdb_domain_name shard_name = nil
  "#{AWS::Record.domain_prefix}#{self.shard_name(shard_name)}"
end

Public Instance Methods

attributes() click to toggle source

@return [Hash] A hash with attribute names as hash keys (strings) and

attribute values (of mixed types) as hash values.
Calls superclass method
# File lib/aws/record/model.rb, line 286
def attributes
  attributes = super
  attributes['id'] = id if persisted?
  attributes
end
id() click to toggle source

The id for each record is auto-generated. The default strategy generates uuid strings. @return [String] Returns the id string (uuid) for this record. Retuns

nil if this is a new record that has not been persisted yet.
# File lib/aws/record/model.rb, line 280
def id
  @_id
end

Protected Instance Methods

create_storage() click to toggle source

@api private

# File lib/aws/record/model.rb, line 405
def create_storage
  to_add = serialize_attributes
  sdb_item.attributes.add(to_add.merge(opt_lock_conditions))
end

Private Instance Methods

delete_storage() click to toggle source

@return [true] @api private

# File lib/aws/record/model.rb, line 442
def delete_storage
  sdb_item.delete(opt_lock_conditions)
  @_deleted = true
end
deserialize_item_data(item_data) click to toggle source

This function accepts a hash of item data (as returned from AttributeCollection#to_h or ItemData#attributes) and returns only the key/value pairs that are configured attribues for this class. @api private

# File lib/aws/record/model.rb, line 371
def deserialize_item_data item_data

  marked_for_deletion = item_data['_delete_'] || []

  data = {}
  item_data.each_pair do |attr_name,values|

    attribute = self.class.attributes[attr_name]

    next unless attribute
    next if marked_for_deletion.include?(attr_name)

    if attribute.set?
      data[attr_name] = values.map{|v| attribute.deserialize(v) }
    else
      data[attr_name] = attribute.deserialize(values.first)
    end

  end
  data
end
hydrate(id, data) click to toggle source
Calls superclass method
# File lib/aws/record/model.rb, line 393
def hydrate(id, data)
  @_id = id
  super
end
populate_id() click to toggle source

@api private

# File lib/aws/record/model.rb, line 399
def populate_id
  @_id = SecureRandom.uuid
end
sdb_domain() click to toggle source

@return [SimpleDB::Domain] Returns the domain this record is

persisted to or will be persisted to.
# File lib/aws/record/model.rb, line 362
def sdb_domain
  self.class.sdb_domain(shard)
end
sdb_item() click to toggle source

@return [SimpleDB::Item] Returns a reference to the item as stored in

simple db.

@api private

# File lib/aws/record/model.rb, line 355
def sdb_item
  sdb_domain.items[id]
end
update_storage() click to toggle source

@api private

# File lib/aws/record/model.rb, line 412
def update_storage

  to_update = {}
  to_delete = []

  # serialized_attributes will raise error if the entire record is blank
  attribute_values = serialize_attributes

  changed.each do |attr_name|
    if values = attribute_values[attr_name]
      to_update[attr_name] = values
    else
      to_delete << attr_name
    end
  end

  to_update.merge!(opt_lock_conditions)

  if to_delete.empty?
    sdb_item.attributes.replace(to_update)
  else
    sdb_item.attributes.replace(to_update.merge('_delete_' => to_delete))
    sdb_item.attributes.delete(to_delete + ['_delete_'])
  end

end