やる気がストロングZERO

やる気のストロングスタイル

【activerecord】joins, includes, eager_load, preloadの違い

joins, includes, eager_load, preloadはよく似た状況を作り出すがいまいち理解し切れなかったので整理。

参考) ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita

N+1問題を回避し、かつ、、
where条件に関連テーブルのカラムを使いたい場合はeager_load、
joinを使いたくない場合はpreload

N+1問題は考慮せず、単にwhere条件に関連テーブルのカラムを使いたい場合はjoinsを使うのが良さげかと思った。

◆joins

 Post.joins(:comments).where("comments.id = 1") 

・関連テーブルデータ先読みはしない
・inner joinを使った絞り込みをしたい時
・joinして1クエリを発行する

先読みしないのでN+1問題の解決にはならない。
※関連データにアクセスした時点でクエリが発行される。
where条件に関連テーブルのカラムを含めたい場合に使う感じ。

◆includes

Post.includes(:comments).where(comments: {id: 1}) 

・関連テーブルデータ先読みする
・条件によってjoinしたりしなかったりする

関連テーブルデータを先読みするので N+1問題の対策になる。
where条件に関連テーブルのカラムを含めたい場合、reference()を使うか、where条件をハッシュで書く必要がある。

◆eager_load

Post.eager_load(:comments).where("comments.id = 1") 

・データ先読みする。
・参照テーブルでの絞り込みok
・joinして1クエリ

関連テーブルデータを先読みするので N+1問題の対策になる。
where条件に関連テーブルのカラムを含められる。

◆preload

Post.preload(:comments) 

・データ先読みする
・参照テーブルでの絞り込みできない
・joinしない、複数クエリが発行される

関連テーブルデータを先読みするので N+1問題の対策になる。
where条件に関連テーブルのカラムを含められない。

【Rails】ブラウザ実行自動テスト設定方法

railsでブラウザ実行のテストの自動化をどうやるのか調べた。
結構把握に時間がかかったのでメモ。

前提知識整理

個人的認識なのでもしかしたら間違っているかも。

Rspec
railsでテストを書く際のインターフェース定義的なもの?
英語の文章のようにテストを書ける。

◆Capybara
railswebブラウザ実行テストを書く際のインターフェース的なもの。
Capybaraが指定する方法でテストコードを記載すると、Capybaraがdriverを操作してブラウザ実行して検証してくれる。
Rspecと一緒に使う事で、Rspecの記法でテストを書く事ができる感じ。

selenium-webdriver
Capybaraだけではブラウザを操作できない。
seleniumが各ブラウザのdriverを使ってwebブラウザを操作する。
seleniumのデフォルトはfirefoxブラウザで、firefoxのdriverをインストールしてパス上に配置することでfirefoxブラウザを操作できるようになる。
設定で対象ブラウザをchromeにすることもでき、この場合はchromeドライバーが必要になる。
ブラウザ本体ももちろん必要。

◆Capybara-webkit
Capybara用に用意されるwebkitのheadlessブラウザ(っぽい)
Capybaraの設定でこのブラウザを使うようにすれば、使用するブラウザもGemfileで管理できるしいいかも。実行も軽いらしい。
ただし、インストールでめんどくさい場合が多い印象。

chromeで実行できるようにしてみた

参考)
GitHub - teamcapybara/capybara: Acceptance test framework for web applications

spec_helper.rbに下記を追記

Capybara.javascript_driver = :selenium_chrome
# Capybara.javascript_driver = :selenium_chrome_headless ヘッドレスにしたい場合はこっち
# current_driverを変更しても良さげ

spec/features/以下にテストコードを配置

require 'spec_helper'

describe "ブラウザ実行テスト", type: :feature, js: true do # js: trueで上記で設定したjavascript_driverが使われるようになる
  context "ログイン状態" do
    before :each do
      login
    end

    describe "表示確認" do
      it "トップページが表示される" do
        click_link('TOP')
        page.save_screenshot('top.png') # スクリーンショットを取れる。
        expect(page).to have_css("body > div > dl:nth-child(1) > dd", text: "TOP") # cssセレクタを使ってtext内容でチェックするなど
      end

      it "アカウント情報ページが表示される" do
        click_link('account')
        page.save_screenshot('accounts.png')
        expect(page).to have_css("body > div > div:nth-child(2) > div > form > div:nth-child(2) > div:nth-child(1) > label", text: "アカウント情報") # cssセレクタを使ってtext内容でチェックするなど
      end
    end
  end
end

def login
  visit '/sign_in'
  fill_in('username', with: 'ユーザー名')
  fill_in('password', with: 'パスワード')
  click_on('sign in')
end

よくわからない点

visit("/")を実行するとbeforeでログインした状態がクリアされてしまう。
ログインした状態で指定urlにジャンプしたい時はどうすればいいのか?

【Rails】migrationコマンド周りまとめ

参考)Active Record マイグレーション - Rails ガイド

$ bin/rails db:migrate
最新状態までマイグレートする
$ bin/rails db:migrate VERSION=20080906120000
指定のバージョンまでマイグレートする
$ bin/rails db:rollback
一つ前に戻す
$ bin/rails db:rollback STEP=3
3つ前に戻す
$ bin/rails db:migrate:redo STEP=3
3つ前に戻して再度migrationを実行しなおす
$ bin/rails db:reset
DBをdropして再度設定する
$ bin/rails db:drop db:setup
と同等

↑※マイグレーションを再実行しているのではなく、schema.rbから反映しているだけ。
※schema.rbにはトリガーなどの情報は保持されていないので、それらが必要な場合はsqlでダンプしておく必要がある。
参考)
Active Record マイグレーション - Rails ガイド

$ bin/rails db:migrate:up VERSION=“0080906120000
$ bin/rails db:migrate:down VERSION=“0080906120000
特定のバージョンのマイグレーションを指定の方向に実行する
$ bin/rails db:migrate RAILS_ENV=test
テスト用DBに対して実行
$ rails db:seed
db/seeds.rbに定義されたシードデータを入れる。

スキーマダンプ

$ bin/rails db:structure:dump
sqlでテーブル構造のダンプを取る

【ActiveRecord】テーブルをまたいでwhere句条件を書きたい時

こういうsqlActiveRecordを使って表現したい

SELECT * FROM posts
INNER JOIN comments ON comments.post_id = posts.id
WHERE comments.created_at >= '2019-01-01'

つまり、2019年以降にコメントがついたpostのデータを取得したい。

こう書いたらできた

Post.joins(:comments).where("comments.created_at >= ? ", Date.new(2019,1,1))

↓実行されたsql

SELECT  "posts".* FROM "posts" 
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" 
WHERE (comments.created_at >= '2019-01-01' ) LIMIT $1  [["LIMIT", 11]]

scopeを使うともう少しいい感じになった。

Commentモデルに2019年以降のデータを取得する条件をscopeで定義する

comment.rb

class Comment < ApplicationRecord
  belongs_to :post

  scope :recent, -> { where(arel_table[:created_at].gteq Date.new(2019,1,1))}
end


# gt は > を意味する。
# gteq は >=
# lt は <
# lteq は <=

この状態で以下の様に書く

Post.joins(:comments).merge(Comment.recent)

↓実行されたsql

 SELECT  "posts".* FROM "posts" 
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" 
WHERE "comments"."created_at" >= '2019-01-01' LIMIT $1  [["LIMIT", 11]]

◆その他
なんとなく文字列でなくシンボルなどでうまく書きたい気がしてしまうが、
普通に文字列でwhere("comments.created_at >= ? ", Date.new(2019,1,1)とか書くほうが可読性が良さそうと思ってしまう。どうなんだろうか。。

DBスペシャリストの問題で出てくる「テーブルサイズ見積もり計算」の解き方

必要になる数値

[見積もり行数(行)]
テーブルの総行数の見積もり。
そのテーブルが最大でどれくらいの行数になるのかの見積もり

[平均行サイズ(バイト)]
テーブルの行長が可変の場合、そのテーブルの平均行サイズ

[ページサイズ(バイト)]
扱われるデータ単位であるページのサイズ。

[ページヘッダサイズ(バイト)]
ページのヘッダ領域(ユーザーデータが入らない)のサイズ

[空き領域率(%)]
ページに、ここに指定されたパーセンテージ領域が空き領域として確保される。

[バッファサイズ(ページ)]
メモリ上にバッファとして保持しておくページ数(下記計算では未使用)

[データ所要量(バイト)]
テーブルの総バイト数

データ所要量を求めるには?

1ページあたりで使えるサイズを求める

1ページあたりで使えるサイズ = (ページサイズ - ページヘッダサイズ)* (1 - 空き領域率)

1ページあたりに入る行数を求める

1ページあたりに入る平均行数 = 1ページあたりで使えるサイズ / 平均行サイズ

何ページ必要になるのかを求める

必要ページ数 = 見積もり行数 / 1ページあたりに入る平均行数

データ所要量を求める

データ所要量 = 必要ページ数 * ページサイズ

【Docker】長期稼働でi-nodeを使い切ってしまう?

※一時解消はできたが原因解明には至らず。

Dockerの勉強の一環で、Twitterの投稿から画像リンク(コスプレ関係)のurlを収集して一覧しているwebサービスをEC2インスタンス一つにDockerコンテナで構築している。

http://cos.mixmaru.com

構成

appサーバーコンテナ:Nginx、uWSGI、Django
dbサーバーコンテナ:Postgresql

appサーバーコンテナで10分に1度スクレイピングを実行している。

現象

先日urlを叩いてみるとステータスコード500のNginxのエラー表示画面になっていた。

nginxのエラーログを見てみると

2019/03/22 14:31:17 [alert] 13#13: *9631 write() to "/var/log/nginx/access.log" failed (28: No space left on device) while logging request, client: (ip_address), server: , request: "GET / HTTP/1.1", upstream: "uwsgi://127.0.0.1:8000", host: "cos.mixmaru.com"

調査

ディスク容量が無いと言われているっぽいので、dfコマンドで調べてみたが容量は残っている。(というか、それほど容量を食うような事はしていないはず)

コンテナ側で容量制限などに引っかかっているのかと思い、docker-compose exec app bashで中に入ろうとしたが、

failed to write all bytes for _bisect.so

とエラーが出て入れず。

調べるとi-nodeを使い切っているとの事らしい。

df -iを実行してみるとたしかに100%になっているものがある。

なにか細かいファイルを大量に作成しているのだろうか、、?

一時対応

ここまで調べた時点でとりあえず、docker-compose restartを実行したらi-nodeの数が3%まで減って正常にアクセスできるようになった。

Rubyのメソッド引数メモ

Rubyのメソッド定義時の引数のパターンをメモ

飾りなし

def some_method(a)
    puts a
end

some_method 100  # 100

飾りなし(デフォルト値指定あり)

def some_method(a=100)
    puts a
end

some_method  # 100

可変長引数(配列受け取り)

def some_method(*a)
    p a
end

some_method 1, 2, 3  # [1, 2, 3]

キー指定

def some_method(a:)
    puts a
end

some_method  a: 100  # 100

キー指定(デフォルト値指定あり)

def some_method(a: 100)
    puts a
end

some_method   # 100

可変長引数(キー指定受け取り)

def some_method(**a)
    p a
end

some_method a: 1, b: 2, c: 3  # {:a=>1, :b=>2, :c=>3}

ブロック

def some_method(&block)
    block.call 100
end

some_method do |value|
    puts value
end   # 100

※&blockはyieldで代用可能

上記全タイプの引数を同時に定義する場合(定義できる順番があるっぽい)

class SomeClass
  attr_accessor :a, :b, :c, :d, :e, :others, :block

  def some_method(a, b, c="c", *d, e: "e", **others, &block)
    @a = a
    @b = b
    @c = c
    @d = d
    @e = e
    @others = others
    @block = block
  end
end

# 使用
obj = SomeClass.new
obj.some_method "a", "b", "c", "d1", "d2" , e: "e", f1: "f1", f2: "f2" do |greet|
  greet
end

# 結果
obj.a # "a"
obj.b # "b"
obj.c # "c"
obj.d # ["d1", "d2"]
obj.e # "e"
obj.others # f1=> "f1", f2=> "f2"
obj.block.call "hello" # "hello"