An ActiveRecord-like interface built ontop of Amazon SimpleDB.
class Book < AWS::Record::Model string_attr :title string_attr :author integer :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
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.
string_attr
boolean_attr
integer_attr
float_attr
datetime_attr
date_attr
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, ... }
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, :deafult_value => 'Me' end Book.new.author #=> 'Me'
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:
values are unordered
duplicate values are automatically omitted
Please consider these limitations when you choose to use the
:set
option with the attribute macros.
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}.
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.
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.
If you only want a single record, you should use first
.
b = Book.first
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:
:where
- Conditions that must be met to be returned
:order
- The order to sort matched records by
:limit
- The maximum number of records to return
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:
where
order
limit
Where accepts aruments in a number of forms:
As an sql-like fragment. If you need to escape values this form is not suggested.
Book.where('title = "My Book"')
An sql-like fragment, with placeholders. This escapes quoted arguments properly to avoid injection.
Book.where('title = ?', 'My Book')
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')
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.
Just call limit
with an integer argument. This sets the
maximum number of records to retrieve:
Book.limit(2)
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
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
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
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 {AWS::Record::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
Creates the SimpleDB domain that is configured for this class.
class Product < AWS::Record::Model end Product.create_table #=> creates the SimpleDB domain 'Product'
If you shard you data across multiple domains, you can specify the shard name:
# create two domains, with the given names Product.create_domain :shard_name => 'products-1' Product.create_domain :shard_name => 'products-2'
If you share a single AWS account with multiple applications, you can provide a domain prefix to group domains and to avoid name collisions:
AWS::Record.domain_prefix = 'myapp-' # creates the domain 'myapp-Product' Product.create_domain # creates the domain 'myapp-products-1' Product.create_domain :shard_name => 'products-1'
@param [Hash] options Hash of options passed to
{SimpleDB::DomainCollection#create}.
@option options [String] :shard_name Defaults to the class name. The
shard name will be prefixed with {AWS::Record.domain_prefix}, and that becomes the domain name.
@return [SimpleDB::Domain]
# File lib/aws/record/model.rb, line 313 def create_domain shard_name = nil sdb.domains.create(sdb_domain_name(shard_name)) end
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
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
Yields once for each record.
# File lib/aws/record/model/finder_methods.rb, line 123 def each &block all.each(&block) end
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
@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
@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
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
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
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
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 in SimpleDB to sort by. @param [:asc,:desc] direction (:asc) The direction to sort, ascending
or descending order.
# File lib/aws/record/model/finder_methods.rb, line 212 def order *args new_scope.order(*args) end
@return [AWS::SimpleDB::Domain] @private
# File lib/aws/record/model.rb, line 319 def sdb_domain shard_name = nil sdb.domains[sdb_domain_name(shard_name)] end
Returns a chainable scope object that restricts further scopes to a particular domain.
Book.domain('books-2').each do |book| # ... end
@param [String] domain @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
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.
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.
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
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
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.
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
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
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
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) @overload where(sql_fragment[, quote_params, …])
@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.
# File lib/aws/record/model/finder_methods.rb, line 194 def where *args new_scope.where(*args) end
# File lib/aws/record/model.rb, line 329 def sdb AWS::SimpleDB.new end
# File lib/aws/record/model.rb, line 324 def sdb_domain_name shard_name = nil "#{AWS::Record.domain_prefix}#{self.shard_name(shard_name)}" end
@private
# File lib/aws/record/model.rb, line 379 def create_storage to_add = serialize_attributes sdb_item.attributes.add(to_add.merge(opt_lock_conditions)) end