はじめに
これまでC++、JavaScirpt、C#、Rustなどなどいろいろな言語を触ってきましたが、Rubyはほとんど触る機会がありませんでした。
またデータベースもSELECT
文というのがあるくらいの知識しか持っていませんでした。
ところでいろいろありお仕事で以下の新しい要素しかないに満ち溢れている領域への挑戦をすることになりました。
- Ruby
- Rails
- Active Record
- ActionView
- etc
- Rspec
- Factory Girl
- MySQL
無事にRailsチョットワカルになれたと思うのでそこで得た知見をまとめてみようかと思います。
環境構築
環境構築のはまりポイントもあったのですが職場の手順書が改善されてしまったので(いいことだけど)、書くモチベがないので割愛します。
haml入門
- content_for :main_pane do
= render 'new', :setting => @foo_setting
= render "common", :settings => @foo_settings
erbではなくhamlを使っている職場です。コードを見ていくと割と初めにこのようなファイルに遭遇します。Ruby/Rail初学者にとって、初見ではただの暗号にしか見えないでしょう。
部分テンプレート(=パーシャル)
まずrender
に注目します。これは別のファイルを解釈して描画させる指令です。ここで大事になるのがdirectory構成です。
├─foo_settings
│ index.html.haml
│ index.js.haml
│ new.html.haml
│ new.js.haml
│ _common.haml
│ _form.haml
│ _new.haml
例えばrender 'new'
のように書かれていたら_new.haml
を描画しようとしているわけです。
この機能のことを部分テンプレート(=パーシャル)と呼びます。
render
の後に,
で区切られて渡されているのは引数と思えばいいです。例えばrender 'new', :setting => @foo_setting
に注目してみます。これで呼び出される_new.haml
には
.arikitari-icon= theme_image_tag(setting.icon_path, :size => "48x48")
のような記載があります。setting
という変数が使われていますね。
- 【Rails】 renderメソッドの使い方を徹底解説! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト
- 【超基本】railsで部分テンプレートを使ってみる(haml) - Qiita
執筆段階になって思い出しましたが、そういえば昔ほんの少しだけ触れたPugにもそんな機能あった気がしますね
Includes – Pug
content_for
では次にcontent_for :main_pane
に着目します。
これはyield :main_pane
(ブロックなしで呼び出すcontent_for
も同じ意味らしい)とされている箇所に以下の内容を描画させるよという意味合いを持ちます。なにかテンプレートがあってそこに中身を差し込んでいく感じです。
執筆段階になって思い出しましたが、そういえば昔ほんの少しだけ触れたPugにもそんな機能あった気がしますね
Template Inheritance – Pug
-
とか=
とか
-
-
: Rubyのコードを書くけどそれらの結果をrenderしないときに -
=
: Rubyとかのコードを書いて結果をrenderするとき
たぶんこれを見るのが一番速いと思います
t
ってどんな関数?
/app/views/functions/foo_settings/_new.haml
.arikitari-summary.ui-helper-clearfix
.arikitari-icon= theme_image_tag(setting.icon_path, :size => "48x48")
%ul.arikitari-description
%li= t("common.new")
.arikitari-tabs
%ul
%li= link_to t("functions.setting"), "#setting_tab"
#setting_tab
= render "form", :setting => setting
hamlを見ているとt
というメソッド呼び出しが見つかると思います。でもt
って何でしょう?ggrbilityが低そうですね。
Rails 国際化 (i18n) API - Railsガイド
国際化(多言語)対応のための関数です。i18n.t
でも呼び出せます。
嘘です違いました。TranslationHelper#translate
のことで、I18n.translate
(I18n.t
)をviewからよぶ上でのいろいろな考慮がされたものになるようです。
余談ですがi18nはinternationalizationの略で先頭の i と語尾の n の間が18文字である所から来ています。
デバッグ環境を作ろう
いろいろ説明されても正直よくわからない、実働を見ながら理解したいことってありますよね。またそもそもバグって動かん時にデバッグできないと困ります。デバッグできるようになりましょう。
VSCode
Ruby plugin入れた後に.vscode/launch.json
をいい感じに書くことで使えるようになります。
どうやって設定作ったかは記憶の遥か彼方に消えてしまったのですが、VSCodeでプロジェクトのdirectoryを開いている状態で左のデバッグタブを押して、create a launch.json
を選び、Rubyを選択すれば大体以下のような感じのものが吐かれた気がします。まあ違ったら手書きすればいいわけで。
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "RSpec - active spec file only",
"type": "Ruby",
"request": "launch",
"program": "${workspaceRoot}/bin/bundle",
"args": [
"exec",
"rspec",
"-I",
"${workspaceRoot}",
"${file}"
],
"useBundler": true,
},
{
"name": "Rails server",
"type": "Ruby",
"request": "launch",
"program": "${workspaceRoot}/bin/rails",
"args": [
"server",
"-p",
"3000",
"-b",
"0.0.0.0"
],
"useBundler": true,
}
]
}
あとはF5でデバッガーが立ち上がってブレークポイント打てばそこで止めて変数見たりできます。
ただしデバッガーでRailsとかRspec動かすのはめちゃくちゃ遅いです。使ったことないですがlogger.debug
で原始的にprint debugとかしたほうがいい場面もあるかもしれないですね。
- VScodeでRailsをデバッグする - Qiita
- VScode上でRailsのステップデバックする方法 - Qiita
- RSpecのデバッグ環境をVS Codeで構築する | memoTech
- Visual Studio Codeでruby-debug-ideを使ってデバッグ - アトトックラボ|株式会社アトトック
binding.pry
コンソールでデバッグしていくやつです。VSCodeのデバッガーより軽量ですし、Active Recordみたいに何のメソッド生えてるのかわからんときに、VSCodeのデバッガーだと追いずらい一方でbinding.pryならメソッド一覧が取って来れるので、どうやって子テーブルアクセスすりゃいいの?ってときにも役立つかもしれないですね。
デバッガーでブレークポイントを打つのと同じ感覚でソースコード中にbinding.pry
と書き、普通にプログラムを実行すると、当該箇所を通過したときにデバッガーになります。
- binding.pryの基本的な使い方 - Qiita
- デバックツール(pry-rails)について binding.pryの使い方 - Qiita
- 今更聞けないpryの使い方と便利プラグイン集 - Qiita
- wtf!!!!!!!!!!!!!!!!!!!!!!?????????????????? - Qiita
- binding.pry から脱出するコマンド - Qiita
- ruby on rails - How do I dump Pry output to a file or Vim? - Stack Overflow
Ruby入門
基本的にるりまを見ることになります。
とくにRubyはほかの言語に比べてよくわからない記号が出てきがちなので
Rubyで使われる記号の意味(正規表現の複雑な記号は除く) (Ruby 3.0.0 リファレンスマニュアル)
にしばしばお世話になることでしょう。
Symbol
class Symbol (Ruby 3.0.0 リファレンスマニュアル)
シンボルは任意の文字列と一対一に対応するオブジェクト
Rubyの内部実装では、メソッド名や変数名、定数名、クラス名などの名前を整数で管理しています。
つまるところ大抵の識別名はSymbolとして管理されます。
:foo
のようにして明示してシンボルを作れますし、また使用できます。
C/C++などでは、コンパイラの最適化によって登場する文字列は定数テーブルに配置されるのが大抵の処理系で行われますが、その代わりとしてRubyではSymbolがあるのでしょう。
Symbolの配列をつくることも時としてあるかと思いますが、%i
記法が便利です。
%i(foo bar)
- class Symbol (Ruby 3.0.0 リファレンスマニュアル)
- Rubyの文字列とシンボルの違いをキッチリ説明できる人になりたい - Qiita
- Rubyで%記法(パーセント記法)を使う - Qiita
- Class: RuboCop::Cop::Lint::PercentSymbolArray — Documentation for rubocop (1.20.0)
- Class: RuboCop::Cop::Style::SymbolArray — Documentation for bbatsov/RuboCop (master)
hash(連想配列)
大抵の言語で存在する連想配列/dictionary/hashのようなkeyとvalueがあるデータ構造ですがもちろんRubyにもあります。
なおhashというとC++erはstd::hash
を思い浮かべるでしょうがそっちではないです。
{} # 最小の(?) Hash
{ foo: 3 } # keyが:fooで値が3
{ :foo => 3 } # 上と同じ意味だけど古い書き方
h = { foo: 3}
p h[:foo] # => 3
h[:foo] = 4
p h[:foo] # => 4
operator ===
vs operator ==
JavaScriptだとoperator ==
は比較が厳密じゃないからoperator ===
使おうというのが一般的な見解になっていますがRubyはどうなのでしょうか?
結論から言うとoperator ===
は常用するものではなさそうです。Rubyのcase
文の判定に内部的に使われているようですが、is_a?
/include?
/match?
を使おうとRubocop先生も怒ってきます。
# bad
Array === something
(1..100) === 7
/something/ === some_string
# good
something.is_a?(Array)
(1..100).include?(7)
/something/.match?(some_string)
- Class: RuboCop::Cop::Style::CaseEquality — Documentation for rubocop (1.20.0)
- Object#=== (Ruby 3.0.0 リファレンスマニュアル)
nullアクセスを避けるには
最近の言語ではオプショナルチェインとかそんな名前で、プロパティ/メソッドに多段階にアクセスするときに、nullチェックをいちいち書かないでもやってくれるような構文がありますが、Rubyはどうでしょうか?
ボッチ演算子
&.
のようにアクセスすることでnull参照を避けられます。その見た目からボッチ演算子と呼ばれることが多いようです。
@nickname = current_user&.nickname
なおかつてはRails(Active Support)のtry
メソッドが用いられたようですが、現在では考古学の対象のようです。
- Ruby ぼっち演算子について - Qiita
- Active Support コア拡張機能 - Railsガイド#try
- 【Ruby on Rails】tryとtry!と&.(ぼっち演算子)の違い - Qiita#tryは使わない方が良さそう
Hash#dig
Q:
foo.key?(:aaa) ? foo[:aaa][:bbb] : nil
をもうちょっとすっきり書く方法、Rubyでご存じの方いますか?
foo.dig(:aaa, :bbb)
NullObject Pattern
ボッチ演算子もtry
もHash#dig
もnil対する対処をしているわけですが、その責務は誰が持つべきか?オブジェクトを生成するところの責務では?というのがNullObject Patternです。
class Element
def bar
# return some string
end
end
class NoElement
def bar
"no element"
end
end
def foo
xxx.element || NoElement.new
end
def hoge
foo.bar # always ok, nil check is not required
end
Hash#new
なんかもこういう用途に使えたりします。
C++では以下のように使うべきではない理由しかないので見向きもされない印象です。
- そもそもC++ではオブジェクトをポインタで扱うとかmove semanticsでも使わない限り全てはコピーされるという契約が成り立つがこれがNullObject Patternと相性が悪い(実現のために書くコードが多い)
- 仮想関数を使うことになるので最適化を阻害する
- しかも実質shared_ptrでの取り回しをユーザーに要求するので不自由
そもそもNullObject Patternを言い出したJava界隈を観測してみましたが否定的な意見が見つかりますし理由も個人的には納得が行きます。
失敗を表現する手法としてNullObjectパターンが不適切でEitherが適切だと思う理由 - Qiita
NOPはinterfaceを用いて成功と失敗の同一視を行います。
対してEitherはinterfaceを用いず成功と失敗は別視します。そしておそらく2つの具象クラスは少し振る舞いやステータスが違う程度なのでしょうから、例えばDB保存もどちらも出来そうです。
その点においてもこの例は「成功と失敗」ではなく「成功1と成功2」であると考えられます。
- NOPを用いている箇所は、以下のどちらかに発想を遷移させるのが適切
- 具象クラスの関係が同一視であれば、適切な抽象化を行いストラテジパターンにする
- 具象クラスの関係が対の関係であれば、interfaceを無くしEitherを用いる
ただまあRubyなら出番もあるのかもしれないですね。
- Rails tips: Null Objectパターンでリファクタリング(翻訳)|TechRacho by BPS株式会社
- Active RecordとNull Objectパターン
- C++で迷走してみる - おっさんプログラマの戯れ言
lambdaとかブロックとかprocとか
2010年代の関数型プログラミングブームの成果もありC++にすら導入されたlambda(式)。Rubyはどうでしょうか?
block
下手にC++のlambda式とかJavaScirptの関数を知っていると理解を阻害されそうですが、めげずに見ていきます。
まずとあるメソッドがあります。
def foo(n)
puts "foo #{n}"
end
foo(2)
このメソッドに処理の塊を渡したい
#{ puts "aaa" }# 単体でblockを書くことはできない
def foo(n, &aaa)
puts "foo #{n}"
end
foo(2) { puts "aaa" }#メソッド呼び出しの文脈でならかける
例から明らかなように、blockはメソッド呼び出しの文脈でのみ許可されます。
proc
blockは単体で存在できません。なぜならばオブジェクトではないからです。これをオブジェクトに変換できれば単体で存在できるようになるはずです。
def bar(&a)
a
end
def foo(&a)
bar(&a)
end
b = foo { puts "a" }
b.call # => a
foo
メソッドはblock引数a
を受け取ります。この段階、つまり&
の効用によってblockはオブジェクトに変身する準備を終えています。そしてそのままbar
メソッドに渡されています。このbar
メソッド内で初めて参照されるわけですが、この段階になってようやくblockはobjectに変身を遂げます。もちろんfoo
メソッドに帰ってきたとき、依然として&a
はproc
オブジェクトではありません。
こうしてできたprocオブジェクトを返すので変数b
はもちろんprocオブジェクトです。
procオブジェクトはcall
することで処理を実行できます。
もちろんいちいち上のようにfoo
メソッドを定義するのは面倒極まるのでproc
というメソッドが用意されています
b = proc { puts "a" }
b.call # => a
block再び
def foo(n, &aaa)
puts "foo #{n}"
aaa.call
end
foo(2) { puts "aaa" }
メソッドはただ一つだけblockを引数として取れるのでした。では受け取ったblockを実行するにはどうするかというと引数で&aaa
と書いた時点ですでにprocオブジェクトになる準備を終えているので、call
すればprocオブジェクトになり、実行できるわけです。
ところでなぜかRubyにはblock引数を実行する方法があります。yield
です。
def foo(n, &aaa)
puts "foo #{n}"
yield if block_given?
end
foo(2) { puts "aaa" }
わざわざaaa
という引数名を付けてあげたのに使ってない・・・だと?したらばそれはいらないのでは?
def foo(n)
puts "foo #{n}"
yield if block_given?
end
foo(2) { puts "aaa" }
かくしてyield
だけになりました。どうしてそうなった。
ただし、ブロックを受け取ることを意図したメソッドはちゃんと引数でもそう示したほうが良いような気がします。びっくりしてしまいますから。
もちろんblockは引数を取れます。
def foo(&a)
yield 3 if block_given?
end
foo {|n| puts "a #{n}" }
do...endもまたブロックである
def foo
yield
end
foo do
p "a"
end
これまで{}
の中に処理を書いてきましたが、do...end
でも同じことができます。複数行になるときはこっちを使いがちな印象があります。
この2つでは結合強度が違うのだそうですが、そういうのに依存しないように書きたいですね。
- メソッド呼び出し(super・ブロック付き・yield) (Ruby 3.0.0 リファレンスマニュアル)
- do...endと{...}の違い - おもしろwebサービス開発日記
- Rubyのブロック構文の書き分け(do end,{}) - ぬいぐるみライフ?
- プロと読み解くRuby 2.7 NEWS - クックパッド開発者ブログ
lambda
基本的にprocと同じですが引数のチェックが行われることとreturnの挙動が違うようです。
a = -> { 3 }
pp a
b = lambda { 3 }
pp b
c = proc {3}
pp c
#<Proc:0x0000564749e07cc0 prog.rb:1 (lambda)>
#<Proc:0x0000564749d94590 prog.rb:3 (lambda)>
#<Proc:0x0000564749d8d060 prog.rb:5>
pp
しても区別されている感がありますね。
なおblockからlambdaは作れますが、procからlambdaを作ろうとする試みは大抵失敗に終わります。lambda(&proc{})
とかがだめな典型例です。
- Ruby block/proc/lambdaの使いどころ - Qiita
- 【Ruby】ブロック・Proc・lambda を理解する - Qiita
- まつもとゆきひろ×結城浩,Rubyを語る(3ページ目) | 日経クロステック(xTECH)
- singleton method Proc.new (Ruby 1.9.3)
- Feature #14045: Lazy Proc allocation for block parameters - Ruby master - Ruby Issue Tracking System
- Class: RuboCop::Cop::Style::StabbyLambdaParentheses — Documentation for rubocop (1.20.0)
- プロと読み解く Ruby 3.0 NEWS - クックパッド開発者ブログ
メソッドを関数の引数に渡すには
Cで言うところの関数ポインタみたいなものをどう実現するかということになりますが、Symbolを経由してやります。次の例を見ながら解説します。
[1].map(&:next)
関連するメソッドは以下です。
Array#map
は各要素について、与えられたprocオブジェクトを実行し、その結果の新たな配列を返します。
:next
はそれ単体はSymbolなのにどうしてprocオブジェクトになるのか、その前の&
で察するかもしれませんが次のように変換されるためです(next_proc
は説明用の変数)。
next_proc = :next.to_proc
[1].map{|i| next_proc.call(i)}
なお、どのクラスのメソッドなのかについてはcall
の段階で第1引数がなにかによって解決されます。この辺はほかの言語と同じですね。
a = :next.to_proc
pp a
p a.call(1) # Integer#next
str = "xyz"
enum = str.each_byte
p a.call(enum) # Enumerator#next
to_proc
はRuby3.0で仕様が変更されていて、lambdaオブジェクトを返すようになりました。結果、メソッドのアクセシビリティが無視されるようになるために次のコードが動くようになります
def foo(n)
p "a #{n}"
end
a = :foo.to_proc
pp a
a.call(nil, 2)
- 【Ruby】array.map(&:method)を理解する - Qiita
- Proc#=== (Ruby 3.0.0 リファレンスマニュアル)
- Symbol#to_proc (Ruby 3.0.0 リファレンスマニュアル)
- Symbol#to_proc (Ruby 2.7.0 リファレンスマニュアル)
- Feature #16260: Symbol#to_proc behaves like lambda, but doesn't aknowledge it - Ruby master - Ruby Issue Tracking System
- yumetodo: "#ruby これ #wandbox が壊れているという認識であってるのかな、 >private m…" - Qiitadon
- Class: RuboCop::Cop::Style::SymbolProc — Documentation for rubocop (1.20.0)
クラスの特異メソッドと特異クラス
C++でいうstatic member functionsに近いものになるのでしょうが、Rubyにもクラスの特異メソッドがそれに該当します。
class Foo
end
def Foo.bar
"a"
end
Foo.bar # => a
Foo::bar # => a
classの中に書くこともできます
class Foo
def self.bar
"a"
end
end
特異クラスという機能を使って特異メソッドを定義することもできます。
class Foo
end
class << Foo
def bar
"a"
end
end
もちろん特異クラスを対象クラス内に書くこともできます
class Foo
class << self
def bar
"a"
end
end
end
- Ruby 初級者のための class << self の話 (または特異クラスとメタクラス)
- 【Ruby】特異メソッドについて - Qiita
- Ruby のクラスメソッド定義まとめ - Qiita
- yumetodo: "#ruby 自由すぎてわけわからんなw 特異メソッドこんなところにも生やせる https://wa…" - Qiitadon
Array#|
一見するとbit or演算子ですが、
foo = nil
foo |= 0
p foo # => true
配列オブジェクトのメソッドとしても定義されていて、配列の和集合を返します。
一部のC++erはstd::valarray::operator|=
を思い出すかもしれませんが、あれは各要素にbit or演算するものなので全く違います。
他言語を呼ぶのにどうするか: Cのdllを呼ぶ
Win32APIを呼ぶようなときなど、CのAPIを叩くことはあろうかと思うのですが、どうすればいいでしょうか?
Win32API界隈で最近話題のwin32metadataを使うような方法はRubyには執筆現在ないようです。
そういうわけでFiddleを使うのが一般的のようです。ffiもあるのですが、Fiddleのほうがいいらしい。
https://gamelinks007.net/@S_H_/106854866461791632
ffiの場合はgemのinstall時にC拡張をビルドする必要があるので詰まる可能性が少なからずありそうかなぁと
fiddleは確かRubyに組み込まれてるのでその辺が楽そうとか思いました
まあ、どっちも使ったことがないのでどっちがいいかはよくわからないですね
Rubyのコアに近いのでfiddleのほうがメンテナンス長くされそうという感じではありますが……
has_scope入門
controllerを触っていくようになると遭遇するであろうhas_scope
。後述するようにRailsにscope
という機能があるがゆえに間違えがちですが全くの別物ですし、外部gemです。
heartcombo/has_scope: Map incoming controller parameters to named scopes in your resources
わざわざ解説せずともReadme読めばわかる気がしますし、なんならライブラリは十分に小さいので実装を読めばいい気もしますが簡単にまとめます。
検索フォームのようなものを実装するときを考えます。ふつうに考えてGET paramに検索キーを足してGET requestをクライアント側は投げてきますよね。
GET /some/endpoint?key1=aaa&key2=bbb
じゃあそれをどうパースして振り分けるのか。そこでhas_scopeの登場なわけです。
class FooController < ApplicationController
has_scope :key1 do |_, scope, value|
# do something
end
end
もしGET paramがつぎのような物だった場合どうするか
GET /some/endpoint?foo[x]=aaa&foo[y]=bbb
こんなかんじですね。value
は要素2の配列になります。ここでscope
は後述のActiveRecordのscope機能のほうを指します。
has_scope :foo, using: %i(x, y), type: :hash do |_, scope, value|
x, y = value
scope.merge(
# some active record relation
)
end
あとはどこかでapply_scopes
を呼び出す必要があります。
has_scope
に長々書かず、modelのscopeを呼ぶための変換器くらいに割り切ったほうが健全な気がします。
SQL入門
リレーショナルデータベースを操作すると言えばSQLですよね。
Railsのログを眺めていれば圧倒的SQLの物量に脳みそが強制的にSQLを理解するモードになるので心配はいりません。
SQL Training 2021 - Speaker Deck
は一読しておくといいでしょう。
DBの操作を強力に支援するツール: HeidiSQL
テーブル覗くのいちいちSQL組み立てるのは生産性がとてもよくないので、適当なツールが欲しいわけですが、HeidiSQLを紹介します。
Windows使いならchocolateyはすでに導入しているはずなので、管理者権限Powershellで以下をたたくだけです。
choco install heidisql
VMの上のDockerでmysqlを動かしている場合、VMのhost名、username/password、portを調べればアクセスできるはずです。
テスト環境にVPN+SSH越しにDB覗く必要があるときは多少面倒で、任意のPuTTYgenに一度OpenSSH形式の鍵を読ませてppk形式で書き出し、それをHeidiSQLのSSHトンネルの項目で指定する必要があります。また本番環境のconfigを見てportを調べる必要もあります。
- MySQLにHeidiSQLでアクセスする - セイコンサルティンググループ
- HeidiSQLのフィルタ機能の使い方 | ノウハウツリー
- Windows と Linux でファイル形式 .pem と .ppk の間の相互変換を行う
Rails
Railsではファイル名やModel名などなどに独特の命名規則の制約があります。だいたい単数形/複数形がよくわからなくなるやつです。公式ドキュメントを見て頑張りましよう。
- Rails uninitialized constant エラーの解決へのシンプルなチェックリスト - Qiita
- ruby on rails - "Uninitialized constant" error when including a module - Stack Overflow
基本構造: MVC
別にRailsに限ったことではなく、何らかのMVCとかMVVMとかなんらかの役割分担がされるのはこんにちの常識になっています。何のことかわからない読者は大急ぎで別途勉強しましょう。
RailsではMVC構造を採用しています。大体Controllerがでかくなって大変になるやつです。
私が触ったコードベースのdirectory構造はこれに大体対応していて大まかには以下の通りでした。
- app
- controller: 飛んできたリクエストを捌く
- model: databaseのtableと一対一対応し、その操作を担う
- view: 画面周りを担う
- helper: viewに書かざるを得ないロジックの共通化がここでなされる
- assets: 画像/JS/CSSなどのリソース
- db
-
schema.rb
: DBのテーブル構造とindex一覧、手動では基本弄らないでmigrationコマンドが書き換える - migrate: migration fileたちが集う。
-
- config
- locales
- 文言系のリソースがここに集う
- routes.rb
- ルーティング(どういうURLが叩かれたらどこのcontrollerにリクエストを転送すればいいか)を定義する
- locales
- lib: model向けのコードの共通化部分とか
- assets: 画像/JS/CSSなどのリソース
- spec: rspec、つまり単体テスト
ActiveRecord
RailsではActiveRecordを利用することで生のSQLを書くことはまああんまありません。実行ログに生のSQLが吐かれているのでそれが読めるようになる必要はあります。
例えば次のようなSQLは
SELECT
*
from
pre.foo
WHERE
foo.name LIKE "%takahasi%"
ORDER BY
foo.age ASC;
だいたい次のようになると思います。orderはcontrolllerのhas_scope
から渡されるんちゃう?とかそういう話は横において。
model
class Foo < ActiveRecord::Base
scope :search_name, lambda { |word|
where('foo.name like ?', "%#{word}%")
.order('foo.age ASC')
}
end
リンク集
SQLおよびそれをActive Recordで扱う時の入門で自分が調べたところをリンク集としてまとめておきます。
- create/build/new
- INSERT
- LIKE
- ORDER BY
- GROUP BY
- JOIN
- 【INNER JOIN, LEFT JOIN , RIGHT JOIN】テーブル結合の挙動をまとめてみた【SQL】 - Qiita
- Rails における内部結合、外部結合まとめ - Qiita
- MySQL、2テーブルより値を結合しソートするテクニック(oreder by、union)|レンタルサーバーナレッジ
- ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita
- 漢(オトコ)のコンピュータ道: RDBにおけるキャッシュという考え方
- 実例で学ぶ、JOIN (NLJ) が遅くなる理屈と対処法 - Qiita
- SQL Joinサンプル集 Joinで遅いSQLの原因を調べる方法 | ポテパンスタイル
- S.H.: "@yumetodo@qiitadon.com それはHashの記法じゃないですかね Rubyだ…" - Creatodon
- 正規化
- SUBSTRING_INDEX
- WHERE IDs
- pluck
scopeとmerge
ModelからControllerに対してレコードの操作を提供する入口として用いられます。驚き最小の法則にしたがい、ちゃんとActiveRecord::Relation
を返しましょう。びっくりしてバグるのでうっかりArrayとかを返さないように・・・。
class Foo < ActiveRecord::Base
scope :search_name, lambda { |word|
where('foo.name like ?', "%#{word}%")
}
end
そうして作ったscopeやらその他ActiveRecord::Relation
たちは最終的にmergeで結合するかと思います。
has_scope :foo, using: %i(display_name, sid), type: :hash do |_, scope, value|
x, y = value
scope.merge(
# some active record relation
)
end
原則としてmergeはそれぞれのリレーションたちをAND条件で結合していきます。
ではORを作りたいときはというとmergeに渡す前にorを作ります
ActiveRecord の or は merge とセットで使え - Qiita
さて、意図せずORになってしまうことがあります。どういうことでしょうか?
child model
class FooChild < ActiveRecord::Base
belongs_to :foo_parents, foreign_key: :foo_child_id
scope :x ->(word) { where('child.x like ?', "%#{word}%")}
scope :y ->(word) { where('child.y like ?', "%#{word}%")}
end
parent model
class FooParent < ActiveRecord::Base
has_many :foo_chilren, :dependent => :destroy, :foreign_key => :foo_child_id
scope :x ->(word) { where(id: FooChild.x(word).pluck(:foo_child_id))}
scope :y ->(word) { where(id: FooChild.y(word).pluck(:foo_child_id))}
end
controller
class FooController < ApplicationController
has_scope :x, allow_blank: true, do |_, scope, value|
scope.merge(FooParent.x(value))
end
has_scope :y, allow_blank: true, do |_, scope, value|
scope.merge(FooParent.y(value))
end
end
ここでx=aaa&y=bbb
というリクエストが来た時、x, yでの絞り込みのOR検索となります。
ポイントはwhere(id: xxx)
の部分です。mergeしたときに2箇所でのidの指定の和集合を取ったものをSQLとして組み立ててしまうのです。
あまりいい解決策が浮かびませんでしたが一つはhas_scopeではhashとして受け取り、愚直にifを書いて分岐する方法です。
parent model
class FooParent < ActiveRecord::Base
has_many :foo_chilren, :dependent => :destroy, :foreign_key => :foo_child_id
scope :p lambda { |x, y|
if x && y
ids = FooChild
.x(x)
.y(y)
.pluck(:foo_child_id)
where(id: ids)
elsif x
ids = FooChild
.x(x)
.pluck(:foo_child_id)
where(id: ids)
elsif y
ids = FooChild
.y(y)
.pluck(:foo_child_id)
where(id: ids)
end
}
end
controller
class FooController < ApplicationController
has_scope :p, using: %i(x, y), type: :hash, do |_, scope, value|
x, y = value
scope.merge(FooParent.p(x, y))
end
end
- RailsのActiveRecordのmergeメソッドを使ってみる - その辺にいるWebエンジニアの備忘録
- Rails の scope をあまり使わない方がいい理由 - ハトネコエ Web がくしゅうちょう
- Railsでよく利用する、Scopeの使い方。 - Qiita
activeredord-import
複数のレコードを追加することを考えるとき、何回もINSERT
文を発行するのは効率が悪そうだということは言われればまあ納得できると思います。一つの文にまとめて発行することをBULK INSERT
と呼びます。
Rails6ではinsert_all
というメソッドがありこれを実現できるのですがそれ以前の環境や一部のユースケースにはactiveredord-import
を用います。
ただしMySQLでは親子関係にあるとき子要素を自動で追加しません。また追加した要素を返してくれたりもしないので自力でなんとかする必要があります
つまりwhere
でうまく絞り直して取ってくる必要があるわけですがなかなか骨です
- Rails 6のB面に隠れている地味にうれしい機能たち(翻訳)|TechRacho by BPS株式会社
- (初心者向け)activerecord-importの使い方 - Qiita
- RubyonRails:activerecord-importを使って複数レコードを一括登録する(BULK INSERT) - Madogiwa Blog
where(id: before_id..after_id)
BULK INSERT
によって新規レコードが追加されるとき、そのidは連続するはずです。ということは追加前後のidを記録しておけば良さそうです。
- BULK INSERTの戻り値にPRIMARY KEYは返らない(activerecord-import) - Qiita
- MySQL - [Rails] activerecord-importを使用したバルクインサートで親子モデルの同時保存|teratail
whereで持ってこれたらあとはそれを使って子テーブルのレコードに親を指定してあげればいいですね。
親テーブルのレコードは変わらないがそれに紐づく子テーブルのレコードが増える場合
この場合次のような戦略を取ることが考えられます
- 予め紐づく親テーブルのレコードのhashを取っておく
- 親テーブルに
BULK INSERT
- 親テーブルのレコード全件を取り直しそれぞれのhashを取る
- 子テーブルに追加するレコードたちを1で取得しておいたhashを元に紐付ける
- 子テーブルに
BULK INSERT
予め紐づく親テーブルのレコードのhashを取っておくというのは、子テーブルを仮にFooElement
とすると次のようにattr_accessor
を書いておいて、
class FooElement < ActiveRecord::Base
belongs_to :foo, class_name: "XxxFoo", foreign_key: :foo_id
attr_accessor :foo_values
end
こういう感じのイメージになる
# Fooテーブルのカラムのうち識別するに十分なカラムたち
FOO_ATTRIBUTES = [:bar1, :bar2]
def store(asset, props)
foos = # do somethig
foo_records = []
foo_element_records = []
foo_ids = asset.foos.pluck(:id)
element_destroy = FooElement.where(foo_id: foo_ids).each_with_object({}) do |element, hash|
# do something
end
# create foo_records
foos.each do |foo|
foo[:elements]&.each do |e|
element = element_destroy.delete(
# something
)
element ||= FooElement.new
# copy e to element
# 1. 予め紐づく親テーブルのレコードのhashを取っておく
foo_values = serialize_foo_values(foo)
element.foo_values = foo_values
foo_element_records << element if element.changed?
end
end
# do something
ActiveRecord::Base.transaction do
# 2. 親テーブルに`BULK INSERT`
asset.foos.import foo_records
# 3. 親テーブルのレコード全件を取り直しそれぞれのhashを取る
imported_foos = build_values_hash_table(asset.foos.reload)
# 4. 子テーブルに追加するレコードたちを1で取得しておいたhashを元に紐付ける
foo_element_records.each do |e|
e.foo = imported_installed_applications[e.foo_values]
end
# 5. 子テーブルに`BULK INSERT`
FooElement.import foo_element_records
end
end
def build_values_hash_table(foos)
foos.each_with_object({}) do |foo, hash|
item_values = serialize_foo_values(foo)
normalize_attributes!(item_values)
hash[item_values] = foo
end
end
end
def serialize_foo_values(foo)
FOO_ATTRIBUTES.map { |attr| foo.send(attr) }
end
migration
テーブルを追加したりカラムを足したりするとき、migrationファイルを作成することで実現できます。
まずは公式のガイドをよく読むことが大事です。
Active Record マイグレーション - Railsガイド
rails generate migration AddPartNumberToProducts part_number:string
class AddPartNumberToProducts < ActiveRecord::Migration
def change
add_column :products, :part_number, :string
end
end
のように簡単なマイグレーションであればコマンドだけで自動生成できます。
カラム名、型、既定値、null制約に注意して定義します。整数型であってもnull制約は意味を持ちます。
マイグレーションファイルを追加するときは必ずup→down→upの確認をしましょう。downに不備があって再度upできないということがあってはまずいです。
- rails generate migrationコマンドまとめ - Qiita
- railsのカラム追加、削除、データ型変更の方法 - Qiita
- 【Rails・MySQL】MySQLのデータ型とRailsのマイグレーションファイルのデータ定義の対応まとめ - Qiita
- Railsでの複数カラムの追加・削除とまとめて行う方法 - Qiita
-
MySQL | CHAR型とVARCHAR型
- limitをかけた文字列カラムにlimit以上の物を入れたらどうなるか
bigint対応
Rails5では既定でidはbigintですが、Rails4ではそうではありません
idをbigintにする方法は色々ありますが、いろいろバグもあったようなのでSQLを直書きするのが安心できます。テーブル追加と同時にやる例です。change
ではなくup
/down
で書くと良いでしょう。
class CreateFooElements < ActiveRecord::Migration
def up
create_table :foo_elements do |t|
t.string :bar1, null: false
t.timestamps null: false
end
execute "ALTER TABLE `foo_elements` MODIFY `id` BIGINT NOT NULL AUTO_INCREMENT;"
end
def down
drop_table :foo_elements
end
end
- Ruby on RailsでシステムIDをbigint型に変更する
- Add bigint primary key support for MySQL. by kamipo · Pull Request #18220 · rails/rails
- rails で id を BIGINT 型 + primary key + AUTO_INCREMENT する - scramble cadenza
- Rails5.1からidカラムがbigintになるのでその対応 - koukiblog
- drop_table | Railsドキュメント
なお余談ですが次のようにマイグレファイルがなっているとき、何事もなくdownに成功してその後のupでコケます
class CreateFooElements < ActiveRecord::Migration
# 前略
# def defしてるのに何故か何事もなく通過!!!
def def down
drop_table :foo_elements
end
end
t.reference
他のテーブルの子テーブルとなるようなときなどに外部キーを使うと思うのですがそれを作る多分一番簡単な方法です。add_foreign_key
/remove_foreign_key
する手法もあるようですが面倒に見えます。
class CreateFooElements < ActiveRecord::Migration
def up
create_table :foo_elements do |t|
t.reference :foo, type: :bitint, index: { name: 'foo_elements_on_foo_id' }
t.string :bar1, null: false
t.timestamps null: false
end
execute "ALTER TABLE `foo_elements` MODIFY `id` BIGINT NOT NULL AUTO_INCREMENT;"
end
def down
drop_table :foo_elements
end
end
- references | Railsドキュメント
- 【Rails】外部キー追加におけるreference型の使用、未使用の違い。外部キー制約によるエラーをコンソールで確認してみた。 - Qiita
-
migration時のreferencesで作成されるindex nameを変更したい [Rails] - Qiita
-
index: { name: }
の解説
-
-
[Rails]referencesを使用した外部キー設定で沼った - Qiita
- 型に注意が必要
- Rails の migration で外部キー制約の設定をする (add_foreign_key, remove_foreign_key) - Qiita
アソシエーション
まずはこれを読みましょう
belongs_to/has_one/has_manyの簡単まとめ | 酒と涙とRubyとRailsと
一応その他関連資料
- Active Record の関連付け - Railsガイド
- [初心者向け] Railsで関連するデータ(親子関係)を保存する方法あれこれ - Qiita
- 【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】🎸 - Qiita
- アソシエーションにおけるclass_nameの定義! - Qiita
- railsアソシエーションオプションのメモ - Qiita
- Ruby on Rails - has_many関係にあるテーブルの値をビューで呼び出し|teratail
- grouping - Rails has_many association count child rows - Stack Overflow
親子関係の設定
親子関係はもともとそういう関係が成立してDBに入っている場合を除けば、自分で設定するものです。なぜならば親レコードのid
がわからないと子レコードの外部キーにそれを設定できないからです。
foo = Foo.create(bar: "aaa")
# 外部キーにid指定してしまうとidの有効性を検証するためにSELECTが走るのであんまりよくない
# element = FooElement.create(bar1: "aaaa", foo_id: foo.id)
# これならレコードがあることはわかってるのでSELECTは走らない
element = FooElement.create(bar1: "aaaa", foo: foo) # belongs_to :foo, ...
elements = []
element = FooElement.new
# これならレコードがあることはわかってるのでSELECTは走らない
element.foo = # すでにcreate/saveされるなどしてDBにいるレコードを代入
elements << element
# activerecord-import
FooElement.import elements
親テーブルのレコードから紐づく子テーブルのレコードにアクセスする
# 直接
foo.foo_elements
# どこかでidが求まってたとして
foo_id = # something
FooElements.where(foo_id: foo_id)
子テーブルのレコードから紐づく親テーブルのレコードにアクセスする
belongs_to
で指定した名前でアクセスする
foo_element.foo
Rspec
RspecはRubyにおけるテストフレームワークです。他にRailsにくっついてくるMinitestってのも人気のようですが今回はRspecを取り扱います。
テストとは
C++初心者のみんな、単体テストとCIを組めるようになって君もライブラリ製作者の仲間入りしよう!#どんな検査を書けばいいのか?の焼き直しになりますが改めて。
すべてのプログラムには少なくとも
- 入力
- 出力
この2つがあるはずです。また場合によっては
- 事前状態
- 事後状態
なんかもあるかもしれません。まあこれも広義の意味で入力と出力と言えるので隠れた入出力といったりします。
テストを書くときはこの入力に対して期待する出力が得られるかを検査することになるわけです。
どのようなテストであるべきか
言語分野問わず言えそうなこととしては次のようなことでしょうか
-
すべての条件分岐を通るように検査する
- 特定のルートだけ検査したのでは不十分
- 例外が投げられる入力を与える検査をする
- 意図した例外を投げているかは調べ忘れやすい
- 条件分岐の境界値になるような入力を与える検査をする
- 「以下」と「未満」を間違えたりド・モルガンの法則をど忘れするのはよくあるミスなので境界値を与えることで効率よくバグが見つかる
- 時間がかかりすぎる検査は可能ならば避ける
- 時間がかかるテストは誰も実行しない
- そのテストがある理由がわかるように記述されている
- 理解不能なテストは機能追加やリファクタリングの妨げになる
またRailsのテストという文脈ということでは次のようなことも言えそうです
-
DBを操作するテストで過度にテストケースが細分化されていないか
- テストケースごとにDBがロールバックするようになっている場合、この操作はとても時間がかかる
-
レコードの作成/更新時刻に依存するテストの場合時刻を入力で明示しているか
- 作成時刻がたまたまおなじになったり時刻同期で時刻が逆転したりするとテストが不安定になる
-
DRYしようとしていないか
- DRYするとかえってテストの可読性を落とす
- テストの数が多くなるとついDRYしたくなるが心を強く持つ(でも負ける)
-
過度に階層化されていないか
-
describe
/context
を入れ子にし過ぎになりがちではありますがそんなにいる?っていうことを心がけましょう
-
テスト観点をどう出すか
-
要件定義/設計を何度も見る
- 仕様を満たすようにテストするという大原則
- 過去の類似機能のテストを見る
- 人を増やす
- 検証の人に聞いてみる
- 強い人に見てもらう
- 過去の検証項目を参考にする
- 1日くらい寝かして明日の私のちからを借りる
- 実働を見る
- (ユーザーの声を見る)
Rspecをやるときにまず読むべき資料
とりあえずこれを隅から隅まで読めば最低限かけるようになります。
その他参考文献。とりあえずjnchitoさんの記事を探せばいいという感はある。
- サヨナラBetter Specs!? 雑で気楽なRSpecのススメ - Qiita
- 使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita
- 使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
- 実用的な新機能が盛りだくさん!RSpec 3.3 完全ガイド - Qiita
- RSpecのletの遅延評価を利用してよりコンテクスチュアルなSpecを書く – もばらぶエンジニアブログ
- RSpecの実行時間を1/5にした話 - Speaker Deck
- performance testing - How much time does "let" really save in RSpec tests? - Stack Overflow
- 【RSpec】配列要素全てにマッチャを適用する - Augmented Usamimi
all
matcher - Built in matchers - RSpec Expectations - RSpec - Relish- RSpec - expectの中を汚さずに複雑な検証ができるsatisfyマッチャがすごい - Qiita
- 【RSpec】described_classについて - Qiita
他の言語のテストフレームワークと比較したときのRspec
生粋のRspec使い達からは異論もたくさんあろうかと思うのですが、他の言語のテストフレームワークに慣れ親しんだ人視点です。
テストフレームワークごとに多少色が出るのはそういうものだと思いますが、あんまりにも違うとびっくりしてしまいます。
まあでも独自路線を突っ走ってしまうのってC++でもあるので(template多用するやつとか)わからなくはないですね?
- 階層化しすぎになりがち
- JavaScript界隈のJestなんかも同様の階層化ができますが、
subject
との組み合わせもあるのかやたら階層化が深くなりがち
- JavaScript界隈のJestなんかも同様の階層化ができますが、
-
let
が乱舞する- 悪いってわけではない
- ただこれによって独特の世界観が作られている気もする
- 上層のletを上書きとかもできるが多用すると可読性を落とす
-
it_behaves_like
したいときは使わざるを得ないですよね・・・引数とかないんですか
-
subject
/before
/let
全部見ないと前提条件がわからない- 言い換えるとそこだけ見ればわかるということもできるが・・・
- なんでもかんでも
subject
を使いすぎ-
subject { super()[:foo] }
してis_expected
を使おうとするのはどう考えてもオーバーキル-
expected(subject[:foo])
でいいのでは?
-
むしろsubject
/before
滅ぼしませんか?
-
-
it
の中をシンプルにしようとしすぎる-
it
が一行に書けると英文っぽく読めるという理論はわからなくないが・・・ - べつに
it
の中に複数のテストがあってもいいのでは?-
aggregate_failures
(Rspec >= 3.3)をつかうともっと便利 - 実用的な新機能が盛りだくさん!RSpec 3.3 完全ガイド - Qiita#1. 特定のエクスペクテーション群をまとめて検証できる(aggregate_failures メソッド)
-
-
it
をシンプルにしようとしすぎるあまりにlet
とかsubject
が増殖するのは本末転倒では?- 何事もバランスが大事
- DB操作は
it
単位でロールバックするのでit
の数が増えるとテストが遅くなる- これは
database_cleaner
ごと外せるならまあ関係ない - Railsアプリからdatabase_cleanerを外したらRSpecが30%速くなった話 - Qiita
- これは
-
いろいろ書きましたが周辺コードと空気感を合わせて書くのがいいとは思います。
subject
の用法・用量
Rspecのchangeマッチャーを使う時、初期状態の設定をsubjectでやるのってありですか? - Qiita
という記事を書いたときの反響が勉強になったので引っ張っておきます。
https://qiita.com/yumetodo/items/cd1fca4e4e56f573e9a2#comment-4026708af1bd356a8005
@jnchito 2021-08-07 07:37
@yumetodo さん、こんにちは。
RSpecの書き方はいろんな流派(?)がありますが、僕個人としては滅多にsubjectを使いません。
少なくともsubjectが向いているのは、以下の例のように副作用がなく、一定の入力に対して常に一定の値を返す関数的な処理です。describe Person do let(:user) { User.new(age: age) } # adult?メソッドには副作用がなく、ageの値に応じて常に一定の真偽値を返す subject { user.adult? } context '20歳未満' do let(:age) { 19 } it { is_expected.to be_falsey } end context '20歳以上' do let(:age) { 20 } it { is_expected.to be_truthy } end end
本記事のコードのように、副作用の結果を期待する処理をsubjectにしようとするといろいろと無理が生じます。(にもかかわらず、何が何でもsubjectにしたい派の人たちは、そういう処理もsubjectを使おうとしてテストコードに無駄な工数をかけようとするんですよね。いくら頑張っても結局トリッキーなテストコードしか出来上がらないのに…
https://gamelinks007.net/@S_H_/106709170737688666
@S_H_@gamelinks007.net 2021年8月6日 21:25
@yumetodo
minitest派なのであんまり知見があるわけではないですが、素直にletとか使う方が良いかなと理由としてはそう書いてる人が多く、あとから引き継ぐ時に楽だからですね
あと、describeにある程度のテストをまとめた方が良いからという判断もありますね
@yumetodo
なのでsubject自体を使わずにRefinementsとかでそのスコープでだけ有効なメソッド生やすとかが楽だし、読みやすいかなと@S_H_ なるほど
たしかにsubjectつかうとdescribeが小さくなりすぎがちというのはわかる気がする
@yumetodo
ですねー
なので、ちょっとそのテストフレームワークから離れるときはModuleとかで便利そうなメソッドを切り出すとかすると良さげですね
またご紹介いただいた記事も非常に参考になります。
【翻訳】RSpecのリードメンテナだけど何か質問ある? - Qiita
Structが便利なこともある
ActiveRecordで各カラムにアクセスするときってFoo.bar
みたいにアクセスすると思います。そしてそういうデータ型を受け取る関数を書くこともあるでしょう。当然それのテストを書きますよね?
ところがRubyで書きやすいデータ構造ってHashなんですよね。テストの期待結果を作ったりするのにこの構造をほしいためだけにわざわざ後述のFactoryBotを使ってテストデータ作るのはオーバーキルです。
これを実現する方法としてclass.new
で頑張る方法もあるのですが面倒です。
OpenStruct
をつかう例もあるようですがもっと高速でミニマムな方法があります。
それがStructです。
めっちゃ便利なRubyのStructクラスのお話 - Qiita
つまりこんなふうに使えます(letを多用しすぎているかもしれない)。
def some_method(elements)
elements&.map { |e| e.bar1 }.join || ""
end
describe "arikitari na test" do
let(:element) { Struct.new(:bar1) }
let(:actual) { some_method(raw_elements&.map { |e| element.new(e) }) }
context "single" do
let(:raw_elements) { ["hoge1"] }
it { expect(actual).to eq "hoge1" }
end
context "mutiple" do
let(:raw_elements) { ["hoge1", "hoge2"] }
it { expect(actual).to eq "hoge1,hoge2" }
end
end
@S_H_@gamelinks007.net 7月27日
Structを簡単に書けるようにしようぜってチケットならこないだ出たね
Feature #16986: Anonymous Struct literal - Ruby master - Ruby Issue Tracking System
test用のデータをDBに入れるには: FactoryBot(旧FactoryGirl)
かつてFactoryGirlと呼ばれていたものは現在ではFactoryBotと名前を変えています。FactoryBotでもFactoryGirlでもどっちでもいいわけですがそもそもこれは何をするためのものなのでしょうか?
Rails テスティングガイド - Railsガイド#4.2 フィクスチャのしくみ
よいテストを作成するにはよいテストデータを準備する必要があることを理解しておく必要があります
FactoryBotはテストデータを作るための支援をしてくれます。
もっとも簡易的な使用方法は各カラムにデフォルト値を入れておくことです。
しかし、よく設計されたテストデータが作れたとき、それはいくつものテストケースをまたいで利用する価値があるはずです。
factoryファイルを記述する
各カラムのデフォルト値設定場所となりがちな気もしますが、意図としてはテストケースをまたいで利用する価値があるテストデータ設定場所です。
FactoryBot.define do
factory :foo, class: Foo do
bar { 'hoge' }
end
factory :foo_element, class FooElement do
bar1 { 'hoge1' }
trait :with_dependents do |e|
foo
end
end
end
- FactoryBot(FactoryGirl)チートシート - Qiita
- FactoryBot 入門 [翻訳][Railsの場合まとめ] - Qiita
- FactoryBotを使う時に覚えておきたい、たった5つのこと - Qiita
- FactoryGirlでtraitを使うとintegration test書くのが捗るという話|TechRacho by BPS株式会社
create/build
レコードを作成して実際にDBに入れるときはFactoryBot.create
(旧: FactoryGirl.create
)を用います。
modelを指定する第1引数にはmodel名をスネークケースにしたもののSymbolやfactoryファイルで指定した名前が利用できます。
FactoryGirl.create(:foo_element, :with_dependents)
同様にしてレコードを作成するだけのときはFactoryBot.build
が利用できます。
FactoryGirl.build(:foo_element, foo)
なおfactoryファイルの恩恵を受けなくていいなら、ActiveRecordのものを使っても問題ない気がします。
foo.foo_elements.build(hoge)
親子関係は上記のように通常のActiveRecordでの操作と変わるところはなく、create/saveするなどして実際にDBにデータが入っているレコードを用いて指定することで作成できます。
N+1 insertを避けてレコードをたくさん作る: create_list死すべし、慈悲はない(?)
すでに上で述べている通り、実行速度の遅いテストはいずれ廃れて実行されなくなる運命にありますし、開発効率を落とします。つまりN+1問題はテストにおいても撲滅されなければなりません。
レコードをたくさん作るとき、create_list
が用いられることがあります。
FactoryGirl.create_list(:foo_element, 10, foo: foo)
これが何を引き起こすかというとなんとINSERT
文が10回発行されます。
これはよろしくないですね。代わりにbuild_list
とactiverecord-importを使います。
elements = FactoryGirl.build_list(:foo_element, 10, foo: foo)
FooElement.import elements
ただしcreate_list
は生成物の配列を返すのでbuild_list
のときと違っていちいちDBを読みに行かなくてもいいという利点もあるのでいつでも置き換えればいいというものでもなさそうです。
create_listに限らず、create/save系のご利用は計画的に!