Ruby on Railsのパフォーマンスと可読性のイディオム集

## 前提

```ruby:Schemafile
# -*- mode: ruby -*-
# vi: set ft=ruby :
# 単語
require 'people.schema'
require 'person_addresses.schema'

```

```ruby:people.schema
# -*- mode: ruby -*-
# vi: set ft=ruby :
create_table "people", id: :integer, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" do |t|
  t.string :japanese_name, null: false
  t.string :english_name, null: true

  t.timestamps
end
```

```ruby:person_addresses.schema
# -*- mode: ruby -*-
# vi: set ft=ruby :
create_table "person_addresses", id: :integer, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" do |t|
  t.integer :person_id, null: false
  t.index [:person_id], name: 'index_person_addresses_on_person_id'

  t.integer :address_type, null: false

  t.timestamps
end
```

```shell:Terminal
bundle exec ridgepole -c config/database.yml -E development --apply -f db/schemas/Schemafile
```

```ruby:app/models/person.rb
# == Schema Information
#
# Table name: people
#
#  id            :integer          not null, primary key
#  japanese_name :string(255)      not null
#  english_name  :string(255)
#  created_at    :datetime         not null
#  updated_at    :datetime         not null
#

class Person < ApplicationRecord

  has_many :person_addresses, dependent: :destroy
  accepts_nested_attributes_for :person_addresses, reject_if: :all_blank, allow_destroy: true
  validates :japanese_name, presence: true
  validates :english_name, format: { with: /\A[\p{ascii}]+\z/ }, allow_blank: true
end
```

```ruby:app/models/person_address.rb
# == Schema Information
#
# Table name: person_addresses
#
#  id            :integer          not null, primary key
#  person_id     :integer          not null
#  address_type  :integer          not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class PersonAddress < ApplicationRecord

  belongs_to :person

  enum address_type: {
    one: :one,
    two: :two,
    three: :three,
    four: :four,
    five: :five,
  }, _prefix: true

  validates :person_id, presence: true, on: :update
  validates :address_type, presence: true
end

```

## Active Record

## pluck & any

```ruby
pry(main)> `ruby -v`
=> "ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]\n"
pry(main)> Rails.version
=> "5.2.0"

CONVERT_MAP = {
  one: :one,
  two: :two,
  three: :three,
  four: :four,
  five: :five,
}.freeze

1000.times.each do |idx|
  person = Person.new(japanese_name: "#{idx}")
  person_addresses = 2.times.map{|_idx| PersonAddress.new(address_type: CONVERT_MAP.symbolize_keys.keys.sample)}
  person.assign_attributes(person_addresses: person_addresses)
  person.save!
end

Benchmark.bm 5 do |row| 
  row.report "pluck and any" do
    Person.last.person_addresses.pluck(:address_type).any? { |address_type| CONVERT_MAP[address_type.to_sym].present? }
  end
   row.report "where" do
    Person.last.person_addresses.where(address_type: CONVERT_MAP.keys)
  end
end 

=> [#<Benchmark::Tms:0x0000556c84fcb2f0
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.009011553949676454,
  @stime=0.0,
  @total=0.006902000000000186,
  @utime=0.006902000000000186>,
 #<Benchmark::Tms:0x0000556c84fc7060
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.004103021929040551,
  @stime=0.0,
  @total=0.0033479999999999066,
  @utime=0.0033479999999999066>]
=> [#<Benchmark::Tms:0x0000556c84aa8908
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.008074268931522965,
  @stime=0.0,
  @total=0.006250000000000533,
  @utime=0.006250000000000533>,
 #<Benchmark::Tms:0x0000556c84b03448
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.00198873202316463,
  @stime=0.0,
  @total=0.0015290000000005577,
  @utime=0.0015290000000005577>]
=> [#<Benchmark::Tms:0x0000556c83d5f670
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.006150746019557118,
  @stime=0.0012540000000003104,
  @total=0.004831000000000252,
  @utime=0.0035769999999999413>,
 #<Benchmark::Tms:0x0000556c83d41f08
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.0022412820253521204,
  @stime=0.0009770000000006718,
  @total=0.0018830000000011893,
  @utime=0.0009060000000005175>]
=> [#<Benchmark::Tms:0x0000556c83031bd8
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.008198636001907289,
  @stime=0.0004149999999993881,
  @total=0.005551999999998891,
  @utime=0.005136999999999503>,
 #<Benchmark::Tms:0x0000556c83029730
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.0025708150351420045,
  @stime=0.0005850000000000577,
  @total=0.002069999999998906,
  @utime=0.0014849999999988484>]
=> [#<Benchmark::Tms:0x0000556c85ba55f8
  @cstime=0.0,
  @cutime=0.0,
  @label="pluck and any",
  @real=0.011462070979177952,
  @stime=0.0,
  @total=0.007191999999999865,
  @utime=0.007191999999999865>,
 #<Benchmark::Tms:0x0000556c85b9b968
  @cstime=0.0,
  @cutime=0.0,
  @label="where",
  @real=0.0026785259833559394,
  @stime=0.00029100000000070736,
  @total=0.002032000000001588,
  @utime=0.0017410000000008807>]
```

* どうも `pluck` のち `any?` などのeachは `where` よりも遥かに遅い様です。


## ビルトインオブジェクト

## Array


### pluck





### ASCII文字をランダム生成

* ASCII文字自体の生成をするには、メタ

```ruby
res = Benchmark.bm 10 do |row|
  row.report "ascii" do
    [*"\0".."~"].sample(200).join
  end  
end
res.each{|res| p [sprintf("%.5f", res.utime.round(5, half: :up)), sprintf("%.5f", res.stime.round(5, half: :up)), sprintf("%.5f", res.total.round(5, half: :up)), sprintf("%.5f", res.real.round(5, half: :up))].join(" | ") }
```

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
| ascii(1) | 0.0 | 0.00018 | 0.00018 | 0.00011 |
| ascii(2) | 0.00026 | 0.0 | 0.00026 | 0.00034 |
| ascii(3) | 0.00005 | 0.00002 | 0.00007 | 0.00007 |
| ascii(4) | 0.00005 | 0.00002 | 0.00007 | 0.00007 |
| ascii(5) | 0.00007 | 0.00003 | 0.00011 | 0.00017 |


## Arrayからあるパターンのキーに一致するものだけフィルタしてそのキーを取りたい場合

|レポート名称|意味|
|:--------|:---|
| forEach | 繰り返しで索引した場合 |
| keep_if | keep_ifメソッドを使用した場合 |
| select | selectメソッドを使用した場合 |

```ruby
images = []; 1000000.times {|row| images << "img#{row + 1}_url" }
result_count = {:forEach=>0, :keep_if=>0, :select=>0}
Benchmark.bm 10 do |row|
  row.report "forEach " do
    fin = 50000
    1.upto(fin) do |row|
      url_key = "img#{row}_url"
      next if images[row - 1] != url_key
      Struct.new(:url, :caption, :title).new(url_key.to_sym, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:forEach] += 1
    end
  end
  row.report "keep_if" do
    images.keep_if {|row| row.to_s.match?(/img([1-9]|[1-9][0-9]{1,3}|[1-4][0-9]{4}|50000)_url/) }.each do |url_key|  
      Struct.new(:url, :caption, :title).new(url_key.to_sym, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:keep_if] += 1
    end 
  end
  row.report "select" do
    images.select {|row| row.to_s.match?(/img([1-9]|[1-9][0-9]{1,3}|[1-4][0-9]{4}|50000)_url/) }.each do |url_key|  
      Struct.new(:url, :caption, :title).new(url_key.to_sym, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:select] += 1
    end 
  end
end

result_count
=> {:forEach=>50000, :keep_if=>50000, :select=>50000}
```

計測結果

1回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
forEach | 1.312843 | 0.026700 | 1.339543 | (  1.349017)
keep_if | 1.768649 | 0.026811 | 1.795460 | (  1.808968)
select |  1.481335 | 0.030547 | 1.511882 | (  1.523911)

2回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
forEach | 1.312844 | 0.034082 | 1.346926 | (  1.365920)
keep_if | 1.766020 | 0.022876 | 1.788896 | (  1.797898)
select |  1.439497 | 0.021102 | 1.460599 | (  1.467751)

３回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
forEach | 1.317690 | 0.018919 | 1.336609 | (  1.350585)
keep_if | 1.763197 | 0.024135 | 1.787332 | (  1.804232)
select |  1.443859 | 0.022458 | 1.466317 | (  1.472827)


４回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
forEach | 1.294830 | 0.015705 | 1.310535 | (  1.318776)
keep_if | 1.958510 | 0.051190 | 2.009700 | (  2.040533)
select |  1.321429 | 0.013722 | 1.335151 | (  1.340082)

５回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
forEach | 1.422966 | 0.018969 | 1.441935 | (  1.449615)
keep_if | 1.733768 | 0.020578 | 1.754346 | (  1.769189)
select |  1.451152 | 0.015573 | 1.466725 | (  1.472972)

* ~~パフォーマンスとしては大差ないが、可読性では後者となる~~keep_ifが僅かに遅く、forEachとselectが誤差値考慮すると大差とは言い難いような印象である（可読性は後者にはなるが）


## Hashからあるパターンのキーに一致するものだけフィルタしてそのキーを取りたい場合

|レポート名称|意味|
|:--------|:---|
| forEach | 繰り返しで索引した場合 |
| keep_if | keep_ifメソッドを使用した場合 |
| select | selectメソッドを使用した場合 |

```ruby
images = []; 1000000.times {|row| images << "img#{row + 1}_url" }; image_map = {}; images.map {|row| image_map[row.to_s.to_sym] = row }
result_count = {:forEach=>0, :keep_if=>0, :select=>0}

Benchmark.bm 10 do |row|
  row.report "forEach " do
    fin = 50000
    1.upto(fin) do |row|
      url_key = "img#{row}_url".to_sym
      next if image_map[url_key].blank?
      Struct.new(:url, :caption, :title).new(url_key, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:forEach] += 1
    end
  end
  row.report "keep_if" do
    image_map.keep_if {|key, value| key.to_s.match?(/img([1-9]|[1-9][0-9]{1,3}|[1-4][0-9]{4}|50000)_url/) }.each_key do |url_key|  
      Struct.new(:url, :caption, :title).new(url_key, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:keep_if] += 1
    end 
  end
  row.report "select" do
    image_map.select {|key, value| key.to_s.match?(/img([1-9]|[1-9][0-9]{1,3}|[1-4][0-9]{4}|50000)_url/) }.each_key do |url_key|  
      Struct.new(:url, :caption, :title).new(url_key, url_key.to_s.gsub(/url/, "caption"), url_key.to_s.gsub(/url/, "title"))
      result_count[:select] += 1
    end 
  end
end

result_count
=> {:forEach=>50000, :keep_if=>50000, :select=>50000}
```

計測結果

1回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
| forEach | 1.257482 | 0.099606 | 1.357088  | (  1.371853) |
| keep_if | 2.062804 | 0.068399 | 2.131203  | (  2.144689) |
| select | 2.317476 | 0.111707 | 2.429183  | (  2.436765) |

2回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
| forEach | 1.292436 | 0.028099 | 1.320535 | (  1.325629) |
| keep_if | 1.595292 | 0.042578 | 1.637870 | (  1.646557) |
| select | 1.285199 | 0.022699 | 1.307898 | (  1.317835) |

3回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
| forEach | 1.609683 | 0.048782 | 1.658465 | (  1.667256) |
| keep_if | 1.148458 | 0.017314 | 1.165772 | (  1.171699) |
| select | 1.340724 | 0.039342 | 1.380066 | (  1.384263) |

4回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
| forEach | 1.439700 | 0.041089 | 1.480789 | (  1.488726) |
| keep_if | 1.220690 | 0.019430 | 1.240120 | (  1.252073) |
| select | 1.331339 | 0.042890 | 1.374229 | (  1.379941) |

5回目

| レポート名称 |　user | system | total | real |
|:----------|------:|------:|------:|------:|
| forEach | 1.173899 | 0.010056 | 1.183955 | (  1.187291) |
| keep_if | 1.328620 | 0.038243 | 1.366863 | (  1.378679) |
| select | 1.267303 | 0.029949 | 1.297252 | (  1.314939) |

* ~~パフォーマンスとしては大差ないが、可読性では後者となる~~誤差値が出てはいるものの、平均的にはkeep_if / selectの方が早い印象を受けなくもない（可読性では後者となる）