20191004のRailsに関する記事は24件です。

railsでscopeがnilを返すとallが返る

railsのscope

  • 文字通り、そのクラスの一部のインスタンスを返します
  • 意外と知られてないですが、nilを返すとallが返ります
  • クラスメソッドと全く同じ挙動ではない。classメソッドはnilを返せばnilが返る

scope

[1] pry(main)> class User < ApplicationRecord
[1] pry(main)*   scope :nil_scope, ->() {  
[1] pry(main)*     nil    
[1] pry(main)*   }    
[1] pry(main)* end  
=> :nil_scope
[2] pry(main)> User.nil_scope
  User Load (4.0ms)  SELECT `users`.* FROM `users`
=> [#<User id: 1, email: "xxx@gmail.com", created_at: "2018-04-14 12:05:50", updated_at: "2019-09-05 01:01:24", provider: "facebook", uid: ...]

pry(main)> User.nil_scope.size
   (0.9ms)  SELECT COUNT(*) FROM `users`
=> 355

class method

[1] pry(main)> class User < ApplicationRecord
[1] pry(main)*   def self.nil_scope
[1] pry(main)*     nil
[1] pry(main)*   end  
[1] pry(main)* end  
=> :nil_scope
[2] pry(main)> User.nil_scope
=> nil
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

turbolinksをfalseのページとtrueのページに分ける

こんばんは!
ポートフォリオ作成中にハマったエラーの簡単な解決方法を備忘録として。

いいね機能をremote: trueで動かす

railsチュートリアルにて学習したremote: trueを使ったajax通信。jsが苦手な私にぴったり!

turbolinksが必要みたい

Uncaught ReferenceError: Turbolinks is not defined

application.js
/=require turbolinks

demo

動きます。

次はこっちが動かないのね

<%= link_to "新規投稿", new_meal_path, class:'hoge'%>に遷移してfile_fieldに画像添付したらimageがpreview表示されなくなった

リロードすれば正常に動作します

demo

解決方法

index.html.erb
<%= link_to "新規投稿", new_meal_path, class:'hoge',data: {"turbolinks"=>false} %>


data: {“turbolinks" => false}は遷移先ページのみturbolinksを切ることが出来ます

まとめ

jsファイルにajax通信を記述すればturbolinksがtrueでも問題ないみたいです

remote: trueを使ったajax通信とjsイベントを両方実装している時にどちらかのイベント発火しない場合があればdata: {"turbolinks"=>false}が役立ちます

終わり

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

[Rails]動画プレビュー機能

はじめに

今回以下の様な動画のプレビュー機能を追加したのでその方法について、説明します。3c6162c281ea3a7829bf724726d7461d.gif

環境

Rails 5.2.3
Ruby 2.5.1

導入方法

ビューファイル

気を付ける点は、file_fieldid: "upload-image"をjQueryを使用する為に記述します。

new.html.haml
= form_for @video do |f|
      省略
 .movie-group-form__field--right--mask
   = f.label :image, for: "upload-image", class: "movie-group-form__movie" do
     = fa_icon "cloud-upload fa-spin", class: "icon"
     = f.file_field :image, class: "hidden", id: "upload-image"
     = f.label :動画を選択してください , class: "movie-group-form__choice"
 = f.submit "投稿する", class: "btn btn-dark"

jQuery

次にJSファイルに動画プレビューをさせる記述をします。
$fileFieldにビューのidに記述したupload-image#を付けて記述します。

$previewには動画をプレビュー表示させたい部分(今回は.movie-group-form__choice)に記述します。
$preview.append($('<video>').attr({内にCSSを記述してます。autoplay: "autoplay"loop: "loop"で動画のプレビューをオート再生させています。
playsinline: "true"はスマホで動画を投稿した際に、オート再生するのに使用します。

preview.js
$(document).on('turbolinks:load', function() {
  $fileField = $('#upload-image')

  $($fileField).on('change', $fileField, function(e) {

      file = e.target.files[0]
      reader = new FileReader(),
      $preview = $(".movie-group-form__choice");

      reader.onload = (function(file) {
        return function(e) {
          $preview.empty();
          $preview.append($('<video>').attr({
            src: e.target.result,
            width: "45%",
            height: "110px",
            class: "preview-image",
            autoplay: "autoplay",
            loop: "loop",
            playsinline: "true",
            title: file.name
          }));
        };
      })(file);
      reader.readAsDataURL(file);
    });
  });

今回はJS内にCSSを直接記述しているので、CSSファイルの変更は必要ないです。後は好みで調整して下さい。

まとめ

動画のプレビューも画像のプレビューとほぼ同じで、後はappendに'<video>''<img>'を記述するだけで、動画か画像のプレビュー表示かを変更する事が出来ます。気になったら試してみて下さい。

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

Rails EngineのRootからRspecを実行すると、Webpackerのエラーが出たので対応する

状況

アプリケーションが複数のUIで構成されているので、以下のような対応をしていた。分離することで、Webpackのバージョンから何やら分けることができる。

Webpackerのインスタンスをクラスメソッドで定義して、別のところから呼び出せるようにする。

module Convenience
  ROOT_PATH = Pathname.new(File.join(__dir__, ".."))

  class << self
    def webpacker
      @webpacker ||= ::Webpacker::Instance.new(
        root_path: ROOT_PATH,
        config_path: ROOT_PATH.join("config/webpacker.yml")
      )
    end
  end
end

ヘルパーに以下を記載し、先程のインスタンスを呼ぶようにすれば、stylesheet_pack_tagやjavascript_pack_tagが呼びだせる。

module Convenience
  module ApplicationHelper
    include ::Webpacker::Helper

    def current_webpacker_instance
      Convenience.webpacker
    end
  end
end

ちなみにこのメソッドは、カレントWebpackerインスタンスを返すので、先程のConvenienceの例だと、ConvenienceのWebpackerインスタンスを返すようにOverrideしてあるようだ。

module Webpacker::Helper
  # Returns current Webpacker instance.
  # Could be overridden to use multiple Webpacker
  # configurations within the same app (e.g. with engines)
  def current_webpacker_instance
    Webpacker.instance
  end

Rspecでエラー

ただ、EngineRootからrspecを実行するとエラーになるので、test環境だけ分岐する必要がある。もしかしたらもっといい方法があるのかもしれないが、一旦EngineRootから直でrspecを呼び出すという要件に合わせるためにコードを追記した。

module Convenience
  module ApplicationHelper
    include ::Webpacker::Helper

    unless Rails.env.test?
      def current_webpacker_instance
        Convenience.webpacker
      end
    end
  end
end

なお、コードは https://github.com/rails/webpacker/blob/master/docs/engines.md#using-in-rails-engines こちらからコピペしたものだと予想できる。

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

【Rails】インスタンスの状態を保ちながら入力 → 確認画面 → 保存 を実装する方法

実現したいこと

画面をまたいでフォームを入力する際、インスタンスの状態を保ちながら画面遷移したいときがあります。
例えばこんな感じで。

create_user.gif

① 各項目を入力して「確認へ」を押す

② 確認画面で確認したら「送信」を押す

③ 完了画面へリダイレクト

さらに、バリデーションにひっかかったり、戻るボタンを押しても状態を維持したいですよね。
このように、画面をまたいでもインスタンスの状態を維持するやり方について簡単に解説していきます。

実装手順

アクションにおける処理の流れは以下の通りです。

new(作成フォーム)

confirm(確認画面)

create(作成)

show(完了画面)

今回はフォームのバリデーション、戻るボタンを考慮するため、イメージはこんな感じです。

無題のプレゼンテーション_-_Google_スライド.png

次のアクションへPOSTするたびに、
・invalid(無効)なら画面を変えない
・valid(有効)なら次の画面を表示する
・back(パラメータ)が渡ってきたら前の画面に戻る
という実装をすればいいわけです。(厳密にはアクションごとに違いますがイメージです)
具体的にコードで見ていきましょう。

Route

config/routes.rb
Rails.application.routes.draw do
  resources :users, only: [:new, :create, :show] do
    collection do
      post :confirm
    end
  end
end

newcreateの流れは今まで通りですが、やはりポイントはconfirmを追加したことであり、newconfirmcreateと挟みこむ感じになります。

フォームではnewconfirmにPOSTするイメージで捉えてください.

Model

app/models/user.rb
# Table name: users
#
#  id         :integer          not null, primary key
#  first_name :string
#  last_name  :string
#  email      :string
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class User < ApplicationRecord
   with_options presence: true do
     validates :first_name
     validates :last_name
     validates :email
   end
end

今回は便宜上、空のバリデーションのみ設定しました。

Controller

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def confirm
    @user = User.new(user_params)
    render :new if @user.invalid?
  end

  def create
    @user = User.new(user_params)
    render :new and return if params[:back] || !@user.save
    redirect_to @user
  end

  def show
    @user = User.find_by(id: params[:id])
  end

  private

    def user_params
      params.require(:user).permit(:first_name, :last_name, :email)
    end
end

一番のポイントは、アクションを移動する度にインスタンス情報の入ったパラメーターを渡しているところです。
ここでいうUser.new(user_params)のとこですね。ここに全てのインスタンス情報が入っているので、確認画面へ橋渡しすれば良いわけです。

そして戻るボタンを押した時の挙動も、params[:back]で制御しています。backが渡ってきたら前の画面にrenderすればOK。

View

※ ここではslim, bootstrap, simple_formを使ってます。
slimに関してよくわからない方は、導入から文法までまとめたので、よかったらこちらを参考にしてみてください。

new

app/views/users/new.html.slim
h2 ユーザー新規登録

= simple_form_for @user, url: confirm_users_path(@user) do |f|
  = f.input :first_name
  = f.input :last_name
  = f.input :email
  = f.submit "確認画面へ", class: "btn btn-primary"

ポイントは, URL指定でconfirm_users_pathへリクエストを投げていることです。
有効であればconfirmアクションへ移動して次の確認画面ではUser.new(user_params)が表示されるので状態が維持されます。

def confirm
  @user = User.new(user_params)
  render :new if @user.invalid?
end

confirm

app/views/users/confirm.html.slim
h2 以下の詳細を確認してください

= render "detail", user: @user

= simple_form_for @user do |f|
  = f.input :first_name, as: :hidden
  = f.input :last_name, as: :hidden
  = f.input :email, as: :hidden
  = f.submit "送信", class: "btn btn-primary"
  = f.submit "戻る", name: :back, class: "btn btn-secondary"

次はconfirmからcreateにPOSTします。
前回のnewアクションから渡ってきたuser_paramsを参照してインスタンスを表示しているため、空にならずにちゃんと表示され、フォームのvalueにもちゃんと入力されてるのが確認できました。
RailsTest.png
フォームが見えても邪魔なので、input typehiddenにしておくと良いです。

戻るボタンはnameでパラメータを指定して、アクション側で存在すれば前の画面にrenderしてあげます。

= f.submit "戻る", name: :back, class: "btn btn-secondary"`
def create
  @user = User.new(user_params)
  render :new and return if params[:back] || !@user.save
  redirect_to @user
end

あとはいつも通り、作成に成功したらリダイレクトしてあげれば完了です。

app/views/users/show.html.slim
h2 ユーザーを作成しました

= render "detail", user: @user
app/views/users/_detail.html.slim
ul
  li
    | First name: #{user.first_name}
  li
    | Last name: #{user.last_name}
  li
    | Email: #{user.email}

さらに入力画面を増やしたい場合

先ほどの例に加えて、次は性別, 年齢, 電話番号, 住所を入力する画面を加えたいとしましょう。
完成デモ↓
create_user2.gif
このように、どこでどんなリクエストを送ろうと、インスタンスの状態を維持できることがゴールです。

実装手順

今回はnextアクション(新しい画面)を追加し、アクションの流れは以下のようにします。
new → next(追加) → confirm → create

Controller側のロジックは以下をイメージしてみてください。
無題のプレゼンテーション_-_Google_スライド.png

Route

config/routes.rb
Rails.application.routes.draw do
  resources :users, only: [:new, :create, :show] do
    collection do
      post :next #追加
      post :confirm
    end
  end
end

入力画面が一つ増えたので、nextを追加してあげましょう。
先ほどと同様、newアクションからnextアクションへPOSTしてあげるイメージです。

Model

app/models/user.rb
# Table name: users
#
#  id           :integer          not null, primary key
#  first_name   :string
#  last_name    :string
#  email        :string
#  gender       :integer
#  age          :integer
#  phone_number :integer
#  address      :string
#  created_at   :datetime         not null
#  updated_at   :datetime         not null
#

class User < ApplicationRecord
   with_options presence: true do
     validates :first_name
     validates :last_name
     validates :email
   end

   #追加
   with_options on: :confirm do
     validates_presence_of :gender
     validates_presence_of :age, presence: true
     validates_presence_of :phone_number, presence: true
     validates_presence_of :address, presence: true
   end

   enum gender: { man: 0, woman: 1, gay: 2, bisexual: 3, transgender: 4 }
end

on: :confirmを記述することで、invalid?(context: :confirm)とした時のみ、カラムの有無を検証できるようにしています。
これは最初のnew画面のみ、次画面のカラムのバリデーションをスキップするためです。

Controller

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end

  # 追加
  def next
    @user = User.new(user_params)
    render :new if @user.invalid?
  end

  # 変更
  def confirm
    @user = User.new(user_params)
    render :new and return if params[:back]
    render :next if @user.invalid?(:confirm)
  end

  # 変更
  def create
    @user = User.new(user_params)
    render :next and return if params[:back]
    render :confirm and return if !@user.save
    redirect_to @user
  end

  def show
    @user = User.find_by(id: params[:id])
  end

  private

    # 変更
    def user_params
      params.require(:user).permit(:first_name, :last_name, :email, :age, :gender, :phone_number, :address)
    end
end

カラムとアクションが増えただけで、やってること自体は最初に実装したものとほとんど同じなのが分かると思います。
基本的には、渡ってくるパラメーターの状態によって画面遷移を条件分岐しています。

View

new

app/views/users/new.html.slim
h2 ユーザー新規登録(1)

= simple_form_for @user, url: next_users_path(@user) do |f|
  = f.input :first_name
  = f.input :last_name
  = f.input :email
  = f.input :age, as: :hidden
  = f.input :gender, as: :hidden
  = f.input :phone_number, as: :hidden
  = f.input :address, as: :hidden
  = f.submit "次へ", class: "btn btn-primary"

newnextへPOSTするために、URLを直接指定するようにしましょう。

ここでのポイントは、状態管理したい全カラムを送信することです。
これを書くことによって、入力→戻る→次へとしたときにも、入力した値が常に維持されます。
例えば、nextで入力→newに戻る→nextへ遷移としたときに、nextで入力した値が維持されます。
create_user3.gif
これを各アクションでも適応してあげればユーザービリティは向上するでしょう。

next

app/views/users/next.html.slim
h2 ユーザー新規登録(2)

= simple_form_for @user, url: confirm_users_path(@user) do |f|
  = f.input :first_name, as: :hidden
  = f.input :last_name, as: :hidden
  = f.input :email, as: :hidden
  = f.input :age
  = f.input :gender, collection: User.genders.keys
  = f.input :phone_number, as: :tel
  = f.input :address
  = f.submit "確認画面へ", class: "btn btn-primary"
  = f.submit "戻る", name: :back, class: "btn btn-secondary"

これも先ほど同様、nextconfirmへPOSTするため、URLを直指定しています。

confirm

app/views/users/confirm.html.slim
h2 以下の詳細を確認してください

= render "detail", user: @user

= simple_form_for @user do |f|
  = f.input :first_name, as: :hidden
  = f.input :last_name, as: :hidden
  = f.input :email, as: :hidden
  = f.input :age, as: :hidden
  = f.input :gender, as: :hidden
  = f.input :phone_number, as: :hidden
  = f.input :address, as: :hidden
  = f.submit "次へ", class: "btn btn-primary"
  = f.submit "戻る", name: :back, class: "btn btn-secondary"

最後はバケツリレーで維持してきたインスタンスからvalueを送信してあげればOK。

app/views/users/_detail.html.slim
ul
  li
    | First name: #{user.first_name}
  li
    | Last name: #{user.last_name}
  li
    | Email: #{user.email}
  li
    | Age: #{user.age}
  li
    | Age: #{user.gender}
  li
    | PhoneNumber: #{user.phone_number}
  li
    | Address: #{user.address}

お疲れ様でした。

まとめ

アクションをまたいでインスタンスの状態を維持するにあたり、色々考えることが多かったです。
初めのころはlink_toから長ったらしいインスタンスのパラメーターを送ったり、sessionで状態を管理したりと割と強引なやり方で実装していました。

しかし今回紹介した、渡ってきたパラメーターからインスタンスを生成し、バケツリレー式に状態を管理するというやり方であれば、比較的簡単に実装できることに気づきました。

長いフォーム画面を分割したいときや、決済フォームを挟んだりするときに使える手法なので、ぜひ参考にしてみてください。

参考記事
https://kossy-web-engineer.hatenablog.com/entry/2018/10/19/063937
https://remonote.jp/rails-confirm-form

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

Rails エラーメッセージの出し方 バリデーションエラー

はじめに

DBに値を保存する際(ユーザー登録やツイートの登録等)のバリデーションエラー文を表示するやり方について備忘録のために記載しておきます。
参考程度にどうぞ。

バリデーションとは?

バリデーションとはDBに値を保存する際に何か値が抜けていると保存できなくするものです。
簡単に言えばフィルターみたいなものですね。

例えばユーザー登録時にpasswordを入力してもらうとします。passwordが「111」とかだったらセキュリティーが甘くなりますね。
そこでバリデーションを記載してpasswordを8文字以上にしないと登録できないようにすれば、「111」と入力しても弾かれて保存できないようになります。
バリデーションの組み方は無数にあり、やり方を変えれば自分の思い通りに値を保存することができます。

例1)
モデルに下記のように記載すれば空では保存できません。

product.rb
class User < ApplicationRecord
  validates :title, presence: true
end

例2)
モデルに下記のように記載すれば漢字のみ保存できるようになります。

product.rb
class User < ApplicationRecord
  kanji = /\A[一-龥]+\z/
  validates :name, format: { with: kanji }
end

例3)
モデルに下記のように記載すれば"2019-10-04"のような形でのみ保存できるようになります。

product.rb
class User < ApplicationRecord
  year_month_day = /\A\d{4}-\d{2}-\d{2}\z/
  validates :birthday, presence: true, format: { with: year_month_day }
end

バリデーションエラーメッセージの出し方

①商品の保存を行うアクション(ここではcreate)に保存の成功/失敗の条件分岐を記載。
ここで気をつけて欲しいのは失敗時のviewの読み込みはrenderを使用しましょう。
理由は後で解説します。

products_controller.rb
  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to controller: :products, action: :index
    else
      render "new"
    end
  end

②エラーメッセージのview作成
htmlのif文をmodel.errors.any? にしておくことでどのモデルのバリデーションにも対応させることができます。

layouts/_error_messages.html.haml
-if model.errors.any? 
  .alert
    %ul
      -model.errors.full_messages.each do |message| 
        %li= message
layouts/error_messages.scss
  .alert {
    olor:#262626; 
    background:#FFEBE8;
    text-align: center;
    border:2px solid #990000;
    padding:12px; font-weight:850;
  }

③エラーメッセージの表示
あとはエラーメッセージを表示させたいところに以下のコードを記載するだけです。

products/new.html.haml
 = render 'layouts/error_messages', model: f.object

完成です。
こんな感じになります。めちゃめちゃ簡単です。

スクリーンショット 2019-10-04 17.31.25.png

redirect_toとrenderの違い

先ほど、失敗時のviewの読み込みはrenderを使用すると記載しましたが、そこについて軽く触れます。
redirect_toとrenderはルーティングを通るか通らないかという違いがあります。
redirect_to…ルーティングを通り、新たにviewページを呼び出す。
render…ルーティングを通らず、viewページに飛ぶ。

正式にはもっと違いがありますが、とりあえずこの認識でOKです。

なので失敗時のviewの読み込み(以下の部分)をredirect_toにすると、
新しいビューページが呼ばれてしまうため、ページに書いたif文が反応しません。

products_controller.rb
  def create
       ~省略~
    else
      render "new"
    end
  end

おわりに

特定のgem(例えばdeviseとか)には初めから実装されていることがあります。
ぜひ調べてみてください:santa::santa:

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

imagemagickを使って画像をアップロードするとSSLが外れてしまう時の対処法

minimagickのgemを使って画像をアップロードすると、その画像が表示されるページのSSLマークが外れるという問題が発生したので、その時の解決方法を残します。

このサイトで目にする画像は、悪意のあるユーザーによって差し替えられたものである可能性があります

事象

・minimagickのgemを使って画像をアップロードすると、その画像が表示されるページでSSLマーク(鍵マーク)が外れ、びっくりマークに変わる
・びっくりマークをクリックすると「このサイトで目にする画像は、悪意のあるユーザーによって差し替えられたものである可能性があります」のメッセージがでる。

解決方法

nginxのconfファイルにproxy_set_header X-Forwarded-Proto https;を追記

/etc/nginx/conf.d/hoge.conf
location @app {
 proxy_set_header Host $http_host;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Host $http_host;
 proxy_set_header X-Forwarded-Server $hostname;
 proxy_set_header X-Forwarded-Proto https; #この一行を追加
 proxy_set_header X-Real-IP $remote_addr;
 proxy_redirect off;
 proxy_pass http://app_server;
 }

Nginx再起動

systemctl restart nginx

解決までの流れ

①画像ファイルへのリンクの確認

この事象はサイト内で使用している画像に、httpアクセスで記述している箇所があると発生するらしいので、デベロッパーツールで画像リンクのプロトコルを確認。
参考:https://stackdesign.jp/how-to-create-ssl-wordpress-site/

→きちんとhttpsになってた。

②Railsの環境設定を変更

production.rbのconfig.force_ssl = trueを有効にすると、本番環境でSSL通信を強制できるので、コメントアウトを外し設定を有効にして再デプロイ。
参考:https://railsguides.jp/configuring.html

/config/environments/production.rb
config.force_ssl = true

→ページにアクセスできなくなる
メッセージ
・このページは動作していません
・リダイレクトが繰り返し行われました。

→再びコメントアウト

③nginxのconfファイル修正

HTTPSアクセスであることをX-Forwarded-ProtoでRailsへ通知することでリダイレクトのループを回避する。
参考:https://keruuweb.com/rails-nginx-https%E3%81%A7%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%82%92%E3%81%99%E3%82%8B/

/etc/nginx/conf.d/hoge.conf
location @app {

 proxy_set_header X-Forwarded-Proto https; #この一行を追加

 }

→無事に全ページでSSL化が有効になりました

まとめ

「proxy_set_header X-Forwarded-Proto https;」はリダイレクトのループを回避するための設定で、本来はSSL化をする際に設定しておく必要があったらしい。

SSL化の際にきちんと設定ができていたら、そもそも今回のエラーは生じなかったってことですね、、、

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

FactoryBotのすごくいいチートシートを発見!

Factory Bot cheatsheet

すごくいい〜??

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

[初学者]findメソッドについて(ruby)

目的

学習の備忘録と初学者の参考資料として投稿

findメソッド

該当するモデルのテーブルからデータを検索する際に使われるメソッド。

例えば・・・

userテーブル

id name age gender
1 taro 12 male
2 hanako 11 female
3 jiro 10 male
4 keiko 9 female

検索して表示させたい→

id name age gender
○○○ ○○○ ○○○ ○○○
users_controller.rb
 def show
  @user = User.find(user:id) idを引数として検索する
 end

 *viewは省略

findは検索条件として指定できるものは「idのみ」です。データを複数取得できます。

findとfind_byの違い

find_byは、複数の条件を指定したりid以外のカラムでも検索できます。データは条件があった中で最初の一つのみ取得できる。


find_by(gender:"male")

上の例から、こんな感じで検索する。

まとめ

テーブルからデータを表示や更新や削除する場合によく使うメソッドです。他にもwhereというメソッドもあります。

今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします:bow_tone1:

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

Railsエラーメッセージのフォーマットを変更したい

発端

  • Nameを入力してください。
  • Emailを入力しください。
  • Passwordを入力してください。

Railsでは上記のように、エラー文の先頭に必ずモデルの属性名が表示されてしまいます。
(今回は、属性名の日本語化が本題ではありません。)
しかし、先頭にモデルの属性名をつけたくない場合やエラーメッセージをカスタマイズしたい場合、このままでは不都合です。

解決策

config/locales/ja.yml
ja:
  errors:
    format:
      "%{message}"

上記を追記してください。
デフォルトの属性名が、"'%{attribute}' %{message}"になっているため、フォーマットをオーバーライドすれば良いようです。
様々、カスタマイズすることも可能ですね!

参考

Rails 6 allows to override the ActiveModel::Errors#full_message format at the model level and at the attribute level

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

【Rails】jQueryでフォームに文字数カウントをつける

フォームに文字を入力していく時、あと何文字まで打てるのか分かった方がユーザーに優しいと思ったため、文字数カウント機能を作りました。

▷今回やること
フォームに残り何文字入力できるかを表示する

ezgif.com-video-to-gif.gif

文字数カウント方法を考える

・文字を入力したり、消したりするたびフォームの文字数を数える → 残り何文字まで入力できるか表示する
・文字数がオーバーしていた場合、文字色を赤にする → ユーザーに警告する

文字数カウントのアルゴリズムを考える

jQueryで文字数カウントをする

▷jsが読み込まれたら以下の処理をする

 ・処理内容(ページが読み込まれた時、フォームに残り何文字入力できるかを数えて表示する)
 フォームのvalueを取得して文字数を数える
 <!-- Macの改行コードは1文字(\n)であるが、Rails側では改行コードが2文字(\r\n)で保存されるため、\nは2文字とカウントするように設定 -->
 最大文字数 ー 取得した文字数 (=残りの入力できる文字数)

 if 最大文字数より取得文字数の方が大きい
  表示する文字色を赤にする

 “あと (残りの入力できる文字数)文字” と表示する

  ▷jQueryのkeyup()でキーボードが押されて離れるたびに処理を行う

   ・処理内容(キーボードを押した時、フォームに残り何文字入力できるかを数えて表示する)
    フォームのvalueを取得して文字数を数える。
    <!-- Macの改行コードは1文字(\n)であるが、Rails側では改行コードが2文字(\r\n)で保存されるため、\nは2文字とカウントするように設定 -->
    入力最大文字数 ー 取得した文字数 (=残りの入力できる文字数)

   if 最大文字数より取得文字数の方が大きい
    表示する文字色を赤にする
    “あと (残りの入力できる文字数)文字” と表示する
   else
    表示する文字色を黒にする
    ”あと (残りの入力できる文字数)文字” と表示する

  処理終了
処理終了

コードで文字数カウント処理を書く

1.htmlでフォーム入力タグと文字数表示タグにclassを設定し、jsで指定できるようにする

フォーム入力タグtext_areaのクラスにjs-text、文字数表示タグpのクラスにjs-text-countを指定する
(フォームにbootstrapを適用しています)

form.html.erb
<%= form_for(@profile) do |f| %>
  <div class="panel-body">
    <div class="form-group">
    <%= f.label :yourself, '自己紹介(最大150文字)' %>

    <%# js-textをclassに追加 %>
    <%= f.text_area :yourself, class: 'form-control  js-text', rows: 6, placeholder: '改行は反映されません' %>

    <%# js-text-count をclassに追加 %>
    <P class="js-text-count pull-right"></P>
  </div>
      
  <div class="text-center">
     <%= f.submit '保存する', class: 'btn btn-info'%>
  </div>
<% end %>    

2.jsファイルにフォームと文字数表示に対する処理を書く

js-textのvalueを数えて、js-text-countに結果を表示する
app/assets/javascripにcount.jsファイルを作成し、文字数カウントの処理を書いていく
(今回のフォームの最大文字数は150)

app/assets/javascrip/count.js
// jquery書きはじめ
$(function (){
  // 処理(ページが読み込まれた時、フォームに残り何文字入力できるかを数えて表示する)

  //フォームに入力されている文字数を数える
  //\nは"改行"に変換して2文字にする。オプションフラグgで文字列の最後まで\nを探し変換する
  var count = $(".js-text").text().replace(/\n/g, "改行").length;
  //残りの入力できる文字数を計算
  var now_count = 150 - count;
  //文字数がオーバーしていたら文字色を赤にする
  if (count > 150) {
    $(".js-text-count").css("color","red");
  }
  //残りの入力できる文字数を表示
  $(".js-text-count").text( "残り" + now_count + "文字");

  $(".js-text").on("keyup", function() {
    // 処理(キーボードを押した時、フォームに残り何文字入力できるかを数えて表示する)
    //フォームのvalueの文字数を数える
    var count = $(this).val().replace(/\n/g, "改行").length;
    var now_count = 150 - count;

    if (count > 150) {
      $(".js-text-count").css("color","red");
    } else {
      $(".js-text-count").css("color","black");
    }
    $(".js-text-count").text( "残り" + now_count + "文字");
  });
});

3.リンクをクリックしてページ遷移した時、jacascriptを読み込むようにする

現在のままでは、ページ遷移してもjsが機能しない

▷原因
rails4.0からデフォルトで導入されたgem「turbolinks」が読み込まれているため

〜turbolinksとは〜

画面遷移を高速化させるライブラリ。
ページ遷移するとき、現在のページのtitleとbodyだけを抜き取り、
新しいhtmlのtitleとbodyに交換することで、ページ遷移を高速化させる。
→ページ読み込みを起点としたJavascriptは機能しなくなる
jQuery を使用している場合、
下記の様にページが読み込まれたときの処理を記述出来ますが、
Turbolinksによって、実行されなくなります。
$(function() {
#処理内容
});

『Turbolinks』って、なんぞ?より抜粋

▷解決方法
count.jsを読み込みたいリンクのtubolinksを無効にする
aタグdata-turbolinks="false"link_toメソッドdata: {"turbolinks" => false}でtubolinksを無効化できる

リンク設定がaタグの場合

<a href=“◯◯” data-turbolinks="false">turbolinksを使わずにリンクする</a>

リンク設定がlink_toメソッドの場合

<%= link_to "turbolinksを使わずにリンクする", ◯◯_path, data: {"turbolinks" => false} %>

これでページ遷移した時、count.jsが読み込まれるようになり、文字数カウント処理が行われる。

参考

[簡単すぎる]Rails + jQueryで文字数カウント
【JavaScript】文字列から改行を全て削除する方法
turbolinksチートシート
『Turbolinks』って、なんぞ?

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

ブログ投稿サイトを作ってみました

こんにちは。
本日は初投稿ということで、プログラミングを始めて2週間少しの初心者が0から作った作品を紹介します。
ーーーーーーーー

ブログ投稿サイト :airplane:

トップ画面

スクリーンショット 2019-10-04 14.46.50.png
スクリーンショット 2019-10-04 14.45.14.png

ユーザー新規登録、ログイン画面

スクリーンショット 2019-10-04 19.58.52.pngスクリーンショット 2019-10-04 19.59.01.png

投稿画面

スクリーンショット 2019-10-04 19.59.38.png

詳細確認画面

スクリーンショット 2019-10-04 19.57.44.png
(写真引用元:https://www.travel.co.jp/guide/article/24996/)

1. 実装機能

・ユーザ登録、ログイン、ログアウト可能
・投稿はタイトル、本文、画像のみ(非ログイン時は投稿不可)
・投稿したユーザ本人による投稿の編集、削除が可能
・ログインしていなくても投稿一覧、詳細が閲覧可能
・ページネーション機能(投稿は新着順)

2. 使用言語・ツール

・Ruby
・Haml/Sass
・JQuery
・Ruby on Rails
・Bootstrap

3. 工夫した点

bootstrapというwebアプリケーションフレームワークを使用し、
ボタンやページネーション等を簡単に作成、装飾してみました。
フォームなどにも適用させて、もっと簡単に作成するのが理想でしたが、
公式サイトはもちろん英語だったのでなかなか上手く使えなかったのが少し残念なところです。。。
あとは綺麗な画像を貼ることで見栄えを良くしました!笑

4. 苦労した点

devise gem をインストール後、userテーブルをmigrateする際にエラーが発生し、
解決に時間が掛かりました。
エラー内容は「max key length is 767 bytes(768バイト以上のカラムに対するインデックスキーは設定できないよ)」と言った内容。
調べてみると、MySQLの単一カラムインデックスのインデックスキーは、デフォルトで
最大 767 バイトまでとのことでした...。
マイグレーションファイルに於いて、string 型の容量にリミットをつけてあげることで解決出来ました。
ex) t.string   :reset_password_token, limit: 191

参考文献
https://blog.e2info.co.jp/2017/04/17/mysql%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB767byte%E3%81%BE%E3%81%A7%E3%81%97%E3%81%8B%E3%81%A4%E3%81%8B%E3%81%88%E3%81%AA%E3%81%84/

5. 感想

制作時間の半分以上がエラーとの奮闘でしたが、解決した時にはこの上ない感動でした。
普段使っているウェブサイト、アプリ、何から何までこんなに苦労しているんだなと感じることができました。
「こんな機能があるといいな」「こんな風に配置したいな」と、色々とこだわりを入れたかったのですが、そこは自分の今の技術的に厳しめだったので断念。
これからも毎日勉強して、良いものを作ろうと思いました!

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

日時の扱い方

日時の扱い方

Rubyには日付や時刻を扱うためのDateやTimeといったクラスがあります。そのため、Rialsアプリケーションで日付や時刻を扱う際にもただこれらのクラスを使えばいいと思われるかもしれませんが、ひとつ課題があります。それは、タイムゾーンです。

例えば、Railsアプリケーションがデータベース内に、ある日時を保存するとしましょう。その日時を東京に住むユーザーが見たときは東京時間、シンガポールに住む人が見たときはシンガポール時間で表示した方が良いでしょう。このように、地域の時間に合わせた日時を表示する必要があるということです。そのため、Railsでは日時のデータをタイムゾーンとともに取り扱うことができるようになっている。

タイムゾーンとともに日時を扱うには、Timeの代わりにActiveSupport::TimeWithZoneクラスを用います。Railsが日時を扱う際(日時型のデータをデータベースから読むときや、created_atなどのタイムスタンプを記録する際)には、自動的にこのクラスが利用されます。

それでは、Railsコンソールを立ち上げて挙動を確認してみましょう。(※Rialsアプリケーションのコンソールで動作します。)

まず、現在有効なタイムゾーンを確認してみましょう。Time.zoneと売ってみてください。何も手を加えていないRailsアプリケーションでは次のように、"UTC"という名前を持つActiveSupport::TimeZoneオブジェクトが返されるはずです。これは、UTC(協定世界時)タイムゾーンを意味しています。

2.6.0 :013 > Time.zone
 => #<ActiveSupport::TimeZone:0x000000000a8e9568 @name="UTC", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>> 

続いて、そのUTCタイムゾーンにおける現在時刻を表すオブジェクトを取得しましょう。それには、Time.zone.nowを実行します。

2.6.0 :003 > Time.zone.now
 => Fri, 04 Oct 2019 13:52:47 JST +09:00 

Time.zone.nowは、ActiveSupport::TimeWithZoneクラスのオブジェクトとなっています。試しに、classメソッドを調べてみましょう。

2.6.0 :004 > Time.zone.now.class
 => ActiveSupport::TimeWithZone 

確かにそのようになっていますね。

オブジェクトの作成日時を表すreated_atなどもActiveSupport::TImeWithZoneのオブジェクトとなっています。

タイムゾーンの変更

タイムゾーンは「現在のタイムゾーン」に当たります。データベースからモデルオブジェクトを取ってくると、オブジェクト内のcreated_atやupdated_atは、Time.zoneで示されたタイムゾーンに対応する時間オブジェクトとなります。

したがって、もしもモデルのcreated_atなどを日本時間での時間オブジェクトとして取得したいのであれば、Time.zoneを日本時間に指定してからオブジェクトを取得すれば良いということになります。試しに、Time.zoneに日本時間を指定してみましょう。次のように、Time.zoneにAsia/Tokyoを代入してみてください。

2.6.0 :009 > Time.zone = 'Asia/Tokyo'
 => "Asia/Tokyo" 

これで設定が変わるので、Time.zone.nowを実行すると、今度は日本時間での現在時刻が取得できます。

2.6.0 :010 > Time.zone
 => #<ActiveSupport::TimeZone:0x000000000a8d9e10 @name="Asia/Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>> 
2.6.0 :011 > Time.zone.now
 => Fri, 04 Oct 2019 14:19:30 JST +09:00 

同様に、クラスのcreated_atも、日本時間のオブジェクトを取得できるようになりました。

2.6.0 :018 > User.first.created_at
=> Fri, 04 Oct 2019 02:14:57 UTC +00:00 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】日付期間の重複チェック

発端

「新規で作成する期間が作成済みの期間と被らないようにバリデーションつけといて〜」

こんな頼みが上司からあり、すぐ実装できるだろうと思ったのですが・・・
なかなか苦戦したので、その備忘録です。

最初に苦戦した点を話すと、
作成済みの開始〜終了の間の期間に新規作成すると検索に引っかからない点です。

その解決策と、Railsでの実装方法について書いていきます。

試したこと①

今回は、例として予約の期間が被らないようにする、Reservationモデルで試します。
最初は、効率がよくないですが、こちらで試しました。

Reservation.where(started_at: self.started_at..self.finished_at) or
    Reservation.where(finished_at: self.started_at..self.finished_at)

試したこと②

次に、こちらなどを参考に、下記を実装しました。

Reservation.where('started_at <= ? and finished_at >= ?', self.started_at, self.finished_at)

しかし、問題点が・・・

しかし、上記2つの方法では、問題点が浮かび上がりました。
同期間validation用.png
対象期間 2019/08/05 ~ 2019/10/15 
期間①  2019/07/01 ~ 2019/07/30
期間②  2019/11/01 ~ 2019/11/30
期間③  2019/07/30 ~ 2019/08/15
期間④  2019/09/15 ~ 2019/10/30
期間⑤  2019/09/01 ~ 2019/09/30

①〜②は対象外のため引っかからない、③〜④は正常に引っかかるのですが、
問題は、⑤が引っかからないのです。
仕様にもよるかもしれませんが、期間が完全に重複しているのに、検索で引っかかってくれないのは大問題です!

解決策

調べたところ、解決策がありました!
期間が重複しているかを判定する条件式の導出方法がとても参考になりました。

qiita.rb
puts 'code with syntax'
条件式
(対象終了日付 > 比較開始日付) AND (比較終了日付 > 対象開始日付)

これをActiveRecordで書き直すと、

Reservation.where('finished_at > ? and ? > started_at', self.started_at, self.finished_at)

これで既存の開始〜終了の間の期間に、作ろうとした新規予約を弾くことができるようになりました!

感想

アルゴリズムの大切さを実感しました。
まず該当しない場合を考え、逆にして該当する場合を導き出すド・モルガンの法則は、今後も使う場面がありそうです!

参考

以下の記事を参考にさせていただきました。ありがとうございました。

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

【Rails】日付期間の重複を完璧にチェックしたい

発端

「新規で作成する期間が作成済みの期間と被らないようにバリデーションつけといて〜」

こんな頼みが上司からあり、すぐ実装できるだろうと思ったのですが・・・
なかなか苦戦したので、その備忘録です。

最初に苦戦した点を話すと、
作成済みの開始〜終了の間の期間に新規作成すると検索に引っかからない点です。

その解決策と、Railsでの実装方法について書いていきます。

試したこと

今回は、例として予約の期間が被らないようにする、Reservationモデルで試します。
最初は、効率がよくないですが、こちらで試しました。

Reservation.where(started_at: self.started_at..self.finished_at) or
    Reservation.where(finished_at: self.started_at..self.finished_at)

しかし、問題点が・・・

しかし、上記の方法では、問題点が浮かび上がりました。
同期間validation用.png
対象期間 2019/08/05 ~ 2019/10/15 
期間①  2019/07/01 ~ 2019/07/30
期間②  2019/11/01 ~ 2019/11/30
期間③  2019/07/30 ~ 2019/08/15
期間④  2019/09/15 ~ 2019/10/30
期間⑤  2019/09/01 ~ 2019/09/30

①〜②は対象外のため引っかからない、③〜④は正常に引っかかるのですが、
問題は、⑤が引っかからないのです。
仕様にもよるかもしれませんが、期間が完全に重複しているのに、検索で引っかかってくれないのは大問題です!

解決策

調べたところ、解決策がありました!
期間が重複しているかを判定する条件式の導出方法がとても参考になりました。

qiita.rb
puts 'code with syntax'
条件式
(対象終了日付 > 比較開始日付) AND (比較終了日付 > 対象開始日付)

これをActiveRecordで書き直すと、

Reservation.where('finished_at > ? and ? > started_at', self.started_at, self.finished_at)

これで既存の開始〜終了の間の期間に、作ろうとした新規予約を弾くことができるようになりました!

感想

アルゴリズムの大切さを実感しました。
まず該当しない場合を考え、逆にして該当する場合を導き出すド・モルガンの法則は、今後も使う場面がありそうです!

参考

以下の記事を参考にさせていただきました。ありがとうございました。

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

RailsアプリにRSpecを導入する手順

はじめに

RSpecはRubyのテストフレームワークです。これ以外にMinitestというものがありますが、実際の現場では、RSpecが使われることが多いようなので導入する手順をメモしておきます。

環境
Widows 10
cloud9

RSpecをインストール

gemを追加
最近はsystemスペックの利用が推奨されています。systemスペックは、バージョンが3.8.0以上である必要があるためそれをインストールしましょう。

Gemfile
group :development, :test do
 gem 'rspec-rails' # バージョン指定する必要があるかも'~> 3.8.0' 
end
$ bundle install

RSpecをインストールする

$ rails generate rspec:install

必須ではないが、RSpec の出力をデフォルトの形式から読みやすいドキュメント形式に変更できる。やっといたほうが見た目が良くなるのでおすすめです。

.rspec
--require spec_helper
--format documentation

テストを速くする
このgemを追加するとSpringにより、テストスイートが速くなる。必要だと思ったら入れてください。

Gemfile
group :development do
  gem 'spring-commands-rspec'
end

# bundle installを忘れずに!

そしたら、以下のコマンドを実行して下さい。

$ bundle exec spring binstub rspec

そしてこれからは、RSpecのコマンドを打つ際先頭に、bin/をつけるようにしましょう。

ジェネレータ
rails g する際に一緒にスペックファイルも生成されるようにする。また、あまり使うことのないファイルは生成されないようにします。コードが複雑になってきたら使うかもしれないので、その時は削除していってください。

config/application.rb
.
.
module Sample
  class Application < Rails::Application

     config.generators do |g|
       g.test_framework :rspec,
         fixtures: false, # factory_bot_railsを使うなら削除してください
         view_specs: false,
         helper_specs: false,
         routing_specs: false
    end

  end
end

これで基本的なセットアップは終了です!

便利なgem

ここからはRSpecを使っていく際に便利なgemを紹介します。最新のものを使うようにしましょう。

Gemfile
group :development, :test do
  gem 'factory_bot_rails' # テストのサンプルデータを生成できる
end

group :test do
  gem 'capybara' #統合テスト(system)をするのに必要
  gem 'launchy' # save_and_open_page をスペック内で呼びだしたときに、HTMLを自動的に開く
  gem 'webdrivers' # js付きののテストができるようになる。
end

Capybaraのセットアップ
Capybaraを読み込めるように、requireする。

spec/rails_helper.rb
# その他の設定
require 'capybara/rspec'

Capybaraで、JavaScriptを使ったテスト

1 設定ファイルを書く
rails_helper.rbに直接書いてもいいが、今後設定が多くなってくると見にくくなるので、独立したファイルに設定を書いていきます。

spec/rails_helper.rb
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } #このコメントアウトを外す。

これで、supportディレクトリ下にあるファイルを読み込めるようになる。このディレクトリを作り、そこにcapybara.rbというファイルを生成し、設定を書きます。

spec/support/capybara.rb
Capybara.javascript_driver = :selenium_chrome
or
Capybara.javascript_driver = :selenium_chrome_headless # ヘッドレスモードを使いたい場合

2 gemを追加
webdriversは、selenium-webdriver も一緒にインストールしてくれるので、selenium-webdriver がある場合は削除しといてください。

Gemfile
gem 'webdrivers'

これで、js: trueオプションをつけることで、JavaScript付きのテストができる。

cloud9でヘッドレスモードを使えるようにする
僕は今、cloud9を使っているので、ここで使えるようにしていきます。cloud9では、ヘッドレスモードしかできないみたいです。

1 Chromeブラウザをインストール
初期のAWSは、Chromeがインストールされてないのでインストールする。

$ curl https://intoli.com/install-google-chrome.sh | bash

2 ヘッドレスモードにする

spec/support/capybara.rb
Capybara.javascript_driver = :selenium_chrome_headless

3 selenium-webdriverとcapybaraを最新版にする

$ bundle update selenium-webdriver capybara

Deviseのヘルパーメソッドを使えるようにする

これを記述することで、コントローラー、システム、リクエストスペックでDeviseのヘルパーメソッドである、sign_inが使えるようになる。
また、リクエストスペックの場合、Deviseのヘルパーメソッド使うために、少し設定が必要。

spec/rails_helper.rb
# その他の設定
RSpec.configure do |config|
  # その他の設定
  config.include Devise::Test::ControllerHelpers, type: :controller 
  config.include Devise::Test::IntegrationHelpers, type: :system
  config.include RequestSpecHelper, type: :request
end

リクエストスペックの追加設定
新しいファイルを作って設定を記述。

spec/support/request_spec_helper.rb
module RequestSpecHelper
  include Warden::Test::Helpers

  def self.included(base)
    base.before(:each) { Warden.test_mode! }
    base.after(:each) { Warden.test_reset! }
  end

  def sign_in(resource)
    login_as(resource, scope: warden_scope(resource))
  end

  def sign_out(resource)
    logout(warden_scope(resource))
  end

 private

  def warden_scope(resource)
    resource.class.name.underscore.to_sym
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初学者]よく起こるエラー〜ActiveRecord::PendingMigrationError 〜

目的

学習の備忘録と初学者の参考資料として投稿

ActiveRecord::PendingMigrationErrorというエラー

MVCの一角であるモデルを作成するため、ターミナルで・・・・

<ターミナル>

$ rails g model ○○○○(モデル名)

・・・ちょっくら確認してみるか

$ rails s

エラー発生!!!!

ActiveRecord::PendingMigrationError〜、あちゃ〜:persevere:

忘れてた:bulb:

解決方法

<ターミナル>

$ rails db:migrate

>>解決!!

そもそもマイグレーションとは

SQLを書くことなくRubyでデータベース内にテーブルを作成することができる機能。わざわざ面倒くさい事をマイグレーションファイルが行ってくれている。なので絶対忘れずにマイグレーションしよう!!

[参考]ちなみによく見る『schema.rb』とは

マイグレーションした際に作成されるファイルで、テーブルのカラムやそのデータ型などマイグレーションした結果が書かれている。マイグレーションするたびに最新の状態へ更新。実際ファイルを見てみるといままでのマイグレーションの履歴が見れる。

まとめ

モデルを作成したら、すぐにマイグレーション実行しよう!!

ちなみにマイグレーションファイルを勝手に削除すると大変な事に・・・・・:scream:

消さないでね:u7981:

今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします:bow_tone1:

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

Rails6 のちょい足しな新機能を試す89(Event object編)

はじめに

Rails 6 に追加された新機能を試す第89段。 今回は、 Event object 編です。
Rails 6 では、ActiveSupport::Notifications.subscribe で、ブロックパラメータを1つにすると、そのブロックパラメータに ActiveSupport::Notifications::Event オブジェクトが設定されるようになりました。
これで、ブロックの中で ActiveSupport::Notifications::Event.new を使う必要がなくなりました。

Ruby 2.6.4, Rails 6.0.0 で確認しました。

$ rails --version
Rails 6.0.0

今回は、User の CRUD を作り、各コントローラのアクションを実行したときの情報を取得してログに出力してみます。

プロジェクトを作る

rails new rails_sandbox
cd rails_sandbox

User の CRUD を作る

name をもつ User の CRUD を作ります

bin/rails g scaffold User name

Subscribe する

config/initializers/notification_subscriber.rb を作成します。
ActiveSupport::Notifications.subscribe を呼び出すときに、ブロックパラメータを event 1つだけにします。

config/initializers/notification_subscriber.rb
ActiveSupport::Notifications.subscribe 'process_action.action_controller' do |event|
  action = "#{event.payload[:controller]}##{event.payload[:action]}"
  Rails.logger.info("#{action} cpu_time=#{event.cpu_time} duration=#{event.duration} allocations=#{event.allocations}")
end

User の登録などをしてログを確認する

実際に rails server を実行してブラウザから http://localhost:3000/users にアクセスします。

以下のようにコンソールに出力されているはずです。

...
UsersController#index cpu_time=833.013591 duration=840.9244199865498 allocations=1237733

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try089_subscriber_event

参考情報

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

ゲストログイン機能の実装方法(ポートフォリオ用)

ポートフォリオ用のWebサイトに必須と言われるゲストログイン機能

特にDeviseconfirmableを使用している場合は,ゲストログイン機能が無いと,新規登録が面倒という理由でポートフォリオを見てすらもらえない可能性が高くなります。

ところが,普通のWebサイトには絶対に導入しないためか,記事が見当たりませんでした。そこで,私のたどりついた実装方法を備忘録としてアップしておきます。

ゲストログイン機能の実装方法

以下,Deviseを使用している前提とします。

config/routes.rb
  post '/homes/guest_sign_in', to: 'homes#guest_sign_in'
app/controllers/homes_controller.rb
class HomesController < ApplicationController
  def guest_sign_in
    user = User.find_or_create_by(email: 'guest@example.com') do |user|
      user.password = SecureRandom.urlsafe_base64
      user.confirmed_at = Time.now  # Devise の confirmable を使用している場合は必須
    end
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
end
app/views/homes/index.html.erb
<%= link_to 'ゲストログイン(閲覧用)', homes_guest_sign_in_path, method: :post %>
  • ポイントはDevisesign_inメソッドを利用することです。

  • ゲストユーザーをあらかじめ作成する手間を省くため,find_byではなく,find_or_create_byを利用しています。パスワードを把握する必要はないので,ランダム文字列にしています。

備考

  • メールアドレス,パスワードを変更できるように設定する場合は,ゲストユーザーのメールアドレスなどを変更されないように注意する必要があります。

  • 実は,最初,devise/sessions/new.html.erbの真似をして,hidden_fieldを加えて……という手順で実装しようとしました。ところが,この方法ではuser_email, user_passwordなどのidセレクタが重複するエラーが出てしまいます。そこで,sign_inメソッドの存在を思い出し,このような実装方法にたどりつきました。

おまけ

ゲストユーザーのデータを定期的に初期化するため,実際にはこのような形式で実装しました。
nameカラムはニックネーム,content自己紹介文の想定です。

app/controllers/homes_controller.rb
class HomesController < ApplicationController
  def guest_sign_in
    new_user_check = false
    user = User.find_or_create_by(email: 'guest@example.com') do |user|
      user.password = SecureRandom.urlsafe_base64
      user.confirmed_at = Time.now
      new_user_check = true
    end
    # 新規ユーザー作成時と,前回のゲストログインor更新から30分以上経過している場合はデータを初期化
    data_initialization(user) if new_user_check || Time.now - user.updated_at > 1800
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end

  private

  def data_initialization(user)
    user_params = {
      # ゲストログイン時にデータを初期化するかどうか判断できるようにupdated_atを更新
      updated_at: Time.now,
      name: 'ゲスト',
      content: 'ゲストユーザーの各種データは30分後に初期化されます。ご了承下さい。',
    }
    user.update(user_params)
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mysql2がldflagsのオプション使ってもbundle installできない

以前試した奴がうまくいかなかった。

以前解決した方法はこちら。
Rails 5.2.3でmysql2がbundle isntallできない

下記のようになってconfigの中身を書き換えて、何度もやっていると当然っちゃ当然ですがリプレイスされただけの結果しか生まれない

You are replacing the current local value of build.mysql2, which is currently "--with-ldflags=-L/usr/local/opt/openssl/lib"

ほう?お前、別件エラーだな?

どうやらまたmysql2の別のエラーのようです。
散々探し回ってようやく見つけた答えがこちらです。

mysql2 (>= 0.3.13, < 0.5)

mysql2 (~> 0.4.10)

にバージョンをあげてあげることです。

githubの方にもissueが上がっておりました。
Support mysql2 0.4.x and 0.5.x #32310

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

【Rspec】railsでテスト用のDB作成

railsでテスト用のDBを作成します。

database.ymlにtest用のDBを記述します。

database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: sample_development

test:
  <<: *default
  database: sample_test

test環境を指定して、migrationしてあげます。

$ bundle exec rake db:migrate RAILS_ENV=test

以上です。

参考: https://qiita.com/tocomi/items/2dba0de52eefdcf33fd7

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

[Rails] GraphQLのディレクトリを整理してみた

ディレクトリ

.
├── enum_types # Enumを定義する。
├── resolvers # queryのresolveを書く。
│   └── sample_resources ── fetch_sample # 〇〇_resourceでディレクトリを分ける。
├── mutations # mutationのresolveを書く。
│   └── sample_resources ── update_sample # 〇〇_resourceでディレクトリを分ける。
├── objective_types # ModelのType定義を行う。
├── input_types # 主に単体ModelのCRUDを行う際の引数を定義する。
├── scalar_types # Date, i18n, JSON, Image, etc... のデータ型を定義する。
├── loaders # graphql-batchで生成されるディレクトリ。
├── query_types
│    ├── query_type
│    └── mutation_type
└── api_schema.rb

これだと大体混乱しない。

使っているGem

面倒臭いところ

  • EnumとかをModelと重複して、書くのはだるすぎる。
  • まじでModelからObjectiveTypeとかを自動生成したい。

気になること

  • エラーハンドリング(現在は、Schemaに定義して、errorを返している。)
  • リソース管理(現在は、GraphQLRubyのProのPundit Integrationを使っている。)
  • セキュリティ周り

以上の3点がいつも気になる。
うまくやってる人いたら教えてくださーい!!

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

railsでプロジェクトを丸々コピーする

この記事でやること

  • railsで作ったプロジェクトを丸々コピーする

手順

  • githubからレポジトリのクローン
  • dbの作成(rails db:migrate)
  • configに含まれているapplication.rbとroutes.rbのアプリ名の変更
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 6.0 の DatabaseSelector middleware を理解する ①使ってみる

このシリーズについて

  • Rails 6.0 の DatabaseSelector middleware を理解する ①使ってみる (この記事)
    • DatabaseSelector を使ってみて、挙動を確認する。
  • Rails 6.0 の DatabaseSelector middleware を理解する ②読んでみる (次回公開)
    • DatabaseSelector のコードリーディングをして、仕組みを理解する。

目的

  • ActiveRecord::Middleware::DatabaseSelector のコードリーディングを通して、その仕組みを理解して使えるようになる。
  • DatabaseSelector を知ることで、 Ruby on Rails 6.0 の複数データベース対応について理解する。

要約

  • HTTP GET / HTTP HEAD では :reading
  • それ以外は :writing
  • :writing を使って数秒間は HTTP メソッドにかかわらず :writing を使わせる

サンプルコード

https://github.com/takeyuweb/rails6-multidb-sample

  • PostgreSQL の非同期レプリケーション
  • Rails 6.0 の Multiple DBs 機能
  • DatabaseSelector を使った HTTP Method による接続先自動切り替え

確認環境

  • Ruby on Rails 6.0
  • Docker 19.03.2 on WSL2

DatabaseSelector

image.png

DatabaseSelector は、Railsガイドの3 コネクションの自動切り替えを有効にする が詳しいです。

今回はサンプルアプリを作り、実際に手元で動かして、挙動を確認してみました。

DatabaseSelector を設定する

ロールと接続先

まずは複数データベース接続設定を書きます。

1 アプリケーションのセットアップ - Active Record で複数のデータベース利用

app/modles/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :primary, reading: :primary_readonly }
end

https://github.com/takeyuweb/rails6-multidb-sample/blob/master/app/models/application_record.rb

Role: :reading :writing Railsにより提供される。変更もできるが、基本このまま。
Specification: :primary :primary_readonlydatabase.yml に記載した接続先名。 :animal :replica など、わかりやすいようにつけられます。
詳しくは Railsガイド

config/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 } %>

development:
  # 仮にこれを `hoge:` にすると `schema.rb` は `hoge_schema.rb` になる。
  primary:
    <<: *default
    database: MyApp_development
    host: <%= ENV.fetch('DATABASE_HOST_PRIMARY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
  primary_readonly:
    <<: *default
    database: MyApp_development
    host: <%= ENV.fetch('DATABASE_HOST_READONLY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
    replica: true

https://github.com/takeyuweb/rails6-multidb-sample/blob/master/config/database.yml

DatabaseSelector を有効にする

DatabaseSelector ミドルウェアを使うには、次のようにします。
少し冗長に思えますが、これによりカスタマイズの余地が生まれています。(次回解説します)

  config.active_record.database_selector = { delay: 2.seconds }
  config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
  config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session

https://github.com/takeyuweb/rails6-multidb-sample/commit/c09eec4e843f166f607e29a295a7d631bc8112e3#diff-a2f09e1db7a47eb67c5849478a97f3d7

DatabaseSelector の挙動を確認する

サンプルアプリ を起動して、どこにどんなクエリが飛ぶか見てみます。

HTTP GET / HTTP HEAD では :reading

GET http://localhost:3000/posts/1/edit をみてみます。
image.png

このとき、ログから :reading に指定したレプリカを使用したことがわかります。

app_1          | Started GET "/posts/1/edit" for 172.21.0.1 at 2019-10-03 19:17:58 +0000
app_1          | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
pg_primary_1   | 2019-10-03 19:17:58.241 UTC [27] LOG:  statement: SELECT 1
app_1          | Processing by PostsController#edit as HTML
app_1          |   Parameters: {"id"=>"1"}
pg_readonly_1  | 2019-10-03 19:17:58.243 UTC [28] LOG:  statement: SELECT 1
pg_readonly_1  | 2019-10-03 19:17:58.243 UTC [28] LOG:  execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2
pg_readonly_1  | 2019-10-03 19:17:58.243 UTC [28] DETAIL:  parameters: $1 = '1', $2 = '1'
app_1          |   Post Load (0.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
app_1          |   ↳ app/controllers/posts_controller.rb:67:in `set_post'
app_1          |   Rendering posts/edit.html.erb within layouts/application
app_1          |   Rendered posts/_form.html.erb (Duration: 1.2ms | Allocations: 747)
app_1          |   Rendered posts/edit.html.erb within layouts/application (Duration: 1.5ms | Allocations: 867)
app_1          | Completed 200 OK in 8ms (Views: 5.4ms | ActiveRecord: 0.4ms | Allocations: 7126)

HTTP GET / HTTP HEAD 以外では :writing

では POST http://localhost:3000/posts ではどうでしょうか?
記事を作成してみます。

このときのログは次のように、先ほどの GET とは異なり、:writing に設定したプライマリを使ったことがわかります。

app_1          | Started POST "/posts" for 172.21.0.1 at 2019-10-03 19:21:22 +0000
app_1          | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
pg_primary_1   | 2019-10-03 19:21:22.039 UTC [27] LOG:  statement: SELECT 1
app_1          | Processing by PostsController#create as HTML
app_1          |   Parameters: {"authenticity_token"=>"BHkgkW4WTj5JAdB4DZ6PmS2ZEq9bDHmSzZ3900V4roTY6CDKHUhd3savRJCag0oaXOlQKlVyd8BPsl9VFhTZQg==", "post"=>{"title"=>"Hello Rails 6.0", "body"=>""}, "commit"=>"Create Post"}
pg_primary_1   | 2019-10-03 19:21:22.042 UTC [27] LOG:  statement: BEGIN
app_1          |    (0.3ms)  BEGIN
app_1          |   ↳ app/controllers/posts_controller.rb:30:in `block in create'
pg_primary_1   | 2019-10-03 19:21:22.043 UTC [27] LOG:  execute <unnamed>: INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"
pg_primary_1   | 2019-10-03 19:21:22.043 UTC [27] DETAIL:  parameters: $1 = 'Hello Rails 6.0', $2 = '', $3 = '2019-10-03 19:21:22.041603', $4 = '2019-10-03 19:21:22.041603'
app_1          |   Post Create (0.9ms)  INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["title", "Hello Rails 6.0"], ["body", ""], ["created_at", "2019-10-03 19:21:22.041603"], ["updated_at", "2019-10-03 19:21:22.041603"]]
app_1          |   ↳ app/controllers/posts_controller.rb:30:in `block in create'
pg_primary_1   | 2019-10-03 19:21:22.045 UTC [27] LOG:  statement: COMMIT
app_1          |    (0.5ms)  COMMIT
app_1          |   ↳ app/controllers/posts_controller.rb:30:in `block in create'
app_1          | Redirected to http://localhost:3000/posts/2
app_1          | Completed 302 Found in 6ms (ActiveRecord: 1.7ms | Allocations: 2959)

:writing へのクエリ発行直後は HTTP GET でも :writing

次のログは、記事を更新後、リダイレクト先で更新した記事を表示したときのログです。

ログから、 GET でも :reading ではなく :writing を使用したことがわかります。

非同期レプリケーションの場合、プライマリでの更新直後、レプリカへの変更が反映されていない可能性があり、これはユーザーに対して期待と異なる挙動を示すため、それを防ぐための措置だと思います。

app_1          | Started PATCH "/posts/2" for 172.21.0.1 at 2019-10-03 19:26:39 +0000
app_1          | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
pg_primary_1   | 2019-10-03 19:26:39.776 UTC [62] LOG:  statement: SELECT 1
app_1          | Processing by PostsController#update as HTML
app_1          |   Parameters: {"authenticity_token"=>"BDy9M4hAOSryBYPDpx3vu/bx03w9GatkTFO2GsBH0GneYILF6Y+pmk7UxF/3WVnr3Cosup5/EDRM3zVS10WdZA==", "post"=>{"title"=>"Hello Rails 6.0!", "body"=>""}, "commit"=>"Update Post", "id"=>"2"}
pg_primary_1   | 2019-10-03 19:26:39.779 UTC [62] LOG:  statement: SHOW search_path
pg_primary_1   | 2019-10-03 19:26:39.780 UTC [62] LOG:  execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2
pg_primary_1   | 2019-10-03 19:26:39.780 UTC [62] DETAIL:  parameters: $1 = '2', $2 = '1'
app_1          |   Post Load (0.8ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
app_1          |   ↳ app/controllers/posts_controller.rb:67:in `set_post'
pg_primary_1   | 2019-10-03 19:26:39.783 UTC [62] LOG:  statement: BEGIN
app_1          |    (0.4ms)  BEGIN
app_1          |   ↳ app/controllers/posts_controller.rb:44:in `block in update'
pg_primary_1   | 2019-10-03 19:26:39.786 UTC [62] LOG:  execute <unnamed>: UPDATE "posts" SET "title" = $1, "updated_at" = $2 WHERE "posts"."id" = $3
pg_primary_1   | 2019-10-03 19:26:39.786 UTC [62] DETAIL:  parameters: $1 = 'Hello Rails 6.0!', $2 = '2019-10-03 19:26:39.783058', $3 = '2'
app_1          |   Post Update (0.7ms)  UPDATE "posts" SET "title" = $1, "updated_at" = $2 WHERE "posts"."id" = $3  [["title", "Hello Rails 6.0!"], ["updated_at", "2019-10-03 19:26:39.783058"], ["id", 2]]
app_1          |   ↳ app/controllers/posts_controller.rb:44:in `block in update'
pg_primary_1   | 2019-10-03 19:26:39.788 UTC [62] LOG:  statement: COMMIT
app_1          |    (0.4ms)  COMMIT
app_1          |   ↳ app/controllers/posts_controller.rb:44:in `block in update'
app_1          | Redirected to http://localhost:3000/posts/2
app_1          | Completed 302 Found in 12ms (ActiveRecord: 2.8ms | Allocations: 3467)
app_1          |
app_1          |
app_1          | Started GET "/posts/2" for 172.21.0.1 at 2019-10-03 19:26:39 +0000
app_1          | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
pg_primary_1   | 2019-10-03 19:26:39.801 UTC [62] LOG:  statement: SELECT 1
app_1          | Processing by PostsController#show as HTML
app_1          |   Parameters: {"id"=>"2"}
pg_primary_1   | 2019-10-03 19:26:39.803 UTC [62] LOG:  execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2
pg_primary_1   | 2019-10-03 19:26:39.803 UTC [62] DETAIL:  parameters: $1 = '2', $2 = '1'
app_1          |   Post Load (0.5ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
app_1          |   ↳ app/controllers/posts_controller.rb:67:in `set_post'
app_1          |   Rendering posts/show.html.erb within layouts/application
app_1          |   Rendered posts/show.html.erb within layouts/application (Duration: 0.3ms | Allocations: 94)
app_1          | Completed 200 OK in 7ms (Views: 4.8ms | ActiveRecord: 0.5ms | Allocations: 6362)
更新系クエリでない場合も、:writing を強制するか?

レプリカへの変更が反映されていない可能性への対応であるならば、 :writing へのクエリ発行直後であっても、そのクエリが INSERT などの更新系クエリでなければ :reading でよいのではないか?そう考えて試してみます。

ログによると、 PATCH により :writingSELECT に使われたことがわかりますが、変更がなかったため UPDATE は発行されませんでした。
しかし、それでも次の GET では :writing を使っています。

このことから、更新系クエリでない場合も「 :writing を使ったら、しばらく :reading 」と考えて良いようです。

app_1          | Started PATCH "/posts/2" for 172.21.0.1 at 2019-10-03 19:36:14 +0000
app_1          | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
pg_primary_1   | 2019-10-03 19:36:14.234 UTC [91] LOG:  statement: SELECT 1
app_1          | Processing by PostsController#update as HTML
app_1          |   Parameters: {"authenticity_token"=>"gcEcnplYdoDMYrET0VxW3hBpLPqEtETt56OSj+QPHZ9bnSNo+JfmMHCz9o+BGOCOOrLTPCfS/73nLxHH8w1Qkg==", "post"=>{"title"=>"Hello Rails 6.0!", "body"=>""}, "commit"=>"Update Post", "id"=>"2"}
pg_primary_1   | 2019-10-03 19:36:14.236 UTC [91] LOG:  statement: SHOW search_path
pg_primary_1   | 2019-10-03 19:36:14.236 UTC [91] LOG:  execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2
pg_primary_1   | 2019-10-03 19:36:14.236 UTC [91] DETAIL:  parameters: $1 = '2', $2 = '1'
app_1          |   Post Load (0.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
app_1          |   ↳ app/controllers/posts_controller.rb:67:in `set_post'
app_1          | Redirected to http://localhost:3000/posts/2
app_1          | Completed 302 Found in 4ms (ActiveRecord: 0.7ms | Allocations: 1312)
app_1          |
app_1          |
app_1          | Started GET "/posts/2" for 172.21.0.1 at 2019-10-03 19:36:14 +0000
app_1          | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
pg_primary_1   | 2019-10-03 19:36:14.247 UTC [91] LOG:  statement: SELECT 1
app_1          | Processing by PostsController#show as HTML
app_1          |   Parameters: {"id"=>"2"}
pg_primary_1   | 2019-10-03 19:36:14.249 UTC [91] LOG:  execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2
pg_primary_1   | 2019-10-03 19:36:14.249 UTC [91] DETAIL:  parameters: $1 = '2', $2 = '1'
app_1          |   Post Load (0.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
app_1          |   ↳ app/controllers/posts_controller.rb:67:in `set_post'
app_1          |   Rendering posts/show.html.erb within layouts/application
app_1          |   Rendered posts/show.html.erb within layouts/application (Duration: 0.3ms | Allocations: 94)
app_1          | Completed 200 OK in 9ms (Views: 7.5ms | ActiveRecord: 0.4ms | Allocations: 6364)

HTTP GET でも :writing 使いたいとき

実務ではいろいろあるものです。
コネクションを手動で切り替えることで、明示的に :writing を使うことはできます。

  # GET /posts/1
  # GET /posts/1.json
  def show
    ActiveRecord::Base.connected_to(role: :writing) do
      @post = Post.lock.find(params[:id])
      @post.touch
    end
  end
app_1          | Started GET "/posts/2" for 172.21.0.1 at 2019-10-03 20:34:20 +0000
app_1          | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
pg_primary_1   | 2019-10-03 20:34:20.236 UTC [35] LOG:  statement: SELECT 1
app_1          | Processing by PostsController#show as HTML
app_1          |   Parameters: {"id"=>"2"}
pg_primary_1   | 2019-10-03 20:34:20.239 UTC [35] LOG:  execute <unnamed>: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 FOR UPDATE
pg_primary_1   | 2019-10-03 20:34:20.239 UTC [35] DETAIL:  parameters: $1 = '2', $2 = '1'
app_1          |   Post Load (0.5ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 FOR UPDATE  [["id", 2], ["LIMIT", 1]]
app_1          |   ↳ app/controllers/posts_controller.rb:14:in `block in show'
pg_primary_1   | 2019-10-03 20:34:20.240 UTC [35] LOG:  statement: BEGIN
app_1          |    (0.3ms)  BEGIN
app_1          |   ↳ app/controllers/posts_controller.rb:15:in `block in show'
pg_primary_1   | 2019-10-03 20:34:20.242 UTC [35] LOG:  execute <unnamed>: UPDATE "posts" SET "updated_at" = $1 WHERE "posts"."id" = $2
app_1          |   Post Update (0.5ms)  UPDATE "posts" SET "updated_at" = $1 WHERE "posts"."id" = $2  [["updated_at", "2019-10-03 20:34:20.240568"], ["id", 2]]
pg_primary_1   | 2019-10-03 20:34:20.242 UTC [35] DETAIL:  parameters: $1 = '2019-10-03 20:34:20.240568', $2 = '2'
app_1          |   ↳ app/controllers/posts_controller.rb:15:in `block in show'
pg_primary_1   | 2019-10-03 20:34:20.243 UTC [35] LOG:  statement: COMMIT
app_1          |    (0.3ms)  COMMIT
app_1          |   ↳ app/controllers/posts_controller.rb:15:in `block in show'
app_1          |   Rendering posts/show.html.erb within layouts/application
app_1          |   Rendered posts/show.html.erb within layouts/application (Duration: 0.3ms | Allocations: 90)
app_1          | Completed 200 OK in 10ms (Views: 4.3ms | Allocations: 8429)

次回

DatabaseSelector のコードリーディングを通して、なぜ今回のような挙動になったか?その仕組みを紐解きます。

参考

Active Record で複数のデータベース利用 - Railsガイド

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