島までは遠い 〜サークルアラウンド株式会社代表佐藤のブログ〜

佐藤正志@サークルアラウンド株式会社のことが少しわかる場所。プログラマーを育てるトレーナーとして、現役のソフトウェア技術者として、経営者の端くれとして、想うことをつづる。

他言語、フレームワークから乗り換える時に Ruby on Rails について知っているとちょっと嬉しいこと

※ 時間がなかったので、プロットだけ書いたものを ChatGPT に書かせただけになっているため、サンプルコードがちゃんと動かないなどあるかもしれません(随時直します)

はじめに

Ruby on Rails(以下、Rails)は「規約より設定(Convention over Configuration)」を重視するフレームワークです。そのため、書きやすく、理解しやすく、楽しい ことを目指しています。しかし、他の言語やフレームワークの経験者にとっては、独特な点が多く、混乱しやすい 側面もあります。

本記事では、特に混乱しやすいポイントにフォーカスし、それぞれの特徴を取り上げていきます。


メソッド名の ?!

これらは形式的なことなのでわかってしまえば簡単かもしれません。演算子ではなくて、名前の一部で使うことができ、ついている場合の意味が慣習として決まっています。

? で終わるメソッド

意味: 真偽値を返すメソッドであることを表す(ように実装します)。

  user.admin?      # => true または false
  order.completed? # => true または false
  • ポイント:
    • ?構文ではなくメソッド名の一部 なので、自由に定義できる。
    • true / false 以外の値(例えば nil)を返しても問題なく動作することがある。

! で終わるメソッド

意味: より危険な操作を行う、または破壊的変更(mutate)を行うメソッド。また、失敗時に例外を投げることを示す意図の時もある。

  user.save!  # 保存に失敗した場合、例外を発生
  name.upcase! # 変数 `name` の値を直接変更
  • ポイント:
    • !構文ではなくメソッド名の一部
    • savesave! の違い:
      • savefalse を返す。
      • save! は失敗時に例外を発生させる。
    • map! などの破壊的メソッドと save! のようなメソッドの違いに注意が必要。

モデルの has_many / belongs_to による自動生成メソッドと ActiveRecord::Relation

Rails の has_manybelongs_to を宣言すると、自動的に 便利なメソッドが追加される

特に has_many で生成される関連オブジェクトは 単なる配列ではなく ActiveRecord::Relation であり、データベースに対するクエリが可能であることを理解しておく必要がある。

has_many によって追加されるメソッド

以下のように has_many :posts を定義すると、user.posts に対して次のメソッドが利用できる。

class User < ApplicationRecord
  has_many :posts
end
メソッド 説明
user.posts.new(attributes) Post.new(attributes) と同じだが、user_id を自動設定
user.posts.build(attributes) new のエイリアス
user.posts.create(attributes) Post.new(attributes).save を実行(失敗時はエラーを持つインスタンスを返す)
user.posts.create!(attributes) Post.new(attributes).save! を実行(失敗時は例外を発生)

ActiveRecord::Relationwhere による絞り込み

has_many の関連オブジェクトは 単なる配列ではなく ActiveRecord::Relation なので、データベースに直接クエリを投げることができる。これによって、検索のコードがスッキリし、想定外の検索を防ぐ効果もある(例えば user.posts ならば、 user_id で絞り込みが入っている前提で考えられるので、別のユーザーの情報は混ざらないことがすぐに推察可能)。

user = User.first
puts user.posts.class
# => ActiveRecord::Relation

上記のように、型としては ActiveRecord::Relation か、それと同様に扱えるいくつかの型で取得できる。そして以下のように続けて検索条件を書ける。

user.posts.where(published: true) # WHERE published = true
user.posts.order(created_at: :desc).limit(5) # ORDER BY created_at DESC LIMIT 5
user.posts.where(title: "特定のタイトル")

belongs_to によって追加されるメソッド

belongs_to :user を定義すると、post.user に対して以下のメソッドが利用できる。

class Post < ApplicationRecord
  belongs_to :user
end
メソッド 説明
post.build_user(attributes) User.new(attributes) を作成し、post.user_id を自動設定
post.create_user(attributes) User.new(attributes).save を実行(失敗時はエラーを持つインスタンスを返す)
post.create_user!(attributes) User.new(attributes).save! を実行(失敗時は例外を発生)

スコープ(scope)の活用

Rails では モデルの scope を使うことで、データベースクエリを簡潔に記述できる。

class Post < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc) }
end

スコープの利用

Post.published   # => 公開済みの投稿を取得
Post.recent      # => 最新の投稿を取得
Post.published.recent # => 公開済みの最新の投稿を取得

スコープの注意点

  • scopeActiveRecord::Relation を返す ため、メソッドチェーンが可能。
  • 引数を受け取るスコープも定義可能。
class Post < ApplicationRecord
  scope :by_author, ->(author_id) { where(author_id: author_id) }
end

Post.by_author(1) # => author_id が 1 の投稿を取得

enum の活用

Rails の enum は整数値を意味のあるシンボルにマッピングするための便利な機能。

class Order < ApplicationRecord
  enum status: { pending: 0, shipped: 1, delivered: 2, canceled: 3 }
end

enum によるメソッドの自動生成

enum を定義すると、以下のようなメソッドが自動的に利用可能になる。

order = Order.new(status: :pending)
order.pending?  # => true
order.shipped?  # => false

order.shipped!
order.status # => "shipped"

enum によるスコープの自動追加

enum を定義すると、各ステータスに対応するスコープも自動的に作成される。

Order.pending     # => status が "pending" のレコードを取得
Order.shipped     # => status が "shipped" のレコードを取得
Order.delivered   # => status が "delivered" のレコードを取得

また、not_xxx という形のスコープも利用可能。

Order.not_pending # => status が "pending" 以外のレコードを取得

enum の注意点

  • enum のキー(pending, shipped など)は シンボルで指定する
  • データベースには整数値として保存されるため、変更時は マイグレーションの管理 に注意。
  • enum の値を変更すると、既存のデータとの整合性が取れなくなる可能性がある。

Controller でよく行われるバリデーションのスタイル

Rails の scaffold で生成される Controller では、バリデーションエラーが発生した場合、不完全なモデルをそのままビューに渡す という設計になっています。

class PostsController < ApplicationController
  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to @post, notice: '投稿が作成されました。'
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :content)
  end
end

Post.new を実行すると、モデルのインスタンスが作成されるが、この時点ではデータベースには保存されていない。save を実行すると、バリデーションが適用され、成功すればデータベースに保存される。失敗した場合、モデルの errors にバリデーションエラーの情報が格納される。

post = Post.new(title: "")
post.valid? # => false
post.save # => false
post.errors.full_messages # => ["タイトルを入力してください"]

この create アクションでは、@post.save に失敗した場合、@post(エラーを持つインスタンス)をそのまま new ビューに渡している。ビュー側では @post.errors を利用してエラーメッセージを表示できるため、ユーザーが入力内容を修正しやすくなっている。

このスタイルは バリデーションエラーが発生しても、ユーザーが入力した内容を保持できる という利点があります。