Active RecordのCHANGELOG(Rails 5.0.0.beta2)を読んでみた! 


2/22(月)に開催された Sendagaya.rb #138 に参加して、
ActiveRecod CHANGELOG
を読んだのでその斜め読みのメモです!


🐝 foreign_key_exists?

migrationファイルで使えるメソッド。テーブルに外部キー制約が付いているかを確認する foreign_key_exists? が追加。

# Check a foreign key exists
foreign_key_exists?(:accounts, :branches)
# Check a foreign key on a specified column exists
foreign_key_exists?(:accounts, column: :owner_id)
# Check a foreign key with a custom name exists
foreign_key_exists?(:accounts, name: "special_fk_name")

アンチパターンにあるらしいから外部キー制約はちゃんと付け足ほうがいい。

🍄 Active Record::Base.suppress

class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
after_create -> { Notification.create! comment: self,
recipients: commentable.recipients }
end

こんな風に Comment を作成したら必ず Notification を作るようなパターンはあるけど、
特定のパターンの場合は、Notification を作成しないためには次のように記述する。

module Copyable
def copy_to(destination)
Notification.suppress do
# この中では Comment を作成しても Notification は作らない。
end
end
end

とすると Notification.suppress のブロックの中では、Notification が作られなくなる。

🚌 Active Record::Base#accessed_fields

こんな感じのリファクタリングができるようになる。

class PostsController < ActionController::Base
after_action :print_accessed_fields, only: :index
def index
@posts = Post.all
end
private
def print_accessed_fields
p @posts.first.accessed_fields #=> :id, :title, :author_id, :updated_at
end
end

とすると :id, :title, :author_id, :updated_at しか使わないので、
次のように書いて必要なカラムだけを取得するようにリファクタリングできる。

class PostsController < ActionController::Base
def index
@posts = Post.select(:id, :title, :author_id, :updated_at)
end
end

🍣 Active Record::Base.ignored_columns

ActiveRecord::Base.ignored_columns でカラムを定義すると、Active Recordでは閲覧できないカラムを定義することができる。
おそらく、DBにあるけどもActiveRecodなどで参照してほしくないようなカラムを定義する。

🎂 Active Record::Relation#update

ActiveRecord::Relation#updateの動作が次のように変わったとのこと。

# Before: idとattriubutesを渡す形式だった。1SQLで実行できるけどcallbackが呼ばれない
Person.update(15, :user_name => 'Samuel', :group => 'expert')
# After: こういった形で呼べる。1件1件更新するのでちょっと遅いけど、callbackが呼ばれる
Comment.where(group: 'expert').update(body: "Group of Rails Experts")

🗻 drop_テーブルのオプション :if_exists

migrationファイル内のオプションで :if_exists を設定できるようになった。

drop_table(:posts, if_exists: true)

🎉 Active Record::Relation#or

Post.where(‘id = 1’).or(Post.where(‘id = 2’)) を正確に解釈するようになりました

Post.where('id = 1').or(Post.where('id = 2'))
#=> SELECT * FROM posts WHERE (id = 1) OR (id = 2)

Added #or to ActiveRecord::Relation by matthewd · Pull Request #16052 · rails/rails

😎 Active Record::Relation#left_outer_joins(#left_joins)

外部結合のための #left_outer_joins(#left_joins) が追加。

User.left_outer_joins(:posts)
#=> SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON
#=> "posts"."user_id" = "users"."id"

added ActiveRecord::Relation#outer_joins by Crunch09 · Pull Request #12071 · rails/rails

🐰 findで与えたidsと同じ順序でActive Recordを返す

records = Topic.find([4,2,5])
assert_equal 'The Fourth Topic of the day', records[0].title
assert_equal 'The Second Topic of the day', records[1].title
assert_equal 'The Fifth Topic of the day', records[2].title

🏈 after_commitのcallback名が変わった

#### Before ####
after_commit :add_to_index_later, on: :create
after_commit :update_in_index_later, on: :update
after_commit :remove_from_index_later, on: :destroy
#### After ####
after_create_commit :add_to_index_later
after_update_commit :update_in_index_later
after_destroy_commit :remove_from_index_later

🏀 Active Record::Relation#in_batches

People.in_batches(of: 100) do |people|
people.where('id % 2 = 0').update_all(sleep: true)
people.where('id % 2 = 1').each(&:party_all_night!)
end

ちなみにちょっとおもしろかったのはパフォーマンスです。 idを指定しつつ検索をしていく場合のほうが、OFFSETを使うよりも早いという結果になりました。

SELECT "posts"."id" FROM "posts" ORDER BY "posts"."id" ASC LIMIT 2
SELECT "posts"."id" FROM "posts" WHERE ("posts"."id" > 2) ORDER BY "posts"."id" ASC LIMIT 2
SELECT "posts"."id" FROM "posts" WHERE ("posts"."id" > 4) ORDER BY "posts"."id" ASC LIMIT 2
# ↑ のパフォーマンス
Benchmark times (50M rows, no index, batch size 1000):
batch time
1 194.7s
2 0.016s
3 0.006s
4 0.008s
5 0.007s
...
SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "posts" LIMIT 2 OFFSET 0) subquery_for_count
SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "posts" LIMIT 2 OFFSET 2) subquery_for_count
SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "posts" LIMIT 2 OFFSET 4) subquery_for_count
# ↑ のパフォーマンス
Benchmark times (50M rows, no index, batch size 1000):
batch time
1 192.5s
2 242.0s
3 257.5s
4 256.8s
5 258.7s
... (average times increase on each iteration, because of higher offset)

🗽 none? / one? の実装の変更

none? や、 one? の実装が改善。

# Before:
users.none?
# SELECT "users".* FROM "users"
users.one?
# SELECT "users".* FROM "users"
# After:
users.none?
# SELECT 1 AS one FROM "users" LIMIT 1
users.one?
# SELECT COUNT(*) FROM "users"

🤔 belongs_toでアソシエーションがなければバリデーションエラー

belongs_to でアソシエーションがなければ、バリデーションエラーになるようになった。
optional: trueをつけるとエラーが出ないようになる。

🐯 Tips

以下は今回のCHANGE LOGとは直接は関係ない部分

(Tips) revertを使うとrollbackできる

migrationファイルの中で以下のようなことができる revert というのがあるそう。何がうれしいかというとロールバックができるそう。

class FixupExampleMigration < ActiveRecord::Migration[5.0]
def change
revert do
create_table(:apples) do |t|
t.string :variety
end
end
end
end

Reverting Previous Migrations - Rails Guide

(Tips) RubyのEnumerable#any?

%w{ant bear cat}.any? {|word| word.length >= 3} #=> true
%w{ant bear cat}.any? {|word| word.length >= 4} #=> true
[ nil, true, 99 ].any? #=> true

(Tips) Active Record::Relation#any?

person.pets # => [#<Pet name: "Snoop", group: "dogs">]
person.pets.any? do |pet|
pet.group == 'cats'
end # => false
person.pets.any? do |pet|
pet.group == 'dogs'
end # => true

(Tips) define_attribute_methods

define_attribute_methods を使うと次のようなことができる。

class Person
include ActiveModel::AttributeMethods
attr_accessor :name, :age, :address
attribute_method_prefix 'clear_'
private
def clear_attribute(attr)
send("#{attr}=", nil)
end
end
person = Person.new
person.name = 'John Due'
person.clear_name
puts person.name #=> nil

DRYを実現するのに便利そう

ActiveModel::AttributeMethods で重複をなくす - Qiita

🚕 あとがき

CHANGELOGを集中して読んだり、いろんな人の意見を聞く機会っていなかなかなかので、本当にいい勉強になりました!
Sendagaya.rb #138 楽しかったです^^

👽 参考リンク

📚 おすすめの書籍