8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RubyもRailsもSQLも知らんC++erですがいきなり戦いを挑んでみた

Last updated at Posted at 2021-10-19

はじめに

これまで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という変数が使われていますね。

執筆段階になって思い出しましたが、そういえば昔ほんの少しだけ触れた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とかしたほうがいい場面もあるかもしれないですね。

binding.pry

コンソールでデバッグしていくやつです。VSCodeのデバッガーより軽量ですし、Active Recordみたいに何のメソッド生えてるのかわからんときに、VSCodeのデバッガーだと追いずらい一方でbinding.pryならメソッド一覧が取って来れるので、どうやって子テーブルアクセスすりゃいいの?ってときにも役立つかもしれないですね。

デバッガーでブレークポイントを打つのと同じ感覚でソースコード中にbinding.pryと書き、普通にプログラムを実行すると、当該箇所を通過したときにデバッガーになります。

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)

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)

nullアクセスを避けるには

最近の言語ではオプショナルチェインとかそんな名前で、プロパティ/メソッドに多段階にアクセスするときに、nullチェックをいちいち書かないでもやってくれるような構文がありますが、Rubyはどうでしょうか?

ボッチ演算子

&.のようにアクセスすることでnull参照を避けられます。その見た目からボッチ演算子と呼ばれることが多いようです。

@nickname = current_user&.nickname

なおかつてはRails(Active Support)のtryメソッドが用いられたようですが、現在では考古学の対象のようです。

Hash#dig

Q:

foo.key?(:aaa) ? foo[:aaa][:bbb] : nil

もうちょっとすっきり書く方法、Rubyでご存じの方いますか?

A: Hash#digを使うとかですかね?

foo.dig(:aaa, :bbb)

NullObject Pattern

ボッチ演算子もtryHash#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なら出番もあるのかもしれないですね。

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メソッドに帰ってきたとき、依然として&aprocオブジェクトではありません。

こうしてできた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つでは結合強度が違うのだそうですが、そういうのに依存しないように書きたいですね。

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{})とかがだめな典型例です。

メソッドを関数の引数に渡すには

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)

実行結果

クラスの特異メソッドと特異クラス

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

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を調べる必要もあります。

Rails

Railsではファイル名やModel名などなどに独特の命名規則の制約があります。だいたい単数形/複数形がよくわからなくなるやつです。公式ドキュメントを見て頑張りましよう。

基本構造: 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にリクエストを転送すればいいか)を定義する
  • 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で扱う時の入門で自分が調べたところをリンク集としてまとめておきます。

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

activeredord-import

複数のレコードを追加することを考えるとき、何回もINSERT文を発行するのは効率が悪そうだということは言われればまあ納得できると思います。一つの文にまとめて発行することをBULK INSERTと呼びます。

Rails6ではinsert_allというメソッドがありこれを実現できるのですがそれ以前の環境や一部のユースケースにはactiveredord-importを用います。

zdennis/activerecord-import: A library for bulk insertion of data into your database using ActiveRecord.

ただしMySQLでは親子関係にあるとき子要素を自動で追加しません。また追加した要素を返してくれたりもしないので自力でなんとかする必要があります

つまりwhereでうまく絞り直して取ってくる必要があるわけですがなかなか骨です

where(id: before_id..after_id)

BULK INSERTによって新規レコードが追加されるとき、そのidは連続するはずです。ということは追加前後のidを記録しておけば良さそうです。

whereで持ってこれたらあとはそれを使って子テーブルのレコードに親を指定してあげればいいですね。

親テーブルのレコードは変わらないがそれに紐づく子テーブルのレコードが増える場合

この場合次のような戦略を取ることが考えられます

  1. 予め紐づく親テーブルのレコードのhashを取っておく
  2. 親テーブルにBULK INSERT
  3. 親テーブルのレコード全件を取り直しそれぞれのhashを取る
  4. 子テーブルに追加するレコードたちを1で取得しておいたhashを元に紐付ける
  5. 子テーブルに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できないということがあってはまずいです。

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

なお余談ですが次のようにマイグレファイルがなっているとき、何事もなく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

アソシエーション

まずはこれを読みましょう

belongs_to/has_one/has_manyの簡単まとめ | 酒と涙とRubyとRailsと

一応その他関連資料

親子関係の設定

親子関係はもともとそういう関係が成立して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さんの記事を探せばいいという感はある。

他の言語のテストフレームワークと比較したときのRspec

生粋のRspec使い達からは異論もたくさんあろうかと思うのですが、他の言語のテストフレームワークに慣れ親しんだ人視点です。

テストフレームワークごとに多少色が出るのはそういうものだと思いますが、あんまりにも違うとびっくりしてしまいます。

まあでも独自路線を突っ走ってしまうのってC++でもあるので(template多用するやつとか)わからなくはないですね?

  • 階層化しすぎになりがち
    • JavaScript界隈のJestなんかも同様の階層化ができますが、subjectとの組み合わせもあるのかやたら階層化が深くなりがち
  • letが乱舞する
    • 悪いってわけではない
    • ただこれによって独特の世界観が作られている気もする
    • 上層のletを上書きとかもできるが多用すると可読性を落とす
    • it_behaves_likeしたいときは使わざるを得ないですよね・・・引数とかないんですか
  • subject/before/let全部見ないと前提条件がわからない
    • 言い換えるとそこだけ見ればわかるということもできるが・・・
    • なんでもかんでもsubjectを使いすぎ
      • subject { super()[:foo] }してis_expectedを使おうとするのはどう考えてもオーバーキル
        • expected(subject[:foo])でいいのでは?
      • むしろsubject/before滅ぼしませんか?
  • itの中をシンプルにしようとしすぎる

いろいろ書きましたが周辺コードと空気感を合わせて書くのがいいとは思います。

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

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系のご利用は計画的に!

8
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?