酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

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

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


foreign_key_exists?

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

1
2
3
4
5
6
7
8
# 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")

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

ActiveRecord::Base.suppress

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

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

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

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

ActiveRecord::Base#accessed_fields

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 しか使わないので、 以下のように書いて必要なカラムだけを取得するようにリファクタリングできる。

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

ActiveRecord::Base.ignored_columns

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

ActiveRecord::Relation#update

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

1
2
3
4
5
# 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_tableのオプション :if_exists

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

1
drop_table(:posts, if_exists: true)

ActiveRecord::Relation#or

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

1
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

ActiveRecord::Relation#left_outer_joins(#left_joins)

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

1
2
3
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と同じ順序でActiveRecordを返す

1
2
3
4
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名が変わった

1
2
3
4
5
6
7
8
9
#### 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

ActiveRecord::Relation#in_batches

1
2
3
4
 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を使うよりも早いという結果になりました。

1
2
3
4
5
6
7
8
9
10
11
12
13
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
...

1
2
3
4
5
6
7
8
9
10
11
12
13
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? の実装が改善。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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 というのがあるそう。何が嬉しいかというとロールバックができるそう。

1
2
3
4
5
6
7
8
9
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?

1
2
3
%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) ActiveRecord::Relation#any?

1
2
3
4
5
6
7
8
9
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 を使うと次のようなことができる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 楽しかったです^

Special Thanks

おすすめの書籍