Madogiwa Blog

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

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

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


RubyonRails:Gemを使わずに画像アップロード処理を実装する

こんばんは、まどぎわですφ(..)
皆さんは画像アップロード処理はどうやって実装していますか?
RailsだとCarrierWaveを使った実装がメジャーだと思いますが、今回はあえてGemを使わずに画像アップロード処理を実装する方法をメモしておきます。
railsのバージョンは、5.1.2を使用しています。

画像アップロード

画像をアップロードする方法には、大きく分けて2つの方法があります。

  • 任意のディレクトリに配置する
  • DBにバイナリとして保存する

それぞれ方法をみていきましょう!

任意のディレクトリに配置する

今回はアップロードのみを行う画面を用意して任意のディレクトリに配置するケースを想定してみました(・∀・)

f:id:madogiwa0124:20180407084733p:plain

View側のポイントは、form_tagの引数にmultipart: trueを設定することと、file_field_tagを使うことです。

view

<%= form_tag(upload_process_path, method: 'post', multipart: true) do %>
  <label>
    ファイルを指定:
    <%= file_field_tag :upfile %>
  </label>
  <%= submit_tag 'アップロード' %>
<% end %>

controller

  def upload_process
    # パラメータからファイルを取得
    file = params[:upfile]
    # ファイル名を取得
    name = file.original_filename
    # 出力先のパスを設定※今回は、public/docs配下に配置
    output_path = Rails.root.join('public/docs', name)
    # ファイルを出力
    File.open(output_path, 'wb'){ |f| f.write(file.read) }
  end

DBにバイナリとして保存する

今回はUserの編集画面でプロフィール画像を設定し、それをDBに保存するようなケースを想定してみました(・∀・)

f:id:madogiwa0124:20180407085925p:plain

ポイントは、string型のctypeとbinary型のphotoプロパティを持たせることと、それぞれに下記値を設定してあげることです。

  • ctype = アップロードファイルのcontent_type
  • photo = アップロードファイルの中身

view(抜粋)

<%= form_with(model: user, local: true, multipart: true) do |form| %>

  <div class="field">
    <%= form.label :photo %>
    <%= form.file_field :photo, id: :author_photo %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

controller(抜粋)

  def create
    @user = Author.new(author_params)
    if @user.save
      redirect_to @user, notice: 'User was successfully created.'
    else
      format.html { render :new }
    end
  end

    def user_params
      if params[:user][:photo]
        # ctypeをパラメータに設定されたアップロードファイルから設定
        params[:user][:ctype] = params[:user][:photo].content_type
        # photoにアップロードファイルの中身を設定
        params[:user][:photo] = params[:user][:photo].read
      end
      params.require(:user).permit(:name, :birth, :address, :ctype, :photo)
    end

おまけ:DBに保存した画像を表示する方法

まずは画像を表示するアクションを定義します。
send_dataを使うことで、ファイルそのものを返却することが出来ます(・∀・)

controller(抜粋)

  def show_image
    send_data @user.photo, :type => @user.ctype, :disposition => 'inline'
  end

そしてそのメソッドへのroutingを定義します。
これによりusers/id/show_imageでユーザーのプロフィール画像にアクセス出来るようになりました!

routes

  resources :users do
    member do
      get 'show_image'
    end
  end
$ rake rotes
show_image_user GET /users/:id/show_image(.:format)  users#show_image

最後にviewのimageのurlに先程作成したroutingを設定すれば、画像を表示することが出来ます(・∀・)

<p>
  <strong>Photo:</strong>
  <%= image_tag(show_image_user_path(@user), :width => 300) %>
</p>

↓こんな感じ

f:id:madogiwa0124:20180407093512p:plain

おわりに

今回は、gemを使わずにファイルアップロード機能を実現する方法を整理してみました。
試してはないですが、herokuにリリースする際に画像アップロードする機能は、careerwaveを使って普通に実装すると24時間で消えてしまいますが、今回のようにDBに保存するようにすれば消えずに残すことが出来る気がしました。φ(..)

簡単なユーザーのアイコン等の容量が小さく、簡単なアップロード処理であればgemを使わずに実装してみても良いかもしれませんね(・∀・)

参考資料

cre8cre8.com

qiita.com

RubyonRails:最近頂いたコードレビューを整理してみました。

みなさん、こんばんは!まどぎわです(・∀・)
最近、ソースコードレビューを受ける機会があったので、指摘頂いた内容を復習も兼ねて備忘目的で下記に整理してみました。

また私が作ったRailsアプリケーション環境関連は下記の通りです。

category version
ruby 2.4.3
rails 5.1.4
mysql 14.14

また、viewのテンプレートエンジンにはhamlを使用しています。

レビュー内容まとめ

Model

migrationとmodelのvalidationは合わせる

modelで必須とした項目は、migrationファイルでもnull: falseとして、DBでもNOTNULL制約を設定するとDBでも必須チェックを行うため安心です(・∀・)

# model
validates :name, presence: true
# migration
# before
t.string :name
# after
t.string :name, null: false

validationの検証の実行条件はifオプションを活用する

validationメソッドには、エラーとなる条件のみの記載し、ifオプション側にvalidationメソッドの実行条件を記載することで、validationメソッドが汎用的になり、可読性も上がりますφ(..)

# before
validate :birthday_cannot_be_in_the_future
def birthday_cannot_be_in_the_future
    if birthday.present? && birthday.future?
    errors.add(:birthday, "can not specify your future date as your birth date.")
  end
end

# after
validate :birthday_cannot_be_in_the_future, if: -> { birthday.present? }
def birthday_cannot_be_in_the_future
  if birthday.future?
    errors.add(:birthday, "can not specify your future date as your birth date.")
  end
end

Model内でしか活用しないメソッドはprivateにする

インスタンス生成後に呼ばれないメソッド等はprivateに定義した方が、不用意に呼ばれたりすることがなくなり良いです(・∀・)

# before
validate :birthday_cannot_be_in_the_future, if: -> { birthday.present? }
def birthday_cannot_be_in_the_future
  if birthday.future?
    errors.add(:birthday, "can not specify your future date as your birth date.")
  end
end

# after
validate :birthday_cannot_be_in_the_future, if: -> { birthday.present? }

private

def birthday_cannot_be_in_the_future
  if birthday.future?
    errors.add(:birthday, "can not specify your future date as your birth date.")
  end
end

Controller

インスタンスの生成処理がシンプルな場合はbeforeアクションに記載せず各アクションに記述する

scaffoldで生成されるコードでは、before_actionでインスタンス変数を設定するようなコードになっていますが、findしてインスタンス変数を設定するぐらいであれば、各アクションに記述した方が可読性が良くなることが多いようですφ(..)

# before
before_action :set_user, only: [:show, :edit, :update, :destroy]

def show
end

private

def set_user
  @user = User.find(params[:id])
end

# after

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

Controllerのbefore_actionにおける インスタンス変数セットについて

あればfind無ければnewする場合は、find_or_initializeメソッドを使用する

特定の値で検索し、無ければその値でインスタンス変数を生成する場合は、find_or_initializeメソッドを使うとスッキリかけますφ(..)

  # before
  def set_profile
    @profile = Profile.find_by(user_id: params[:id])
    @profile ||= Profile.new
  end

  # after
  def set_profile
    @profile = Profile.find_or_initialize_by(user_id: params[:id])
  end

※findする条件とインスタンスを生成する際の値が違う場合は、検索条件を引数にwhereを呼び出し、インスタンス生成時のプロパティを引数にfirst_or_initializeを呼び出すと良いです。

  # before
  def set_profile_user
    @profile = Profile.find_by(id: params[:id])
    @profile ||= Profile.new(profile_params)
    @user = User.find(@profile.user_id)
  end
  
  #after
  def set_profile_user
    @profile = Profile.where(id: params[:id])
                      .first_or_initialize(profile_params)
    @user = User.find(@profile.user_id)
  end

first_or_initializeとfind_or_initialize_by - Qiita

View

省略出来る{}は削除する

シンプルなことですが、意外とやりがちだと思うので、注意したいですねφ(..)

- # before
= render 'form',{ user: @user, next_action: 'create' }

- # after
= render 'form', user: @user, next_action: 'create'

機能単位にpartialを切りだす

viewファイルは機能単位にpartialを切り出してあげた方が、application.html.erbは特にモリモリになりがちなので良いです。
また、ファイルが分割されることにより、可読性があがるだけでなくファイル競合の防止等のメリットもあるかと思いました(・∀・)

- # before
  %body
    %header
      - unless current_user
        = link_to "Login", login_path
        = link_to "Signup", signup_path
      - else
        = link_to "Profile", current_user
        = link_to "Logout", logout_path, method: :delete
        
- # after
  %body
    %header
      = render 'layouts/navbar'

TestCode

登録成否は、change matcherを使用する

change matcherだけでなく、色々なmatcherを使って綺麗なテストコードを書けるように意識したいです。

# before
let(:user_before_cnt){ User.count }
context "User#create時にエラーが発生した場合" do
  before do
    post signup_path, params: invalid_user_param
  end
  it "ユーザーが登録されないこと" do
    expect(User.count).to eq user_before_cnt
  end
end

[使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita](https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5)

# after
context "User#create時にエラーが発生した場合" do
  it "ユーザーが登録されないこと" do
    expect {
      post signup_path, params: invalid_user_param
    }.to_not change(User, :count)
  end
end

メソッド実行結果のtrue/false判定はbe_methodnameを使用する

メソッドの返り値のtrue/falseを検証する場合は、eqではなくbe_methodnameを使用した方が綺麗にかけますφ(..)

# before
it "ログイン状態となること" do
  post signup_path, params: valid_user_param
  expect(!!current_user).to eq true
end

# after
it "ログイン状態となること" do
  post signup_path, params: valid_user_param
  expect(current_user).not_to be_blank
end

インスタンス変数ではなく、letを使う

letは、遅延評価(使用される時に変数が生成される)ため、実行負荷の軽減が見込めるようですφ(..)
他にもメリットがいくつかあるようです、下に参考のリンクを記載しておきました!

# before
@user = FactoryBot.create(:valid_user)

# after
let!(:user){ FactoryBot.create(:valid_user) }

RSpecのletを使うのはどんなときか?(翻訳) - Qiita

factoryの共通部分はtraitでまとめる

traitを使うとfactory(テストデータ)の共通部分をまとめることが出来ます。
でもなんとなくfactory入れ子にするのは、まだ少し違和感がある・・・(._.)

# before
FactoryBot.define do
  factory :user do
    name 'Example User'
    email "User@example.com"
    password 'foobar'
    password_confirmation 'foobar'
  end
  factory :valid_user_with_career, class: User do
    name 'Example User'
    email 'User_with_career@example.com'
    password 'foobar'
    password_confirmation 'foobar'
    after :create do |user|
      create(:vallid_career, user: user)
    end
  end
end

# after
FactoryBot.define do
  factory :user_difine do
    trait :valid do
      name 'Example User'
      email "User@example.com"
      password 'foobar'
      password_confirmation 'foobar'
    end
    trait :with_profile do
      after :create do |user|
        create(:profile, user: user)
      end
    end
    factory :valid_user, class: User, traits: [:valid]
    factory :valid_user_with_profile, class: User, traits: [:valid,:with_profile]
  end
end

FactoryGirlのtransientとtraitを活用する - Qiita

1度しか使わないようなデータは、factoryに定義せずテストコードで生成する

1度しか使わないのもは、factoryに書くよりもテストコードに直接書いた方が、factoryがスッキリし、テストコード上に意図が現れるのでわかりやすいですね(・∀・)

# before
FactoryBot.define do
  factory :no_name_user, class: User do
    name ''
    email 'user@example.com'
    password 'foobar'
    password_confirmation 'foobar'
  end
end
RSpec.describe User, type: :model do
  describe 'Userのモデリングに関するテスト' do
    it 'nameが必須項目となっていること' do
      user = FactoryBot.build(:no_name_user)
      user.name = ''
      expect(user.valid?).to eq false
    end
  end
end

# after
RSpec.describe User, type: :model do
  describe 'Userのモデリングに関するテスト' do
    let(:user){ FactoryBot.build(:user) }
    it 'nameが必須項目となっていること' do
      user = FactoryBot.build(:no_name_user)
      user.name = ''
      expect(user.valid?).to eq false
    end
  end
end

おわりに

個人開発だけだと自分が書いているコードが良い感じなのかが良くわからないので、ソースコードレビューは、本当に学びが多いですね・・・!
これからもレビュー内容を定期的に整理して、きちんと身につけられるようにしたいですφ(..)

RubyonRails:has_oneで1:1のAssociationを定義し、モデルを適切に分割するメモ

こんばんは、まどぎわです(・∀・)

RailsアプリケーションでUserモデルにプロパティがモリモリマッチョになっていませんか?? 例えばこんな感じ。

Userモデル

  • id
  • email
  • password
  • name
  • birthday
  • sex
  • job etc...

こうなってくるとプロパティだけでなくて、Userモデルにメソッドもモリモリマッチョにになっちゃう可能性がありますよね・・・!
そういうときは、has_oneを使ってモデルを分割してあげるとスッキリします(・∀・)

has_oneとは?

まずhas_oneとは一体なんなのか?ということですが、Railsリファレンスを見てみると下記と記載されています。

指定のクラスがこのクラスの子であることを宣言

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

これだと正直わからないのですが、Railsガイドに良い感じの説明が記載されていました。

has_one関連付けも、他方のモデルとの間に1対1の関連付けを設定します。しかし、その意味と結果はbelongs_toとは若干異なります。has_one関連付けの場合は、その宣言が行われているモデルのインスタンスが、他方のモデルのインスタンスを「まるごと含んでいる」または「所有している」ことを示します。

railsguides.jp

上記説明の通り、has_oneを使えば1:1のアソシエーションを定義することが出来ます!φ(..)

Userモデルを分割する

has_oneについて、概要が理解出来たところで実際にUserモデルを分割してみます。
今回は一番最初に書いた下記のようなプロパティが定義されているUserモデルを例にして分割方法を考えてみようと思います。

  • id
  • email
  • password
  • name
  • birthday
  • sex
  • job

上記プロパティを分類するとログイン関連の情報を表すアカウント系とユーザーの情報を表すプロフィール系の項目に分けられるんじゃないないかと思いますので、下記のように分けてみようと思います!(・∀・)

プロパティ 分割後モデル
id User
email User
password User
name Profile
birthday Profile
sex Profile
job Profile

Profileモデルを定義する

まずはUserのプロフィール関連の項目を設定するProfileモデルを定義するために下記コマンドを実行します。

rails g model Profile name:string birthday:date sex:interger user:references

準備はこれでOK!(・∀・)

associationを定義する

それでは、実際にhas_oneを使ってassociationを定義していきます。
実装はかなりシンプルでUserProfileに1行追加するだけです!(・∀・)

profile.rb

  belongs_to :user

user.rb

  has_one :profile, dependent: :destroy

belongs_toをUserとProfileどちらに記載するか迷うところですが、これはどちらがメインのモデルかを考えて、メインのモデルにhas_oneを記載します。
例えば、UserはProfileを持つこれが成り立つならUserモデルがメインと言えます。※ProfileはUserを持つとは言えないですよね?

おわりに

いかがだったでしょうか?has_oneを使うとモデルが分割できて、モデルの実装がスッキリしますね!(・∀・)
またモデルを分割出来るとcontrollerも分割出来る可能性も大きいので、fat controllerをより防ぎやすくなると思います。
適切にモデルを分割して、可読性の高いコード、わかりやすいテーブル設計が出来るように気をつけて行きたいですねφ(..)

RubyonRails:Rails5アプリケーションにBootstrap4を導入する

みなさん、こんにちは。まどぎわです(._.)

最近、作成しているRailsアプリケーションにBootstrap4を導入してみたので、その手順をメモしておきますφ(..)

手順

環境情報

試した環境を下記に記述しておきます。

gem version
rails 5.1.4
bootstrap 4.0.0

Gemのインストール

Gemfileに下記を追記し、bundle installを実行します。

gem 'bootstrap', '~> 4.0.0'
gem 'jquery-rails'

ちなみに、sprockets-railsv2.3.2.以上がインストールされている必要があります。
確認方法は下記の通りです。

$ bundle show |fgrep sprockets-rails 
  * sprockets-rails (3.2.1)

application.js、application.cssの編集

application.jsは、下記のように編集します。

// 省略
//= require rails-ujs
//= require jquery3 // 追記
//= require popper // 追記
//= require bootstrap-sprockets // 追記
//= require turbolinks
//= require_tree .

application.cssは、application.scssにリネームし、下記のように修正します。

// 省略
 *= require_tree .
 *= require_self
 */
 @import "bootstrap"; // 追記

FontAwesomeのインストール

最後にIconを使用するためにFontAwesome(ver 4.7.0)を導入します。

下記のようにapplication.html.erbに追記すれば、OKです。

# haml
= stylesheet_link_tag "https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
# erb
<%= stylesheet_link_tag "https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" %>

サンプル

f:id:madogiwa0124:20180312005054p:plain

参考資料

github.com

qiita.com

以上です。