20200814のRailsに関する記事は17件です。

ransack でカラム同士を比較する

まとめ

下記の ransacker とパラメータの組み合わせで (model.column_A <= model.column_B) = TRUE が条件に追加されます。同じテーブルのカラム同士の比較を条件に加えることができます。

モデルに書くransacker
ransacker "compare", args: %i[parent ransacker_args] do |parent, args|
  columns = args.values
  Arel::Nodes::Grouping.new(
    Arel::Nodes::InfixOperation.new(
      '<=',
      parent.table[columns[0].to_sym], parent.table[columns[1].to_sym]
    )
  )
end
パラメータ
'q' => {
    'c' => {
        '0' => {
            'a' => {'0' => {'name' => 'compare',
                            'ransacker_args' => {
                                '0' => 'column_A',
                                '1' => 'column_B'
                            }
            }
            },
            'p' => 'true',
            'v' => {'0' => {'value' => 1}
            }
        }
    }
}

解説

ransacker

ransacker "compare", args: %i[parent ransacker_args] do |parent, args|
  columns = args.values
  Arel::Nodes::Grouping.new(
    Arel::Nodes::InfixOperation.new(
      '<=',
      parent.table[columns[0].to_sym], parent.table[columns[1].to_sym]
    )
  )
end

ransacker で追加の ransack 用のメソッドを用意してあげます。
args: %i[parent ransacker_args]ransacker_args を使えるようにして、ブロック引数にも args を書いて、ブロックの中で使えるようにします。
Arel::Nodes::Grouping() に引数の結果を収めてくれます。これを書かないと SQL の式が成り立たないです。
Arel::Nodes::InfixOperation は第一引数に演算子をとって、第二引数と第三引数を演算する式を返します。
この ransacker は (model.column_A <= model.column_B) という式を作ります。

パラメータ

'q' => {
    'c' => {
        '0' => {
            'a' => {'0' => {'name' => 'compare',
                            'ransacker_args' => {
                                '0' => 'column_A',
                                '1' => 'column_B'
                            }
            }
            },
            'p' => 'true',
            'v' => {'0' => {'value' => 1}
            }
        }
    }
}

Advanced Mode を使います。
比較するだけなので p や v は不要に思えますが、a p v の3つが揃わないと ransack が条件として追加してくれないようなので p や v も書きます。
a の name には素直に追加した ransacker のメソッド名を書きます。
ransacker_args には比較したいカラムを書きます。
p は true、value は 1 にします。でも、value は何でもいいです。

考察

ransacker_args を使わないと、比較するカラムの組み合わせの数だけ ransacker を書かないといけなくなりますが、このように ransacker_args を使えばコンパクトに書けます。
便利ですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

foreign_keyを設定しているカラムのカラム名を変更したときにはまったこと。

困っていたこと

先日、foreign_keyを設定していたカラムのカラム名を変更したときに、エラーにはまってしまったので、備忘も兼ね記事に残しておこうと思います。

なお、実行環境は下記のとおりです。

  • Rails 5.2.4.2
  • PostgreSQL 12.2

前提条件

従業員の活動を記録するデータベースにおいて、従業員(employees) は 活動(activities)と多対多の関係にあります。下記のようなUIで一気に活動に参加した従業員を記録したく、

ui.png

以下のコードを書きました。

作成したコード

Model
activitiesとemployeesで多対多の関係を定義し、中間テーブルをactivity_employeesとしています。

models/employee.rb
class Employee < ApplicationRecord
  has_many :activity_employees, dependent: :destroy
  has_many :activities, through: :activity_employees
end
models/activity.rb
class Activity < ApplicationRecord
  has_many :activity_employees, dependent: :destroy
  has_many :employees, through: :activity_employees
end
class ActivityEmployee < ApplicationRecord
  belongs_to :activity
  belongs_to :employee
end

Controller
一気にデータを受け取れるよう、employee_ids: []をstrong paramaterに入れています。

class ActivitiesController < ApplicationController
  # 略
  def create
    @activity = @facility.activities.build(activity_params)
    if @activity.save
      # 処理
    else
      # 処理
    end
  end

  private
  def activity_params
    params.require(:activity).permit(
      :activity_type,
      # 略,
      employee_ids: [],
      # 略
      )
  end
end

View

= form_with model: [@facility, @activity], class: "p-input-form", local: true do |f|

  = f.collection_check_boxes :employee_ids, @employees, :id, :name, include_hidden: false do |t|
    = t.label(class: "c-check-box__label mr-4") { t.check_box(class: "c-check-box") + t.text }

起こっていた問題

上記のコードでデータを保存しようとすると、下記のエラーが出ていました。
error.png
え。。。。staffsテーブルにデータを保存しようとなんでしていない。。。。なぜ、staffsテーブルの参照整合性を崩すというエラーになるの???

調べたこと

  1. activity_paramsとして送っているデータの中身を調べました。
    → こちらに間違いはなかったので、略

  2. db:schema:dumpをしてschemaファイルを最新化したのち、値を調べました。

db/schema.rb
create_table "activity_employees", force: :cascade do |t|
  t.bigint "activity_id"
  t.bigint "employee_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["activity_id"], name: "index_activity_employees_on_activity_id"
  t.index ["employee_id"], name: "index_activity_employees_on_employee_id"
end

…カラム名は正しい。。。おかしくなさそう。。。。

いったい何が。。。?と思っていたら、annotateというgemによってモデルに自動生成されている記述の中に、見つけました!

models/activity_employee.rb
# Foreign Keys
#
#  fk_rails_...  (activity_id => staffs.id)
#  fk_rails_...  (employee_id => employees.id)

カラム名がactivity_idなのに、参照先がstaffsテーブルだ!!:eyes:

問題の原因

原因を探るためmigrationファイルをたどっていくと。。。。
▼こちらのマイグレーションの後に、

migration1
class CreateActivityEmployees < ActiveRecord::Migration[5.2]
  def change
    create_table :activity_employees do |t|
    t.references :staff, foreign_key: true
    t.references :facility_employee, foreign_key: true
    t.timestamps
  end
end

カラム名間違えたわ!!!…と
▼こんなマイグレーションを作っていました

migration2
class RenameStaffIdColumnToActivityEmployees < ActiveRecord::Migration[5.2]
  def change
    rename_column :activity_employees, :staff_id, :activity_id
  end
end

その結果、カラム名は変更できたのですが、foreign_keyは変更できていなかったようです。

解決策

上記を踏まえて、以下のようなマイグレーションを作成し、実行することで解決しました。

migration3
class ChangeReferencesOfActivityEmployeesTable < ActiveRecord::Migration[5.2]
  def change
    remove_foreign_key :activity_employees, :staffs
    add_foreign_key :activity_employees, :activities
  end
end

postgresにまだ慣れていなかったので、gemのannotateを入れてDBの中身が見えるようにしておかないと、気づけないエラーでした。*_idというカラムでは、カラム名を変更してもforeign_keyを変更したことにはならないのね^^;

マイグレーションファイルを作るときのカラム名には、今後はより一層の注意を払おうと思います:cry:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ハッシュや配列に特定要素が存在するか判別 [ActiveRecord] [has_attribute?]

ハッシュや配列の中に特定の要素が存在するか確認する方法です。
emailが登録されていなかったら登録、されていたらメッセージ、みたいなイメージです。

任意のハッシュを判別

このようなハッシュを例にします。
nil empty に注目です。

@user = {
 "id" => 1,
 "name" => "Taro",
 "age" => 26,
 "from" => nil,
 "sex" => "",
 "created_at" => Sun, 21 Aug 2020 09:55:05,
 "updated_at" => Sun, 21 Aug 2020 09:55:05
}

has_attribute?で判別

特定の要素自体があるかどうか判別します。
has_attribute?でこのような結果が得られます。

@user.has_attribute?(:name) # => true
@user.has_attribute?(:from) # => true
@user.has_attribute?(:sex) # => true
@user.has_attribute?(:userid) # => false

少しややこしいのが、中身が nil empty だとしても結果はtrueというところです。
要素自体があるかないかの判別ということです。

attribute_present?で判別

特定の要素の中身が存在するか判別します。
attribute_present?でこのような結果が得られます。

@user.attribute_present?(:name) # => true
@user.attribute_present?(:from) # => false
@user.attribute_present?(:sex) # => false
@user.attribute_present?(:userid) # => エラー

要素は存在する前提で、中身が空かどうか判別します。

参考

ActiveRecordやハッシュ・配列などで使えるメソッドは他にもたくさんありますよー
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで日本語をurlに使いたい時の書き方備忘録

この記事で書くこと

  • /animals/隣の猫さん /animals/山奥の熊さん のようなurlを実現させるために何をする必要があるのか。

何も対処しないと、以下のようなanimals#showにリクエストすると URI::InvalidURIError が発生してしまう。 :disappointed:

animals_controller.rb
class AnimalsController < ApplicationController
  def show
    @animal = Animal.find_by(name: params[:name])
  end
end

結論

Controller

以下のようにutf-8にエンコードしてあげれば良い.

animals_controller.rb
class AnimalsController < ApplicationController
  before_action :encode_with_utf_8, only: :show

  def show
    @animal = Animal.find_by(name: params[:name])
  end

  private

  def encode_with_utf_8
    request.url.force_encoding("utf-8")
  end
end

テストはどう書くか

Ruby 2.7.0 リファレンスマニュアル | URI.encode_www_form_component を使用する。

spec/requests/animals_spec.rb
RSpec.describe 'Animals', type: :request do
  let(:animal) { create(:animal, name: '隣町の鳥さん') }

  describe 'GET /animals/:name' do
    context 'when animal exist' do
      it 'successes.' do
        url_encoded = URI.encode_www_form_component animal.name
        get "/animals/#{url_encoded}"
        expect(response).to be_success
      end
    end
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

assign_attributes で中間テーブルが保存されてしまう

  • 入力フォームから値を受け取り、バリデーションを行ってから保存したい、みたいな時に一旦インスタンスの変数を更新するために assign_attributes を利用したら思わぬ結果になった
  • rails の中間テーブルにおいては、assign_attributes 実行時にDBインサートが走り、コミットまでされてしまう
    • そのため、modelのバリデーションエラーで保存に失敗した際に、中間テーブルが保存されっぱなしの状態になってしまった
    • 以下のように理解していたが、これは正確ではなさそう
      • update_attributes はインスタンス変数の更新を行い、DBの更新まで行う
      • assign_attributes はインスタンス変数の更新を行うが、DBの更新は行わない
  • つまり、assign_attributes してからバリデーションにかかったり、何らかの理由で保存されたくない時は明示的にロールバックするか、先にインサートしないという工夫が必要になる

ユースケース

  • 以下のようなユースケースを想定
    • UserGroup が多対多で、中間テーブルのモデルとして GroupUsers が存在
    • User を保存する際に、同時に GroupUsers も保存したい
    • view から受け取ったパラメータをセットし保存を行うが、バリデーションエラーの時は保存せずエラーを返す
groups.rb
class Group < ApplicationRecord
  has_many :group_users
  has_many :users, through: :group_users
end
users.rb
class User < ApplicationRecord
  has_many :group_users
  has_many :groups, through: :group_users

  validates :name, presence: true
end
group_users.rb
class GroupUsers < ApplicationRecord
  belongs_to :group
  belongs_to :user
end
  • User に以下のフィールドが生えるが、そのフィールドに値をセットするとインサートが走ってしまう
    • groups
    • group_ids
    • group_users
    • group_user_ids

案1 transaction を張る

  • assign_attributes から save までを transaction で囲んで、保存できない時はロールバックするようにする
users_controller.rb
class UsersController < ApplicationController
  def update
    @user = Users.new

    begin
      ActiveRecord::Base.transaction do
        @user.assign_attributes(user_params)     #ここで一旦中間テーブルに保存されるが、save!でエラーならロールバックされる
        @user.save!
      end
      p "更新成功"
    rescue
      p "更新失敗"
    end
  end
end

案2 after_save を利用する

  • after_save は、データベースへの COMMIT の直前に実行される
    • つまり、model のバリデーションなどを実行後に、そのメソッドを呼び出してくれる
  • 自動で生えるリレーション用のフィールドとは別の attribute 名で view からパラメータを送り、model のバリデーション後にそのパラメータの値を保存したい中間テーブルの attribute にセットして保存することで、バリデーション通過後に中間テーブルが保存されるようにする
    • assign_attributes 実行時にはリレーションを意味するフィールドには何もセットせず、after_save で実行するメソッドの中で、値をセットするようにする
users.rb
class User < ApplicationRecord
  has_many :group_users
  has_many :groups, through: :group_users

  validates :name, presence: true

  after_save :save_group_user

  attr_accessor :g_ids

  # User のバリデーション実行後、コミットする直前に呼ばれる
  def save_group_user
    self.group_ids = @g_ids
  end
end
  • model には after_save を定義し、実行するメソッドの中で中間テーブル用の group_ids をセットするようにしておく
  • また view からきたパラメータをセットしておく用の attribute として attr_accessor :g_ids を定義しておく
  • view からは g_ids のように適当なフィールド名でパラメータを送り、after_save のメソッドで group_ids にセットすることで中間テーブルを保存する
users_controller.rb
class UsersController < ApplicationController
    def update
      @user = Users.new
      @user.assign_attributes(user_params)
      @user.save ? (redirect_to root_path notice: '更新成功') : (render :edit)
    end
end
  • コントローラーでは普通に save を実行して保存すれば良い

ちなみに

  • 新規作成時など、User が id フィールドが nil の状態であれば、中間テーブル用のフィールドに値をセットしても自動でインサートが走ることはないので、あくまでも更新時に気をつければいいっぽい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Invalid route name, already in use: 'new_user_session'のエラー解決方法

環境

Mac OS Catalina
Ruby 2.7.1
Rails 6.0.3.2

現象

ログイン機能用にdeviseを導入しrouteを設定。
その後、Rails routesを実行すると下記のエラーが出た。

Invalid route name, already in use: 'new_user_session'  (ArgumentError)
You may have defined two routes with the same name using the `:as` option, or you may be overriding a route already defined by a resource with the same naming. For the latter, you can restrict the routes created with `resources` as explained here: 
https://guides.rubyonrails.org/routing.html#restricting-the-routes-created

どうやら、'new_user_session'という名前のルートが既に使われているぞ!と言ってる様子。

現象発生時のルートは下記の通り。

routes.rb
Rails.application.routes.draw do
  devise_for :users
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html

  root to: 'toppages#index'

  resources :users, only: [:index, :show, :edit, :update, :destroy] do
    member do
      get :followings
      get :followers
      get :likes
    end
  end

  devise_for :users,
    path: '',
    path_names: {
      sign_up: '',
      sign_in: 'login',
      sign_out: 'logout',
      registration: 'signup'
    },
    controllers: {
      registrations: 'users/registrations',
      sessions: 'users/sessions',
      passwords: 'users/passwords'
    }
  devise_scope :user do
    get 'signup', to: 'users/registrations#new'
    get 'login', to: 'users/sessions#new'
    get 'logout', to: 'users/sessions#destroy'
  end

  resources :posts, only: [:create, :destroy] do
    collection do
      get :search
    end
  end

  resources :relationships, only: [:create, :destroy]
  resources :favorites, only: [:create, :destroy]

end

解決方法

「2箇所定義とかしてないのにな〜」とか考えていましたが、ふと一番上に行が追加されていることに気づきましたw
devise_for :users の行を削除することで解決。

routes.rb
Rails.application.routes.draw do
  root to: 'toppages#index'

  resources :users, only: [:index, :show, :edit, :update, :destroy] do
    member do
      get :followings
      get :followers
      get :likes
    end
  end

  devise_for :users,
    path: '',
    path_names: {
      sign_up: '',
      sign_in: 'login',
      sign_out: 'logout',
      registration: 'signup'
    },
    controllers: {
      registrations: 'users/registrations',
      sessions: 'users/sessions',
      passwords: 'users/passwords'
    }
  devise_scope :user do
    get 'signup', to: 'users/registrations#new'
    get 'login', to: 'users/sessions#new'
    get 'logout', to: 'users/sessions#destroy'
  end

  resources :posts, only: [:create, :destroy] do
    collection do
      get :search
    end
  end

  resources :relationships, only: [:create, :destroy]
  resources :favorites, only: [:create, :destroy]

end

無事、起動することができました!

最後に

already in use系のエラーは、今回と同様に同じルーティングが既に設定されていると考えて良さそうですね。
(そのままですが)
今後同じエラーが出た際は、サクッと解決できそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのテンプレート機能(ERB)をController(Action View)以外で使う

概要

Railsは、テンプレートをアクションで紐付けてくれるので便利ですよね。
Action ViewとERB (Embedded Ruby)ってやつです。
たまに、そのテンプレートを、他の箇所で使いたいことがあります。
調べましたがあまり情報が出てこなかったり、見つけたと思ったら動かなかったりしたので解決した方法を備忘として投稿します。

動作環境

ruby 2.6.5
rails 5.2.3

サンプル

例えばJobとかで利用。

app/views下(任意の場所可)にtemplateを作成。

sample.html.erb
<%= user_name %>さんへ

これはサンプルです。

次にJob。

send_template_job.rb
class SendTemplateJob < ApplicationJob
  queue_as :default

  def perform(*args)
    # テンプレートを使う
    content = ActionView::Base.new('app/views').render(file: 'sample',
                                                       locals: { user_name: current_user.name },
                                                       layout: false)
    # テンプレートを利用して生成した文字列をどこかに送る処理を記載
    # 例えばslackとか

  end
end

こんな感じで書くと、erb内のuser_nameて定義してある変数が文字列として展開され取得できます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

localhostで(google_oauth2) Authentication failure! csrf_detected: OmniAuth::Strategies::OAuth2::CallbackError, csrf_detected | CSRF detectedって言われた時の対応

結論

今回はlocalhostで試していたので、config/devise/session_store.rbを下記のように変更すればOK。

if Rails.env.production?
  Rails.application.config.session_store :cookie_store, key: '_my_session', expire_after: 1.weeks, domain: 'mydomain.com'
else
  Rails.application.config.session_store :cookie_store, key: '_my_session', domain: 'localhost'
end

原因など

Ruby On Railsのdeviseは本当に優秀であっという間にFacebookのログインは出来るようになったのですが、どうもlocalhostで試した時に「Google OAuthの挙動がおかしいような?」となり調べました。

どうもエラーでググると「provider_ignores_state: true を設定して解決!」みたいな記事がちらほら見つかりましたが、普通に考えてそれって必要だからチェックをしてエラーを吐いているわけですよね?

なので、もう少し調べてみると、ドンピシャの記事を発見!

ドンピシャの記事によると、「/config/initializer/session_store.rb」に定義されているドメインとGoogle APIの「origin/redirect_uri」に定義されているドメインが違うと発生するとのこと。

というわけで、冒頭の対応方法で解決できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Railsのflashの使い方をまとめてみた

プログラミングの勉強日記

2020年8月14日 Progate Lv.226

flashとは

 ページ上に一度だけ表示されるメッセージのこと。ユーザに対してページを移動したときに簡単なメッセージを表示させることができる。ユーザの登録完了などのサクセスメッセージや、投稿などの操作が失敗したときのエラーメッセージなどを表示するときに使う。

0814.PNG

flashの書き方

 flashはハッシュのような形で記述する。

コントローラ
flash[:キー名]="表示したいメッセージ"

 キー名は好きな名前を付けることができる。
 フラッシュメッセージを表示したい箇所には以下のように記述する。

ビューファイル
<%= flash[:キー名] %>

scaffoldで自動的に作成した場合

posts_controller.rb
def update
  respond_to do |format|
    if @post.update(post_params)
      format.html { redirect_to ("/posts"), notice: 'Post was successfully updated.' }
      format.json { render :show, status: :ok, location: @post }
    else
      format.html { render :edit }
      format.json { render json: @post.errors, status: :unprocessable_entity }
    end
  end
end
views/posts/index.html.erb
<p id="notice"><%= notice %></p>

自分で作成した場合

users_controller.rb
def login
  @user = User.find_by(email: params[:email])
  if @user && @user.authenticate(params[:password])
    session[:user_id] = @user.id
    redirect_to("/users/index")
  else
    flash[:alert] = "メールアドレスまたはパスワードが間違っています"
    @email = params[:email]
    @password = params[:password]
    render("users/login_new")
  end
end
views/users/new.html.erb
<p id="alert"><%= alert %></p>

noticeとalertオプション

 キーには好きな名前を付けることができるが、noticealertはオプションがある。これらは<%= flash[:キー名] %>と書く必要がなく、以下のようにflashを省略して書くことができる。

ビューファイル
<%= notice %>
<%= alert %>

 noticeは通知に、alertは警告に使うことが望ましい。

redirect_toと合わせて使うときの書き方

 redirect_toでnoticeとalertを使うときはまとめて書くことができる。scaffold時のように以下のように書ける。

コントローラ
redirect_to ("パス"), notice: '表示するメッセージ' 

他のキー名でもまとめて書くことができ、その場合は以下のようになる。

コントローラ
redirect_to ("パス"), flash{キー名: '表示するメッセージ' }

 redirect_toはアクションが実行されてからビューが表示される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのflashの使い方をまとめてみた

プログラミングの勉強日記

2020年8月14日 Progate Lv.226

flashとは

 ページ上に一度だけ表示されるメッセージのこと。ユーザに対してページを移動したときに簡単なメッセージを表示させることができる。ユーザの登録完了などのサクセスメッセージや、投稿などの操作が失敗したときのエラーメッセージなどを表示するときに使う。

0814.PNG

flashの書き方

 flashはハッシュのような形で記述する。

コントローラ
flash[:キー名]="表示したいメッセージ"

 キー名は好きな名前を付けることができる。
 フラッシュメッセージを表示したい箇所には以下のように記述する。

ビューファイル
<%= flash[:キー名] %>

scaffoldで自動的に作成した場合

posts_controller.rb
def update
  respond_to do |format|
    if @post.update(post_params)
      format.html { redirect_to ("/posts"), notice: 'Post was successfully updated.' }
      format.json { render :show, status: :ok, location: @post }
    else
      format.html { render :edit }
      format.json { render json: @post.errors, status: :unprocessable_entity }
    end
  end
end
views/posts/index.html.erb
<p id="notice"><%= notice %></p>

自分で作成した場合

users_controller.rb
def login
  @user = User.find_by(email: params[:email])
  if @user && @user.authenticate(params[:password])
    session[:user_id] = @user.id
    redirect_to("/users/index")
  else
    flash[:alert] = "メールアドレスまたはパスワードが間違っています"
    @email = params[:email]
    @password = params[:password]
    render("users/login_new")
  end
end
views/users/new.html.erb
<p id="alert"><%= alert %></p>

noticeとalertオプション

 キーには好きな名前を付けることができるが、noticealertはオプションがある。これらは<%= flash[:キー名] %>と書く必要がなく、以下のようにflashを省略して書くことができる。

ビューファイル
<%= notice %>
<%= alert %>

 noticeは通知に、alertは警告に使うことが望ましい。

redirect_toと合わせて使うときの書き方

 redirect_toでnoticeとalertを使うときはまとめて書くことができる。scaffold時のように以下のように書ける。

コントローラ
redirect_to ("パス"), notice: '表示するメッセージ' 

他のキー名でもまとめて書くことができ、その場合は以下のようになる。

コントローラ
redirect_to ("パス"), flash{キー名: '表示するメッセージ' }

 redirect_toはアクションが実行されてからビューが表示される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リクエストパラメーターとその型について

最近のAPIの開発に着手し始めて、クライアントから送られてくるリクエストパラメーターの型について意識し始めた。クエリストリングやボディーのフォーマットに応じてどのような型がくるのか整理したく記録に残す。

クエリストリングで送られてくる場合

localhost:3000/shops?pageno=1&pagesize=60

キーに値が指定していればその値の型は文字列となる。

*ちなみにkey=valueの記述がない場合はnullとなり、key=のみ記述した場合はその値は空文字列となる。
*(Railsの場合)paramsで文字列として値がセットされるので、API側で文字列以外の値を使いたいときはその型へキャストする必要がある。

ボディーで送られてくる場合

ボディーのフォーマットがContent-Type: application/x-www-form-urlencodedの場合

WEBブラウザのフォームのリクエストでよく使われる。
キーに指定した値はRFC1866が定める変換フォーマットにしたがって文字列に変換される。

*(Railsの場合)paramsで文字列として値がセットされるので、API側で文字列以外の値を使いたいときはその型へキャストする必要がある。

ボディーのフォーマットがContent-Type: application/jsonの場合

アプリのリクエストやAjaxのリクエストでよく使われる。
jsonは型を持つことができるので、クライアント指定した型を持った状態でリクエストを投げることができる。

Chromeを使った確認方法

「右クリック>検証>Networkタブ」でリクエストとレスポンスの情報を確認することができる。

以下は、Content-Type: application/x-www-form-urlencodedでリクエストを投げた時の例。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ページネーションの実装

「kaminari」というGemをインストール

Gemfile
gem 'kaminari'

ターミナル(実装したいアプリのディレクトリ)でbundle installを実行し、rails sで再起動

kaminariを導入すると、モデルクラスにpageメソッドとperメソッドが定義されます。
2つのメソッドは、よく以下のように使われます。
【例】

変数名 = クラスを利用して取得したレコードのインスタンス.page(params[:page]).per(1ページで表示したい件数)

コントローラーを編集

tweets_controller.rb
def index
  @tweets = Tweet.includes(:user).order("created_atDESC").page(params[:page]).per(5)
end

ビューを編集

以下の1行を任意の場所に記述すれば実装が完了です。

index.html.erb
<%= paginate(@tweets) %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CarrierWaveを使い、複数の画像を一斉に投入する2つの方法/①seedの初期データで投入/②CSVでインポートする

他のtextカラムは簡単に複数投入できるのですが、image(画像)カラムだけはいずれもnilになってしまい苦労したので、ここに複数の画像カラムを一斉に投入する2つの方法を記しておきます。

①複数の画像カラムをseedの初期データで投入する方法

db/migrate/seed.rbに、

seed.rb
Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,
park: 谷戸口公園,
outline: 程久保の知る人ぞ知る,
location: 東京都日野市程久保1丁目20−14,
access: モノレール程久保駅から徒歩5)

のように記載し、rails db:resetしてから、rails db:seedするが、

seed.rb
=> [#<Post:0x00007fa6c66bc8e8
  id: 27,
  image: nil,
  park: "谷戸口公園",
  outline:
   "程久保の知る人ぞ知る。",
  location: "京王線高幡不動駅より百草園駅方面へ徒歩10分",
  access: "日野市高幡1024番地",
  created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  likes_count: nil,
  tag_list: ["#水遊び", "#アスレチック"]>,
 #<Post:0x00007fa6c7b48f28
  id: 28,
  image: nil,
  park: "雨乞公園",
  outline: "明るさと落ち着いた雰囲気の両面を持つ公園です。",
  location: "日野市百草881番地の8",
  access: "三沢台小学校バス停より東へ徒歩3分",
  created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  likes_count: nil,
:...skipping...

このように、画像カラムは直接URLだけをを記載するとnilになってしまう。

解決方法

imageカラムの画像URLの記載を下記のように書き換える。

seed.rb
Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),
park: 谷戸口公園,
outline: 程久保の知る人ぞ知る,
location: 東京都日野市程久保1丁目20−14,
access: モノレール程久保駅から徒歩5)

imageカラムの記載を、

Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,

ではなく、

Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),

に変更する。そして、rails db:resetしてから、rails db:seedする。

/image/1/yatoguti.jpg には、実際に画像が入っているかどうか、予めファイルを確認しておいて下さい。

これで、seed初期データの一斉投入が出来ました!

②複数の画像カラムをCSVでインポートする方法

seed.rbで初期データ投入では成功したので、今度はdb/csv_dara/csv_data.csvlib/tasks/import_csv.rakeで、同じように記載して、CSVインポートでも試してみる。

csv_data.csv
image,park,outline,location,access,tag_list
File.open("#{Rails.root}/public/uploads/post/image/6/ajisai.jpg"),芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場
File.open("#{Rails.root}/public/uploads/post/image/8/hohoemi.jpg"),ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ
File.open("#{Rails.root}/public/uploads/post/image/7/hodokubo.jpg"),ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生

lib/tasks/import_csv.rake
require 'csv'

namespace :import_csv do

  desc "postテーブルのCSVデータをインポートするタスク"
  task posts: :environment do
    path = File.join Rails.root, "db/csv_data/csv_data.csv"
    list = []
    CSV.foreach(path, headers: true) do |row|
      list << {
          image: row ["image"],
          park: row["park"],
          outline: row["outline"],
          location: row["location"],
          access: row["access"],
          tag_list: row["tag_list"]
      }
    end
    puts "インポート処理を開始"
     Post.create!(list)
      puts "インポート完了!!"
  end

end


seedの初期投入ができた記載だから、CSVインポートも出来ると思っていました。すると…

rake aborted!
CSV::MalformedCSVError: Illegal quoting in line 2.
/Users/sekishinya/Desktop/park_app/lib/tasks/import_csv.rake:11:in `block (2 levels) in <main>'
Tasks: TOP => import_csv:posts
(See full trace by running task with --trace)

エラーでインポートできません。
元々imageカラム抜きでインポートしていたので、imageカラムが問題なのは間違いないはずですが。

解決方法

CSVは、seedの時とは異なり、全て文字列扱いとなるので記載が異なる。

そこで、lib/tasks/import_csv.rakeと、db/csv_dara/csv_data.csvを、以下のように書き直す。

import_csv.rake
require 'csv'

namespace :import_csv do

  desc "postテーブルのCSVデータをインポートするタスク"
  task posts: :environment do
    path = File.join Rails.root, "db/csv_data/csv_data.csv"
    list = []
    CSV.foreach(path, headers: true) do |row|
      list << {
          image: File.open("#{Rails.root}/#{row["image"]}"),
          park: row["park"],
          outline: row["outline"],
          location: row["location"],
          access: row["access"],
          tag_list: row["tag_list"]
      }
    end
    puts "インポート処理を開始"
     Post.create!(list)
      puts "インポート完了!!"
  end

end

import_csv.rakeのimageカラムの記載を、

image: row ["image"],

ではなく、

image: File.open("#{Rails.root}/#{row["image"]}"),

に変更する。

そして、csv_data.csvのimageカラムを以下のように変更する。

csv_data.csv
image,park,outline,location,access,tag_list
"public/uploads/post/image/8/fuyou.jpg",芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場
"public/uploads/post/image/8/hohoemi.jpg",ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ
"public/uploads/post/image/8/hodokubo.jpg",ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生

seedの時は、URLのみの記載で失敗しましたが、逆にCSVデータはURLのみにした方が良いですね。

これで見事に、複数の画像の一斉インポートに成功しました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6】CarrierWaveを使い、複数の画像を一斉に投入する2つの方法/①seedの初期データで投入/②CSVでインポートする

他のtextカラムは簡単に複数投入できるのですが、image(画像)カラムだけはいずれもnilになってしまい苦労したので、ここに複数の画像カラムを一斉に投入する2つの方法を記しておきます。

環境

  • Ruby 2.6.5
  • Rails 6.0.3.2

①複数の画像カラムをseedの初期データで投入する方法

db/migrate/seed.rbに、

seed.rb
Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,
park: 谷戸口公園,
outline: 程久保の知る人ぞ知る,
location: 東京都日野市程久保1丁目20−14,
access: モノレール程久保駅から徒歩5)

のように記載し、rails db:resetしてから、rails db:seedするが、

seed.rb
=> [#<Post:0x00007fa6c66bc8e8
  id: 27,
  image: nil,
  park: "谷戸口公園",
  outline:
   "程久保の知る人ぞ知る。",
  location: "京王線高幡不動駅より百草園駅方面へ徒歩10分",
  access: "日野市高幡1024番地",
  created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  likes_count: nil,
  tag_list: ["#水遊び", "#アスレチック"]>,
 #<Post:0x00007fa6c7b48f28
  id: 28,
  image: nil,
  park: "雨乞公園",
  outline: "明るさと落ち着いた雰囲気の両面を持つ公園です。",
  location: "日野市百草881番地の8",
  access: "三沢台小学校バス停より東へ徒歩3分",
  created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  likes_count: nil,
:...skipping...

このように、画像カラムは直接URLだけをを記載するとnilになってしまう。

解決方法

imageカラムの画像URLの記載を下記のように書き換える。

seed.rb
Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),
park: 谷戸口公園,
outline: 程久保の知る人ぞ知る,
location: 東京都日野市程久保1丁目20−14,
access: モノレール程久保駅から徒歩5)

imageカラムの記載を、

Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,

ではなく、

Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),

に変更する。そして、rails db:resetしてから、rails db:seedする。

/image/1/yatoguti.jpg には、実際に画像が入っているかどうか、予めファイルを確認しておいて下さい。

これで、seed初期データの一斉投入が出来ました!

②複数の画像カラムをCSVでインポートする方法

seed.rbで初期データ投入では成功したので、今度はdb/csv_dara/csv_data.csvlib/tasks/import_csv.rakeで、同じように記載して、CSVインポートでも試してみる。

csv_data.csv
image,park,outline,location,access,tag_list
File.open("#{Rails.root}/public/uploads/post/image/6/ajisai.jpg"),芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場
File.open("#{Rails.root}/public/uploads/post/image/8/hohoemi.jpg"),ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ
File.open("#{Rails.root}/public/uploads/post/image/7/hodokubo.jpg"),ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生

lib/tasks/import_csv.rake
require 'csv'

namespace :import_csv do

  desc "postテーブルのCSVデータをインポートするタスク"
  task posts: :environment do
    path = File.join Rails.root, "db/csv_data/csv_data.csv"
    list = []
    CSV.foreach(path, headers: true) do |row|
      list << {
          image: row ["image"],
          park: row["park"],
          outline: row["outline"],
          location: row["location"],
          access: row["access"],
          tag_list: row["tag_list"]
      }
    end
    puts "インポート処理を開始"
     Post.create!(list)
      puts "インポート完了!!"
  end

end


seedの初期投入ができた記載だから、CSVインポートも出来ると思っていました。すると…

rake aborted!
CSV::MalformedCSVError: Illegal quoting in line 2.
/Users/sekishinya/Desktop/park_app/lib/tasks/import_csv.rake:11:in `block (2 levels) in <main>'
Tasks: TOP => import_csv:posts
(See full trace by running task with --trace)

エラーでインポートできません。
元々imageカラム抜きでインポートしていたので、imageカラムが問題なのは間違いないはずですが。

解決方法

CSVは、seedの時とは異なり、全て文字列扱いとなるので記載が異なる。

そこで、lib/tasks/import_csv.rakeと、db/csv_dara/csv_data.csvを、以下のように書き直す。

import_csv.rake
require 'csv'

namespace :import_csv do

  desc "postテーブルのCSVデータをインポートするタスク"
  task posts: :environment do
    path = File.join Rails.root, "db/csv_data/csv_data.csv"
    list = []
    CSV.foreach(path, headers: true) do |row|
      list << {
          image: File.open("#{Rails.root}/#{row["image"]}"),
          park: row["park"],
          outline: row["outline"],
          location: row["location"],
          access: row["access"],
          tag_list: row["tag_list"]
      }
    end
    puts "インポート処理を開始"
     Post.create!(list)
      puts "インポート完了!!"
  end

end

import_csv.rakeのimageカラムの記載を、

image: row ["image"],

ではなく、

image: File.open("#{Rails.root}/#{row["image"]}"),

に変更する。

そして、csv_data.csvのimageカラムを以下のように変更する。

csv_data.csv
image,park,outline,location,access,tag_list
"public/uploads/post/image/8/fuyou.jpg",芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場
"public/uploads/post/image/8/hohoemi.jpg",ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ
"public/uploads/post/image/8/hodokubo.jpg",ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生

seedの時は、URLのみの記載で失敗しましたが、逆にCSVデータはURLのみにした方が良いですね。

これで見事に、CSVでも複数の画像の一斉インポートに成功しました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでAjaxでいいね機能を実装する方法

「Railsでいいね機能を実装する方法」でいいね機能の実装方法をご紹介しましたが、今回はそのいいね機能をAjax(非同期通信)実装する方法をご紹介いたします。
完成系は以下のような感じです。
いいね_Ajax.gif

環境

  • Ruby 2.5.7
  • Rails 5.2.4

前提

  • この記事によって、いいね機能が実装済みであること

index.html.erbを編集

2つのlink_to(method: :delete と method: post)に remote: trueを追記します。
remote: trueを記載することで、Ajaxでの処理を実行することができます。

index.html.erb
<div class="container">
  <h1>記事一覧</h1>
  <table class="table">
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.title %></td>
        <td>
          <% if post.liked_by?(current_user) %>
            <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %>
            <%= link_to like_path(like), method: :delete, remote: true do %>
              <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;">
              <span><%= post.likes.count %></span>
            <% end %>
          <% else %>
            <%= link_to post_likes_path(post), method: :post, remote: true do %>
              <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;">
              <span><%= post.likes.count %></span>
            <% end %>
          <% end %>
        </td>
      </tr>
    <% end %>
  </table>
</div>

いいね機能の部分をテンプレート化する

index.html.erbと同じディレクトリに以下のファイルを作成し、いいね機能の部分をコピーし、貼り付けます。

_like.html.erb
<% if post.liked_by?(current_user) %>
  <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %>
  <%= link_to like_path(like), method: :delete, remote: true do %>
    <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;">
    <span><%= post.likes.count %></span>
  <% end %>
<% else %>
  <%= link_to post_likes_path(post), method: :post, remote: true do %>
    <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;">
    <span><%= post.likes.count %></span>
  <% end %>
<% end %>

部分テンプレート(_like.html.erb)を呼び出すため、いいね機能の部分があったところにrenderを記述します。
また、Ajaxの処理がされる部分を識別できるように id を記述します。

index.html.erb
<div class="container">
  <h1>記事一覧</h1>
  <table class="table">
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.title %></td>
        <td id="like-<%= post.id %>">  <!--idで識別できるようにする-->

       <%= render "like", post: post %>  <!--renderで部分テンプレートを呼び出す-->

        </td>
      </tr>
    <% end %>
  </table>
</div>

controllerの編集

各アクションの最後にredirect_backをしていましたが、redirect_backをすると再読み込みをしていまい、Ajaxが機能しません。
そのため、redirect_backを削除します。

likes_controller.rb
  def create
    like = Like.new(user_id: current_user.id, post_id: params[:post_id])
    @post = like.post
    like.save
  end

  def destroy
    like = Like.find(params[:id])
    @post = like.post
    like.destroy
  end

jsファイルの作成

remote: trueによってjs形式のリクエストを送信しているため、実行するアクション名(createやdestroy)のjsファイルを最終的に探しに行きます。
そのため、app/views/配下にlikesフォルダを作成し、そのフォルダの中に create.js.erb と destory.js.erb を作成します。

create.js.erb
$("#like-<%= @post.id %>").html("<%= j(render 'posts/like', post: @post) %>");
destory.js.erb
$("#like-<%= @post.id %>").html("<%= j(render 'posts/like', post: @post) %>");

idで識別し部分的にhtmlを書き換えます。
これで完成です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでいいね機能を実装する方法

Railsでいいね機能を実装する方法をご紹介いたします。
完成系は以下のような感じです。
いいね1.gif

環境

  • Ruby 2.5.7
  • Rails 5.2.4

前提

  • deviseによるログイン機能が実装できていること
  • ユーザーのテーブルは"users"、記事のテーブルは"posts"、いいねの中間テーブルは"likes"とする。
  • postsのカラムはtitleを追加する

Likeモデルを追加

$ rails g model like user_id:integer post_id:integer

$ rails db:migrate

アソシエーションを設定

それぞれ以下を記述し、アソシエーションを設定します。

like.rb
  belongs_to :user
  belongs_to :post
user.rb
  has_many :likes, dependent: :destroy
post.rb
  has_many :likes, dependent: :destroy

ルーティングを設定する

routes.rb
  resources :posts, shallow: true do
    resources :likes, only: [:create, :destroy]
  end

liked_by?メソッドを作成

post.rb
  def liked_by?(user)
    likes.where(user_id: user.id).exists?
  end

controllerの記述

likes_controller.rb
  def create
    like = Like.new(user_id: current_user.id, post_id: params[:post_id])
    @post = like.post
    like.save
    redirect_back(fallback_location: posts_path)
  end

  def destroy
    like = Like.find(params[:id])
    @post = like.post
    like.destroy
    redirect_back(fallback_location: posts_path)
  end

Bootstrapを導入

Gemfile
gem 'bootstrap-sass', '~> 3.3.6'
gem 'jquery-rails'
$ bundle install

application.cssのファイル名をapplication.scssに変更する

$ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss

Bootstrapをscssに読み込ませる

application.scssの一番下に以下を記述

application.scss
@import "bootstrap-sprockets";
@import "bootstrap";

application.jsを編集する

application.jsの以下の部分を

application.js
//= require rails-ujs
//= require turbolinks
//= require_tree .

から

application.js
//= require rails-ujs
//= require jquery
//= require bootstrap-sprockets
//= require_tree .

に書き換えます。

index.html.erbを編集

index.html.erb
<div class="container">
  <h1>記事一覧</h1>
  <table class="table">
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.title %></td>
        <td>
          <% if post.liked_by?(current_user) %>
            <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %>
            <%= link_to like_path(like), method: :delete do %>
              <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;">
              <span><%= post.likes.count %></span>
            <% end %>
          <% else %>
            <%= link_to post_likes_path(post), method: :post do %>
              <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;">
              <span><%= post.likes.count %></span>
            <% end %>
          <% end %>
        </td>
      </tr>
    <% end %>
  </table>
</div>

これで完成です。

参考記事

https://qiita.com/soehina/items/a68ab66da3ea1d260301

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者がDeviseのコードを見て仕組みをふんわり理解する【registrations new】

はじめに

Railsチュートリアルがやっと1周終わった者です。
gemのDeviseを入れてみたら、Railsチュートリアルでの5~6章分の実装が10分ほどで終わり、驚愕しています。

なんでできたのか分からない...
そして、テストも書きたいけどどうやって書いたらいいのか分からない。
そこで「公式やソースコードを読みましょう」ということで、読んでみたけどこれまたさっぱり分からない...ながらも少しずつ理解したいので、読み込んでいきます。

読みながら自分が調べたことをまとめていきます。

・初心者だけどDeviseの仕組みを知りたい

そんな方の役に立てたら幸いです。

DeviseのGitHub
https://github.com/heartcombo/devise

Deviseの概要

GitHubのREADMEの一番最初に、概要の説明があります。

  • Deviseは、Wardenに基づくRails向けの柔軟な認証ソリューションです。
  • ラックベースです。
  • Railsエンジンに基づく完全なMVCソリューションです。
  • 複数のモデルに同時にサインインさせることができます。
  • モジュール性の概念に基づいています。本当に必要なものだけを使用してください。

ここで出てくるWardenというのは認証のためのgemで、devise内でこれを引っ張ってきているようです。

また、10個のモジュールで構成されており、必要なものはコメントアウトを外したりしながら使ってね、ということのようです。
モジュールというかもう機能ですね。
Qiita内で表にしてくださっている方がいらっしゃったので、下記にて引用します。

機能 概要
database_authenticatable サインイン時にユーザーの正当性を検証するためにパスワードをハッシュ化してDBに登録します。認証方法としてはPOSTリクエストかHTTP Basic認証が使えます。
registerable 登録処理を通してユーザーをサインアップします。また、ユーザーに自身のアカウントを編集したり削除できるようにします。
recoverable パスワードをリセットし、それを通知します。
rememberable 保存されたcookieから、ユーザーを記憶するためのトークンを生成・削除します。
trackable サインイン回数や、サインイン時間、IPアドレスを記録します。
validatable Emailやパスワードのバリデーションを提供します。独自に定義したバリデーションを追加することもできます。
confirmable メールに記載されているURLをクリックして本登録を完了する、といったよくある登録方式を提供します。また、サインイン中にアカウントが認証済みかどうかを検証します。
lockable 一定回数サインインを失敗するとアカウントをロックします。ロック解除にはメールによる解除か、一定時間経つと解除するといった方法があります。
timeoutable 一定時間活動していないアカウントのセッションを破棄します。
omniauthable intridea/omniauthをサポートします。TwitterやFacebookなどの認証を追加したい場合はこれを使用します。

引用元:[Rails] deviseの使い方(rails6版)
https://qiita.com/cigalecigales/items/16ce0a9a7e79b9c3974e

また、Deviseは、コントローラーとビュー内で使用するヘルパーを作成します。
よく使用するコマンドを予め設定したものです。

デバイスモデルが 'User'であると想定してヘルパー名は例示していますが、
デバイスモデルがユーザー以外の場合は、「_ user」を「_yourmodel(任意のモデル名)」に置き換えると、同じロジックが適用されます。

こちらもQiita内で表にしてくださっている方がいらっしゃったので引用します。

メソッド 用途   
before_action :authenticate_user! コントローラーに設定して、ログイン済ユーザーのみにアクセスを許可する
user_signed_in? ユーザーがサインイン済かどうかを判定する
current_user サインインしているユーザーを取得する
user_session ユーザーのセッション情報にアクセスする

引用元:Rails deviseで使えるようになるヘルパーメソッド一覧
https://qiita.com/tobita0000/items/866de191635e6d74e392

registrations -- サインアップ・アカウント編集・削除

最も基本的なアカウントのCRUDはこのregistrationが担っているので、ここが分からないと応用の機能部分のコードリーディングは難しそうです。

コード全体を上から調べた範囲で記入してきますが、ちまちま区切っていくので、分かりくいかもしれません。
横にソースコードを置きながら見てもらえれば、ちょっとは分かりやすいかも...

frozen_string_literal

devise/app/controllers/devise/registrations_controller.rb
# frozen_string_literal: true

コメントアウトされていますが、Rubyのバージョンアップに備えた1文のようです。
参考:frozen_string_literalが入って気づいた、メソッド設計の原則
https://qiita.com/jkr_2255/items/300b5db8c1f04e1e2815

prepend_before_action

devise/app/controllers/devise/registrations_controller.rb
class Devise::RegistrationsController < DeviseController
  prepend_before_action :require_no_authentication, only: [:new, :create, :cancel]   
  prepend_before_action :authenticate_scope!, only: [:edit, :update, :destroy]
  prepend_before_action :set_minimum_password_length, only: [:new, :edit]
  • DeviseControllerを継承しています。 ソースコードのファイルをみると、devise_controller.rbはこのモジュールだけでなく、すべてのモジュールへ引き継いでいます。
  • prepend_before_actionbefore_actionより前に実行するメソッドです。アクセスできるアクションがユーザーのログイン状態で制限されるようにしています。

newアクション

devise/app/controllers/devise/registrations_controller.rb
 # GET /resource/sign_up
  def new
    build_resource
    yield resource if block_given?
    respond_with resource
  end
  • ログインするための最初の部分です。
  • resourceはすでにdevise_controllerで定義されています。
devise/app/controllers/devise_controller.rb
  def resource
    instance_variable_get(:"@#{resource_name}")
  end

  # Proxy to devise map name
  def resource_name
    devise_mapping.name 
  end

  alias :scope_name :resource_name
  • instance_variable_getメソッドはインスタンス変数の値を取得して返します。@user =からの定義と同じもののようです。
  • resource内に#{resource_name}という変数がありますが、その下部分で定義されています。
  • devise_mapping.nameこちらもすでにdevise_controllerで定義されてますが、引用記事をみると、別部分にヒントがありそうです。
devise/app/controllers/devise_controller.rb
  def devise_mapping 
    @devise_mapping ||= request.env["devise.mapping"]
  end

nameに注目してコードを繋げてみると、認証モデルがUserである場合は@singular = :users.to_s.tr('/', '').singularize.to_symとみれます。singularizeは複数形を単数形に変換するメソッドで、最終的に@singular = :userとなりsingularのエイリアスがnameとなっているのでmapping.nameで:userが取得できます。
するとdefine
methodsの引数に:userが渡されauthenticate_user!が出来上がるという流れになっています。
引用元:DeviseのコードリーティングでRailsを学ぶ
https://qiita.com/irisAsh/items/513b8b58f54421b9a1a0

端的に言うと、mapping.name:userが取得できるのでそれをresource_nameにしているということでしょうか。

  • registrations_controller下部にbuild_resourceがあります。セッションを新しく作るという意味のようです。
devise/app/controllers/devise/registrations_controller.rb
 def build_resource(hash = {}) #build_resource(hash = {})の定義
    self.resource = resource_class.new_with_session(hash, session)
  end
  • 元のregistrations_controller上部へ戻りましょう。buildはほぼnewと近い役割をしています。build_resourceという1文は、データベースから取り出したユーザーのインスタンス変数とセッションを作るということになります。
  • block_given?はメソッドを実行する時にブロックが渡されていればtrueを返し、渡されていない時はfalseを返します。
    • ブロックとは

do ... end または { ... } で囲まれたコードの断片 (ブロックと呼ばれる)を後ろに付けてメソッドを呼び出すと、そのメソッドの内部からブロックを評価できます。ブロック付きメソッドを自分で定義するには yield 式を使います。
引用元:Ruby 2.7.0 リファレンスマニュアル
https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#block

devise/app/controllers/devise_controller.rb
  def respond_with_navigational(*args, &block)
    respond_with(*args) do |format|
      format.any(*navigational_formats, &block)
    end
  end
devise/app/controllers/devise_controller.rb
  def navigational_formats
    @navigational_formats ||= Devise.navigational_formats.select { |format|Mime::EXTENSION_LOOKUP[format.to_s] }
  end

ピンときていないので勉強して加筆・修正したいと思います。

ここまで複雑なんだなGem...たった数行のコードにいろいろなものが凝縮されていてとても勉強になりました。
newアクションしか書けなかったけど、最終的にはregistrations_controllerの各アクションのだけでも読んだ記録を残したい...!

お付き合いいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む