20201215のRailsに関する記事は30件です。

RailsのFormオブジェクトをついに実装した

前書き

  • モノリシックなRailsアプリで、ActiveRecordに紐づかないモデルに対するフォームは、Form Objectを使えば綺麗に書けるらしい。
  • というのは前から知っていたが、実際自分の手で実装したことはなかった。
  • この度、自分の手で実装する機会が業務で得られたので、記録を残す。
    • 公表して問題ない程度に実装をぼやかしているので、その過程でタイポなどがあるかもしれないです。ご了承ください。

作りたいもの

  • アプリへのpush通知を送ってくれるページを管理者用のページとして追加したい。

Formオブジェクトを使わない実装

  • まずはFormオブジェクトを使わずに以下のように実装した。
config.rb
namespace :admin do
  resources :push_notifications, only: [:new, :create]
end
app/controllers/admin/push_notifications_controller.rb
class Admin::PushNotificationsController < Admin::BaseController
  def new
    @demo_form = Admin::PushNotificationDemoForm.new
  end

  def create
    @demo_form = Admin::PushNotificationDemoForm.new(
      # user_idが空文字あるいはintに変換不可能な文字列の場合エラーが吐かれるが、formでf.number_fieldを指定しているので良しとする
      user_id: Integer(demo_form_params['user_id']),
      title: demo_form_params['title'],
      body: demo_form_params['body'],
      url: demo_form_params['url'],
    )

    if @demo_form.save
      flash[:notice] = "ユーザー#{@demo_form.user_id}#{Rails.env}アプリへのプッシュ通知が送信されました。"
      redirect_to new_admin_push_notification_path
    else
      flash.now[:alert] = @demo_form.errors.full_messages.join(" / ")
      return render :new
    end
  end

  private
  def demo_form_params
    params.require(:admin_push_notification_demo_form).permit(:user_id, :title, :body, :url)
  end
end
app/views/admin/new.html.haml
%h1= "push通知のテスト送信"

= form_with(url: admin_push_notifications_path, local: true) do |f|
  .div
    = f.label :user_id, 'User ID (必須)'
    = f.number_field :user_id, autofocus: true, required: true
  .div
    = f.label :title, 'Title: (必須)'
    = f.text_area :title, autofocus: true, required: true
  .div
    = f.label :body, 'Body'
    = f.text_area :body, autofocus: true
  .div
    = f.label :url, 'URL'
    = f.text_field :url, autofocus: true
  .div
    = f.submit("送信")

  • ご覧の通り、やりたい処理``以前に、Controller内で行われているバリデーションの連発が悪い意味で印象的です。

これをFormオブジェクトを使って実装する

config.rb
namespace :admin do
  resources :push_notifications, only: [:new, :create]
end
app/controllers/admin/push_notifications_controller.rb
class Admin::PushNotificationsController < Admin::BaseController
  def new
    @demo_form = Admin::PushNotificationDemoForm.new
    render 'new'
  end

  def create
    @demo_form = Admin::PushNotificationDemoForm.new(demo_form_params)

    if @demo_form.save
      flash[:notice] = "ユーザー#{@demo_form.user_id}のへプッシュ通知が送信されました。"
      redirect_to new_admin_push_notification_path
    else
      flash.now[:alert] = @demo_form.errors.full_messages.join(" / ")
      return render :new
    end
  end

  private
  def demo_form_params
    params.require(:admin_push_notification_demo_form).permit(:user_id, :title, :body, :url)
  end
end
app/forms/push_notification_demo_form.rb
class Admin::PushNotificationDemoForm
  include ActiveModel::Model

  validates :user_id, presence: true
  validates :title, presence: true
  validate :is_valid_user_id_and_has_individual_job

  def save
    return false if invalid?

    VisitNotificationService.new.notify(
      user_job_id: @job.id,
      user_id: @user_id,
      push_notification_content: VisitNotificationService.build_push_notification(
        title: @title,
        body: @body,
        url: @url,
      )
    )
    return true
  end

  private

  def is_valid_user_id_and_has_individual_job
    user = User.find_by(id: @user_id)
    unless user
      errors.add(:user_id, 'が無効です。')
      return false
    end

    @job = user.individual_job
    unless @job
      errors.add(:base, "指定されたUser(#{@user_id})に紐づくIndividualJobが存在しません。")
      return false
    end

    true
  end
end
app/views/admin/new.html.haml
%h1= "push通知のテスト送信"

= form_for(@demo_form, url: admin_push_notifications_path, local: true) do |f|
  .div
    = f.label :user_id, 'User ID (必須)'
    = f.number_field :user_id, autofocus: true, required: true
  .div
    = f.label :title, 'Title: (必須)'
    = f.text_area :title, autofocus: true, required: true
  .div
    = f.label :body, 'Body'
    = f.text_area :body, autofocus: true
  .div
    = f.label :url, 'URL'
    = f.text_field :url, autofocus: true
  .div
    = f.submit("送信")

  • ご覧の通り、バリデーション(要はビジネスロジック)がformオブジェクトの中に押し込めれたために非常に綺麗に実装できます。

  • 自分の今回の実装では、form_withで指定されるモデルとurlが異なっています。

  • また、エラーメッセージを(active record同様に)formオブジェクトのインスタンス変数であるerrors内に収納できるのも便利です。

参考

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

[Rails] Gemが正しくインストールされているか確認する方法

前提

# bundle install実行後

Bundle complete! 20 Gemfile dependencies, 83 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

このように表示された場合、
「Gemfileに記述してあるGemは全て正しくインストールされている」
状態です。

一文字でもミスがあれば、エラーメッセージを返してくれます。

確認方法①

"bundle info Gem名"コマンド

・Gem名のGem情報が表示される

確認方法②

"Gemfile.look"を確認

・"Gemfile.look"は「インストールされているGemの名前とVersionが記載されているファイル」

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

【Rails】ハッシュタグ機能を実装

概要

ハッシュタグ機能を実装する方法をまとめます。

参照

https://glodia.jp/blog/3936/
https://qiita.com/Naoki1126/items/4ea584a4c149d3ad5472

これら2つの記事の情報を組み合わせて実装しました。
ありがとうございます。

完成イメージ

今回は、写真投稿アプリを題材に、写真のキャプションにハッシュタグを導入する方法を説明します。
661b81756d7347895004cbab39aaf1732.png

写真詳細画面のキャプションにリンク付きハッシュタグが記載され、クリックするとそのハッシュタグのページが表示される。

Image from Gyazo

開発環境

  • macOS Catalina 10.15.7
  • ruby 2.6.5
  • Rails 6.0.3.4

実装の流れ

  • 各種モデルとマイグレーションファイルを準備
  • ハッシュタグ保存・更新アクションをモデルに追加
  • ルーティングを設定
  • ヘルパーメソッドを作成
  • コントローラーにhashtagアクションを作成
  • ビューの編集

今回のコード

コードは、必要な部分以外は省略して記載します。

1. 各種モデルとマイグレーションファイルを準備

hashtagモデルを作成

%rails g model hashtag

中間テーブルを作成

%rails g model photo_hashtag_relation

マイグレーションファイルを編集

テーブルのカラムを設定します。

create_hashtags.rb
class CreateHashtags < ActiveRecord::Migration[6.0]
  def change
    create_table :hashtags do |t|
      t.string :hashname
      t.timestamps
    end
    add_index :hashtags, :hashname, unique: true
  end
end
create_photo_hashtag_relations.rb
class CreatePhotoHashtagRelations < ActiveRecord::Migration[6.0]
  def change
    create_table :photo_hashtag_relations do |t|
      t.references :photo, index: true, foreign_key: true
      t.references :hashtag, index: true, foreign_key: true
      t.timestamps
    end
  end
end

モデルを編集

バリデーションとアソシエーションを設定します。

hashtag.rb
class Hashtag < ApplicationRecord
  validates :hashname, presence: true, length: { maximum:99}
  has_many :photo_hashtag_relations
  has_many :photos, through: :photo_hashtag_relations
end
photo_hashtag_relation.rb
class PhotoHashtagRelation < ApplicationRecord
  belongs_to :photo
  belongs_to :hashtag
  with_options presence: true do
    validates :photo_id
    validates :hashtag_id
  end
end

マイグレート

%rails db:migrate

2. ハッシュタグ保存・更新アクションをモデルに追加

photoモデルに以下のコードを追加します。
これで、写真投稿(create)、編集(update)時にハッシュタグがhashtagsテーブルに保存されます。

photo.rb
# 省略

  has_many :photo_hashtag_relations
  has_many :hashtags, through: :photo_hashtag_relations
.
.
# 省略
.
.
  #DBへのコミット直前に実施する
  after_create do
    photo = Photo.find_by(id: self.id)
    hashtags  = self.caption.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
    photo.hashtags = []
    hashtags.uniq.map do |hashtag|
      #ハッシュタグは先頭の'#'を外した上で保存
      tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
      photo.hashtags << tag
    end
  end

  before_update do 
    photo = Photo.find_by(id: self.id)
    photo.hashtags.clear
    hashtags = self.caption.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
    hashtags.uniq.map do |hashtag|
      tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
      photo.hashtags << tag
    end
  end

end

ルーティングを設定

photosコントローラーにhashtagアクションを定義し、getで各ハッシュタグのページを表示させます。
URLは末尾にハッシュタグ名が来るようにしました。
:nameに入る値はこの後ヘルパーメソッドで設定します。

例えば、#カメというハッシュタグだとURLは
.../photo/hashtag/カメ
となります。

route.rb
get '/photo/hashtag/:name', to: "photos#hashtag"

ヘルパーメソッドを作成

photos_helper.rbに以下のコードを記載します。
photos_helper.rbファイルがない場合は自分で作成します。

helperについては、https://www.sejuku.net/blog/28563 などを参照

このヘルパーメソッド使用により、リンク付きのハッシュタグが入ったキャプションが作成されます。
コード内では、ハッシュタグ名が末尾に入ったURLが作成され、ハッシュタグクリック時のリンク先として設定されています。
ハッシュタグ名の前のURLには、先程route.rbに書いたURLを書き込みます。

photos_helper.rb
module PhotosHelper
  def render_with_hashtags(caption)
    caption.gsub(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/){|word| link_to word, "/photo/hashtag/#{word.delete("#")}"}.html_safe
  end 
end

コントローラーにhashtagアクションを作成

ハッシュタグに紐付いた写真を@photosに代入し、hashtagビューで使用し表示させます。

photos_controller.rb
  def hashtag
    @user = current_user
    @tag = Hashtag.find_by(hashname: params[:name])
    @photos = @tag.photos
  end

ビューの編集

先程作成したヘルパーメソッドrender_with_hashtagsを記載することにより、リンク付きハッシュタグが記載されたキャプションを写真詳細画面に表示させます。

show.html.erb
# 省略

<%= render_with_hashtags(@photo.caption) %>

ハッシュタグ画面では、hashtagアクションで設定した@photosを取り込み、ハッシュタグに紐づく写真を1枚ずつ表示させます。

hashtag.html.erb
<h2>ハッシュタグ #<%= @tag.hashname %></h2>
  <ul>
    <% @photos.each do |photo| %>
      <li>
        <%= link_to photo_path(photo.id) do %>
          <%= image_tag photo.image.variant(gravity: :center, resize:"640x640^", crop:"640x640+0+0"), if photo.image.attached? %>
        <% end %>
      </li>
    <% end %>
  </ul>

おわりに

以上が、今回行ったハッシュタグ機能実装の方法です。
意味が理解できていないコードが多々ありますが、追々理解していければと思います。

初学者なので間違いがありましたら、ご指摘いただきたいですm(_ _)m

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

ポートフォリオ開発まとめ

これまで、2週間かけて取り組んだポートフォリオの要点をまとめたので、アウトプットしたいと思います。
間違いがあれば、ご指摘いただければと思います。

①要件定義
https://qiita.com/by-miwa30/items/93c4fc92b85e034b75cd

②README
https://qiita.com/by-miwa30/items/d7e0395c8c4a13dcff6f

③アプリの起動
https://qiita.com/by-miwa30/items/fbdaee77efaea6b6df11

④ユーザー管理機能を実装
https://qiita.com/by-miwa30/items/4d868f447bfe7ce040f1

⑤ストロングパラメーター
https://qiita.com/by-miwa30/items/fc761332dd0de9ef5bc2

⑥新規登録
https://qiita.com/by-miwa30/items/65e175ee8c86e05c83ca

⑦アプリ投稿
https://qiita.com/by-miwa30/items/dbdcccd494b33c1e713d

⑧アプリの詳細ページ
https://qiita.com/by-miwa30/items/d01dd9a6ca68063affcd

⑨コメント機能
https://qiita.com/by-miwa30/items/a97cebfb6f4617b7a275

⑩ユーザー詳細ページ
https://qiita.com/by-miwa30/items/e78811610bfdeac8d735

⑪AWSのアカウント作成
https://qiita.com/by-miwa30/items/ca356f6c021eb14fcfef

⑫S3で保存先を用意する
https://qiita.com/by-miwa30/items/dc068e273d8e1d0c1764

⑬EC2の初期設定
https://qiita.com/by-miwa30/items/09c972145fea0e919046

⑭本番環境でデータベースを作成
https://qiita.com/by-miwa30/items/b1a73dbbde0f1bbcfa74

⑮EC2のRailsを起動しよう(手動デプロイ)
https://qiita.com/by-miwa30/items/dd455dcb73d03e19012e

⑯環境変数の設定
https://qiita.com/by-miwa30/items/924a653faef7a03eba36

⑰本番環境Railsの起動エラー(手動)
https://qiita.com/by-miwa30/items/d7a37a9deccbe76c047e

⑱Webサーバーの設定
https://qiita.com/by-miwa30/items/31251da3c26a8129aa60

⑲デプロイを自動化
https://qiita.com/by-miwa30/items/d4b7b1fa3601bf1185ba

⑳IPアドレスにアクセスエラー(自動デプロイ
https://qiita.com/by-miwa30/items/fbc620dfe6e71d4a417c

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

ポートフォリオまとめ

これまで、2週間かけて取り組んだポートフォリオの要点をまとめたので、アウトプットしたいと思います。
間違いがあれば、ご指摘いただければと思います。

①要件定義
https://qiita.com/by-miwa30/items/93c4fc92b85e034b75cd

②README
https://qiita.com/by-miwa30/items/d7e0395c8c4a13dcff6f

③アプリの起動
https://qiita.com/by-miwa30/items/fbdaee77efaea6b6df11

④ユーザー管理機能を実装
https://qiita.com/by-miwa30/items/4d868f447bfe7ce040f1

⑤ストロングパラメーター
https://qiita.com/by-miwa30/items/fc761332dd0de9ef5bc2

⑥新規登録
https://qiita.com/by-miwa30/items/65e175ee8c86e05c83ca

⑦アプリ投稿
https://qiita.com/by-miwa30/items/dbdcccd494b33c1e713d

⑧アプリの詳細ページ
https://qiita.com/by-miwa30/items/d01dd9a6ca68063affcd

⑨コメント機能
https://qiita.com/by-miwa30/items/a97cebfb6f4617b7a275

⑩ユーザー詳細ページ
https://qiita.com/by-miwa30/items/e78811610bfdeac8d735

⑪AWSのアカウント作成
https://qiita.com/by-miwa30/items/ca356f6c021eb14fcfef

⑫S3で保存先を用意する
https://qiita.com/by-miwa30/items/dc068e273d8e1d0c1764

⑬EC2の初期設定
https://qiita.com/by-miwa30/items/09c972145fea0e919046

⑭本番環境でデータベースを作成
https://qiita.com/by-miwa30/items/b1a73dbbde0f1bbcfa74

⑮EC2のRailsを起動しよう(手動デプロイ)
https://qiita.com/by-miwa30/items/dd455dcb73d03e19012e

⑯環境変数の設定
https://qiita.com/by-miwa30/items/924a653faef7a03eba36

⑰本番環境Railsの起動エラー(手動)
https://qiita.com/by-miwa30/items/d7a37a9deccbe76c047e

⑱Webサーバーの設定
https://qiita.com/by-miwa30/items/31251da3c26a8129aa60

⑲デプロイを自動化
https://qiita.com/by-miwa30/items/d4b7b1fa3601bf1185ba

⑳IPアドレスにアクセスエラー(自動デプロイ
https://qiita.com/by-miwa30/items/fbc620dfe6e71d4a417c

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

(解決方法)Mysql2::Error: Field 'カラム名' doesn't have a default value

ユーザ管理機能実装にあたって、deviseを導入し、いざ新規登録をしようとすると、タイトルのエラーが発生した。
その解決方法と原因について記す。

解決方法

deviseのストロングパラメーターを設定する。
以下にコード例を示す。

application_controller
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  private
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname, :first_name, (複数のカラム名)])
  end
end

エラーが発生した原因

エラー文の内容は、『mysqlに指定したカラム名はデフォルトの値で設定されていません』というもの。
deviseにデフォルトで設定されているカラムは、emailとpasswordだけである。そのため、それ以外のカラムをdeviseを通じて保存しようとしても、当然ユーザ情報を保存できない。

なぜ解決されるのか

上記のコードにより、emailとpassword以外のカラムをユーザ情報として、deviseを通じて保存できるようにしている。
したがって、デフォルトで設定されていないカラムも保存できるようになり、エラーは解決される。

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

Railsのエラーメッセージの日本語化がうまくいかない

現象

 エラーメッセージを日本語化するため、カラム名の日本語訳を記述したファイルがうまく機能しませんでした。

解決方法

 記述のスペーシングを整えたらうまくいきました。
 誤字脱字以外にもスペーシングで機能しないことがあることを新たに学ぶことができました。

config/locales/models/ja.yml
  # 階層構造をスペーシングで表現しないとうまく機能しない
  ja:
    activerecord:
      models:
        event: イベント
      attributes:
        event:
          name: イベント名
          place: 開催場所
          content: イベント内容

エラーメッセージを日本語化する手順

「rails-i18n」というgemをインストール

Gemfile
gem 'rails-i18n'


bundle install

config/application.rbの記述を編集

config/application.rb
config.i18n.default_locale = :ja

これで日本語化はできました。
しかし、カラム名はまだ英語表記のため、
(例)emailを入力してください
のようなエラーメッセージになってしまいます。

カラム名を日本語に訳す記述をする

 config/locales/models/に「ja.yml」というファイルを作成します。
 作成したファイル内にカラム名に対応する日本語を記述します。

config/locales/models/ja.yml
  # 階層構造をスペーシングで表現しないとうまく機能しない
  ja:
    activerecord:
      models:
        event: イベント
      attributes:
        event:
          name: イベント名
          place: 開催場所
          content: イベント内容

参考文献

Railsのバリデーションエラーのメッセージの日本語化

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

MySql 接続エラー

rails s後に、頻繁にmysqlのエラーが出るので考えてみたい。

Image from Gyazo

Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

エラー文の意味としては、ソケットと言われるクライアントとサーバーを接続する通信機構が存在していないので、'/tmp/mysql.sock'を通じてMySQLサーバーに接続できないとのこと。

対処法

$ sudo mysql.server restart
Starting MySQL
.. SUCCESS!

基本、sudo mysql.server restartで解決できる

原因としては、Mysqlサーバーが起動していないと考えられる。

参考にしたサイト

http://www.hi-ho.ne.jp/tsumiki/book_sup2.html

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

Rails それぞれの命名規則をまとめみた

ControllerやModelをコマンドで作成する際に
単数形だっけ、複数形だっけ?となるのでまとめました

概要

Railsでは、
基本理念の1つ CoC (Convention over Configuration, 設定より規約) が定義されています

CoCとは、初心者が意識しなくてもよく

熟考をしなくてよくなるだけでなく、より深い抽象化を育ててくれる

と、記事に書いてあります
引用記事

つまり、開発者の決定すべきことを減少させ、単純にするということ

Controller編

複数のactionを持つため、複数

種類 説明
コントローラー名 users 複数形
ファイル名 blogs_controller.rb 複数形
コントローラークラス名 UsersController キャメルケース
コマンド rails g controller users 複数形
app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
  end
end

Model編

Modelは設計書のようなもの
設計書は1つなので単数

テーブルには複数の情報が保持されているので複数
マイグレーションファイルや、クラスはテーブルを作るものなので
それに関連して、同じく複数

種類 説明
モデル名 user 単数形
ファイル名 blog.rb 単数形
モデルクラス名 User 単数系、頭文字大文字
テーブル名名 users 複数形
マイグレーションファイル名 ☓☓☓_create_users.rb 複数形
コマンド rails g model User 単数形
app/models/user.rb
class User < ApplicationRecord
  has_many :articles
end
db/migrate/2020☓☓☓_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end

View編

フォルダ配下に複数のViewファイルを持つため、複数

種類 説明
フォルダ名 users 複数形

Routing編

Routingのリソース名はController名に基づいているため、複数

種類 説明
リソース名 users 複数形

参考記事

https://diveintocode.jp/blogs/Technology/NamingRole

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

Herokuでデプロイをやり直す方法

<この記事について>
Herokuでデプロイした後に、本番環境に反映されていなくて再度デプロイをやり直す
ことで無事に解決できたので備忘録として投稿!

[環境]
・Ruby 2.6.5,
・Rails 6.0.0
・macOS

<状況>
railsでアプリを制作時に、本番環境での動きを確認するために、Herokuでデプロイを実施。
一度目は問題なく動作したが、2回目で不具合発生。ビューについて、ローカル環境と同様の
実装となっていないため、試行錯誤するも解決できず。メンターに質問したところ、
新たに1つの操作をすることで解決できました。

以下、全てターミナルで操作となります。

$ git commit --allow-empty -m "任意のコミットメッセージ"

→通常、差分がないとcommit/pushできないが、差分がなくても強制実行してくれるコマンド

あとは、通常のHerokuへのデプロイと同じ流れです。

$ git push heroku master
$ heroku run raills db:migrate

本番環境で確認して問題なければ完了。

<終わりに>
ローカル環境で問題ないのに本番環境で同じように動作しないと焦りやすいですが、
このようなコマンドで解決できることを知っているだけで精神的にも安定すると思います!

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

Railsチュートリアル(第6版) マイクロポストの投稿機能のバックグラウンド動作

概要

マイクロポストの投稿は以下の2stepで進む。

  1. ログインしてホーム画面を表示させる。ログイン状態のホーム画面にはマイクロポストの投稿フォームが設置されている。
  2. マイクロポストの内容を入力して送信ボタンを押すと、マイクロポストの内容がDBに保存される。

詳細

上記の各stepで実行されるバックグラウンドの動作は以下の通りである。

  1. ログイン時のバックグラウンド動作についてはこちらを参照。/static_pages/homeへのGETリクエストを送信するとStaticPagesコントローラのhomeアクションが実行される。このhomeアクションでは、ユーザーIDに紐付ける形で@micropostが新規作成される(マイクロポスト関連部分のみ抜粋)。homeアクションの最後で、対応したview(/static_pages/home.html.erb)が呼び出され、ホーム画面が表示される。このviewへはhomeアクションで作成された@micropostが渡されており、Railsはこの中身が空であることから、マイクロポストの新規作成であると判断している。
  2. ユーザーがマイクロポスト入力フォームに内容を入力して投稿ボタンを押すと、/micropostsへのPOSTリクエストが送信され、Micropostsコントローラのcreateアクションが実行される。このcreateアクション実行の直前には、現在ログイン中のユーザーであるかどうかが確認される。当のcreateアクションではparams[:micropost][:content]とparams[:micropost][:image]の値のみを許可して@micropostに格納し、DBへの保存を試みる。DBへの保存に成功すると、root_urlへのリダイレクトが行われ、ホーム画面が表示される。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails のアプリケーションサーバについて

お久しぶりです。井出です。
前回の記事から1年経ちましたね。

今年もやってきましたアドベントカレンダー!
昨日は若手のホープ、ダニエル・バレンボイムが大好きなダニーさんが記事を書いてくれました。
記事はこちら

中途で入ったイケイケ系の彼ですが、
サーバばりばり触れるようになってきていて将来有望です!

入社してからの振り返り記事とのことで
プラコレが気になる方は、ぜひ雰囲気を感じてもらえたらと思います!

ダニーさんの流れで、自分も今年の振り返りをしてみようと思います。

去年まで自分のメイン言語はPHPでしたが、今年からRubyをメインにシステム開発していました。
記載方法が独特で最初は苦しめられましたが、振り返ってみると凄い楽しい言語です。
まだまだ理解できていない部分もあるので、来年はより深く理解できるよう進めていきます!

そんなこんなで、rubyのFWであるRailsで有名なサーバアプリケーション
puma / unicorn について、先日というか昨日なんですが
コマンドを調べる機会があったので
機能の違いとあわせてまとめてみようと思います。

unicornについて

前提:unicornはマルチプロセス/シングルスレッドです。

Unicornはプロセスごとに通信処理を行います。
そのため低速なio処理があった場合そこで足止めを食らい全体が重くなってしまいます。
これを回避するためにorkerの数を増やす必要がありますが
workerはCPUのコア数に依存する部分もあります。

コマンド

起動

bundle exec unicorn -E production -c config/unicorn.rb -D

-E 実行環境変数
-c 設定ファイルの位置
-D デーモン化

停止

kill -QUIT cat pidファイルのパス
kill -QUIT masterのpid

再起動

kill -HUP cat pidファイルのパス
kill -HUP masterのpid

緩やかな再起動

kill -USR2 cat pidファイルのパス
kill -USR2 masterのpid

旧プロセスを保持した状態で、新プロセスを起動
起動後旧プロセスに停止シグナル(QUIT)を実施し停止させる。

(仕様は https://github.com/phusion/unicorn 参考)

pumaについて

前提:pumaはマルチプロセス/マルチスレッドです。

rails5.2から標準機能となった。
リクエスト処理をスレッド単位で溜めて、それをマルチに実行することでの並列処理を可能としています。
リソースが少ない中でも効率的にリクエストをさばくことが可能になります。

コマンド

起動

bundle exec puma -C config/puma.rb -d

-C 設定ファイルの位置
-d デーモン化

停止

kill -QUIT cat pidファイルのパス
kill -QUIT masterのpid

再起動(設定ファイルリロード無)

kill -USR1 cat pidファイルのパス
kill -USR1 masterのpid

再起動(設定ファイルリロード有)

kill -USR2 cat pidファイルのパス
kill -USR2 masterのpid

使用用途によって使い分けることができれば素敵ですが
大規模システムでもない限り、パフォーマンスへの影響は無さそうですね。
こちらの記事が、両者の性能部分などをまとめてくださっています。

明日はフロントの将来有望株の好君が記事を書いてくれるので、ご期待を!
それではまた1週間後に会いましょう!:raised_hand:

Now hiring!
プラコレでは、自由な未来をつくるために
一緒に冒険したいエンジニア・デザイナーを募集しています!
https://www.wantedly.com/projects/262436
運営サービス
PLACOLE(プラコレウェディング)
DRESSY(ドレシー)byプラコレ
farny(ファーニー)byプラコレ

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

【Ruby】演算子"::(コロンコロン)"【定数】

まえがき

Railsを使っていれば、誰でも以下の記述を見たことあると思います。

models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

ApplicationRecordActiveRecord::Baseを継承していますよね。

なんにも変わったことありませんね。

・・・

・・

って、え、え、ちょっと待って下さい。
なんですか、::って!?!?!?
怖い!!!

ということで、Ruby初学者である私が、::について調べたことを健忘録として残します。

結論

::は2つの特徴があります。

1.名前空間の絶対パスを指定する

module Hoge
  class Fuga
    def self.fuga
      p "fugaです"
    end
  end
end

Hoge::Fuga.fuga #=> "fugaです"
Fuga.fuga #エラー

#Fugaクラスを作成
class Fuga
  def self.fuga
    p "fugaです"
  end
end

Hoge::Fuga.fuga #=> "fugaです"
Fuga.fuga #=> "fugaです"

2.定数のスコープ演算子

クラスまたはモジュールで定義された定数をスコープ外部から参照するためには::を使います。

class Hoge
  FUGA = "FUGA" #定数FUGA

  p FUGA #=> "FUGA"
end

#スコープ外から定数FUGAを出力する。
p Hoge::FUGA #=> "FUGA"

また、Rubyではクラスやモジュールも定数として扱われます。
例えばHogeクラスを生成した時、Hogeクラスオブジェクトは、Hogeという名前の定数に代入されています。

結局、ActiveRecord::Baseの::は??

ActiveRecord::Baseの::ActiveRecordモジュール内のBaseクラスを指しています!!
ActiveRecordにネストされたBaseクラスなんですね〜。
簡単に書くと以下の通りです。

active_record/base.rb
module ActiveRecord

#省略

  class Base
  #省略
  end

#省略

end

ActiveRecord::Baseの正式な中身は以下の通りです。
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/base.rb

ActiveRecordの機能についてはこの方が深くまとめております。
Rails: ActiveRecord::Baseメソッドのまとめ

参考

Ruby公式リファレンス 変数と定数
https://docs.ruby-lang.org/ja/latest/doc/spec=2fvariables.html#const

Railsガイド Active Record の基礎
https://railsguides.jp/active_record_basics.html

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

【rails】dockerでrailsコンソールを使う方法

個人メモです。

dockerで起動しているアプリケーションのrails consoleにアクセスする方法。

dockerのコンテナ内でrails cを実行する。

rails consoleの起動

ターミナル
##入りたいコンテナ名を確認
docker ps

##コンテナに入る
docker exec -it コンテナ名 /bin/bash

##railsコンソールを開く
root@96913c74e902:/app# rails c

`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use `exists?` instead.
Loading development environment (Rails 6.0.3.2)

irb(main):001:0>

ターミナルが、ローカル($) -> docker(root@96913c74e902:) -> rails(irb(main))の順に変わっていく。

irbはrailsの対話モード。
Interactive Rubyの略。


終了方法と注意点

終了するときはexitquit

※末尾の番号が0以外のときはctrl + cでクリアしてから

##末尾が0以外だと抜けられない(式の途中と見做されている)
irb(main):014:2> quit
irb(main):015:2> exit
irb(main):016:2> 

##ctrl + c でクリアしてからexit
irb(main):017:0> exit 


コマンドの実行例

1行の場合
irb(main):001:0> p "hello rails"
"hello rails"
=> "hello rails"


irb(main):002:0> 1+2
=> 3
複数行の場合
irb(main):015:0> for i in 1...11 do
irb(main):016:1*   print "#{i} "
irb(main):017:1> end
1 2 3 4 5 6 7 8 9 10 => 1...11
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】dockerでrailsコンソールを使う方法

個人メモです。

dockerで起動しているアプリケーションのrails consoleにアクセスする方法。

dockerのコンテナ内でrails cを実行する。

rails consoleの起動

ターミナル
##入りたいコンテナ名を確認
docker ps

##コンテナに入る
docker exec -it コンテナ名 /bin/bash

##railsコンソールを開く
root@96913c74e902:/app# rails c

`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use `exists?` instead.
Loading development environment (Rails 6.0.3.2)

irb(main):001:0>

ターミナルが、ローカル($) -> docker(root@96913c74e902:) -> rails(irb(main))の順に変わっていく。

irbはrailsの対話モード。
Interactive Rubyの略。


終了方法と注意点

終了するときはexitquit

※末尾の番号が0以外のときはctrl + cでクリアしてから

##末尾が0以外だと抜けられない(式の途中と見做されている)
irb(main):014:2> quit
irb(main):015:2> exit
irb(main):016:2> 

##ctrl + c でクリアしてからexit
irb(main):017:0> exit 


コマンドの実行例

1行の場合
irb(main):001:0> p "hello rails"
"hello rails"
=> "hello rails"


irb(main):002:0> 1+2
=> 3
複数行の場合
irb(main):015:0> for i in 1...11 do
irb(main):016:1*   print "#{i} "
irb(main):017:1> end
1 2 3 4 5 6 7 8 9 10 => 1...11
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのform_withはデフォルトでAjax通信をする

生じた現象

  • formを入力してPOST送信をして返ってきたレスポンスのHTMLが、ブラウザでレンダリングされなかった。

対策

form_with(model: @form, local: true) do |f|
   # inputたち
end

のように、local: true指定してやれば良い。

参考

Forms generated with form_with by default has data-remote set to true.
If the data-remote is set to true your form makes an AJAX call. So your view is getting rendered as the response of that AJAX call. That is why you are not getting any errors.
Add local: true in your form_with.

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

RSpecまとめ 3.モデルスペック

前回の続き。

モデルに対するテスト。モデルのバリデーションやクラスメソッド、インスタンスメソッドをテストしていきます。

モデルスペックの構造

モデルスペックには、以下の3つのテストを最低限入れます。

1.有効な属性で初期化された場合は、モデルの状態が有効(valid)になっていること
2.バリデーションを失敗させるデータであれば、モデルの状態が有効になっていないこと
3.クラスメソッドとインスタンスメソッドが期待通りに動作すること

モデルスペック作成

まずはUserモデルのテストです。
前回、スペックファイルの自動生成を設定しましたが、今回は既存のモデルに対するテストなので、手動でスペックファイルを生成します。

$ rails g rspec:model user

このコマンドで、spec/models/user_spec.rbが生成されるので、このファイルにスペックを書いていきます。

spec/models/user_spec.rb
repuire 'rails_helper'  #ヘルパーの読み込み

RSpec.describe User, type: :model do  #ブロック内にUserモデルのスペックをまとめている

  #name, email, passwordがあれば有効な状態であること
  it 'is valid with a name, email, and password' do  #一つ一つのスペックはitで始まるブロック内に記述

    user = User.new(
      name:     "Zeisho",
      email:    "hoge@hoge.com",
      password: "hogehoge"
    )
    expect(user).to be_valid  #userが有効(valid)ならパスする
  end

end

RSpecでは、テストしたい値をexpect()に渡し、後ろに続くマッチャを呼び出すことでテストを行います。

今回の場合、userをbe_valid(有効か)というマッチャでテストしています。
また、(user)の後ろのtoは、テストする値がマッチャに適合すればsuccess、しなければfaildをテスト結果として返します。
toの反対の意味を持つto_notもよく使うので、覚えておくと良いでしょう。

バリデーションのテスト

正しい値を与えたときにモデルが有効になっているかテストできたので、今度はバリデーションを失敗させるデータを与えたときにモデルが無効な状態かテストしていきます。

spec/models/user_spec.rb
  #nameがなければ無効であること
  it 'is invalid without a name' do
    user = User.new(
      name:     nil
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("can't be blank")
  end

今回のスペックでは、userのnameをnilにした状態でvalid?メソッドを使ってuserの有効性を検証し、最後にuser.errors[:name]に"can't be blank"という内容のものが含まれていればパスするという内容です。
nameがないことがバリデーションに失敗した原因であるかを知りたいので、発生したエラーの内容をテストしています。
user.valid?で有効性を検証しないとエラーメッセージが出ないので、注意しましょう。

この方法に沿って、他のバリデーションのスペックも書いていきましょう。

spec/models/user_spec.rb
  #emailが重複していれば無効であること
  it 'is invalid without a duplicate email address' do
    User.create(
      neme:     "zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user = User.new(
      name:     "Skywalker"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("has already been taken")
  end

createを使ってユーザーを保存し、その後で同じemailを持ったユーザーを生成するとemailの重複エラーが出るかという内容のスペックです。

この調子でUserモデルのスペックを完成させましょう。

完成したら、Projectモデルのスペックも作っていきます。

$ rails g rspec:model project
spec/models/project_spec.rb
require 'rails_helper'

RSpec.describe Project, type: :model do
  # ユーザー単位では重複したプロジェクト名を許可しないこと
  it "does not allow duplicate project names per user" do
  user = User.create(
    name:     "Zeisho",
    email:    "hoge@hoge.com",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  new_project = user.projects.build(
    name: "Test Project"
  )
  new_project.valid?
  expect(new_project.errors[:name]).to include("has already been taken")
  end

  # 二人のユーザーが同じ名前を使うことは許可すること
  it "allows two users to share a project name" do
  user = User.create(
    name:     "Zeisho",
    email:    "hoge@hoge.com",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  other_user = User.create(
    name:     "Skywalker",
    email:    "hogehoge@hoge.com",
    password: "hogehoge"
  )
  other_project = other_user.projects.build(
    name: "Test Project"
  )
  expect(other_project).to be_valid
  end
end

ProjectはUserに紐づいているので、今の書き方だとテストに必要なインスタンスを作るだけでコードが冗長化してしまいました。この辺りの問題は後で解決していきます。

クラスメソッド、インスタンスメソッドのテスト

このアプリには、Projectモデルに紐づいたNoteがあり、プロジェクトのメモとして文字列を格納できるようになっており、Noteモデルには検索機能を実装しています。

app/model/note.rb
scope :search, ->(term) {
  where("LOWER(message) LIKE ?", "%#{term.downcase}%")
}

今回はこのクラスメソッドとスコープをテストしていきます。

$ rails g rspec:model note
spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do
  #検索文字列に一致するメモを返すこと
  it "returns notes that match the search term" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("first")).to include(note1, note3)
    expect(Note.search("first")).to_not include(note2)
  end

  #検索結果が見つからなければ空のコレクションを返すこと
  it "returns an empty collection when no results are found" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("message")).to be_empty
  end
end

マッチャについてもっと詳しく

これまで、3つのマッチャ(be_valid, include, be_enpty)を使ってきましたが、RSpecが提供するマッチャをもっと知りたい場合、rspec-expectationsを参照すると良いでしょう。

スペックをDRYにする

discribe, context, before, afterを使ってスペックをDRYにしていきます。

discribe, context

スペック群を分類してブロック内にまとめることができます。
Noteモデルのスペックを例に見ていきましょう。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  #バリデーション用のスペック群

  #メッセージ検索機能のスペック群
  discribe "search message for a term" do
    #一致するデータが見つかるとき
    context "when a match is found" do
      #一致する場合のexample群
    end
    #一致するデータが見つからないとき
    context "when no match is found" do
      #一致しない場合のexample群
    end
  end

end

before, after

全てのテストで使用するテストデータを一箇所にまとめることができます。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  before do
    @user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    @project = user.projects.create(
      name: "Test Project"
    )
  end

  #バリデーション用のスペック群

  #メッセージ検索機能のスペック群

end

beforeには以下のオプションを設定できます。

before(:each)
describeまたはcontextブロック内の各(each)テストの前に実行

before(:all)
describeまたはcontextブロック内の全(all)テストの前に一回だけ実行

before(suite)
テストスイート全体の全ファイルを実行する前に実行

exampleの後に後片付けが必要であれば、afterを使うことができます。

テストはDRYにし過ぎない!

テストは開発・本番環境とは違って可読性を優先してDRYにしていくので、もしスペックファイルの内容を確認するのにエディタのスクロールや、複数のファイルの行き来を頻繁に行っているならば、DRY過ぎます。
必要に応じてコードを重複させることを検討したり、ファイルを行き来しなくても役割のわかる変数・メソッド名をつけるよう心がけましょう。

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

Everyday Railsまとめ 3章『モデルスペック』

前回の続き。

モデルに対するテスト。モデルのバリデーションやクラスメソッド、インスタンスメソッドをテストしていきます。

モデルスペックの構造

モデルスペックには、以下の3つのテストを最低限入れます。

1.有効な属性で初期化された場合は、モデルの状態が有効(valid)になっていること
2.バリデーションを失敗させるデータであれば、モデルの状態が有効になっていないこと
3.クラスメソッドとインスタンスメソッドが期待通りに動作すること

モデルスペック作成

まずはUserモデルのテストです。
前回、スペックファイルの自動生成を設定しましたが、今回は既存のモデルに対するテストなので、手動でスペックファイルを生成します。

$ rails g rspec:model user

このコマンドで、spec/models/user_spec.rbが生成されるので、このファイルにスペックを書いていきます。

spec/models/user_spec.rb
repuire 'rails_helper'  #ヘルパーの読み込み

RSpec.describe User, type: :model do  #ブロック内にUserモデルのスペックをまとめている

  #name, email, passwordがあれば有効な状態であること
  it 'is valid with a name, email, and password' do  #一つ一つのスペックはitで始まるブロック内に記述

    user = User.new(
      name:     "Zeisho",
      email:    "hoge@hoge.com",
      password: "hogehoge"
    )
    expect(user).to be_valid  #userが有効(valid)ならパスする
  end

end

RSpecでは、テストしたい値をexpect()に渡し、後ろに続くマッチャを呼び出すことでテストを行います。

今回の場合、userをbe_valid(有効か)というマッチャでテストしています。
また、(user)の後ろのtoは、テストする値がマッチャに適合すればsuccess、しなければfaildをテスト結果として返します。
toの反対の意味を持つto_notもよく使うので、覚えておくと良いでしょう。

バリデーションのテスト

正しい値を与えたときにモデルが有効になっているかテストできたので、今度はバリデーションを失敗させるデータを与えたときにモデルが無効な状態かテストしていきます。

spec/models/user_spec.rb
  #nameがなければ無効であること
  it 'is invalid without a name' do
    user = User.new(
      name:     nil
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("can't be blank")
  end

今回のスペックでは、userのnameをnilにした状態でvalid?メソッドを使ってuserの有効性を検証し、最後にuser.errors[:name]に"can't be blank"という内容のものが含まれていればパスするという内容です。
nameがないことがバリデーションに失敗した原因であるかを知りたいので、発生したエラーの内容をテストしています。
user.valid?で有効性を検証しないとエラーメッセージが出ないので、注意しましょう。

この方法に沿って、他のバリデーションのスペックも書いていきましょう。

spec/models/user_spec.rb
  #emailが重複していれば無効であること
  it 'is invalid without a duplicate email address' do
    User.create(
      neme:     "zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user = User.new(
      name:     "Skywalker"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("has already been taken")
  end

createを使ってユーザーを保存し、その後で同じemailを持ったユーザーを生成するとemailの重複エラーが出るかという内容のスペックです。

この調子でUserモデルのスペックを完成させましょう。

完成したら、Projectモデルのスペックも作っていきます。

$ rails g rspec:model project
spec/models/project_spec.rb
require 'rails_helper'

RSpec.describe Project, type: :model do
  # ユーザー単位では重複したプロジェクト名を許可しないこと
  it "does not allow duplicate project names per user" do
  user = User.create(
    name:     "Zeisho",
    email:    "hoge@hoge.com",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  new_project = user.projects.build(
    name: "Test Project"
  )
  new_project.valid?
  expect(new_project.errors[:name]).to include("has already been taken")
  end

  # 二人のユーザーが同じ名前を使うことは許可すること
  it "allows two users to share a project name" do
  user = User.create(
    name:     "Zeisho",
    email:    "hoge@hoge.com",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  other_user = User.create(
    name:     "Skywalker",
    email:    "hogehoge@hoge.com",
    password: "hogehoge"
  )
  other_project = other_user.projects.build(
    name: "Test Project"
  )
  expect(other_project).to be_valid
  end
end

ProjectはUserに紐づいているので、今の書き方だとテストに必要なインスタンスを作るだけでコードが冗長化してしまいました。この辺りの問題は後で解決していきます。

クラスメソッド、インスタンスメソッドのテスト

このアプリには、Projectモデルに紐づいたNoteがあり、プロジェクトのメモとして文字列を格納できるようになっており、Noteモデルには検索機能を実装しています。

app/model/note.rb
scope :search, ->(term) {
  where("LOWER(message) LIKE ?", "%#{term.downcase}%")
}

今回はこのクラスメソッドとスコープをテストしていきます。

$ rails g rspec:model note
spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do
  #検索文字列に一致するメモを返すこと
  it "returns notes that match the search term" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("first")).to include(note1, note3)
    expect(Note.search("first")).to_not include(note2)
  end

  #検索結果が見つからなければ空のコレクションを返すこと
  it "returns an empty collection when no results are found" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("message")).to be_empty
  end
end

マッチャについてもっと詳しく

これまで、3つのマッチャ(be_valid, include, be_enpty)を使ってきましたが、RSpecが提供するマッチャをもっと知りたい場合、rspec-expectationsを参照すると良いでしょう。

スペックをDRYにする

discribe, context, before, afterを使ってスペックをDRYにしていきます。

discribe, context

スペック群を分類してブロック内にまとめることができます。
Noteモデルのスペックを例に見ていきましょう。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  #バリデーション用のスペック群

  #メッセージ検索機能のスペック群
  discribe "search message for a term" do
    #一致するデータが見つかるとき
    context "when a match is found" do
      #一致する場合のexample群
    end
    #一致するデータが見つからないとき
    context "when no match is found" do
      #一致しない場合のexample群
    end
  end

end

before, after

全てのテストで使用するテストデータを一箇所にまとめることができます。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  before do
    @user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    @project = user.projects.create(
      name: "Test Project"
    )
  end

  #バリデーション用のスペック群

  #メッセージ検索機能のスペック群

end

beforeには以下のオプションを設定できます。

before(:each)
describeまたはcontextブロック内の各(each)テストの前に実行

before(:all)
describeまたはcontextブロック内の全(all)テストの前に一回だけ実行

before(suite)
テストスイート全体の全ファイルを実行する前に実行

exampleの後に後片付けが必要であれば、afterを使うことができます。

テストはDRYにし過ぎない!

テストは開発・本番環境とは違って可読性を優先してDRYにしていくので、もしスペックファイルの内容を確認するのにエディタのスクロールや、複数のファイルの行き来を頻繁に行っているならば、DRY過ぎます。
必要に応じてコードを重複させることを検討したり、ファイルを行き来しなくても役割のわかる変数・メソッド名をつけるよう心がけましょう。

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

RSpecのテスト中にMySQL client is not connected

開発環境

macOS Catalina 10.15.7
Ruby on Rails 6.0.0
RSpec 4.0.1
pry rails 0.3.9
FactoryBot 6.1.0

エラー内容

console
Failure/Error: _query(sql, @query_options.merge(options))

ActiveRecord::StatementInvalid:
  Mysql2::Error: MySQL client is not connected

どうやらMySQLクライアントとの接続が確立できていないようだ。

定義を見る限り、client が初期化されているにも関わらず、network socket (file descriptor) が無効な状態だとこのエラーになるみたいですね。
Mysql2 の "MySQL client is not connected" について

検証

テスト結果
テストの実行結果を見ると、途中まではテストが成功しているため、ひとまずbinding.pryで処理を止めながらテスト内容を確認してみたところ、なぜかすべてのテストが成功した。

console
Finished in 16.16 seconds (files took 2.21 seconds to load)
15 examples, 0 failures

仮説

FactoryBotのインスタンス生成の記述を増やしたタイミングでエラーがエラーが発生しはじめたため、ここで負荷がかかって処理が止まった可能性があると考えた。

対処法1

インスタンスを生成するタイミングでsleepで処理を待機させることにした。

RSpec.describe OrderItem, type: :model do
  describe '購入情報の保存' do
    before do
      @user = FactoryBot.create(:user)
      @item = FactoryBot.create(:item)
      @order_item = FactoryBot.build(:order_item)
      sleep 0.1 # 0.1秒待機
    end
# 省略

結果

エラーを吐かずにテストが安定して成功するようになった。

対処法2

config/environments/test.rbに以下の記述をすることでも対処出来た。

config/environments/test.rb
Rails.application.configure do
  config.active_job.queue_adapter = :inline
# 省略
end

参考リンク

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

authenticateメゾットについて。

user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])

user.authenticate(password)で
合っていればユーザーの情報を出す。
間違っていれば、falseを返すメゾット。

主にログイン等に使われる。

if user && user.authenticate(params[:session][:password])

この文は&&の論理積(and)はnilとfalse以外は
Rubyではnilとfalse以外のすべてのオブジェクトは、真偽値ではtrueになるという性質を利用して、ログインできるかできないかをif文を使って実行している。

真偽値についての理解が浅かったため理解するのに時間がかかった。。。

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

Railsのform_withについて掘り下げる

軽く自己紹介

はじめましての方は初めまして。伊東と申します。
大卒後未経験でプログラマーになりまして、気づけば7,8年くらい働いています。
2020年3月からDMM WebCampのメンターをしています。

対象読者

  • Ruby on Railsで初めてMVCフレームワークを触った
  • 検証ツールであまり見ない
  • 初学者の方

前提

$ rails -v
Rails 5.2.4.3
$ ruby -v
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-linux]
$

Rubyのバージョン、Rails5系だよってだけです。バージョン互換などは考慮しておりませんが、書く内容はそんなに影響するような話じゃないのでは??って思っています。 authenticity_tokenの箇所でデフォルトの挙動が違うぽいです...本題というわけではないので各バージョンで調べてくださいmm

伝えたいこと

検証ツール見ていきましょうって話です。
普段からinput nameとかの属性をちゃんと見てる人は読む必要がない記事になってます。
本当に基本的なことしか書かないので悪しからず

そもそもActionView::Helpersってなに??

よく使うのはform系ではないかな?と思います。

  • form_with
  • text_field_tag
  • text_area_tag
  • submit_tagなど

詳しくは以下リンクから見てみてください。
https://api.rubyonrails.org/classes/ActionView/Helpers.html

事前準備

細かい説明は割愛して作っていきます。

プロジェクト作成らへん

ちなみに行頭の$マークはrootユーザー#じゃないって意味です。

$ rails new without_action-view-helpers # プロジェクト作成
# 省略
$ cd without_action-view-helpers/
$ rails g controller books index new # コントローラー(アクション含)作成
# 省略
$ rails g model Book title:string price:integer description:text # モデル作成
# 省略
$ rails db:migrate # DB反映
# 省略
$

ルーティングの設定

config/routes.rb
Rails.application.routes.draw do
  resources :books, only: [:index, :new, :create, :show, :edit, :update]
end
ルーティングの確認
$ rails routes | grep book # grepは文字列検索しています
                    books GET   /books(.:format)                                                                         books#index
                          POST  /books(.:format)                                                                         books#create
                 new_book GET   /books/new(.:format)                                                                     books#new
                edit_book GET   /books/:id/edit(.:format)                                                                books#edit
                     book GET   /books/:id(.:format)                                                                     books#show
                          PATCH /books/:id(.:format)                                                                     books#update
                          PUT   /books/:id(.:format)                                                                     books#update
$

controller

めーっちゃシンプルな形。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def index
    @books = Book.all
  end

  def new
    @book = Book.new
  end

  def create
    book = Book.new(book_params)
    book.save
    redirect_to books_path
  end

  def show
    @book = Book.find(params[:id])
  end

  def edit
    @book = Book.find(params[:id])
  end

  def update
    book = Book.find(params[:id])
    book.update(book_params)
    redirect_to books_path
  end

  private
  def book_params
    params.require(:book).permit(:title, :price, :description)
  end
end

view

app/views/books/new.html.erb
<h1>Books#new</h1>

<div>
  <%= render 'books/form', book: @book %>
</div>

部分テンプレート化しています。

app/views/books/_form.html.erb
<%= form_with model: book, local: true do |f| %>

  <div>
    <%= f.label :title %>
    <%= f.text_field :title %>
  </div>

  <div>
    <%= f.label :price %>
    <%= f.number_field :price %>
  </div>

  <div>
    <%= f.label :description %>
    <%= f.text_area :description %>
  </div>

  <div>
    <%= f.submit :submit %>
  </div>

<% end %>

ブラウザの検証ツールを使用して掘り下げて見ていきます

new

スクショだとこんな感じ
スクリーンショット 2020-12-10 20.26.02.png

まずformタグやinput>hiddenの部分から見ていきます
<form action="/books" accept-charset="UTF-8" method="post">
  <input name="utf8" type="hidden" value="✓">
  <input type="hidden" name="authenticity_token" value="cgX+LsxfzE7A7F5Tm8GAlF2KdaifSdqdrWd6cOgazlcbZx8OQ4c6LazJtdRYIdPjzmfmPUGHkuBoxSQhdSqiIA==">
</form>

action?accept-charset?method?hidden?指定していない項目がいくつもでてきました。

method

methodはsubmitされた時のhttpリクエストメソッドです。

action

actionはformタグ内のsubmitボタンがポチィされた時に実行されるURI(パス)を指定します。
/books で methodがpostになっているので、books_controllerのcreateアクションに飛ぶという仕組みです。

form_withでaction指定してなくない?

urlというパラメーターの指定が無いときはmodelからactionのpathを勝手に作ってくれています。モデル名とコントローラー名が一致している場合は 省略可能です。 model: book と書くだけでいいので簡単ですね。

ちなみにnamespaceを使用している場合は model: [:admin, book] としてあげれば、/admin/books というURIを作ってくれます。便利

hidden

name="utf8" については調べてないです!割愛します
name="authenticity_token" についてはCSRFの対策として自動発行されているものです。他のサイトから不正なリクエストを受けないように発行してるものです。
今回は5.2系で試しているのでデフォルトで有効になっているようですがそれ以外のバージョンであれば必要に応じて設定すれば有効になります。

inputタグ

入力項目はどのようになっているか
<div>
  <label for="book_title">Title</label>
  <input type="text" name="book[title]" id="book_title">
</div>

スクリーンショット 2020-12-13 16.21.27.png

inputタグを見てみると
<input type="text" name="book[title]" id="book_title"> となっています。
type="text"は単純にテキストを入力出来るという意味です。text以外にもあるのでこれ見ればわかると思います。 (詳しくはこれ: http://www.htmq.com/html5/input.shtml )
name="book[title]" これが多分一番大事になってくると思います。Rails的にはコントローラー側で params[:book][:title] で入力された値が取得出来るという意味になります。これを理解していれば、入力された値がDBに保存できない?みたいなことは少なくなると思います。

edit

スクリーンショット 2020-12-13 17.08.31.png

適宜省略してます
<form action="/books/1" accept-charset="UTF-8" method="post">
  <input type="hidden" name="_method" value="patch">
  <div>
    <label for="book_title">Title</label>
    <input type="text" value="たーいとる" name="book[title]" id="book_title">
  </div>

  <div>
    <input type="submit" name="commit" value="submit" data-disable-with="submit">
  </div>

</form>

formタグ, hiddenタグ

action部分は action="/books/1" になっています。model: bookで指定している変数bookが保存済みの場合URIを自動的にbook.idを設定してくれています。

hiddenタグで新規の時には無かったものが存在しています。 <input type="hidden" name="_method" value="patch"> これでhttpメソッドをpatchにしています。
なんでformタグで指定しないの??ってなると思うんですけど、多分ですがform>methodではpost/getしか指定できないからだと思います(間違ってたら指摘してもらえると :bow: )。 http://www.htmq.com/html5/form.shtml

inputタグ

<input type="text" value="たーいとる" name="book[title]" id="book_title">

value="たーいとる" という項目が増えています。これで入力部分の値を指定しています。それ以外は同じなので割愛します。

まとめ

HTMLも単純に画面に表示されたものを見るのではなく、どのようにHTMLが生成されているかを確認したり意識することで、HTMLであったりRailsの仕組みを少しずつ理解出来るようになっていくと思います。

あまり長い文章書くの得意じゃないんですが、役に立てると嬉しく思います。。。
またお声掛け頂いたメンターの方ありがとうございます。

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

Rails:modelとデータベースについてちょっとまとめてみた。

最初に

カレンダー企画2020の15日目
プログラミングの勉強を始めて3ヵ月程経ったので学んだことのメモをアウトプットとして記事に残します。
これからプログラミングの世界に入る人の手助けになれたら嬉しい限りです。
間違っていたり、言葉が違っていたり、誤解されるような言葉があったら教えてください^^
言葉を長々と読みづらかったら申し訳ありません。少しずつなれてがんばります。

モデルとデータベース(DB)についてちょっとまとめてみる

modelとは?

モデルはここで簡単に説明している

アプリケーションのデータを取り扱う場所。データベースに対してデータの登録や取得、更新、削除などといった処理を役割として担っている。
たとえば
データベースという巨大な倉庫を管理しているイメージかな。
必要な時に必要な情報を取り出したり、しまったりしてくれる感じです。

基本的にDBとのやり取りになる。

データベース(DB)とは?

DBはデータの保存先です。
DBのおかげでデータを効率的に保存できたり、検索できるようになっている。

ゲームでいうところのメモリカードとかセーブデータを保存してある場所かな!
modelがゲーム機本体
今回は出てこないけど、Controllerはコントローラー
テレビ画面に表示するコードやテレビがViewになるにかな!
そんなイメージです。

railsではDBに保存されているデータを「テーブル」にまとめてテーブル同士が関連して保存してあります。

テーブルはデータを整頓してくれるのでデータを取り出しやすくなります。

ゲームで過去のセーブデータがゴチャゴチャな並びだったら最新のものを探すの大変じゃないですか?
そういったことをなくす為にテーブルというので関連しているもの同士をわかりやすく保存してあるのです。

テーブルについてもう少しだけ

ブログサイトをイメージしてください。

  • タイトル
  • 本文
  • 投稿者
  • 投稿日

こんな情報があったとします。

これらをテーブルにしてみると

ID タイトル 本文 投稿者 投稿日
1 初めまして 本日からブログを始めます yuta 12/1
2 こんな空スゴイ 今朝の青空 yuta 12/5
3 今日はどこにいるでしょう? 写真をみて当ててみて yuta 12/9
4 朝から、、、 黙々と仕事しています yuta 12/12

こんな感じになります。Excelみたいですね!でもまさにそのイメージです!!
このようにしてあると、どこがタイトルでいつ投稿したのかとか一目瞭然ですよね!これが整頓して分かりやすくするということです。

テーブルにも名前がついており、
全体の事はテーブルと言います。
縦列:カラムと言います。
横列:レコードと言います。

railsではテーブルが作られてデータが作られるとIDは自動生成されます。
ID1のレコードは?

ID タイトル 本文 投稿者 投稿日
1 初めまして 本日からブログを始めます yuta 12/1

このテーブルのカラム名は?

ID タイトル 本文 投稿者 投稿日

これになります。

タイトルカラムの中身は?

タイトル
初めまして
こんな空スゴイ
今日はどこにいるでしょう?
朝から、、、

これらが抽出されます。

DBとデータのやり取りはデータベース言語(SQL)で行われます。

最後に

正直カラムを理解するのが最初は大変だったことを思い出しました^^;
ここではわかるけど実際作るとあれ?とよく混乱していました。^^;

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

Formクラス作る時は、ActiveModelが便利 

以前、「Virtusが便利」というタイトルでFormクラスを作る記事を書かせていただきました。
この時は、Virtusを利用していたのですが、Rails5.2以降であれば、ActiveModelだけで済みます
正確には、ActiveModel::Attributesを使います

class ConstructionsSearchForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :building_name,    :string
  attribute :sales_staff_id,   :integer
  attribute :department_id,    :integer

  ....(略)

  def search
    Construction.ransack(
      building_name_cont: building_name,
      sales_staff_id_eq: sales_staff_id,
      sales_staff_department_id_eq: department_id,
    ).result
  end 
end

上記のようにFormクラスを作って、controllerで

@constructions = ConstructionsSearchForm.new(search_params).search

呼び出すだけ。
簡単ですね!!

validation等を追加したい時も前回と同様、ActiveModel::Modelをincludeしているので、

  attribute :building_name,    :string
  attribute :sales_staff_id,   :integer
  attribute :department_id,    :integer

  validate :building_name, presence: true

ActiveRecordと同じ用に書いて、.valid?で検証するだけです。
ActiveModel::Attributesで指定できる型は、こちらに書いてあります。

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

未経験で大学を休学したエンジニアが2020年に学んだこと

CAMPFIRE Communityでエンジニアをしております。Matsuiです。

趣味はスポーツ観戦、旅行などです。
2020年はプロ野球、UEFAチャンピオンズリーグ、F1&F2が個人的に盛り上がりました。好きなドライバーはMax Verstappenです。(誰にも伝わらないと思いますが、Daniel Ricciardoとのコンビのシーンがすごく好きです。)

2020年は大学を休学し未経験からエンジニアに挑戦をした一年でした。このアドベントカレンダーを利用して2020年に私が経験したこと、学んだこと、意識したことをまとめます。

エンジニアにチャレンジする前(2020/1時点)のスペック

- プログラミング経験はほぼゼロ。(SQLのみ若干経験あり)
- コンピュータやインターネットには比較的馴染みがある。(作る側ではなく、利用する側として。)
- 国立大学文系学部に通っている。

やったこと

2020年1~2月

  • 初めて rails new をする。
  • Railsガイドのチュートリアル?をやる。

2~3月

  • CAMPFIREのコードを読むようになる。
  • 社内でデータ取得用のカラム追加とタスクに日時挿入の処理を追加する。

4~6月

  • サポートサービスのコード分離とサービス追加。
    • 密結合状態にあったコードをコントローラーレベルで切り分け、コードの分離を行いました。
    • またクラウドファンディングとコミュニティではサービスの性質が異なる部分もあるため、コミュニティ独自のサポートサービスを追加しました。

7~8月

9~11月

  • サポートサービスをDBで管理の上、社内の管理画面から追加、編集などをできるようにしました。
    • これまでは追加、編集のためにデプロイをエンジニアに依頼する必要がありましたが、エンジニアなしで追加、編集できるようにしました。

11~12月

  • 社内で使用する管理画面の機能改修、脱ライブラリ化。
    • CAMPFIRE全体で社内で使用する管理画面を脱ライブラリ化し、独自のシステムを構築し移行する流れがあったのでそれに合わせて、主にコミュニティに関する機能の移行を担当しました。(しています。現在も進行中です。)

意識したこと、学び

※個人の意見です

組織をどうしたいか、プロダクトをどうしたいかは一旦考えるのをやめた。

  • 組織のことを考えることも大事ですが、自分の実力が不足している状態だったのでとにかく自分のことに集中しました。自分のことに集中することが組織のためくらい割り切っていました。

一通り業務がこなせるようになるまでは、情報もシャットアウト。

  • 技術は進歩も早く常に新しい情報が流れてくるのでできるだけキャッチアップしたほうが良いですが、基礎が無いままキャッチアップをしても効率も低いままですし、アウトプットにつながらないのでまずは基礎固めに集中しました。(特に初期は。)

GitHubのPR,issue,Slackの議論やレビューなどはできる限り目を通す。

  • どこに学びが転がっているか分からないのでGitHubのPR,issue,Slackの議論やレビューなどは自分に関係ないことでも、意識して積極的に目を通すようにしていました。(そして実装の際は徹底的にパクり参考にしました。他人の思考や脳は外部ライブラリだと思っています。)

分からない、困ったは一定時間たったら詳しい人に質問するべき。

  • ありきたりですが、初期はこれが出来ず聞いたら迷惑なのでは、自分で突き詰めて考えた方が良いのではと思っていました。しかし、わからないことは一定時間(1時間など)以上悩んでも大体解決しないのでスパッと聞いたほうが良いと考えるようになりました。聞くことは迷惑でもないですし、迷惑だったとしても他のところで取り返せば良いくらいに開き直るようになりました。

さいごに

足りない部分、課題はありすぎて書ききれない、認識しているものが全てでないので省略します。

最後まで読んでいただきありがとうございます。

質問、ご意見、アドバイスなどあればコメントまでお願いします。

大学は卒業する予定です。おわり。

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

Nuxt.js + Rails APIをDocker上で立ち上げCRUD操作してみる

今回初めて Nuxt.js を触りました。
Todoアプリを作ろうかなと思ったのですが, せっかくならAPIを叩こうじゃないかということでサーバーサイドも用意してみました。

サーバーサイドはRuby on Rails(API), クライアントサイドはNuxt.ts(Nuxt.js + TypeScript), DBはpostgresという構成で実装していきます。

環境構築に関しては, サーバーサイド/クライアンドサイド共にDocker上で動かしており, ディレクトリ構成はモノシリックにまとめました。

↓ソースコードはこちら
Kazuhiro-Mimaki/nuxt-rails-crud - GitHub

動作環境

macOS Catalina : version 10.15.4
Docker for macはインストール済みとする。

ディレクトリ構成

ディレクトリ構成
.
├── client-side
├── server-side
└── docker-compose.yml

1. サーバーサイド(Ruby on Rails)

Dockerfile作成

server-side/ 配下にdockerfileを作成。

Dockerfile
FROM ruby:2.7.0

RUN apt-get update -qq && \
  apt-get install -y \
  build-essential \
  libpq-dev \
  nodejs \
  postgresql-client

WORKDIR /app

COPY Gemfil Gemfile.lock /app/
RUN bundle install

Gemfile, Gemfile.lock作成

同じく server-side/ 配下にGemfileとGemfile.lockを作成。

Gemfile内に以下を記述。

Gemfile
source 'https://rubygems.org'
gem 'rails', '6.0.3'

Gemfile.lockは空のままで大丈夫。

docker-compose.yml作成

railsとpostgresの設定をdocker-compose.ymlに書いていきます。

docker-compose.yml
version: '3.8'

volumes:
  db_data:

  services:
    db:
      image: postgres
      volumes:
        - db_data/var/lib/postgresql/data
      environment:
        POSTGRES_PASSWORD: password

    server-side:
      build: ./server-side/
      command: bundle exec rails server -b 0.0.0.0
      image: server-side
      ports:
        - 3000:3000
      volumes:
        - ./server-side:/server-app
      tty: true
      stdin_open: true
      depends_on:
        - db
      links:
        - db

APIモードで rails new

以下のコマンドを叩けば, server-side/ 配下にrails関連のファイル群が作成されます。

$ docker-compose run server-side rails new . --api --force --database=postgresql --skip-bundle

database.yml の内容を修正

このままだとserver-sideのコンテナからDBのコンテナにアクセスできないので database.yml の内容を修正します。

以下のようになっていると思うので

database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

以下のように編集。

database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  user: postgres
  password: password
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

server-side ホストを受け入れるように修正

この設定をすることで, Nuxtからserver-sideにアクセスできます。

server-side/config/environments/development.rb
config.hosts << "server-side"

DBを作成

以下のコマンドを叩いてdbを作成。

$ docker-compose run server-side rails db:create

動作させてみる

以下のコマンドを打って, localhost:3000 にアクセス。
railsのデフォ画面が表示されればOK!

$ docker-compose up -d

サーバーサイドのAPIを実装

以下のコマンドを叩き, コンテナの中に入った上で作業を進めていきます。

$ docker exec -it server-side bash

ルーティングを設定。

routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  namespace :api do
    namespace :v1 do
      resources :todos do
        collection do
          get :complete
        end
      end
    end
  end
end

Todoモデル, todosコントローラーを作成。

$ rails g model Todo title:string isDone:boolean
$ rails db:migrate
$ rails g controller api::v1::todos

controllerの中身は以下のように書きました。

api/app/controllers/api/v1/posts_controller.rb
class Api::V1::TodosController < ApplicationController
  before_action :set_todo, only: [:update, :destroy]

  def index
    todos = Todo.where(isDone: false)
    render json: { status: 'SUCCESS', message: 'Loaded todos', data: todos }
  end

  def complete
    todos = Todo.where(isDone: true)
    render json: { status: 'SUCCESS', message: 'Loaded todos', data: todos }
  end

  def create
    todo = Todo.new(todo_params)
    if todo.save
      render json: { status: 'SUCCESS', data: todo }
    else
      render json: { status: 'ERROR', data: todo.errors }
    end
  end

  def destroy
    @todo.destroy
    render json: { status: 'SUCCESS', message: 'Deleted the todo', data: @todo }
  end

  def update
    if @todo.update(todo_params)
      render json: { status: 'SUCCESS', message: 'Updated the todo', data: @todo }
    else
      render json: { status: 'ERROR', message: 'Not updated', data: @todo.errors }
    end
  end

  private

    def set_todo
      @todo = Todo.find(params[:id])
    end

    def todo_params
      params.require(:todo).permit(:title, :isDone)
    end
end

動作確認

この記事を参考に, Postmanを利用してCRUD操作ができるかどうか確認します。
curlコマンドでも確認できますが, たぶんPostmanの方が楽。

2. クライアントサイド(Nuxt.js)

環境構築

基本的には 公式のInstallation に沿って進めるだけ。
nodeはインストール済みとします。(今回の環境では12/15現時点でのLTS ver. 14.15.1を使用しています。)

プロジェクトの作成

まずは create-nuxt-app で雛形作りましょう。

$ npx create-nuxt-app client-side

色々質問されると思うのですが, 今回は以下のように設定しました。(その他はデフォルト)

terminal
? Project name: client-side
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: None
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: None

この辺りの設定は各自の好みで設定してください。
全てのオプションは ここから 確認できます。

Dockerfile作成

client-side/ 配下にDockerfileを作成。

Dockerfile
FROM node:14.15.1

WORKDIR /client-app

COPY package.json yarn.lock ./

RUN yarn install

CMD ["yarn", "dev"]

docker-compose.ymlに client-side の設定を追加

server-side の設定を記述したdocker-compose.yml に client-side の設定を追加します。

docker-compose.yml
version: '3.8'

volumes:
  db_data:

services:
  db:
    image: postgres
    volumes:
      - db_data/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password

  server-side:
    build: ./server-side/
    image: server-side
    ports:
      - 3000:3000
    volumes:
      - ./server-side:/server-app
    command: bundle exec rails server -b 0.0.0.0
    tty: true
    stdin_open: true
    depends_on:
      - db
    links:
      - db

  # ここから下を追加
  client-side:
    build: ./client-side/
    image: client-side
    ports:
      - 8000:8000
    volumes:
      - ./client-side:/client-app
      - /client-app/node_modules
    command: sh -c "yarn && yarn dev"

portの設定

このままだとエラーが出るので, portとhostを以下のように設定します。

nuxt.config.js
export default {
  // Disable server-side rendering (https://go.nuxtjs.dev/ssr-mode)
  ssr: false,

  // ここを追記
  server: {
    port: 8000,
    host: '0.0.0.0',
  },

// 以下省略
}

動作させてみる

以下のコマンドを打って, localhost:8000 にアクセスするとNuxt.jsのデフォ画面が表示されます。

$ docker-compose up -d

これで環境構築は完了!

3. サーバーサイドとクライアントサイドの連携

いよいよクライアント側からサーバーサイドのAPIを叩きにいきます。
感動の瞬間。。。

CORS (オリジン間リソース共有) 問題を解消

CORSについては こちらの記事 が参考になると思います。
公式GitHubのREADMEに解決方法がありました。
READMEの記述を参考に @nuxtjs/proxy をインストールし, app/nuxt.config.js を以下のように編集します。
サーバーサイドのポート番号をは3000で指定していたので, ここは server-side:3000で。(コンテナ間の通信はコンテナ名で解決するため, localhostではなくserver-sideにしている。)

$ yarn add @nuxtjs/proxy
app/nuxt.config.js
modules: [
  '@nuxtjs/axios',
  '@nuxtjs/proxy'
],
// 以下を追加
proxy: {
  '/api': {
    target: 'http://server-side:3000',
    pathRewrite: {
      '^/api': '/api/v1/',
    },
  },
},

Composition APIとaxiosを設定

この辺り使いたいので設定しましたが, なくてもCRUD操作はできます。

shell
$ yarn add @nuxtjs/composition-api
client-side/nuxt.config.js
modules: [
  '@nuxtjs/proxy',
  //追加
  '@nuxtjs/axios',
  '@nuxtjs/composition-api',
],
client-side/tsconfig.json
"types": [
  "@types/node",
  "@nuxt/types",
  #追加
  "@nuxtjs/axios"
]

型定義

client-side に新たに models/todo.ts ディレクトリを作り, 以下を記述。

todo.ts
export interface ITodo {
  id: number;
  title: string;
  isDone: boolean;
}

viewを記述

本当はコンポーネントに分割して書くべきですが, 今回は1ファイルにまとめた方が見やすいかなと思ったのでまとめます。

client-side/pages/index.vue に以下の内容を記述。

client-side/pages/index.vue
<script lang="ts">
import {
  defineComponent,
  reactive,
  ref,
  onMounted,
} from "@nuxtjs/composition-api";
import { ITodo } from "../models/todo";
import $axios from "@nuxtjs/axios";

export default defineComponent({
  setup(_, { root }) {
    onMounted(() => {
      getTodo();
    });

    const todoItem = reactive({
      title: "",
      isDone: false,
    });

    const todoList = ref<ITodo[]>([]);
    const completeTodoList = ref<ITodo[]>([]);

    // todoをpost
    const addTodo = async () => {
      try {
        await root.$axios.post("/api/todos/", {
          title: todoItem.title,
          isDone: todoItem.isDone,
        });
        getTodo();
        todoItem.title = "";
      } catch (e) {
        console.log(e);
      }
    };

    // todoをget
    const getTodo = async () => {
      try {
        const response = await root.$axios.get("/api/todos");
        todoList.value = { ...response.data.data };
        getCompleteTodo();
      } catch (e) {
        console.log(e);
      }
    };

    // todoをupdate
    const updateTodo = async (i: number, todo: ITodo) => {
      try {
        const newTodo = todoList.value[i].title;
        await root.$axios.patch(`/api/todos/${todo.id}`, { title: newTodo });
      } catch (e) {
        console.log(e);
      }
    };

    // todoをdelete
    const deleteTodo = async (id: number) => {
      try {
        await root.$axios.delete(`/api/todos/${id}`);
        getTodo();
      } catch (e) {
        console.log(e);
      }
    };

    // todoをdone
    const completeTodo = async (todo: ITodo) => {
      try {
        todo.isDone = !todo.isDone;
        await root.$axios.patch(`/api/todos/${todo.id}`, {
          isDone: todo.isDone,
        });
        getTodo();
      } catch (e) {
        console.log(e);
      }
    };

    // complete_todoをget
    const getCompleteTodo = async () => {
      try {
        const response = await root.$axios.get("/api/todos/complete");
        completeTodoList.value = { ...response.data.data };
      } catch (e) {
        console.log(e);
      }
    };

    return {
      todoItem,
      todoList,
      completeTodoList,
      addTodo,
      deleteTodo,
      updateTodo,
      completeTodo,
    };
  },
});
</script>

<template>
  <div class="container">
    <section class="todo-new">
      <h1>Add todos</h1>
      <input v-model="todoItem.title" type="text" placeholder="todoを記入" />
      <button @click="addTodo()">Todoを追加</button>
    </section>

    <section class="todo-index">
      <h1>Incomplete todos</h1>
      <ul>
        <li v-for="(todo, i) in todoList" :key="i">
          <input
            class="item"
            type="checkbox"
            :checked="todo.isDone"
            @change="completeTodo(todo)"
          />
          <input
            class="item"
            type="text"
            v-model="todo.title"
            @change="updateTodo(i, todo)"
          />
          <button @click="deleteTodo(todo.id)">削除する</button>
        </li>
      </ul>
    </section>

    <section class="todo-complete">
      <h1>Complete todos</h1>
      <ul>
        <li v-for="(todo, i) in completeTodoList" :key="i">
          <input
            class="item"
            type="checkbox"
            :checked="todo.isDone"
            @change="completeTodo(todo)"
          />
          {{ todo.title }}
          <button @click="deleteTodo(todo.id)">削除する</button>
        </li>
      </ul>
    </section>
  </div>
</template>

<style>
.container {
  margin: 80px auto;
  min-height: 100vh;
  text-align: center;
}

section {
  margin-bottom: 30px;
}

.item {
  font-size: 1rem;
  margin: 0 10x;
}

li {
  list-style: none;
  margin-bottom: 0.5em;
}
</style>

実際に動作させてみる

docker-compose upさせて, localhost:8000 にアクセスすると以下のような画面になると思います。

スクリーンショット 2020-12-09 10.36.16.png

実際にtodoを追加/編集/削除してみてください。

まとめ

Dockerfileを1から書いたのも初めてだったので良い勉強になりました。
Nuxt.jsに関しては知らないことしかないので勉強していきます。
「ここのコードもっとこうした方がいいよ!」というのがあればぜひアドバイスお願いします。

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

[Rails]resources, member, collection ルーティング/名前付きパス/URLの覚え方[初学者向け]

はじめに

初めまして。ながえもんと申します。
3ヶ月超かけて、ようやくRailsチュートリアルと言う山を登り切ったので、

  • resourcesで生成されるRESTfulな7つのルーティング
  • memberメソッド、collectionメソッドで生成される任意ルーティング
  • それらのアクション名、URL、名前付きパス

についてまとめました。

resourcesで生成されるパス/URLは非常に便利な一方で、
「あれ、名前付きパスはuser_path?users_path?」「URLは/users? /user? /:id?」と混乱する場面が多く、何となくモヤモヤしたままチュートリアルを一周してしまった方も多いはずです(…よね?)。

そんな初学者仲間の皆様の参考になれば幸いです。

[環境]
ruby 2.6.3
Rails 6.0.3.4

RESTfulな7つのルーティング(基礎)

リソース名は、(例によって)users リソースとします。

routes.rb
resources :users

このコードを実装すると生成される7つのルーティングは、
アクション順でみると

action名 HTTPreq URL 名前付きパス
users#index GET /users users_path
users#show GET /users/:id user_path
users#new GET /users/:id/new new_user_path
users#create POST /users users_path
users#edit GET /users/:id/edit edit_user_path
users#update PATCH/PUT /users/:id user_path
users#destroy DELETE /users/:id user_path

こうですね。
(※アクション名は コントローラ名#アクション名 と言う記法に倣っています)

7つのアクション名とその順番に関しては、…頑張って覚えましょう。
ちょっとした覚えるコツとしては、
indexとshowはペア。indexはuserの集合(全体)を、showはuserの個体を「表示する機能」としてまとめて覚えましょう。
newとcreateもペア。newテンプレートのフォームに入力した情報を基に、createアクションに繋げてuserインスタンスを生成する事が多いでしょう。
editとupdateもペア。editテンプレートのフォームに入力した情報を基に、updateアクションに繋げてuserインスタンスの情報を更新する事が多いでしょう。
最後にdestroy。これはペアがいない孤独なアクション君です。

URLと名前付きパスの関係

次に、URLや名前付きパスの2つの命名ルールを理解しましょう。

【ルール①】 /:id の有無で分類
まず前提として、URLは/users で始まります。ここは複数形で統一です。
その後ろに/:idが有るか無いかで分類します。

/:idなしの時→ 名前付きパスは「複数形」のusers_path
/:idありの時→ 名前付きパスは「単数形」のuser_path

【ルール②】 URLの後ろに付くオプションは、名前付きパスでは文頭に
ここで言うオプションとは、

/users/○○
/users/:id/○○

上の○○部分。

このパターンのURLの名前付きパスはルール①も踏まえて

オプション名_users_path または
オプション名_user_path となります。

このように、頭にオプション名がくると言うルールで命名されています。

以上の2つのルールが分かれば、
/users/:id/new の名前付きパスは new_user_path

同様に
/users/:id/edit の名前付きパスは edit_user_path

となる事が理解できますね。

(この法則は、後で出てくるmemberやcollectionで生成する任意ルーティングにも当てはまりますので、覚えておきましょう!)

名前付きパスから7つのルーティングを再整理

今度は、今覚えた名前付きパスごとにルーティングを再整理しましょう。

名前付きパス HTTPreq URL 反応するaction
users_path GET /users users#index
POST /users users#create
new_user_path GET /users/:id/new users#new
edit_user_path GET /users/:id/edit users#edit
user_path GET /users/:id users#show
PATCH/PUT /users/:id users#update
DELETE /users/:id users#destroy

こうなります。

indexとcreateの2つがusers_path。
これはuserの集合(コレクション)に対するアクションと覚えましょう。
index:コレクション全体を表示する
create:コレクション全体に、1個インスタンスを加える

あとの5つはuser_pathがベースになります。
newとeditは先ほど見たようにオプション付きの名前付きパスですね。

このように2つの方向から整理すると、だいぶ理解も深まって来たでしょうか。

ちなみに、$ rails routes を実行した際にはこちらの「名前付きパスごと」にルーティングが表示されるので、じっくり眺めてみると更に理解が深まるでしょう。

memberメソッドとcollectionメソッド

次に、usersリソースに任意のルーティングを加えるmemberメソッド、collectionメソッド
この2つのメソッドが生成するURLと名前付きパスについても見ていきましょう。

これらは、resources :users にブロックとして渡します(ネストします)。

routes.rb
resources :users do
  member do
    get :foo, :bar
  end
  collection do
    get :hogehoge
  end
end

各メソッドの特性:
memberメソッドは、「user_path」に対してアクションを追加。
collectionメソッドは、「users_path」に対してアクションを追加。

メソッドの書き方:
get :foo, :bar の行に注目して下さい。
「HTTPリクエストの型 :任意アクション名, :任意アクション名 」と言う書き方になります。
※アクション名は複数渡すことも可能です。

上の例では、users#foo, users#bar, users#hogehoge
と言う3つのアクション(メソッド)が生成されました。

各アクションに対応するルーティングは、

『アクション名=URLや名前付きパスのオプション』

と考えると分かりやすいです。
オプションですから、URLでは「後ろ」に、名前付きパスでは「文頭」に来るルールでしたね。

生成されたfooアクション、barアクション、hogehogeアクションを含めて
名前付きパスごとにルーティングを整理し直します。

今の皆様方であれば、なぜこのような名前付きパスになるのか。
なぜこのようなURLなのか、理解できるはずです。

名前付きパス HTTPreq URL 反応するaction
hogehoge_users_path GET /users/hogehoge users#hogehoge
foo_user_path GET /users/:id/foo users#foo
bar_user_path GET /users/:id/bar users#bar
users_path GET /users users#index
POST /users users#create
new_user_path GET /users/:id/new users#new
edit_user_path GET /users/:id/edit users#edit
user_path GET /users/:id users#show
PATCH/PUT /users/:id users#update
DELETE /users/:id users#destroy

以上となります。

最後までお付き合い頂きありがとうございました。
拙い内容だったかと思いますが、
本記事が少しでも名前付きパスで混乱していた方の助けになれば幸いです。

またよろしくお願いいたします:runner:

ながえもん

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

railsのArrayでnilと空白文字列の配列を除去して連結して表示する

色々なメソッドの結果を1つの文字列として出したかったが、メソッドの返り値には nil のケースと "" のケースの2種類が存在していた。
railsのArrayの #compactnil しか省いていくれない。
reject を使って省いた。

pry(main)> ["メソッドA", nil, "", "メソッドB", "メソッドC"].reject { |e| e.to_s.empty? }.join('、')
=> "メソッドA、メソッドB、メソッドC"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】HTTPリクエストでJSONが返る外部APIを使う

やりたいこと

HTTPリクエストをしてJSONが返る外部APIをRubyで使いたい。
JSONからRubyで使えるハッシュを取得したい。

 結論

#各種ライブラリを読み込む
require 'net/http'
require 'json'
require 'uri'

#uriライブラリ>URIモジュール
#該当するインスタンスを生成して返す
uri = URI.parse('http://hoge.com')
# => #<URI::HTTP http://hoge.com>

#net/httpライブラリ>Net::HTTPクラス
#URL先にGETリクエストを送り、そのボディを"文字列"として返す。
json = Net::HTTP.get(uri)

#josonライブラリ>JSONモジュール
#取得したJSON形式の文字列を、Rubyオブジェクトに変換して返す
JSON.parse(json)

以上で、URLにGETリクエストを送り、JSON形式のデータをRubyで使えるハッシュを取得できる。

公式リファレンス

URIモジュール
Net::HTTPクラス
JSONモジュール

JSONとは

JavaScript Object Notation(JSON、ジェイソン)はデータ記述言語の1つである。軽量なテキストベースのデータ交換用フォーマットでありプログラミング言語を問わず利用できる[1]。名称と構文はJavaScriptにおけるオブジェクトの表記法に由来する。
https://ja.wikipedia.org/wiki/JavaScript_Object_Notation

JavaScript だけではなく、Java, PHP, Ruby, Python など、様々な言語間のデータ交換、特に Ajax や REST API などで使用されています。
これまでは、共通データ定義言語として XML が利用されてきましたが、現在では、簡易的な JSON が利用されるケースが増えてきています。
http://www.tohoho-web.com/ex/json.html

異なる言語間でデータをやり取りするための記述形式で、記法はJavaScriptに由来する。
以前は、データをやり取りする時はXMLが使われおり、今はJSONが主流となっている。

JSONのルール

キーと値にコロンがつく。
文字列は""ダブルクォーテーションで囲む。
{ "hoge": "fuga" }

シングルクォーテーションは使用できない。
{ 'hoge': 'fuga' }

カンマを使うことで、複数のキーと値を指定することができる
{ "hoge": "fuga", "foo": 123 }

配列や値のみでもOK!
["hoge","fuga"]
"hoge"
123

まとめ

APIからJSONを取得し、Rubyで使うやり方とJSONについて、簡単にまとめました。

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

doorkeeper gemの導入手順

doorkeeper gemの用途

doorkeeperはOAuthのプロバイダ機能を提供するためのgemです。
例えば公開用のAPIを作成した場合にアクセストークンを発行するためOAuth2.0で認可しトークン発行させるなどで使用します。

この記事の注意点

今回は実際にdoorkeeperを導入してOAuthプロバイダとして機能する確認までを行いますが細かな設定は省略しています。
実際に業務で開発するアプリケーションではroutingの設定や画面のカスタマイズ、doorkeeper.rbのカスタマイズなどもっと丁寧に設定する必要があるのでご注意ください。

ドキュメントについて

以下を参考にdoorkeeperの導入を行いました。
doorkeeper gem はドキュメントが整っているので導入しやすいと思います。
https://github.com/doorkeeper-gem/doorkeeper
https://github.com/doorkeeper-gem/doorkeeper/wiki
https://doorkeeper.gitbook.io/guides/

doorkeeper導入環境

今回は以下の環境のrailsアプリケーションが既にある想定で進めていきます。

rails: 6.0.3.4
ruby: 2.7.1

その他gem:
devise(4.7.3)

userテーブルの作成

今回userテーブルを事前に用意してログイン周りはdevise gemで管理します。
userテーブルのschemaは以下になります。
(今回Doorkeeperの導入のために作成した簡易的なテーブルです)

create_table :users do |t|
  ## Database authenticatable
  t.string :email,              null: false, default: ""
  t.string :encrypted_password, null: false, default: ""

  ## Recoverable
  t.string   :reset_password_token
  t.datetime :reset_password_sent_at

  ## Rememberable
  t.datetime :remember_created_at

  # Trackable
  t.integer  :sign_in_count, default: 0, null: false
  t.datetime :current_sign_in_at
  t.datetime :last_sign_in_at
  t.string   :current_sign_in_ip
  t.string   :last_sign_in_ip

  t.string   :fullname
  t.string   :fullname_ja
  t.boolean  :admin, null: false, default: false

  t.timestamps null: false
end

add_index :users, :email,                unique: true
add_index :users, :reset_password_token, unique: true

gemを追加

Gemfileへ以下を追加しbundle installします。

gem 'doorkeeper'
bundle install

現時点(2020/12/01)では以下のバージョンがinstallされました。

doorkeeper (5.4.0)

 doorkeeperの環境をセットアップ

doorkeeperをinstallします。

rails generate doorkeeper:install

実行結果

      create  config/initializers/doorkeeper.rb
      create  config/locales/doorkeeper.en.yml
       route  use_doorkeeper
===============================================================================

There is a setup that you need to do before you can use doorkeeper.

Step 1.
Go to config/initializers/doorkeeper.rb and configure
resource_owner_authenticator block.

Step 2.
Choose the ORM:

If you want to use ActiveRecord run:

  rails generate doorkeeper:migration

And run

  rake db:migrate

Step 3.
That's it, that's all. Enjoy!

===============================================================================

以下の2つのファイルの作成とdoorkeeperを使うためのroutingが生成されます。

config/initializers/doorkeeper.rb
config/locales/doorkeeper.en.yml

migrationファイル作成

次にOAuth Applications、 Access Grants、 Access Tokensのテーブル用のmigrationファイルを作成するために以下を実行します。

bundle exec rails generate doorkeeper:migration

実行結果

create  db/migrate/20201201064749_create_doorkeeper_tables.rb

db/migrate/yyyymmddhhmmss_create_doorkeeper_tables.rbファイルが作成されるのでこれを編集していきます。
今回はuser model が resource ownerとなるように設定していきます。

# frozen_string_literal: true

class CreateDoorkeeperTables < ActiveRecord::Migration[6.0]
  def change
    create_table :oauth_applications do |t|
      t.string  :name,    null: false
      t.string  :uid,     null: false
      t.string  :secret,  null: false

      # Remove `null: false` if you are planning to use grant flows
      # that doesn't require redirect URI to be used during authorization
      # like Client Credentials flow or Resource Owner Password.
      t.text    :redirect_uri, null: false
      t.string  :scopes,       null: false, default: ''
      t.boolean :confidential, null: false, default: true
      t.timestamps             null: false
    end

    add_index :oauth_applications, :uid, unique: true

    create_table :oauth_access_grants do |t|
      t.references :resource_owner,  null: false
      t.references :application,     null: false
      t.string   :token,             null: false
      t.integer  :expires_in,        null: false
      t.text     :redirect_uri,      null: false
      t.datetime :created_at,        null: false
      t.datetime :revoked_at
      t.string   :scopes,            null: false, default: ''
    end

    add_index :oauth_access_grants, :token, unique: true
    add_foreign_key(
      :oauth_access_grants,
      :oauth_applications,
      column: :application_id
    )

    create_table :oauth_access_tokens do |t|
      t.references :resource_owner, index: true

      # Remove `null: false` if you are planning to use Password
      # Credentials Grant flow that doesn't require an application.
      t.references :application,    null: false

      # If you use a custom token generator you may need to change this column
      # from string to text, so that it accepts tokens larger than 255
      # characters. More info on custom token generators in:
      # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator
      #
      # t.text :token, null: false
      t.string :token, null: false

      t.string   :refresh_token
      t.integer  :expires_in
      t.datetime :revoked_at
      t.datetime :created_at, null: false
      t.string   :scopes

      # The authorization server MAY issue a new refresh token, in which case
      # *the client MUST discard the old refresh token* and replace it with the
      # new refresh token. The authorization server MAY revoke the old
      # refresh token after issuing a new refresh token to the client.
      # @see https://tools.ietf.org/html/rfc6749#section-6
      #
      # Doorkeeper implementation: if there is a `previous_refresh_token` column,
      # refresh tokens will be revoked after a related access token is used.
      # If there is no `previous_refresh_token` column, previous tokens are
      # revoked as soon as a new access token is created.
      #
      # Comment out this line if you want refresh tokens to be instantly
      # revoked after use.
      t.string   :previous_refresh_token, null: false, default: ""
    end

    add_index :oauth_access_tokens, :token, unique: true
    add_index :oauth_access_tokens, :refresh_token, unique: true
    add_foreign_key(
      :oauth_access_tokens,
      :oauth_applications,
      column: :application_id
    )

    # userがresource_ownerとなるようにしてコメントアウトを外します。
    # Uncomment below to ensure a valid reference to the resource owner's table
    # add_foreign_key :oauth_access_grants, <model>, column: :resource_owner_id
    # add_foreign_key :oauth_access_tokens, <model>, column: :resource_owner_id
    ↓↓↓↓↓
    add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id
    add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id
  end
end

migrationを実行します。

rails db:migrate RAILS_ENV=development

次の3つのテーブルが作成されます。

oauth_applications
oauth_access_grants
oauth_access_tokens

doorkeeper.rb の設定

次にdoorkeeperの設定ファイルである config/initializers/doorkeeper.rb を編集していきます。
設定方法は
https://doorkeeper.gitbook.io/guides/ruby-on-rails/configuration
を参考に進めていきます。

編集した箇所のみ記載しています。

# frozen_string_literal: true

Doorkeeper.configure do

 〜〜略〜〜

  # このブロックは、resource owner が認証されているかどうかを確認するために呼び出されます。
  # 今回はdeviseを使用しているので 
  # https://doorkeeper.gitbook.io/guides/ruby-on-rails/configuration 
  # を参考に修正します。
  # This block will be called to check whether the resource owner is authenticated or not.
  resource_owner_authenticator do
    raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"
    # Put your resource owner authentication logic here.
    # Example implementation:
    #   User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)
  end
 ↓↓↓↓
  # 以下のように修正します。
  resource_owner_authenticator do
    current_user || warden.authenticate!(scope: :user)
  end

  # ここではoauth application のへのアクセスを制御することができます。
  # 今回はusersテーブルへadminフラグを追加してadminフラグが立っている場合にのみアクセスできるようにするため
  # コメントアウト部分を外します。
  # admin以外のuserがアクセスした場合は 403 Forbidden レスポンスを返します。
  # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb
  # file then you need to declare this block in order to restrict access to the web interface for
  # adding oauth authorized applications. In other case it will return 403 Forbidden response
  # every time somebody will try to access the admin web interface.
  #
  # admin_authenticator do
  #   # Put your admin authentication logic here.
  #   # Example implementation:
  #
  #   if current_user
  #     head :forbidden unless current_user.admin?
  #   else
  #     redirect_to sign_in_url
  #   end
  # end
 ↓↓↓↓
  admin_authenticator do
    # Put your admin authentication logic here.
    # Example implementation:

    if current_user
      head :forbidden unless current_user.admin?
    else
      redirect_to new_user_session_url
    end
  end

 〜〜略〜〜

  # access_token_expires_in オプション
  # アクセストークンの有効期限の設定です。
  # defaultでは2時間。変更する場合はコメントアウトを外して編集します。
  # 有効期限を無効にする場合は、これを「nil」に設定します。
  #
  # 今回は24時間へ変更します。
  # Access token expiration time (default: 2 hours).
  # If you want to disable expiration, set this to `nil`.
  #
  # access_token_expires_in 2.hours
  access_token_expires_in 24.hours

 〜〜略〜〜

  # scope
  # プロバイダのアクセストークンスコープの定義
  # 詳細については、次のサイトを参照してください。
  # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes
  # 
  # 今回は 
  # default_scopes  :public と
  # optional_scopes :write
  #  を設定しておきます。
  # Define access token scopes for your provider
  # For more information go to
  # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes
  #
  # default_scopes  :public
  # optional_scopes :write, :update
  ↓↓↓↓
  default_scopes  :public
  optional_scopes :write

  # enforce_configured_scopes オプション
  # 「default_scopes」や「optional_scopes」にない任意のスコープでのアプリケーションの作成や更新を禁止します。
  # (デフォルトでは禁止しない)
  # 
  # 使用するためコメントアウトを外します。
  # Forbids creating/updating applications with arbitrary scopes that are
  # not in configuration, i.e. +default_scopes+ or +optional_scopes+.
  # (disabled by default)
  #
  # enforce_configured_scopes
  ↓↓↓↓
  enforce_configured_scopes

  # force_ssl_in_redirect_uri オプション
  # ネイティブではないリダイレクト用の uris で HTTPS プロトコルを強制的に使用します。
  # (development環境以外ではデフォルトで有効になっています)
  # OAuth2 は通信のセキュリティを HTTPS プロトコルに委譲するので、これを有効にしておくのが賢明です。
  # proc, lambda, block などの呼び出し可能なオブジェクトは、
  # 条件付きチェックを可能にするために使用することができます 
  # (例えば、localhostへの非SSLリダイレクトを許可するなど)
  #
  # 今回は使用するため以下の設定を行います。
  # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled
  # by default in non-development environments). OAuth2 delegates security in
  # communication to the HTTPS protocol so it is wise to keep this enabled.
  #
  # Callable objects such as proc, lambda, block or any object that responds to
  # #call can be used in order to allow conditional checks (to allow non-SSL
  # redirects to localhost for example).
  #
  # force_ssl_in_redirect_uri !Rails.env.development?
  #
  # force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' }
  ↓↓↓↓
  #development, test環境ではSSL以外でも許容し、それ以外はSSLのみ許可します。
  force_ssl_in_redirect_uri !(Rails.env.development? || Rails.env.test?)

  #  Client Credentials や Resource Owner Password Credentialsのような
  # URI の無い OAuth グラントフローを使用するように Doorkeeper が設定されている場合に、
  # アプリケーションに空のリダイレクト URI を設定できるようにします。
  # このオプションはデフォルトではオンに設定されており、設定されたグラントの種類をチェックしますが、
  # データベーステーブル「oauth_applications」の「redirect_uri」カラムから「NOT NULL」制約を
  # 手動で削除する必要があります。
  # この機能を完全に無効にするには allow_blank_redirect_uri false のコメントアウトを外すか
  # カスタムチェックを定義することができます。
  # 
  # 今回はredirect_uri にNULLを許可しないのでコメントアウトを外します。
  # Allows to set blank redirect URIs for Applications in case Doorkeeper configured
  # to use URI-less OAuth grant flows like Client Credentials or Resource Owner
  # Password Credentials. The option is on by default and checks configured grant
  # types, but you **need** to manually drop `NOT NULL` constraint from `redirect_uri`
  # column for `oauth_applications` database table.
  #
  # You can completely disable this feature with:
  #
  # allow_blank_redirect_uri false
  #
  # Or you can define your custom check:
  #
  # allow_blank_redirect_uri do |grant_flows, client|
  #   client.superapp?
  # end
  ↓↓↓↓
  allow_blank_redirect_uri false

  # どのようなgrant flowsを有効にするかを文字列の配列で指定します。
  # 有効な文字列と有効なフローは以下の通りです。
  # 
  # 今回は authorization_code のみ許可するため以下のように記述します。
  # Specify what grant flows are enabled in array of Strings. The valid
  # strings and the flows they enable are:
  #
  # "authorization_code" => Authorization Code Grant Flow
  # "implicit"           => Implicit Grant Flow
  # "password"           => Resource Owner Password Credentials Grant Flow
  # "client_credentials" => Client Credentials Grant Flow
  #
  # If not specified, Doorkeeper enables authorization_code and
  # client_credentials.
  #
  # implicit and password grant flows have risks that you should understand
  # before enabling:
  #   http://tools.ietf.org/html/rfc6819#section-4.4.2
  #   http://tools.ietf.org/html/rfc6819#section-4.4.3
  #
  # grant_flows %w[authorization_code client_credentials]
  ↓↓↓↓
  grant_flows %w[authorization_code]

end

OAuthアプリケーションの作成

(※)事前にadminフラグの付いたアカウントと付いていないアカウントを2つ作っておきます。

> rails s

で local Serverを起動します。

http://localhost:3000/users/sign_in

へアクセスしadminフラグが付いていないアカウントでログインし

http://localhost:3000/oauth/applications

へアクセスするとstatus 403でアクセスできないことを確認します。

今後はadminフラグが付いているアカウントでログインし

http://localhost:3000/oauth/applications

へアクセスすると以下の画面が表示されると思います。
01.jpg

[New Application]ボタンをクリックして
OAuthのアプリケーションを作成していきます。

Name: ExampleApp
Redirect URI: http://localhost:3001/callback
Confidential: ON
Scopes: public write

を設定して[Submit]をクリックします。

02.jpg

OAuthアプリケーションが作成され以下のような画面が表示されると思います。
03.jpg

実際に作成されたデータを rails consoleで見てみると以下のようなデータが存在することが分かります。

irb(main):003:0> Doorkeeper::Application.all
=> #<ActiveRecord::Relation [#<Doorkeeper::Application id: 1, name: "ExampleApp", uid: "4y5OWnkpJM9wHwk6F9LHTKBB4Daz0x_3cwDdeqCcQD4", secret: "7Ufjfqcdt36rSkvx8PLHVRSHh-IL2KLk_9v1Tp4XBFs", redirect_uri: "http://localhost:3001/callback", scopes: "public write", confidential: true, created_at: "2020-12-14 17:44:08", updated_at: "2020-12-14 17:44:08">]>
irb(main):009:0>

続いてアクセストークンの発行テストをするためにはDoorkeeperのClientが必要なので↓のclientを使用します。

https://github.com/Nobuo-Hirai/sample_oauth_client

以下の手順でセットアップしていきます。

git clone git@github.com:Nobuo-Hirai/sample_oauth_client.git

cd sample_oauth_client

bundle config set --local path 'vendor/bundle'
bundle install

# .env.developmentファイルを作成します。
touch .env.development

# .env.developmentファイルへ以下の内容を記述します。
# 先ほどOAuthプロバイダの方で作成したOAuthアプリケーションのUIDとSECRET、REDIRECT URIなどを記述します。
## ID for your app registered at the provider
CLIENT_ID=4y5OWnkpJM9wHwk6F9LHTKBB4Daz0x_3cwDdeqCcQD4
## Secret
CLIENT_SECRET=7Ufjfqcdt36rSkvx8PLHVRSHh-IL2KLk_9v1Tp4XBFs

## URL to the provider
SITE=http://localhost:3000/
AUTHORIZE_URL=oauth/authorize
CALLBACK_URI=http://localhost:3001/callback
OAUTH_PROVIDER_URL=http://localhost:3000/oauth/authorize
TOKEN_URL=oauth/token
SCOPE=public write

# port 3001で起動します。
rails s -p 3001

http://localhost:3001
へアクセスすると下の画面が表示されるので[Authorize]をクリックします。

http://localhost:3000/
の認可画面が表示されると思います。
これは[public write]という権限を持ったアクセストークンの発行に同意するかどうかという意味になります。
[Authorize]をクリックします。
05.jpg

http://localhost:3001/callback
の画面が表示されアクセストークンが表示されたと思います。
06.jpg

認可画面で[Authorize]をクリック後にcallbackURLへ認可コードパラメータが返ってくるのでそれをもとにアクセストークンを取得しています。

# 認可コードよりaccess_tokenを取得
  def callback
    client = OAuth2::Client.new(
      ENV["CLIENT_ID"],
      ENV["CLIENT_SECRET"],
      site: ENV["SITE"],
      authorize_url: ENV["AUTHORIZE_URL"],
      token_url: ENV["TOKEN_URL"],
    )

    @access_token = client.auth_code.get_token(
      params[:code],
      redirect_uri: ENV["CALLBACK_URI"],
    )
  end

実際に作成されたAccessGrantとAccessTokenを rails consoleで確認する場合は以下のような感じで確認できます。

irb(main):010:0> Doorkeeper::AccessGrant.all
=> #<ActiveRecord::Relation [#<Doorkeeper::AccessGrant id: 1, resource_owner_id: 1, application_id: 1, token: "j0UY2MVee0mW9RmjVhPTZ6hY7yzw44nM5YOS51ZhxUk", expires_in: 600, redirect_uri: "http://localhost:3001/callback", created_at: "2020-12-14 17:54:24", revoked_at: "2020-12-14 17:54:24", scopes: "public write">]>

irb(main):011:0> Doorkeeper::AccessToken.all
=> #<ActiveRecord::Relation [#<Doorkeeper::AccessToken id: 1, resource_owner_id: 1, application_id: 1, token: "l2wm7_P1Fk2ZZEAI5jhEFWfsJYUzbZYk_QcpWKZCSFU", refresh_token: nil, expires_in: 86400, revoked_at: nil, created_at: "2020-12-14 17:54:24", scopes: "public write", previous_refresh_token: "">]>

最後に

doorkeeper gemはドキュメントが豊富なため導入のハードルは低いと思います。
ただdoorkeeper.rbで設定できるオプションが多いため全て確認するには非常に時間がかかります。
doorkeeper.rbでの設定がこのgemのポイントなので色々試してみようと思います。

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

プログラミングは神になれる。

プログラミングの楽しさについて書きます。

それはオブジェクト思考がまるで生き物を作る神になった気分になれるところです。

そのコードの例が下記になります。

class Human
  @name
  @age
  @wanryoku

  def walk
  end

  def eat
  end

  def age= num
    if num < 0
      puts 
      @age = 0
      return 
    end
    @age = num

  end

end

class SuperMan < Human
  def me_kara_beam
  end
end


human = Human.new
human.walk #ok
human.me_kara_beam #error
spMan = SuperMan.new
spMan.walk #ok
spMan.me_kara_beam #ok

これは、ヒューマンは歩くことができるが、目からビームが出せない
スーパーマン(spMan)は歩くこと、目からビームを出すことができる
という処理です。

一見難しそうに見えるプログラミングも、こうみると面白いのかなとおもったり。

日々の学習をドキュメントに残してるので、とっても暇な時みてみてください。
https://www.notion.so/Ruby-Ruby-on-Rails-84f5c88189474fa4aae5e00cba856226

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