Rails 4 has native support for postgresql’s array and hstore primitive types. From a database design perspective, I really like arrays. It saves having tables with just an ID and value for every field that can have multiple values.
A great summary of how to use these types in rails is Adam Sanderson’s amazing railsconf presentation. But for the basics, here’s a migration I’ve taken from relatabase.
1 2 3 4 5 6 7 8 9 10 11 |
class CreateContacts < ActiveRecord::Migration def change create_table :contacts do |t| t.string :name t.text :emails, array: true, null: false, default: '{}' t.text :phones, array: true, null: false, default: '{}' t.text :urls, array: true, null: false, default: '{}' end end end |
One huge caveat of this approach is that rails doesn’t clone the array when saving the original for dirty checking, so any in-place modification to the array won’t register that it’s changed.
1 2 3 4 5 6 7 8 9 10 |
homer = Contact.create name: 'Homer Simpson', emails: ['homer@example.com', 'hsimpson@gmail.com'] homer.emails << 'homer.simpson@outlook.com' homer.emails_changed? # => false homer.emails_was # => ['homer@example.com', 'hsimpson@gmail.com', 'homer.simpson@outlook.com'] homer.save! homer.reload.emails # => ['homer@example.com', 'hsimpson@gmail.com'] |
You can either manually signal that the record has changed, or you can be sure to only use methods that replace the array.
1 2 3 4 5 |
# This works homer.emails += 'homer.simpson@outlook.com' # So does this homer.emails << 'homer.simpson@outlook.com' homer.emails_will_change! |
One interesting aspect of this is we can store an array of foreign keys on an N side of the N:1 relationship. This means we can store keys where they’re most relevant, and get rid of join tables in some cases. Here’s a great example of where this could be a huge improvement from the status quo.
It’s pretty easy to get this sort of setup working in rails too.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Tables < ActiveRecord::Migration def change create_table :products do |t| t.string :name, null: false end create_table :coupons do |t| t.integer :product_ids, array: true, null: false, default: [] end end end class Coupon < ActiveRecord::Base def products Product.where id: self.product_ids end end class Product < ActiveRecord::Base def coupons Coupon.where 'product_ids @> ARRAY[?]', self.id end end |
Unfortunately, if you care about referential integrity (and you should), then there is no syntax for foreign keys from inside the elements of an array, though there has been some discussion on the preferred syntax.
One thing that I haven’t delved into yet is a nice way to validate the contents of an array. Suggestions welcome.