大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【Rails】技術面接対策の記事の質問を多少深ぼる記事③

はじめに

この記事は前回の記事の続きものです。

(前回の記事)

sakitadaiki.hatenablog.com

本記事ではQ13 ~ Q18を深掘りします。

Q13: コールバックとは何かを説明してください

回答:

コールバック(callback)は誤解を招きがちな用語です(訳注: 英語圏では電話の「折り返し」を想像させるためと思われます)。コールバックは、オブジェクトのライフサイクルの中でメソッドを実行するフックを指します。 before_validationafter_saveafter_destroyなど、オブジェクトの作成、更新、削除などに多くのコールバックが存在します。 コールバックは、たとえばUserレコードが作成されたときに、それに関連付けられているContactレコードを作成するといった条件付けのロジックを記述するのに有用です。

オブジェクトのライフサイクルとは、オブジェクトの状態が切り替わるサイクルのことです。例えば、Userクラスのインスタンスが作成される時、そのモデルは作られた状態に切り替わります。

User.create(name: "daiki", email: "hogehoge@sample.com")

上記のユーザを更新する時もそのインスタンスの状態が切り替わります。

User.find_by(email: "hogehoge@sample.com").update(email: "fugafuga@sample.com")

インスタンスを削除する時も削除された状態に切り替わります。

User.find_by(email: "hogehoge@sample.com").destroy

このようにオブジェクトは一度作成されてから削除されるまで状態の切り替わるタイミングがあります。これがオブジェクトのライフサイクルです。

そして、オブジェクトの状態が切り替わるタイミングの前後にイベントを発生させるフックをコールバックといいます。例えば、作成される前に呼び出すコールバックはbefore_createとなります。

例えば、ユーザーが作成された時にクラスにbefore_createを定義していたとします。

class User < ApplicationRecord
    before_create :hello
    
    def hello
        p "hello"
    end
end

すると、インスタンスが作成される前に文字列「hello」が出力されます。このようにオブジェクトのインスタンスの状態が切り替わるイベントの前後(before or after)のタイミングで実行したいメソッドを指定することでフックできます。

※ コールバック一覧は下記に書いておきます。

  • after_initialize・・・オブジェクトがインスタンス化された
  • before_validation・・・バリデーションが行われる直前
  • after_validation・・・バリデーションが行われた直後
  • before_save・・・オブジェクトがDBに保存される直前
  • before_create・・・オブジェクトがDBに新規保存される直前
  • before_update・・・オブジェクトによりDBを更新(UPDATE)する直前
  • after_create・・・オブジェクトがDBに新規保存(INSERT)された直後
  • after_update・・・オブジェクトによりDBを更新(UPDATE)した直後
  • after_save・・・オブジェクトをDBに保存した直後
  • before_destroy・・・destroyメソッドで削除される直前
  • after_destroy・・・destroyメソッドで削除された直後

ちなみに回答の「Userレコードが作成されたときに、それに関連付けられているContactレコードを作成する」ですが、これもわかりやすい例で説明します。例えば、emailがUserではなくContactのカラムだとします。そしてUser has_one Contactの関連付けがあるとします。

class User < ApplicationRecord
    has_one :contact, dependent: :destroy
end
class Cotanct < ApplicationRecord
    belongs_to :user
end

そして、Userのインスタンスが作成された時にそのインスタンスのContactも同時に作成したいとします。その場合、ユーザが作成された後にContactが作成されるようにします。

class User < ApplicationRecord
    has_one :contact, dependent: :destroy

    after_create do # ブロックにして実行したい処理を直接書くこともできる
    create_contact # has_oneのアソシエーションの時に使えるヘルパーメソッド
  end
end

これでUser作成時にContactのレコードも同時に作成できるようになります。

Q14: before_saveコールバックとafter_saveコールバックの使い分けについて説明してください

回答:

あるオブジェクトがsaveされた後に更新をかける場合、更新を永続化させるために追加のデータベーストランザクションが必要になります。つまり、あるオブジェクトの「属性」を更新する場合はbefore_saveコールバックの方が効率的です。 しかし、オブジェクトを保存するまでは存在しない情報もあります(idなど)。つまり、関連付けられたレコードの作成にidが必要な場合は、after_saveコールバックを実行しなければならないでしょう。

まずbefore_saveafter_saveについて詳しく説明します。


**before_save**

バリデーションに成功し、実際にオブジェクトが保存される直前で実行されます。つまり、after_validationの後です。オブジェクトが保存される時なので、オブジェクトが作成されるタイミングとオブジェクトが更新されるタイミングで実行されます。つまり、、、

before_save = before_create(作成前) + before_update(更新前)

after_save

after_update の直後、データベースへの COMMIT の直前に実行されます。回答の説明にある通り、関連オブジェクトを操作する際に使われます。


上記の説明を踏まえた上で解答を解説します。

まずafter_saveについては問題ないでしょう。オブジェクトが保存される前までは対象のオブジェクトにはないデータが存在します。それらを使う場合はafter_saveの方がコールバックの使い方としては正しいでしょう。下記は保存されたidを使用した処理ですが、before_saveの場合は値がないのでエラーになります。

class User < ApplicationRecord
    after_save do
    p id # 保存されたidを使用した処理
  end
end

class User < ApplicationRecord
    before_save do
    p id # 作成時にidがないのでエラーとなる
  end
end

before_saveはどうでしょうか。

「あるオブジェクトがsaveされた後に更新をかける場合、更新を永続化させるために追加のデータベーストランザクションが必要」とありますがこれはどういうことでしょうか。

ここでいう”永続化”ですが、これはオブジェクトの永続化とGoogle先生に聞けば何度なくの概要で出てきました。

永続化とは、インスタンスの状態を半永久的に保存し、いつでも復元できるようにすることです

https://www.atmarkit.co.jp/fdb/rensai/javapersis01/javapersis01_2.html

しかし、Railsではbefore_saveを使わずともインスタンスの状態を更新できます。なので、更新を永続化させるために追加のデータベーストランザクションが必要ないような気がします、、、。はて、自分の理解が足りていないのでしょうか、、、?誰か解答を持っていれば解説お願いします💦

自分個人の回答ですが、before_saveについてはafter_saveに当てはならない、つまり、保存された後のデータを必要としない場合に使用する、で問題ないと思っています。

Q15: Railsの「イニシャライザ」について説明してください

回答:

イニシャライザには、アプリの起動時にのみ実行する設定ロジックを置きます。つまり、イニシャライザの内容を変更した場合はRailsサーバーの再起動が必要です。イニシャライザは/config/initializers/ディレクトリの下に置かれます。

これは回答の説明で十分かなと思っています。

一応深掘りすると、イニシャライザは/config/initializers/ディレクトリ配下に置かれる設定ファイル群のことです。

https://i.gyazo.com/b8bfc0b1a1ffa3bedcea752c6b8f76d0.png

基本的にRailsはサーバが起動している状態でファイルを編集しても再起動することなく編集箇所が反映されます。例えば、app/ディレクトリ配下のコントローラーやモデル、ビューなどがそうです。

しかし、それとは異なりサーバー起動時にのみ読み込む設定ファイルがあり、それがイニシャライザです。イニシャライザのファイルには例えばgemの設定ファイルなどが置かれます。以下はsorcery.rbの中身の一部です。

Rails.application.config.sorcery.submodules = []

# Here you can configure each submodule's features.
Rails.application.config.sorcery.configure do |config|
・
・
・
・
end

イニシャライザのファイルを編集したら必ずサーバーを再起動するように心がけましょう。そうしないと意図した通りにアプリが動かないと思います。

Q16: deleteとdestroyの違いを説明してください

回答:

delete・・・レコードを1件削除する destroy・・・レコードを1件削除し、コールバックを実行する Railsアプリのモデルファイル関連付けで最もよく使われるのはdestroyコールバックです。たとえば、以下のコードはarticleがdestroyされると関連するcommentsもdestroyされます。

class Article < < BaseController
  has_many :comments, dependent: :destroy
end

まずはdestroyから。上述のようにdestroyで最もよく使われるのはコールバックdestroyだと思います。dependent: :destroyとすることで、インスタンスが削除された時に関連モデルも全て削除されるように設定できます。

Article.create(title: "hogehoge", body: "fugafuga")
Article.last.comments.create(text: "xxxxxxxx") # コメントを1つ作成
Article.last.destroy # コメントが全て削除される

これはRubyインスタンスでライフサイクルにdestroyがあるためですね。なのでdestroy前後のコールバックを呼び出すことができます。

before_destroy
after_destroy

次にdeleteですが、deleteはインスタンスのライフサイクルにないので、コールバックを実行することなくレコードの削除のみ実行します。よって、destroyのように関連付けモデルに対してはレコードの削除を行えないということになります。

関連モデルを削除できない理由はdeleteはActiveRecordを介さないと説明しています。対象のレコードにSQLであるDELETEを直接実行している感じですかね。

Q17:「ファットモデル、薄いコントローラ」の意味を説明してください

回答:

ビジネスロジックはコントローラではなくモデルに配置すべきです。そうすることでロジックの単体テストが行いやすくなり、再利用性も向上します。コントローラは、ビューとモデルの間で情報を受け渡しするための場でしかありません。これはあくまで新人Rails開発者向けの一般的なアドバイスであり、特に巨大なアプリでは実際には推奨されていません。 訳注: 巨大なアプリでは、モデルやコントローラ以外のロジックの置き場所を別途定めるのが普通です。

Skinny Controller, Fat Model」(=薄いコントローラ、ファットモデル)という2006年の記事があったそうですが、どうやらそれが由来のようです。

Buckblog: Skinny Controller, Fat Model

ビジネスロジックはコントローラではなくモデルに配置すべき」とあるように、コントローラーにはシンプルな処理のみ担当させ、その他複雑なロジックはモデルが担当すべきというものです。ActiveRecordの複雑なロジックはモデル側に記述すべきであり、コントローラーはモデルとビューの間でデータの受け渡しを行うためだけの場所に過ぎません。

「ファットモデル、薄いコントローラ」を成り立たせるために1番に連想されるのはscopeだと思います。

class Note < ApplicationRecord
  scope :search, ->(term) {
    where("LOWER(message) LIKE ?", "%#{term.downcase}%")
  }
end

scopeはActiveRecordを切り出してモデルに記述することで、コントローラーから取り出したいデータをscopeを介して取得するメソッドを定義できます。

def index
    @notes = Note.search("hogehoge")
end

もしscopeを使わないのであればコントローラに冗長なActiveRecordのロジックを記述しなければなりません。

def index
    @notes = Note.where("LOWER(message) LIKE ?", "%#{term.downcase}%")
end

結果コントローラの中の記述量が多くなってしまいます。それを回避するためにscopeがあるわけです。

しかし、「新人Rails開発者向けの一般的なアドバイスであり、特に巨大なアプリでは実際には推奨されていません。」とあるように何でもかんでもロジックをモデルに移せばいいというわけでもなさそうですね。。。ロジックの置き場所をモジュールで切り出す方法もありますし、一概にも「ファットモデル、薄いコントローラ」がいつも正しくなるということではないんですね。これは次の問でより詳しく解説します。

Q18:「薄いコントローラ、薄いモデル」の意味を説明してください

回答:

コードベースが成長するに連れて、ファットモデルが手に負えなくなり、モデルの責務が過剰になって管理不能に陥ってしまいます。モデルは永続化に専念し、モデル内のロジックを肥大化させないようにすべきです。「単一責任の原則」を常に意識し、ロジックをモデルから他のデザインパターン(Service Objectなど)に追い出すことで、モデルをもっと薄くできます。

Q17とはまた違った回答にですね。「Skinny Controller, Skinny Model」(=薄いコントローラ、薄いモデル)とあるようにどちらにも記述量を最小限に留めるということでしょうか。

Rails: skinny controller, skinny model

まずこれはオブジェクト指向の考え方になるのですが、「クラスには単一責任のみを課す」というそもそもの考えがあります。クラスごとに責任を徹底させることでコード変更の際に変更するコード量を可能な限り少なくするのが理想的なコードの状態です。

モデルも当然クラスです。もしファットモデルを許容するのであれば、そのモデルは複数の責任が課されてしまうためオブジェクト指向的にもよくないでしょう。自分もポートフォリオでUserモデルに100行以上のコードを記述しましたが、個人開発で100行となるとより大きなサービスだと確かに手に負えなくなりそうです。上記の記事ではapp/libディレクトリ配下にロジックごとにクラスを作成しコントローラー側で呼び出しています。

# app/controllers/users_controller.rb
# total: 20 lines (from 20 lines)
class UsersController < ApplicationController
  def create
    user = User.create!(user_params)
    BusinessLogicA.new
    BusinessLogicB.new
    render json: { status: "OK", message: "User created!" }, status: 201
  end

  def update
    user = User.update!(user_params)
    BusinessLogicB.new
    render json: { status: "OK", message: "User updated!" }, status: 200
  end
  ...
end

# app/controllers/companies_controller.rb
# total: 21 lines (from 21 lines)
class CompaniesController < ApplicationController
  def create
    company = Company.create!(company_params)
    BusinessLogicC.new
    BusinessLogicD.new
    render json: { status: "OK", message: "Company created!" }, status: 201
  end

  def update
    company = Company.create!(company_params)
    BusinessLogicC.new
    BusinessLogicE.new
    render json: { status: "OK", message: "Company updated!" }, status: 200
  end
  ...
end

# app/models/user.rb
# total: 3 lines (from 24 lines)
class User < ApplicationRecord
  has_secure_password
end

# app/models/company.rb
# total: 2 lines (from 53 lines)
class Company < ApplicationRecord
end

# app/lib/business_logic_a.rb
class BusinessLogicA
  # 10 lines of BusinessLogic-A
end

# app/lib/business_logic_b.rb
class BusinessLogicB
  # 5 lines of BusinessLogic-B
end

# app/lib/business_logic_c.rb
class BusinessLogicC
  # 20 lines of BusinessLogic-C
end

# app/lib/business_logic_d.rb
class BusinessLogicD
  # 14 lines of BusinessLogic-D
end

# app/lib/business_logic_e.rb
class BusinessLogicE
  # 9 lines of BusinessLogic-E
end

これはオブジェクト指向的な考え方をまさに取り入れていると言えるでしょう。「単一責任の原則」の通り、各クラスは単一の責任を保っているのでコードを変更する時はその一つのファイルを編集するだけですみそうですし。

終わりに

今回はここまで!!!!

次回はQ19からです!