📛

【Rails】URLをid以外のカラム名にする方法(paramオプション)

2023/08/16に公開

以下のようにresourceのidを使ったURLでなく、登録したアカウント名でURLを設定していく方法を説明する。

ページ :id を使用したURL param: :account を使用したURL
ユーザー情報表示ページ http://example.com/users/1 http://example.com/users/ganmo
ユーザー情報編集ページ http://example.com/users/1/edit http://example.com/users/ganmo/edit

テーブル設定

Userテーブルにaccountカラムがあることを前提とする。

accountカラムがデータベース内で重複しないようにユニーク制約を追加する。
ターミナルで以下を入力。

ターミナル
rails g migration AddUniqueIndexToUsers

マイグレーションファイルに以下を追加する。

class AddUniqueIndexToUsersAccount < ActiveRecord::Migration[6.1]
  def change
    add_index :users, :account, unique: true
  end
end

マイグレーションを実行する。
これで、同じアカウント名を複数回登録できないようする。

ルーティングの設定

paramオプションを使ってアカウント名をURLの一部にする設定をする。

config/routes.rb
resources :users, param: :account, only: [:show, :edit, :update]

これでusers/:accountでアクセスできるようになる。

モデルの設定

app/models/user.rb
class User < ApplicationRecord
  :
  # idの代わりにアカウントを使用
  def to_param
    account
  end
  
  # 英数字(小文字)と"ハイフン(-)" "アンダーバー(_)"のみ許可
  validates :account,
    format: { with: /\A[a-z0-9_-]+\z/ },
    length: { minimum: 3, maximum: 25 }
  :
  • def to_param
    このメソッドを使うことで、IDでなく、アカウント名でURLを作るための処理をする。

  • validates :account
    アカウント名が、英小文字(a-z)、数字(0-9)、ハイフン(-)、アンダースコア(_)のみの登録になるようにバリデーションを設定。
    また、アカウント名の長さを3文字以上25文字以下にする。

コントローラの設定

パラメータが:idから:accountに変わるため、コントローラを対応できるようにする。

  • 通常
app/controllers/public/users_controller.rb
class Public::UsersController < ApplicationController
 :
 def show
  @user = User.find(params[:id]
 end
 :
  • 変更後
app/controllers/public/users_controller.rb
class Public::UsersController < ApplicationController
  :
  def show
    # accountをシンボル化する
    account_str = params[:account] # ユーザーからの入力を取得
    sanitized_account_str = account_str.gsub(/[^a-zA-Z0-9_]/, '') # 文字列から不要な文字(コロン以外の特殊文字など)を削除
    account_sym = sanitized_account_str.to_sym # 文字列をシンボルに変換
    @user = User.find_by(account: account_sym) # サニタイズされたシンボルを使用してクエリを実行
  :
  end
  1. account_str = params[:account]
    URLのパラメータとして受け取ったアカウント名を、params から取得するようにする。paramsには、URLに含まれる情報(クエリパラメータやパスの一部など)が格納されている。

  2. sanitized_account_str = account_str.gsub(/[^a-zA-Z0-9_]/, '')
    アカウント名がURLに直接公開されてしまうため、サニタイズしてセキュリティ対策をする。
    ここでは、-_を含む英数字以外の文字を削除することで、悪意のあるユーザーが特殊な記号やコードを入力して、サーバーやデータベースに対する攻撃を防ぐ。

  3. account_sym = sanitized_account_str.to_sym
    サニタイズされたアカウント名をシンボルに変換する。シンボル化とは、名前を効率的に管理するための方法のこと。文字列に似たもので、プログラム内での操作を高速化し、メモリ効率をよくする。

  4. @user = User.find_by(account: account_sym)
    シンボル化されたアカウント名を使って、User モデルからデータベースを検索して該当するユーザー情報を取得する。検索条件として account: account_sym を指定し、シンボル化されたアカウント名に一致するユーザーを取得する。

上記の定義を他アクションでも使えるようにする

以下のように、set_userで呼び出してあげると、同じ記述を書かなくて良いのでまとまった記述になりますよ。

uses_controller
app/controllers/public/users_controller.rb
class Public::UsersController < ApplicationController
  before_action :authenticate_user!, except: [:show, :rising_users]
  before_action :set_user, except: [:edit, :update, :withdraw_input, :withdraw_process, :rising_users]

  def show
    @latest_post = @user.posts.published.order(created_at: :desc).first
    @posts = @user.posts.published.where.not(id: @latest_post&.id).order(created_at: :desc).page(params[:page]).per(10)
    @total_views = @user.posts.published.sum(&:impressions_count)
    @post_ranking = @user.posts.published.order(impressions_count: :desc).limit(5)
  end

  def edit
    @user = current_user
  end

  def update
    @user = current_user

    # if current_user.guest_user?
    #   flash[:error] = "ゲストユーザーは更新できません。"
    #   redirect_to request.referer
    # else
      if @user.update(user_params)
        flash[:success] = "ユーザー情報を更新しました。"
        redirect_to user_path(@user)
      else
        render :edit
      end
    # end
  end

  def withdraw_input
    @user = current_user
  end

  def withdraw_process
    user = current_user

    if user.guest_user?
      flash[:error] = "ゲストユーザーは退会できません。"
      redirect_to request.referer
    else
      user.update(status: :inactive)
      reset_session
      redirect_to root_path, notice: "退会しました。"
    end
  end

  def follower_list
    @followers = @user.followers
    render partial: "follower_list"
  end

  def following_list
    @followings = @user.followings
    render partial: "following_list"
  end

  def liked_posts
    @liked_posts = Post.liked_posts(current_user, params[:page], 12)
  end

  def rising_users
    @rising_users = User.calculate_ranking(20)
  end

  private

  def set_user
    account_str = params[:account]
    sanitized_account_str = account_str.gsub(/[^a-zA-Z0-9_-]/, '')
    account_sym = sanitized_account_str.to_sym
    @user = User.find_by(account: account_sym)
  end

  def user_params
    params.require(:user).permit(:nickname, :account, :email, :introduction, :icon, :channel)
  end
end

まとめ

上記の情報を基に、URLにid以外のカラム(例: nameやaccountなど)を含める方法について学んだ。paramオプションを使用することで、URL内の識別子をカスタムなカラムで置き換えることができる。ただ、このアプローチには注意点もある。

  1. 一意性の確保
    URL内のカスタムカラムの値は一意である必要があるため、ユニーク制約を適切に設定することで、データの整合性を保てる。

  2. セキュリティの確保
    URL内のカラム値は直接公開されるため、サニタイズが重要。悪意のある入力に対する対策をし、セキュリティリスクを最小限に抑えることが必要。

  3. データベースへのアクセス方法に注意
    paramオプションを使用すると、通常のidによるデータベースアクセスができなくなるため、代わりの方法を理解しておく必要がある。カスタムなカラムを使用した検索やバリデーションを実装することが大切。

俺の場合はエラーで苦しんだので、同様のエラーに遭遇した方にとって役立つ情報になることを願っています。


この3日ほど妻が体調を崩してしまい、勉強を中断し息子とずっと一緒に過ごしていました。
ある程度実装には余裕があったので、余裕を持って進めることってとても大事。
妻の体調が戻ってきたのでまた明日から巻き返していこう!

息子がどんどん言葉を覚えてきており、理解力も本当にすごい!1歳6か月ですがコミュニケーションに不自由を全く感じない。
子どもと同じように俺も技術を覚えていきます♪

Discussion