Has Metadata Column -- Keep your tables narrow
Author | Tim Morgan |
Version | 1.1.2 (Sep 3, 2013) |
License | Released under the MIT License. |
About
So you're wondering why it is you need to make, test, schedule, and deploy a whole nother migration just to add one more freeform "Favorite Music"-type column to your users model? Wish there were an easier way?
There is! Combine all of those "about me," "favorite music," etc. type fields
into one JSON-serialized TEXT
column, and now every model can have
schemaless, migration-free data.
If you're interested in moving your metadata out to another table (or database) entirely, consider using HasMetadata.
This gem does use some "metaprogramming magic" to make the metadata fields appear like first-class fields, for purposes of validation and easy access. If this is unsettling to you, I recommend using my gem JsonSerialize instead, as it does not get its little fingers all up in ActiveRecord's business.
(Why yes, I do have a gem for a every use case!)
h2. Installation
Important Note: This gem is only compatible with Ruby 1.9 and Rails 4.0.
Merely add the gem to your Rails project's Gemfile
:
gem 'has_metadata_column'
Usage
The first thing to think about is what columns to keep. 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 column are
the TEXT
- and VARCHAR
-type columns that you only need to render a page or
two in your app.
You'll need to add a TEXT
column to your model to store the metadata. You can
call it what you want; metadata
is assumed by default.
t.text :metadata
Next, include the HasMetadataColumn
module in your model, and call the
has_metadata_column
method to define the schema of your metadata. You can get
more information in the {HasMetadataColumn::ClassMethods#has_metadata_column}
documentation, but for starters, here's a basic example:
class User < ActiveRecord::Base
include HasMetadataColumn
has_metadata(
:my_metadata_column,
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.
You can only assign types that can be JSON-serialized: strings, numbers, arrays,
hashes, dates/times, booleans, and nil
.
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
... and validations.
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.
... Unless you use PostgreSQL 9.2, and define your metadata column as type
json
. Support for that is coming...