Madogiwa Blog

主に技術系の学習メモに使っていきます。

RubyonRails:collection_selectにデフォルト値を設定する

Railsには、データベースの値からselectタグを作るcollection_tagという非常に便利なメソッドがあるんですが、デフォルト値の設定方法で少しハマったので、そのやり方をメモしておきますφ(..)

やり方

今回は、タスクにユーザーを割り当てる際に使用するセレクトボックスをイメージしてます。 ポイントは、selectedオプションに値を渡すことです(・∀・)

%div.form-group
  = label_tag :user, Task.human_attribute_name(:user)
  = collection_select :task, :user_id, User.all, :id, :name, { include_blank: true, selected: search_attr[:user_id] }, { class: 'form-control' }

selectedオプションに値を設定すれば、collection_selectにデフォルト値を設定することが出来ます!!

参考

http://railsdoc.com/references/collection_select


RubyonRails:検索条件を引き継いで検索結果のソート順を変更する

検索結果を指定したプロパティで並び変える際に検索条件を引き継いで、ソートする際に少しハマったのでやり方をメモφ(..)

イメージは下記みたいな、検索後のタスクの一覧を期限の降順で並び替えるような機能を想定してます(・∀・)

f:id:madogiwa0124:20180506224144g:plain

検索条件を引き継いでソートする方法

そもそもなぜ検索条件を引き継ぐ必要があるか

何も考えずにソート処理を実装すると下記のような形になるんじゃないかなぁと思うんですが、、、

  • 画面上のソートボタンを押下した際にgetパラメータとして項目とソート(降順・昇順)を設定
  • controllerでパラメーターを取得し、ソート順を設定しタスクの再取得
  • 一覧画面を再描画

上記実装では、少し問題があります。それは、入力された検索条件をcontollerに渡せていないので、ソートボタンを押すと検索条件が未設定の状態で検索処理が行われてしまう恐れがあり下記のように使いにくいものとなってしまいます。。。

f:id:madogiwa0124:20180506225012g:plain

検索条件を引き継いでソートを変更する

先程でも書きましたが、問題はソートを行う際にcontrollerに対して既存の検索条件を設定出来ていないために問題が発生してしまっているので、ソート時にも検索条件をパラメーターとして送信する対応を行いましたφ(..)

_order_form.html.hamlでソートする際にhidden_tagを使って検索条件も合わせてgetパラメーターとして送信するようにしています。

index.html.haml

= render partial: 'search_form', locals: { search_attr: @search_attr }
%table{ border: 1 }
  %thead
    %tr
      %th= Task.human_attribute_name(:title)
      %th= Task.human_attribute_name(:status)
      %th
        = Task.human_attribute_name(:priority)
        = render partial: 'order_form', locals: { target: 'priority', search_attr: @search_attr, order: 'asc' }
        = render partial: 'order_form', locals: { target: 'priority', search_attr: @search_attr, order: 'desc' }

_search_form.html.haml

%div
  = form_tag(search_tasks_path, method: :get) do
    = label_tag :task_title, Task.human_attribute_name(:title)
    = text_field_tag 'task[title]', search_attr[:title]
    = label_tag :task_status, Task.human_attribute_name(:status)
    - statuses = Task.statuses_i18n.invert
    = select_tag 'task[status]', options_for_select(statuses, search_attr[:status]), prompt: '-'
    = submit_tag t('common.search')

_order_form.html.haml

= form_tag(search_tasks_path, method: :get, style: 'display: inline') do
  = hidden_field_tag 'task[title]', search_attr[:title]
  = hidden_field_tag 'task[status]', search_attr[:status]
  = hidden_field_tag "order[#{target}]", order
  = submit_tag order_icon(order) # asc: ▲、desc:▼

ちなみにdeadline DESC等の文字列は、order_stringメソッドを使って生成するようにしてみました(・∀・)

task_controller.rb

class TasksController < ApplicationController
  def index
    prepare_search_attr
    @tasks = Task.all.order(order_string)
  end

  def search
    prepare_search_attr
    @tasks = Task.search(@search_attr).order(order_string)
    render :index
  end

  private

  def order_string
    return 'created_at DESC' unless params.key?(:order)
    order_params.to_h.map { |key, val| "#{key} #{val.upcase}" }.join(',')
  end

  def prepare_search_attr
    @search_attr = { title: '' }
    @search_attr = task_params.delete_if { |_key, val| val.blank? } if params.key?(:task)
  end

  def task_params
    params.require(:task).permit(:title, :description, :status, :priority, :deadline)
  end

  def order_params
    params.require(:order).permit(:deadline, :priority)
  end
end

おわりに

今回はhidden_tagを使って検索条件を引き継いでソートする方法で今回は対応してみました。やりたいことは出来ましたが、もっとRailsっぽい良い方法があるような気がしますね・・・!
なにかまたいい方法があればまとめてみようと思いますφ(..)

参考

hidden_field - リファレンス - - Railsドキュメント

RubyonRails:form_tagを使って入力値がリセットされない検索フォームを作る

Railsでフォームを作る際にform_forを使うとページの再描画が発生しても入力値がリセットされませんが、form_tagを使うとリセットされてしまいます。。。

f:id:madogiwa0124:20180504220013g:plain

検索フォーム等では、form_tagを使用することが多いような気がするのですが、検索ボタンを押してもリセットされずに残ってほしいなー!と思い対応してみたので、その方法をメモしておきますφ(..)

入力値がリセットされない検索フォームの作り方メモ

今回は、indexアクションでは全件取得、searchアクションでは検索条件に合致したレコードを取得して、最終的に共通なViewを描画するようなケースを想定してます。

Controller側

controller側のポイントは、prepare_search_attrです。
indexの場合はパラメーターが無いケースがあるので、検索条件を保持するインスタンス変数@search_attrに初期値を入れておき、検索条件が設定されていた場合は、それを上書きするようにしています。

controller

class TasksController < ApplicationController
  def index
    prepare_search_attr
    @tasks = Task.all
  end

  def search
    prepare_search_attr
    @tasks = Task.search(@search_attr)
    render :index
  end

  private

  def prepare_search_attr
    @search_attr = { title: '', status: '' }
    @search_attr = task_params.delete_if { |_key, val| val.blank? } if params.key?(:task)
  end

  def task_params
    params.require(:task).permit(:title, :description, :status, :priority, :deadline)
  end
end

View側

view側のポイントは_search_form.html.hamltext_field_tagselect_tagを記載しているところです。
text_field_tagは第二引数に渡された値が初期値として設定され、select_tagoptions_for_selectの第二引数に渡された値が初期値として設定されます。

そのため、controller側で定義しておいた@search_attrを、渡して初期値を設定することが出来ます。

index.html.haml

%h1= t '.title'
= render partial: 'search_form', locals: { search_attr: @search_attr }
%br
%table{ border: 1 }
  %thead
    %tr
      %th= Task.human_attribute_name(:title)
      %th= Task.human_attribute_name(:status)
      %th= Task.human_attribute_name(:priority)
      %th= Task.human_attribute_name(:deadline)
      %th Actions

_search_form.html.haml

%div
  = form_tag(search_tasks_path, method: :get) do
    = label_tag :task_title, Task.human_attribute_name(:title)
    = text_field_tag 'task[title]', search_attr[:title]
    = label_tag :task_status, Task.human_attribute_name(:status)
    - statuses = Task.statuses_i18n.invert
    = select_tag 'task[status]', options_for_select(statuses, search_attr[:status]), prompt: '-'
    = submit_tag t('common.search')

おわりに

Railsの検索フォームの作り方は、毎度悩んでこれで良いのかなぁ感が大きいですね・・・(._.)もっと良い作り方等があれば、教えてください!!🙌

またコードの全文は万葉さんの公開している新人研修をやっている私のリポジトリにあるので見てアドバイスもらえるとありがたいですm( )m

github.com

参考

qiita.com

text_field - リファレンス - - Railsドキュメント

select_tag - リファレンス - - Railsドキュメント

RubyonRails:acts-as-taggable-onのN+1問題を解決する

自分で運営しているサービスのタグ付け機能にacts-as-taggable-onを使っていたのですが、N+1が発生しまくってしまったので、その解決方法をφ(..)メモメモ

acts-as-taggable-onのバージョンは下記の通りです。

$ gem list
acts-as-taggable-on (5.0.0)

解決法

修正前のコード

まず修正前のコードですが、普通にPageの全件を取得してtag_listでタグの名称のリストを取得して、それを表示するようなコードですね(・∀・)

controller

  def index
     @pages = Page.all
  end

view

<% @pages.each do |page| %>
  <% if page.tag_list.present? %>
    タグ:<%= page.tag_list.join(', ') %>
  <% end %>
<% end %>

しかし、以下のようなコードだと下記のようにタグの検索SQLがレコード単位に発生してパフォーマンスが良くないです。。。(N+1問題が発生してます、、、。)

log

  ActsAsTaggableOn::Tagging Load (0.5ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2  [["taggable_id", 23], ["taggable_type", "Page"]]
  ActsAsTaggableOn::Tag Load (0.7ms)  SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" ="taggings"."tag_id" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 AND (taggings.context = 'tags' AND taggings.tagger_id IS NULL)  [["taggable_id", 23], ["taggable_type", "Page"]]
  ActsAsTaggableOn::Tagging Load (0.6ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2  [["taggable_id", 16], ["taggable_type", "Page"]]
  ActsAsTaggableOn::Tag Load (1.0ms)  SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" ="taggings"."tag_id" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 AND (taggings.context = 'tags' AND taggings.tagger_id IS NULL)  [["taggable_id", 16], ["taggable_type", "Page"]]

修正後のコード

修正のポイントは、下記の2つですφ(..)

  • tagsincludesし、キャッシュすること
  • タグの名称取得にがtag_listを使用しないこと

controller側で、Pageを取得する際に.includes(:tags)を追記し、キャッシュするようにしています。

controller

  def index
     @pages = Page.all.includes(:tags)
  end

また、viewで表示するときにはtagsを使用し、名前だけ取得するときにはpluckを使うようにします(・∀・)

view

<% @pages.each do |page| %>
  <% if page.tags.present? %>
    タグ:<%= page.tags.pluck(:name).join(', ') %>
  <% end %>
<% end %>

これにより、毎回タグ関連のTBLを取得するSQLが発生を防ぐことが出来ました!\(^o^)/

参考

qiita.com

RubyonRails:activerecord-importを使って複数レコードを一括登録する(BULK INSERT)

みなさん、こんにちは。まどぎわです(・∀・)
バッチ処理等で複数のレコードを一括で登録する際にどのようなコードを書いていますか?今回は、そんなときに便利なactiverecord-importの使い方を調べたのでメモしておきますφ(..)

activerecord-importの使い方

改善対象のコード例

複数のレコードを取得して登録する処理でパッと思いつくのは下記のようなコードでしょうか?

 # 今月以降に登録された本を新作本を管理するTBLに登録する
class NewBook < ApplicationRecord
  def self.make_new_books
    books = Book.where('created_at >= ?', Time.current.beginning_of_month)
    books.each do |book|
      NewBook.create(book_id: book.id)
    end
  end
end

しかし、これを実行すると下記のようにレコード数分のINSERTを行うSQLが発行され、効率がよくありません。。。

  (0.1ms)  begin transaction
  SQL (1.5ms)  INSERT INTO "new_books" ("book_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["book_id", 2], ["created_at", "2018-04-21 08:41:30.705444"], ["updated_at", "2018-04-21 08:41:30.705444"]]
   (1.0ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "new_books" ("book_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["book_id", 3], ["created_at", "2018-04-21 08:41:30.710686"], ["updated_at", "2018-04-21 08:41:30.710686"]]
   (1.1ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "new_books" ("book_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["book_id", 6], ["created_at", "2018-04-21 08:41:30.715154"], ["updated_at", "2018-04-21 08:41:30.715154"]]
   (0.9ms)  commit transaction
以下省略

activerecord-importを使う準備

こんな時に便利なのが、activerecord-importというGemです!

github.com

使い方は、簡単でGemfileに下記を追記してbundle installを実行するだけです。

gem 'activerecord-import' 

activerecord-importを使ってコードを修正してみる

上で書いたコードをactiverecord-importで書き直すとこんな感じ。

class NewBook < ActiveRecord::Base
  # 今月以降に登録された本を新作本を管理するTBLに登録する
  def self.make_new_books
    destroy_all
    start_at = Time.current.iso8601(2)
    book_ids = Book.where('created_at >= ?', Time.current.beginning_of_month).pluck(:id)
    NewBook.import book_ids.map{ |book_id| NewBook.new(book_id: book_id) }
    end_at = Time.current.iso8601(2)
  end
end

実行されるSQLは下記のような感じです。INSERT文が一つになっていて効率的ですね(・∀・)

  Class Create Many Without Validations Or Callbacks (2.5ms)  INSERT INTO "new_books" ("id","book_id","created_at","updated_at") VALUES (NULL,2,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,3,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,6,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,7,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,8,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,9,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,10,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529')

修正前後のベンチマーク

レコード件数は、7件とかなりイマイチですが、、、ベンチマークをとってみました。

case start_at end_at time
nomal 2018-04-21T14:09:51.75 2018-04-21T14:09:51.82 0.7s
user import 2018-04-21T14:10:29.37 2018-04-21T14:10:29.41 0.4s

これぐらいの件数でも意外と差がでますね!(・∀・)
やはりactiverecord-importの方が効率が良いみたいです!

おまけundefined methodimport' for Model`と出る場合

良くわからないけど、rubyのバージョンを2.4.0から2.4.3に変更したら治った・・・。

おわりに

夜間バッチでランキングを作ったりと実際の業務だと一括登録の処理は実装する機会が多そうですね!
一括登録処理は結構重くなりがちになりそうなので、activerecord-importを使って効率的な処理で行えるようにしたいですね(・∀・)

RubyonRails:ActiveRecord::Relationをto_aすると色々とはかどるかも知れない件

最近ActiveRecord::Relationをto_aすると色々とはかどるかも知れないという知見を得たのでメモしておきますφ(..)

whereを使って取得した結果(ActiveRecord::Relation)をselectとか使って結果から特定の値を持つレコードを取ってこようとするとやDBアクセスが走っちゃうとかあると思うんですけども、そういうときはto_aして配列に変換してあげるとDBアクセス発生させずに操作が出来るので便利という話しです。

下記は例ですが、カテゴリ別に投稿されたブログのタイトルの一覧を取得しようとした場合、view内で@blogsをカテゴリで絞込を行おうとするとDBアクセスが発生し、パフォーマンスが落ちてしまいます。

controller

# controller
def index
  @blogs = Blog.where(user_id: current_user.id)
  @category = Category.all
end

view

<% @category.each do |c| %>
  <% # ここのwhereでDBアクセスが入ってしまう。。。 %>  
  <% blogs = @blogs.where(category_id: c.id)%>
  <h3><%= c.name %></h3>
  <% blogs.each do |blog| %>
    <p>blog.title</p>
  <% end %>
<% end %>

しかし、to_aしてあげるとArrayのメソッドが使えるようになり下記のように書き直すことが出来ます!
配列の操作なのでDBアクセスは発生しませんし、group_byを使ってカテゴリをkeyとしたhashに分割することで綺麗に処理が書けますね\(^o^)/

controller

# controller
def index
  @blogs = Blog.where(user_id: current_user.id)
               .to_a.group_by{ |blog| blog.category_id }
  @category = Category.all
end

view

<% @category.each do |c| %>
  <h3><%= c.name %></h3>
  <% blogs[c.id].each do |blog| %>
    <p>blog.title</p>
  <% end %>
<% end %>

以上です(・∀・)

RubyonRails:rails s実行時にAddress already in useが発生する。。。

rails s実行時に下記のようなErrorが発生して少しハマったので、メモφ(..)

Memo

事象

rails s実行時に下記Errorが発生し、ローカルでWebサーバーが起動しなくなってしまった(T_T)

$ rails s
=> Booting Puma
=> Rails 5.1.3 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.10.0 (ruby 2.4.0-p0), codename: Russell's Teapot
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Exiting
.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/puma-3.10.0/lib/puma/binder.rb:270:in `initialize': Address already in use - bind(2) for "0.0.0.0" port 3000 (Errno::EADDRINUSE) 

解決策

Errorメッセージを見てみると、すでに0.0.0.0:3000が使われてしまっているとのことなので、lsof -i:3000でポート3000番を使用しているプロセスを確認

$ lsof -i:3000
COMMAND     PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ruby       1090  hoge   19u  IPv4 0xbf3ec388665ab961      0t0  TCP *:hbci (LISTEN)
ruby       1090  hoge   24u  IPv4 0xbf3ec388655a2ef1      0t0  TCP localhost:hbci->localhost:63436 (CLOSE_WAIT)
ruby       1090  hoge   25u  IPv4 0xbf3ec388656663d1      0t0  TCP localhost:hbci->localhost:63438 (CLOSE_WAIT)
Google    59176  hoge  132u  IPv4 0xbf3ec38866494231      0t0  TCP localhost:64018->localhost:hbci (ESTABLISHED)

確かにrubyのプロセスが既に起動してしまっているようなので、killします。

$ kill -9 1090
$ lsof -i:3000
# 結果なし

0.0.0.0:3000のプロセスがkillされたので、再度rails sを実行します。

$ rails s
=> Booting Puma
=> Rails 5.1.3 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.10.0 (ruby 2.4.0-p0), codename: Russell's Teapot
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
Started GET "/users" for 127.0.0.1 at 2018-04-07 13:44:58 +0900

正常に起動しました(・∀・)