On Github canweriotnow / rails4-postgres
CREATE TABLE test ( id integer, title varchar(255), body text, child_ids integer[]); --This is an array
class CreateTest < ActiveRecord::Migration
def change
create_table :test do |t|
t.title :string
t.body :text
t.child_ids :integer, array: true, default: []
t.timestamps
end
end
acts_as_taggable_on is great, but it can slow down your queries.
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
t.string :title
t.text :body
t.integer :tag_array, array: true, default: []
t.timestamps
end
add_index :posts, :tag_array, using: :gin
end
end
class Post < ActiveRecord::Base
acts_as_taggable
before_save :cache_tags
private
def cache_tags
tag_array = self.tag_list
end
end
Post.where('tag_array @> ?', 'tag')
or
Post.where('tag_array @> ?', ['tag1', 'tag2'])
class EnableHstore < ActiveRecord::Migration
def change
enable_extension 'hstore'
end
end
Then:
class AddMetadataToPosts < ActiveRecord::Migration
def change
add_column :posts, :metadata, :hstore
add_index :posts, :metadata, using: :gin
end
end
[1] pry(main)> p = Post.first
=> #<Post id: 1, title: "Test", body: "Lorem ipsum dolor sit amet", tag_array: [], metadata: nil>
[2] pry(main) p.metadata = {foo: 'bar', baz: 'quux'}
=> {:foo=>"bar", :baz=>"quux"}
[3] pry(main) p.save
...
=> true
[4] pry(main) p
=> #<Post id: 1, title: "Test", body: "Lorem ipsum dolor sit amet", tag_array: [], metadata: {:foo=>"bar", :baz=>"quux"}>
With store_accessor, ActiveRecord lets us treat hstore keys like column attributes.
class Post < ActiveRecord::Base
store_accessor :metadata, :foo, :baz
end
[5] pry(main)> p.foo
=> "bar"
[6] pry(main)> p.foo = 'xyzzyx'
=> 'xyzzyx'
[7] pry(main)> p.save
...
=> true
[8] pry(main)> Post.where("metadata -> 'foo' = 'xyzzyx'")
=> [#<Post id: 1, title: "Test", body: "Lorem ipsum dolor sit amet", tag_array: [], metadata: {:foo=>"xyzzyx", :baz=>"quux"}>]
You might have noticed this:
add_index :posts, :metadata, using: :gin
Postgres defaults to using btree indices, but provides GiN (Generalized Inverted Index) and GiST (Generalized Search Tree) index types as well.
As of version 4.0.0, ActiveRecord supports the using keyword to specify index types.
Basically, GiN is three times faster on read operations, and three times slower on writes, compared to GiST.
I suggest reading the fine manual for your use case.
(But you'll mostly want GiN.)
UUIDs are pretty useful, and Postgres has an extension for a UUID column type.
We'll have to enable it:
class EnableUuid < ActiveRecord::Migration
def change
enable_extension 'uuid-ossp'
end
end
Now we can even use UUID primary keys:
class CreateComments < ActiveRecord::Migration
def change
create_table :comments, id: :uuid do |t|
t.text :body
t.references :post
t.timestamps
end
end
end
You'll have to work around some ActiveRecord defaults, like references in migrations (creates integer fk columns), as well as redefining class methods like first and last.
ActiveRecord 4 gives us some nice support, but we can do more.
Enter the schema_plus gem.
Normally, if we wanted to use basic features like views in our DB, it meant writing irreversible SQL migrations and converting schema.rb to SQL.
Not anymore.
This is from actual production code. Don't worry, it's long since dropped:
class CreateStreamItemView < ActiveRecord::Migration
def up
execute <<-SQL
CREATE OR REPLACE VIEW stream_items AS
SELECT a.id, a.user_id, a.title, a.body, a.private, a.type,
a.created_at, a.updated_at, a.comments_count, a.hearts_count,
a.allow_feedback, a.goal_commitment_id, a.caption,
a.evidence_file_name, a.evidence_file_size, a.evidence_updated_at,
( SELECT ARRAY( SELECT v.name
FROM ( SELECT ts.tag_id, ts.taggable_id,
ts.taggable_type, ts.tagger_id,
ts.tagger_type, ts.context, t.name
FROM taggings ts
JOIN tags t ON ts.tag_id = t.id) v
WHERE v.taggable_id = a.id AND v.taggable_type::text = 'Artifact'::text) AS "array") AS tags,
NULL::integer[] AS committed_user_ids, a.slug
FROM artifacts a
UNION
SELECT g.id, g.creator_id AS user_id, g.title, g.body, g.private,
'Goal'::character varying AS type, g.created_at, g.updated_at,
g.comments_count AS comments_count, g.hearts_count AS hearts_count,
g.allow_feedback, NULL::integer AS goal_commitment_id,
NULL::character varying AS caption,
NULL::character varying AS evidence_file_name,
NULL::integer AS evidence_file_size,
NULL::timestamp without time zone AS evidence_updated_at,
( SELECT ARRAY( SELECT v.name
FROM ( SELECT ts.tag_id, ts.taggable_id,
ts.taggable_type, ts.tagger_id,
ts.tagger_type, ts.context, t.name
FROM taggings ts
JOIN tags t ON ts.tag_id = t.id) v
WHERE v.taggable_id = g.id AND v.taggable_type::text = 'Goal'::text) AS "array") AS tags,
( SELECT ARRAY( SELECT v.user_id
FROM ( SELECT gc.user_id, gc.goal_id
FROM goal_commitments gc
JOIN goals g ON gc.goal_id = g.id) v
WHERE v.goal_id = g.id) AS "array") AS committed_user_ids, g.slug
FROM goals g;
SQL
end
def down
execute <<-SQL
drop view stream_items
SQL
end
end
It gets better:
class CreatePostsViews < ActiveRecord::Migration
# We can use the ActiveRecord query interface
create_view :commented_posts, Post.joins(:comments).where.not(comments: {id: nil})
# Or SQL
create_view :uncommented_posts, 'SELECT * FROM posts LEFT OUTER JOIN comments ON comments.post_id = posts.id WHERE comments.id IS NULL'
end
In either case, schema.rb will reflect the change, so we can still use rake db:schema:load and not have to convert our schema to SQL.
Schema Plus does a whole lot more:
There's still a lot that isn't baked into ActiveRecord, but there are great gems and clever hacks that support additional PG features:
There's a lot more to Postgres than I can cover here, and the folks at Heroku have put together a great tour: Postgres - The Bits You Haven't Found Yet
Email: jason@anestuary.com
Twitter: @canweriotnow
Github: https://github.com/canweriotnow
Blag: http://decomplecting.org