h1. has_metadata -- Keep your tables narrow | *Author* | Tim Morgan | | *Version* | 1.2.3 (Jan 23, 2012) | | *License* | Released under the MIT License. | h2. Important note for those upgrading from 1.0 to 1.1 You must re-run the @rails generate metadata@ task to update your @Metadata@ model. When finished, your @app/models/metadata.rb@ file should look like:
# Stores information about a model that doesn't need to be in that model's
# table. Each row in the @metadata@ table stores a schemaless, serialized hash
# of data associated with a model instance. Any model can have an associated row
# in the @metadata@ table by using the {HasMetadata} module.
#
# h2. Properties
#
# | @data@ | A hash of this metadata's contents (YAML serialized in the database). |
class Metadata < HasMetadata::Model
end
Note that this is significantly emptier than the previous model.
h2. About
Wide tables are a problem for big databases. If your @ActiveRecord@ models have
10, maybe 15 columns, some of which are @VARCHARs@ or maybe even @TEXTs@, it's
going to slow your queries down when you start to scale up.
The easy solution to this problem is to limit your projections; in other words,
to only @SELECT@ the columns that you actually need. If you've got a @users@
table with a giant @about_me@ text column, and you're only trying to look up the
user's login, then just select the @login@ column.
In the long run, though, a superior solution is to just move those
@about_me@-type columns to a completely different table. This table has just one
JSON-serialized field, making it schemaless, so it doesn't waste space. Each row
in this table is associated with a record in another table (@Metadata@ @has_one@
of your models).
This way, when your website gets huge, all of your giant, freeform data is in
one table that you can shard, or move off to an alternate database, or even a
NoSQL-type document store, or otherwise manage as you please. Your relational
tables remain slim and efficient, containing only columns that a) are indexed,
or b) you need frequent access to.
This gem includes a generator that creates the @Metadata@ model, and a module
that you can include in your models to define which fields have been spun off to
the metadata record.
h2. Installation
*Important Note:* This gem is only compatible with Ruby 1.9 and Rails 3.0.
Firstly, add the gem to your Rails project's @Gemfile@:
gem 'has_metadata'
Next, run the generator, which will add the @Metadata@ model and its migration
to your application.
rails generate metadata
h2. Usage
The first thing to think about is what columns to keep in your model. You will
need to keep any indexed columns, or any columns you perform lookups or other
SQL queries with. You should also keep any frequently accessed columns,
especially if they are small (integers or booleans). Good candidates for the
metadata table are the @TEXT@- and @VARCHAR@-type columns that you only need to
render a page or two in your app.
You'll need to change your model's schema so that it has a @metadata_id@ column
that will associate the model with its @Metadata@ instance:
t.belongs_to :metadata
Next, include the @HasMetadata@ module in your model, and call the
@has_metadata@ method to define the schema of your metadata. You can get more
information in the {HasMetadata::ClassMethods#has_metadata} documentation, but for starters, here's a
basic example:
class User < ActiveRecord::Base
include HasMetadata
has_metadata({
about_me: { type: String, length: { maximum: 512 } },
birthdate: { type: Date, presence: true },
zipcode: { type: Number, numericality: { greater_than: 9999, less_than: 10_000_} }
})
end
As you can see, you pass field names mapped to a hash. The hash describes the
validation that will be performed, and is in the same format as a call to
@validates@. In addition to the @EachValidator@ keys shown above, you can also
pass a @type@ key, to constrain the Ruby type that can be assigned to the field.
Each of these fields (in this case, @about_me@, @birthdate@, and @zipcode@) can
be accessed and set as first_level methods on an instance of your model:
user.about_me #=> "I was born in 1982 in Aberdeen. My father was a carpenter from..."
... and thus, used as part of @form_for@ fields:
form_for user do |f|
f.text_area :about_me, rows: 5, cols: 80
end
The only thing you _can't_ do is use these fields in a query, obviously. You
can't do something like @User.where(zipcode: 90210)@, because that column
doesn't exist on the @users@ table.