【Rails】技術面接対策の記事の質問を多少深ぼる記事③
はじめに
この記事は前回の記事の続きものです。
(前回の記事)
本記事ではQ13 ~ Q18を深掘りします。
Q13: コールバックとは何かを説明してください
回答:
コールバック(callback)は誤解を招きがちな用語です(訳注: 英語圏では電話の「折り返し」を想像させるためと思われます)。コールバックは、オブジェクトのライフサイクルの中でメソッドを実行するフックを指します。
before_validation
、after_save
、after_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_save
とafter_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/
ディレクトリ配下に置かれる設定ファイル群のことです。
基本的に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からです!