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

HamlとSassの短縮方法

はじめに

コードを書いていくうちに思いついた、Hamlコードを短くする方法の共有です。
正直当たり前な内容ではありますが、意外とこれが出来ることに気づきにくいのではないかと思い、投稿しました。

実行

例えば次のようなHamlコードがあるとします。

.header
  ヘッダー
.body
  .body__content1
    .body__content1__red
      一つ目のサンプル
    .body__content1__blue
      first sample
  .body__content2
    .body__content2__red
      二つ目のサンプル
    .body__content2__blue
      second sample
.footter
  フッター

bodyの部分にcontents1contents2があり、
それぞれ子要素にredblueの二つのクラスがあります。
redの方には日本語、blueの方には英語でテキストを入力しています。

このビューファイルに対し二つの文章を横並びにし、redクラスは赤文字、blueクラスは青文字にするためには次にようなCSSファイルを読み込めば良いです。

.body{
  &__content1{
    display: flex;
    .red{
      font-color: red;
    }
    .blue{
      font-color: blue;
    }
  }
  &__content2{
    display: flex;
    .red{
      font-color: red;
    }
    .blue{
      font-color: blue;
    }
  }
}

この状態だと、content1content2も内容は一緒なので、とても無駄です。
無駄だけならまだしも、コードを書き換える際に間違いが生じることもあります。
このように、異なる全く同じCSSを当てる場合クラス名を同じにすると間違いが起こりにくくなります。
先ほどのHamlコードは次のように書き換えられます。

.header
  ヘッダー
.body
  .body__content
    .body__content__red
      一つ目のサンプル
    .body__content__blue
      first sample
  .body__content
    .body__content__red
      二つ目のサンプル
    .body__content__blue
      second sample
.footter
  フッター

このように書けば同じクラス名の別のオブジェクトとして扱われます
こうすればCSSを短縮して次のように書くことができます。

.body{
  &__content{
    display: flex;
    .red{
      font-color: red;
    }
    .blue{
      font-color: blue;
    }
  }
}

これが2つのオブジェクト両方に適用されるので、コードを書き直す前と同じ結果になります。

終わりに

今回の内容は、htmlで書いている時は普通にやっていたことですが、
hamlでは階層を作りながら書いていくため全てのオブジェクトは別のクラスを持っていたなければいけないという思いこみがありましたが、その誤解が上手い具合に解けた気がします。

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

railsのlow level cache

low level cache

RailsGuideより

lowレベルキャッシュの最も効果的な実装方法は、Rails.cache.fetchメソッドを利用することです。このメソッドは、キャッシュの書き込みと読み出しの両方に対応しています。引数が1つだけの場合、キーを読み出し、キャッシュから値を取り出して返します。ブロックを引数として渡すと、キャッシュにヒットしなかった場合にブロックが実行されます。ブロックの戻り値は、指定のキャッシュキーの下にあるキャッシュに書き込まれます。キャッシュにヒットした場合は、ブロックを実行せずにキャッシュの値を返します。

def cache_users
  key = "cache_users"

  Rails.cache.fetch(key, expired_in: 60.minutes) do
    User.all.to_a
  end
end

オブジェクトをキャッシュしたい場合

class User < ApplicationRecord
  def cache_users(user_id)
    key = "#{cache_key}/cache_users"

    Rails.cache.fetch(key, expired_in: 60.minutes) do
      User.find(user_id)
  end
end

cache_keyメソッドを使っているので、キャッシュキーは233-20140225082222765838000/cache_usersのような形式になります。cache_keyで生成される文字列は、モデルのidとupdated_at属性を元にしています。
その為userが更新されるたびにキャッシュを無効にできるので、古いデータが返される心配がありません。

注意

キャッシュする値は、ブロックの中の値です。
User.allUser.where()などの返り値はActiveRecord_Relationクラスのオブジェクトなので、
このオブジェクトをキャッシュしても、DB問い合わせは発生してしまいます。
DB問い合わせ結果をキャッシュしたい場合はto_aする事で、DB問い合わせ行い、問合せ結果をArrayにした値をキャッシュさせます。

参考

https://railsguides.jp/caching_with_rails.html
https://qiita.com/srockstyle/items/3f1dad0c88c9ef4c5288
https://qiita.com/yamashun/items/bf9a3d29de749cf18f2e

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

Rails リファクタリング まとめてみた

書いてあること

  • リファクタリングとは
  • リファクタリングは何のため?
  • ビューのリファクタリング
    1.enum
    2.enumの使い方

  • コントローラーのリファクタリング
    1.scopeを使って検索ロジックをモデルに移す
    2.コントローラに書かれたロジックをモデルのメソッドで書き直す

  • 参考ページ

  • 終わりに

リファクタリングとは

リファクタリングは処理の内容を変えずに冗長なコードを削除したり、コードを改善すること。

リファクタリングは何のため?

  • 読み手が最短時間でコードを理解できるようになる(チーム開発など、人と作業をするとき負担を減らす)
  • 修正がしやすくなる(未来の自分への優しさ)

ビューのリファクタリング

1.enum

int型、boolean型で定義されたカラムを、文字列で表現できる

schema.rb
こんなテーブルができてます
  create_table "tasks", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
    t.string "title", null: false
    t.string "content"
    t.datetime "start_at", null: false
    t.datetime "finish_at", null: false
    t.integer "kind", null: false ←今回はこのカラムのロジックでビューの表示が変わるような記述がある
    t.boolean "finished", default: false, null: false
index.html.haml
- if task.kind == 0
      私用
- if task.kind == 1
      仕事
- if task.kind == 2
      その他

この記述で保守性が下がる理由

  • kindの0, 1, 2がそれぞれ何を意味するのか、ビューを見ないと分からない
  • kindの種類が増える度、新たにif文を足さなければいけない

2.enumの使い方

1.モデルにenumを定義

モデル
class Task < ApplicationRecord
  enum kind: [:individual, :work, :others]
end

2.hamlの書き換え

haml
%tr
  %td
    - if task.kind == 0
      私用
    - if task.kind == 1
      仕事
    - if task.kind == 2
      その他
  %td
   = task.title
↓
/if文を削除し、task.kindと書き換える/
%tr
  %td
    = task.kind
  %td
    = task.title
/以下省略/

3.enumを使った書き換え
enumを定義すると、[モデル名].[カラム名の複数形] のような形で、そのカラムに設定したenumを全て表示することができる

haml
.form-wrapper
  = form_for @task, html: {class: 'form-group'} do |f|

    = f.label :kind
    = f.select :kind, [0, 1, 2], {},class: 'form-control', placeholder: 0  
/配列に数字がベタが記されている部分を変えます/
/以下略/
"f.select"に引数として渡す
.form-wrapper
  = form_for @task, html: {class: 'form-group'} do |f|

   = f.label :kind
   = f.select :kind,Task.kinds.keys, {}, class: 'form-control', placeholder: 0 
 /Task.kinds.keysのように記述することによって、enumで設定したkeyの一覧を得ることができる/

Task.kindsのなかみ
=> {"individual"=>0,"work"=>1, "others"=>2}
Keyをつけると下記のようになる
=> ["individual", "work", "others"]

4.enum_helpのインストール
enum_help」というgemを利用すると、enumの日本語化が簡単に行える

gemfile
# 末尾に追記
gem 'enum_help'

$ bundle installする

5.ja.ymlとビューを編集して日本語化

ja.yml
ja:
  activerecord:

#ja:の下に記述を追加する
ja:
  enums:
    task:
      kind:
        individual: "私用"
        work: "仕事"
        others: "その他"
  activerecord:
haml
%td
    = task.kind
↓
#書換える
 %td
    = task.kind_i18n
haml
= f.label :kind
= f.select :kind,Task.kinds.keys, {}, class: 'form-control', placeholder: 0
↓
#書換える
 = f.label :kind
 = f.select :kind, Task.kinds_i18n.invert, {}, class: 'form-control', placeholder: 0

コントローラーのリファクタリング

1.scopeを使って検索ロジックをモデルに移す

index.html.haml
def index
    @task = Task.new
    @tasks = Task.where('start_at > ?', Time.zone.now).order(start_at: :asc)
  end

@tasksの定義が冗長という問題は、モデルにscopeを定義することによって解決できます。

モデルにscopeを定義

models/task.rb
class Task < ApplicationRecord
  enum kind: [:individual, :work, :others]
end

class Task < ApplicationRecord
  enum kind: { individual: 0, work: 1, others: 2 }

  scope :incoming,  -> { where('start_at > ?', Time.zone.now) }
end

定義したscopeを使って@tasksを再定義

controller
  def index
    @task = Task.new
    # モデルに定義したscopeはメソッドのように呼び出せる
    @tasks = Task.incoming.order(start_at: :asc)
  end

2.コントローラに書かれたロジックをモデルのメソッドで書き直す

コントローラのupdateアクション内の「finishedカラムをtrueにする処理」を、Taskモデルのインスタンスメソッドとしてmodels/task.rbに定義し直す

models/task.rb
class Task < ApplicationRecord
  enum kind: { individual: 0, work: 1, others: 2 }

  scope :incoming,  -> { where('start_at > ?', Time.zone.now) }

  # finishedカラムをtrueに更新するメソッドを定義
  def update_finished_true
    self.finished = true if self.finished == false
    self.save
  end
end
controller
  def update
    @task = Task.find(params[:id])
    if @task.finished == false
      @task.finished = true
      @task.save
      redirect_to tasks_path
    else
      render :index, alert: '既にタスク「#{task.title}」は完了しています '
    end
  end

# 定義しておいたメソッドを呼び出す
  def update
    @task = Task.find(params[:id])
    @task.update_finished_true
    redirect_to tasks_path
  end

元々は「finishedがtrueか、そうでないか」で更新を行うかどうかを決定していましたが、「現状finishedがtrueのtaskについて、updateアクションを呼び出す導線がビューにないこと」 「元々値がtrueのカラムにtrueを入れても問題がないこと」を理由に、if文を削除しています。

参考ページ

復習用:修正済

Rails Webook

終わりに

リファクタリングって書き直すの理解しずらいよね。書いてはみたものの色々ぶっ飛んでてわかりずらいので知識が深まったタイミングで書き直したいな。
でも、綺麗にかければ未来の自分がわかりやすい〜ってなるのかな。いや、なってくれないと困る。笑

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

rails-tutorial第14章

ユーザーをフォローする

まずはRelationshipモデルを作っていこう

$ rails generate model Relationship follower_id:integer followed_id:integer

作成されたマイグレーションファイルにインデックスを書き足していく。

db/migrate/[timestamp]_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[5.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

上二つは高速化のためのインデックスである。
最後は一意性を担保するためのインデックス。
これはfollower_idとfollowed_idの組み合わせは一つしかないよーって意味。

これで2回同じユーザーをフォローするとかはできなくなる。

UserとRelationshipの関連付け

まずは以下を見てみよう

app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

belongs_to :followerとすると、Relationshipのfollower_idとFollowerクラスのidカラムを結びつけますよーという意味になる。

ただ、結びつけたいのはUserクラスのidカラムなので、オプションでclass_name: "User"とすることで、follower_idとUserクラスのidカラムが結びつく。

では、Userモデルのファイルにはどのように書けば良いのだろうか?

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  .
  .
  .
end

まず、has_many :active_relationshipsとすると、active_relationshipsクラスを探そうとするが、そんなクラスはないのでオプションでclass_name: "Relationship"と指定している。逆に言えば、active_relationshipsの部分はなんでも良いのである。

次は、foreign_key: "follower_id"の部分だ。
これを指定しないと、UserクラスのidカラムをRelationshipクラスのどのカラムと関連づけるのかがわからなくなってしまう。(デフォルトではuser_idカラムと関連付けようとする)

なので、外部キーとして、Userクラスのidカラムと関連づけるカラムを指定してあげている。

これにより、@user.active_relationships
というような参照方法が可能になる。

また、@relationship.follower
という参照方法も可能になるってこと。

逆に言えば、belongs_toやhas_manyはメソッドを定義するメソッドであるとも言える。
(このケースだと、active_relationshipメソッド、followerメソッド)

なので、@user.active_relationships.first.followed
とすると、@userが最初にフォローしたユーザーが返ってくるということになる。

ここで全く関係ない余談

&.(ぼっち演算子)はレシーバーであるオブジェクトに対してあるメソッドを実行した時、そのオブジェクトがnilの場合、nilを返すことでエラーを出さなくしています。&.(ぼっち演算子)とはレシーバーであるオブジェクトがnilでなければそのまま結果を返し、nilの場合はnilを返すメソッドなのです。

もっと簡潔に

@user.active_relationships.first.followedだと長いのでどうにかできないか

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  has_many :following, through: :active_relationships, source: :followed
  .
  .
  .
end

これは、@user.active_relationships.followedを

followingメソッドを定義することにより、

@user.followingというように簡潔にかけるようにする。
(active_relationshipsメソッドを経由してfollowedメソッドを実行するのをfollowingと名付けますよーってこと)

relationshipモデルのバリデーションのテストをする

test/models/relationship_test.rb
require 'test_helper'

class RelationshipTest < ActiveSupport::TestCase

  def setup
    @relationship = Relationship.new(follower_id: users(:michael).id,
                                     followed_id: users(:archer).id)
  end

  test "should be valid" do
    assert @relationship.valid?
  end

  test "should require a follower_id" do
    @relationship.follower_id = nil
    assert_not @relationship.valid?
  end

  test "should require a followed_id" do
    @relationship.followed_id = nil
    assert_not @relationship.valid?
  end
end

次はバリデーションを設定する

app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end

さらに、生成されたRelationship用のfixtureでは、マイグレーション (リスト 14.1) で制約させた一意性を満たすことができません。ということで、ユーザーのときと同じで (リスト 6.31でfixtureの内容を削除したように)、今の時点では生成されたRelationship用のfixtureファイルも空にしておきましょう

ちなみにテストが全て落ちるというときは、fixtureを見た方が良い。
そもそも、fixtureのサンプルデータが間違っており、dbにそれを伝えたことで全てのテストが落ちるらしい。

これでテストは通る。

その他のメソッド定義

次にfollowingなどの便利メソッドをテストする

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "should follow and unfollow a user" do
    michael = users(:michael)
    archer  = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end
end

この状態では、まだ定義してないメソッドがあるので、それを定義していかないといけない。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  def feed
    .
    .
    .
  end

  # ユーザーをフォローする
  def follow(other_user)
    following << other_user
  end

  # ユーザーをフォロー解除する
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # 現在のユーザーがフォローしてたらtrueを返す
  def following?(other_user)
    following.include?(other_user)
  end

  private
  .
  .
  .
end

少しわかりにくいのは、

def follow(other_user)
  following << other_user
end

これは、そもそもfollow(other_user)メソッドがインスタンスメソッドなので、

self.following << other_user

の省略形だったことがわかる。
また、self.followingはフォローしているユーザーの配列を返すので、<<で問題ない。

ただ、どうやってrelationshipインスタンスが生成されるか、dbに保存されるかわからないので、安ラボの動画で書いてあるコードに変更。

def follow(other_user)
  self.active_relationships.create(followed_id: other_user.id)
end 

また、

def following?(other_user)
  following.include?(other_user)
end

include?メソッドは配列の要素に引数が含まれているかを判断してくれるメソッド。

余談:ダックタイピング

改良前

class EmploymentHandler
  def work(employees)
    employees.each do |employee|
      case employee
      when Staff   then employee.work
      when Manager then employee.work
      end
    end
  end
end

class Staff
  def work
    do_clean_up
  end
  def do_clean_up
    # ...
  end
end

class Manager
  def work
    do_check
  end
  def do_check
    # ...
  end
end

ダックタイピングを使うと、

class EmploymentHandler
  def work(employees)
    employees.each do |employee|
      employee.work
    end
  end
end

class Staff
  def work
    do_clean_up
  end
  def do_clean_up
    # ...
  end
end

class Manager
  def work
    do_check
  end
  def do_check
    # ...
  end
end

when case文がいらなくなったし、workメソッドを持っているクラスであれば、work(employees)メソッドを使えるようになった。

workメソッドを持っているということが、「ガーと鳴けば」に当たるのでは?

フォロワーを考える

これはフォローの時と逆のことをすれば良い

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower
  .
  .
  .
end

これで自分のフォロワーが@user.followersで参照できるようになったので、
テストを書いていこう。

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "should follow and unfollow a user" do
    michael  = users(:michael)
    archer   = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    assert archer.followers.include?(michael)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end
end

UIを実装していこう

これでメソッドなどは定義できたので、UIを実装していく。

まずはフォローのサンプルデータを作るために

db/seeds.rb
# リレーションシップ
users = User.all
user  = users.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) }
followers.each { |follower| follower.follow(user) }

ちなみに、followingとfollowersはローカル変数?

$ rails db:migrate:reset
$ rails db:seed

ルーティング設定

次にフォローしてる一覧ページとフォローされてる一覧ページを作るためのルーティングを設定していく。
urlは
/users/1/following や /users/1/followersのような形にしたい。

それを踏まえると、

config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
end

memberメソッドを使うと、
/users/:id/...
この...に何を入れますか?というのを書き足すことができる

じゃあ、followingアクションとfollowersアクションはどこに作るの?
Usersコントローラに書けば良い。

statsパーシャルを作る。

次にフォロワーなどの統計情報を表示するstatsパーシャルを作る。

app/views/shared/_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

そしたら、できたパーシャルをhomeページ(ログイン時)と、プロフィールページに挿入しよう。

その前に、なぜ<% @user ||= current_user %>のようなコードになるか見ていこう。

これは、static_pageコントローラのhomeアクションとuserコントローラのshowアクションで定義されているインスタンス変数が異なるからである。

showアクションには@userが定義されているが、static_pageコントローラでは定義されていない。なので、@userがいなければcurrent_userメソッドを呼び出すようにしている。

また、current_userメソッドはapplicationコントローラにmoduleをincludeしているので使える。

では、実際に埋め込んでいこう

```app/views/static_pages/home.html.erb
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="stats">
        <%= render 'shared/stats' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
  .
  .
  .
<% end %>

次にcssを整えてあげる

app/assets/stylesheets/custom.scss
.
.
.
/* sidebar */
.
.
.
.gravatar {
  float: left;
  margin-right: 10px;
}

.gravatar_edit {
  margin-top: 15px;
}

.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $gray-lighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
  a {
    padding: 0;
  }
}

.users.follow {
  padding: 0;
}

/* forms */
.
.
.

フォローボタンの設置

これは、自分以外のユーザーのプロフィールページで、フォローしてなかったらフォローボタンが、フォローしたらフォロー解除ボタンが表示されるようにする。

これをdryに書くにはフォローボタンをそのままパーシャルにすると便利。

具体的には、フォローボタン、アンフォローボタン、その二つをif文で表示するfollow_formパーシャルの計3つを作る。

app/views/users/_follow_form.html.erb
<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

<% unless current_user?(@user) %>
はもし@userが自分と同じなら、フォローボタンを表示しないようにする。

次はfollowパーシャルと、unfollowパーシャルを作成する。

app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

unfollowの方は、そのままform_forの引数にインスタンスを渡すと、patchリクエストになってしまうので、html: {method: :delete}としている。

三つのパーシャルができたので、follow_formパーシャルをusersのshowアクションのページに設置しよう。

app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="col-md-8">
    <%= render 'follow_form' if logged_in? %>
    <% if @user.microposts.any? %>
      <h3>Microposts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>

そしたら、relationshipsコントローラのcreateアクションとdestroyアクションのルーティングを設定しておこう。

config/routes.rb
Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup'  => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
  resources :relationships,       only: [:create, :destroy]
end

フォロワーやフォローしてる人一覧が出るページを作る。

そしたら、次はrelationshipsコントローラにdestroyアクションを定義していこう。
また、別々のアクションなのに、同じテンプレートを表示するように実装していく。

/users/id/followingと/users/id/followersで同じビューを使うってこと。

では、早速作っていこう

TDDで作っていくので、

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect following when not logged in" do
    get following_user_path(@user)
    assert_redirected_to login_url
  end

  test "should redirect followers when not logged in" do
    get followers_user_path(@user)
    assert_redirected_to login_url
  end
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers]
  .
  .
  .
  def following
    @title = "Following"
    @user  = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = "Followers"
    @user  = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

  private
  .
  .
  .
end

まずは、共通の変数をもつ、followingアクションと、followersアクションを定義する。

次に、フォローしているユーザーとフォロワーの両方を表示するshow_followビューを定義する。
注意点としては、パーシャルではなく、usersリソースのビューであるという点だ。

これで、異なるアクションから同じビューを呼び出し、内容をごっそり変えるというテクニックができる。

app/views/users/show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

注目すべきは

<% if @users.any? %>
  <div class="user_avatars">
    <% @users.each do |user| %>
      <%= link_to gravatar_for(user, size: 30), user %>
    <% end %>
  </div>
<% end %>

これは、もしフォローしているユーザー、自分をフォローしているユーザが1人以上いれば、顔画像の集合体を表示しますよーってこと。

で、その下の、

<% if @users.any? %>
  <ul class="users follow">
    <%= render @users %>
  </ul>
  <%= will_paginate %>
<% end %>

これは、フォローしているユーザー、または自分をフォローしているユーザーがいればその集合体を表示するということ。

render @usersなので、eachメソッドを使って、_userパーシャルが展開される。

また、will_pagenateに関しては、@usersを指定しても良いが、usersのviewでuserリソースを扱うことがデフォルトで設定されているので、別に書かなくても良い。

次に、統合テストを作る。

$ rails generate integration_test following

まずはテスト用のサンプルデータを作っていく

test/fixtures/relationships.yml
one:
  follower: michael
  followed: lana

two:
  follower: michael
  followed: malory

three:
  follower: lana
  followed: michael

four:
  follower: archer
  followed: michael

作ったサンプルデータを使って統合テストを書いていく

test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end
end

これでテスト通る。

Relationshipsコントローラの実装。

フォローボタンを設置したはいいが、肝心のコントローラがまだなので、それを実装していく。

まずは、コントローラを作成する。

$ rails generate controller Relationships

次にrelationshipsコントローラの基本的なアクセス制御についてテストを書いていこう。

test/controllers/relationships_controller_test.rb
require 'test_helper'

class RelationshipsControllerTest < ActionDispatch::IntegrationTest

  test "create should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      post relationships_path
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      delete relationship_path(relationships(:one))
    end
    assert_redirected_to login_url
  end
end

このテストを通るようにするために、各アクションとバリデーションを設定してく

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

次に二つのアクションの中身を実装していく。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    user = User.find(params[:followed_id])
    current_user.follow(user)
    redirect_to user
  end

  def destroy
    user = Relationship.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
  end
end

まずはcreateアクションから。
hidden_fieldタグで@user.idを格納したfollowed_idを送ったので、
params[:followed_id]と参照することができる。
ここにはプロフィールページの@userのidが入る。

次にdestroyアクションを見ていこう。

params[:id]のidはrelationshipのidカラムが入る。
なので、params[:id]をもつrelationshipにfollowedメソッドを実行することにより、
フォローされているユーザーが戻り、userという変数に格納される。

correct_userのバリデーションを設定しないのは?

それはcurrent_user.followにある。

もし、仮にいたずらでポストリクエストやデリートリクエストを送ったとする。

だとしても、フォローするのもフォローを解除するのもcurrent_userなので、誰かのアカウントを乗っ取ってフォローしたりフォローを解除しているわけではない。
他人と他人の関係性を勝手に作られるのなら困るけど、別に攻撃者のフォロワーやフォローしてる人が増え用が増えまいが関係ないよって話。

一応これでアクションの実装は完了。

発展編Ajaxを使ったフォローボタンの実装

これは、フォローする前とした後で、htmlにそこまで差がないことから、もう一度1から描画しなくても、変わった部分だけJS使って変えられないか?という話。

app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>

まず、Ajaxを有効にするには、form_forの引数?オプションでremote: trueを有効にする。

そして、フォロー解除フォームでも同じことをする

app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete },
             remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

フォームの更新が終わったので、今度はこれに対応するRelationshipsコントローラを改造して、Ajaxリクエストに応答できるようにしましょう。こういったリクエストの種類によって応答を場合分けするときは、respond_toメソッドというメソッドを使います。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

これ何をやっているかというと、current_user.followでDBへの処理は終わってる。
その時点で、jsを使ってデータを再読み込みしてるらしい。

また、ユーザーの中にはjsが無効になっている人もいるので、無効だったらhtmlを使うように以下の処理をしておく

config/application.rb
require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    # 認証トークンをremoteフォームに埋め込む
    config.action_view.embed_authenticity_token_in_remote_forms = true
  end
end

次にjsと埋め込みRubyを使ってフォローの関係性を作成する。

app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');
app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');

ここで何をやってるかというと、id = follow_formの部分を見つけて、以下(html("<%= escape_javascript(render('users/unfollow')) %>");)のhtmlに切り替えますよーっていう意味らしい。

これでAjaxの実装は終了。ただ、そこまで重要ではない。

Ajaxのテストをする

xhrオプションをtrueにすることで対応できるらしい。

test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user  = users(:michael)
    @other = users(:archer)
    log_in_as(@user)
  end
  .
  .
  .
  test "should follow a user the standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, params: { followed_id: @other.id }
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, xhr: true, params: { followed_id: @other.id }
    end
  end

  test "should unfollow a user the standard way" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship), xhr: true
    end
  end
end

このままでテストは通る。

これでフォローボタン、アンフォローボタンのAjax版が終了

ステータスフィードを完成させよう

具体的には、フォローしてるユーザーの投稿が自分のタイムラインに出るということ。

で、答えがわかったので、まずはテストを書いていこう

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "feed should have the right posts" do
    michael = users(:michael)
    archer  = users(:archer)
    lana    = users(:lana)
    # フォローしているユーザーの投稿を確認
    lana.microposts.each do |post_following|
      assert michael.feed.include?(post_following)
    end
    # 自分自身の投稿を確認
    michael.microposts.each do |post_self|
      assert michael.feed.include?(post_self)
    end
    # フォローしていないユーザーの投稿を確認
    archer.microposts.each do |post_unfollowed|
      assert_not michael.feed.include?(post_unfollowed)
    end
  end
end

この状態ではfeedメソッドがプロトタイプなので落ちてしまう。

さて、これをどうやって実現するか?

current_user.microposts + current_user.following.map { |n| n.microposts }

これでも、自分と自分のフォローしている人の投稿を取得できるけど、発行されるSQL文が非常に多くなってしまう。

これをどうやったら簡潔に、そして1回のSQL文の発行で収めることができるか?

フィードを実装していく。

フィードはタイムラインのことだよ。

これはsqlに関係してて難しい。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # パスワード再設定の期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  # ユーザーのステータスフィードを返す
  def feed
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
  end

  # ユーザーをフォローする
  def follow(other_user)
    following << other_user
  end
  .
  .
  .
end

これは、一つ目の?にfollowing_ids
following_idsはhas_manyメソッドを定義すると同時に使えるようになるメソッド。
フォローしているユーザーのidの集合体を引っ張ってくる。

2つ目の?にはidが入る。self.idの略かな?

これでテストが通る。

サブセレクト

SQLの発行回数を減らして高速化しようねーって話。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # ユーザーのステータスフィードを返す
  def feed
    Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
     following_ids: following_ids, user_id: id)
  end
  .
  .
  .
end

最終的な実装

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # ユーザーのステータスフィードを返す
  def feed
    following_ids = "SELECT followed_id FROM relationships
                     WHERE follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids})
                     OR user_id = :user_id", user_id: id)
  end
  .
  .
  .
end

renderのurl指定について

renderのurlは基本的にはapp/views以下の相対パスを入れる。

例えば、render 'shared/stats'
html.erbは省略してもok

便利コマンド

$ rails test
$ git add -A
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users

$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed

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

RSpec導入、記述ルール

目的

新しいポートフォリオを作成する際に、RSpecを利用したTDD(テスト駆動開発)
に挑戦するため、RSpec導入します。ついでにRSpecによるテストの簡単な記述ルールについて説明したいと思います。

RSpecとは

RSpec とは Ruby プログラマー向けのBDD(Behaviour-Driven Development) ツールです。
BDD(Behaviour-Driven Development)は、
できることは普通のテストと同じで、加えて、
これから作成しようとするプログラムに期待される「振る舞い」や「制約条件」、
つまり「要求仕様」に近い形で、自然言語(英語)を併記しながらテストコードを記述することができるものです。

メリットは、テストコードの可読性があがる上、テストコードが要求仕様となりうることと、
要求仕様からテストコードを起こす際も、スムーズにコードに移行しやすいことです。

RSpecの導入

まずは RSpec の Gem パッケージをインストールします。

・・・
group :development, :test do
  gem 'rspec-rails', '~> 3.5'
end
・・・

ちなみに、RSpec は test フレームワークなのに、なぜインストールグループに development を追加するかというと、RSpec にはテストファイルを作成する generator があり、それを利用するために default の RAILS_ENV である development にインストールしておくと楽だからです。

インストールします。

$ bundle install

Gem パッケージがインストールされたら、次は Rails ソフトウェアに対して RSpec 用の初期ファイルをインストールする。

$ rails generate rspec:install
Running via Spring preloader in process 9045
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

テストファイルの記述ルール

テスト記述ルールとしては、テストする内容を説明する文章を引数として、describe, context, it を使って記述します。
1 つのテスト内容は it で記述し、期待する動作(expect)を 1 つ含めます。

require 'rails_helper'

RSpec.describe Diff, type: :model do
  it "is valid with diff string" do
    diff = Diff.new(diff: "some diff")
    expect(diff).to be_valid
  end
end

it に続く文字列は動詞から始める。

ここまでで 1つのテスト項目の記述ですが、実行すると、なぜitの次が動詞から始まる説明文を記述していたのか理由が分かります。

Diff
  is valid with diff string

Finished in 0.02296 seconds (files took 0.35495 seconds to load)
1 example, 0 failures

このように、describe, it で記述した内容がテスト結果に表示されるため、実行結果を見れば何をテストしているのかが分かるようになっています。

テストが特定の条件を想定する場合は context を使ってその条件を記述します。
ここで describe, context は入れ替えても動作しますし、以下の例では describe, context は記述せず it だけ記述しても動作しますが、テストが想定する内容が分かりやすくなるので積極的に使いましょう。

require 'rails_helper'

RSpec.describe Micropost, type: :model do
  describe "search posts by term" do
    context "when no post is found" do
      it "returns an empty collection" do
        expect(Micropost.search("John Doe")).to be_empty
      end
    end
  end
end

感想

RSpecの導入は思っていたより簡単でした。
RSpecテストの作成についてまだ知識が浅いため今回は初歩のところだけ説明しました。
知識がついたらテストの作成方法について詳しいブログを投稿します!!

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

【Rails】Sassファイルから画像を指定する

Railsアプリを作成中、Sassファイルから背景画像でつまづいたのでメモ

前提

sass-railsを使っている。(デフォルトで入っていた)
画像はapp/assets/images/に保存。

指定方法

image-url('<ファイル名>')で指定

app/assets/stylesheets/style.scss
h1 {
background-image: image-url('logo.jpg')
}

その他

画像はpublic/に保存する方法もあるらしい。その場合の指定方法は知りません。
CSSで使うようなファイルはapp/assets/images/に保存して、public/にはユーザーがアップロードした画像とかを保存する、といった使い分けが一般的なようです。

参考

Rails で背景に画像を表示したいのですが取り込み方を教えてください。
Railsで扱う画像はassetsとpublic、どちらに置くといいのでしょうか?

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

はじめてのRuby on Rails環境構築(Mac)(2019/12/09)

はじめに

こんにちは。

ここでは、これまで主に、JavascriptやTypescriptでフロントを開発していたエンジニアが、ローカルのMacにRuby on Railsの開発環境を作ってみましたのでその手順をまとめてみました。

エンジニアスペック(主な開発経験)

Java
Python
Javascript
Typescript
React
ReactNative
Next
Node
Vue
など。

Homebrewのインストール

今回、購入直後の真新しいMacに環境構築を試みたので、まずはHomebrewのインストールから始めます。
https://brew.sh/index_ja

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

おっと、、エラーが出た。↓

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
==> This script will install:
/usr/local/bin/brew
/usr/local/share/doc/homebrew
/usr/local/share/man/man1/brew.1
/usr/local/share/zsh/site-functions/_brew
/usr/local/etc/bash_completion.d/brew
/usr/local/Homebrew

Press RETURN to continue or any other key to abort
You have not agreed to the Xcode license.
Before running the installer again please agree to the license by opening
Xcode.app or running:
    sudo xcodebuild -license

xcodeのライセンス承認をしろとのこと。
ライセンス承認後、再度上記コマンドを実行します。

成功!

Rubyのインストール

現時点でv2.6.5が使えそうなので、こちらを選択します。

$ brew install rbenv ruby-build
$ rbenv install --list
2.6.3
2.6.4
2.6.5
2.7.0-dev
2.7.0-preview1
2.7.0-preview2
2.7.0-preview3
$ rbenv install 2.6.5

pathを通します。

$ echo 'export PATH="~/.rbenv/shims:/usr/local/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile

global環境を汚さないように、特定のディレクトリ下でv2.6.5を使えるようにします。

$ cd ./特定のディレクトリ
$ rbenv local 2.6.5

bundlerのインストール

次に、bundlerをインストールします。
gem でインストールするのですが、rbenv local したディレクトリで行います。

$ cd rbenv local 2.6.5 を実行したディレクトリ
$ gem install bundler
$ bundler -v
Bundler version 2.0.2

それ以外のディレクトリで bundler -v すると、

rbenv: bundler: command not found

The `bundler' command exists in these Ruby versions:
  2.6.5

のエラーが出ます。

Railsのインストール

まず、workspace 用のディレクトリを作ります。

mkdir workspace

Gemfile を生成します。下記コマンドを実行すると Gemfile が生成されます。

$ bundle init

Gemfile の下記1行のコメントアウトを削除します。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails" ← ここ
# railsのインストール
$ bundle install --path=vendor/bundle

# railsのバージョンチェック
$ bundle exec rails -v
Rails 6.0.1

# workspace内にプロジェクトを作成
$ bundle exec rails new .

# 起動
$ bundle exec rails server
=> Booting Puma
=> Rails 6.0.1 application starting in development
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.1 (ruby 2.6.5-p114), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
* Listening on tcp://[::1]:3000
Use Ctrl-C to stop

Yay! You’re on Rails!

最後に、 http://localhost:3000/ へアクセスしてページが表示されたら完了です!
Screen Shot 2019-12-09 at 19.43.26.png

ruby のバージョンが2.6.5になってました!
作業時間としてはおおよそ1h程度で、非常に簡単に環境が構築できました!

以上でRuby on Railsの開発環境ができましたので、いろいろ遊んでみようかと思います!

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

Rails6 数値入力時にtypeをnumberにしたのに数値として扱われず困った話

目的

  • 勉強の実施時間を記録するwebアプリ作成の際に数値のinputで詰まり、コミュニティの方に助けていただいた話をまとめる
  • そもそもセオリーを理解できていなかったので二度とこんなことない様にまとめる。
  • .to_iを使って数値に変換できることはなんとなく知識としてあったが定着していなかったのでまとめる。

目標

  • すでにDBに格納されている数値に入力数値を足してDBに格納する。
  • 不動小数点や符号などはとりあえず考えず前述の目標をクリアする。

結論

  • 教えていただいた現役エンジニアさんのお言葉「入力値はtype指定してもStringになってしまうので受け取った側で型を指定して変換する」

問題のコード

  • 下記に問題のコードの一部を抜粋する。

  • コントローラ

    def update
      @post = Post.find_by(id: params[:id])
      @post.study_time = @post.study_time + params[:study_time]
      @post.save
      redirect_to("/posts/#{@post.id}")
    end
    
  • ビュー

    <%= form_tag("/posts/#{@post.id}/update") do %>
      <p>今日つみかさねた時間</p>
      <input type="number" name="study_time">
      <input type="submit" value="今日のつみかさね登録">
    <% end %>
    

詰まったところまでの概要

  1. 前述の問題のコードにて「今日のつみかさね登録」ボタンを押したところ下記のエラーが出た。

    no implicit conversion of integer into string
    
  2. エラーの内容から足そうとしている数値の型があっていないことがわかった。

  3. 筆者はビューファイルのinputtype="number"を指定して入力型を数値にしようと試みたが同じエラーが出た。

  4. 解決しようと試みたがいろいろ試してくうちに混乱してしまった。

解決方法

  • コントローラで受け取った値の型を数値に変換すことにより問題は解決した。

正常動作したコード

  • 下記に教えていただいた内容を元に修正を行なったコードを記載する。

  • コントローラ

    def update
      @post = Post.find_by(id: params[:id])
      @post.study_time = @post.study_time + params[:study_time].to_i
      @post.save
      redirect_to("/posts/#{@post.id}")
    end
    
  • ビュー

    <%= form_tag("/posts/#{@post.id}/update") do %>
      <p>今日つみかさねた時間</p>
      <!-- type="number"だと0~9までの入力しか受け付けられないためおって修正が必要 -->
      <input type="number" name="study_time">
      <input type="submit" value="今日のつみかさね登録">
    <% end %>
    

反省

  • .to_iで数値に変換できることは知ってはいたが使いどころが理解できてなかった。
  • そもそもセオリーを理解できていなかった。
  • 今考えると諦めなければ自己解決できたかもしれない。

よかったこと

  • エラー文をコピペで解決することをしなかった。
  • エラー文から問題箇所を特定することができた。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】エラーメッセージを使い回す

全てのフォームで、エラーメッセージを使いまわせるようにします。

関連:【Rails】バリデーションを実装する

リファクタリング前

以下のような書き方だと、@taskに対するエラーメッセージしか表示できないので、エラーメッセージ出力部分の記述をパーシャルに分けて使い回すことができません。

view
<h1>新規タスク</h1>
<% if @task.errors.any? %>
  <div>
    <ul style="color: red">
      <% @task.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>
<%= form_for(@task) do |f| %>
.
.

リファクタリング後

フォームの中に<%= render 'shared/error_messages', object: f.object %>と書き込み

view
<h1>新規タスク</h1>
<%= form_for(@task) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
・
・

パーシャルでobject.errorsを使ってエラーメッセージを呼び出します。

shared/error_messages.html.erb
<% if object.errors.any? %>
  <div>
    <ul>
    <% object.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

注意

エラーメッセージが特定のActive Recordオブジェクトに関連付けられている場合のみ使える
→SessionはActive Recordオブジェクトに管理づけられていないので、ログインなどでは使えない
→flashで代用する必要がある

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

Terraform + Ansible + Rails

はじめに

Railsのアプリケーションを複数管理しているので今後の手間も考え、
何かテンプレ的なものが出来たらいいなと思って作成してみました。 :hammer:
今後の運用において改善点もまだまだあると思ってますのでご意見ご要望はどしどし頂けたらと思います。 :bow:

今回のゴール

Terraformで構築したEC2インスタンスにAnsibleを使ってRailsアプリケーションをデプロイするまでをゴールとします。 :golf:

事前準備

Docker, Terraform, aws-cli, Ansibleのインストールをお願いします。

brew install docker
brew install terraform
brew install awscli
brew install ansible

ちなみに私の環境は下記のようになっています。
macOS Mojave 10.14.6
Docker 18.09.2
Compose 1.23.2
Terraform 0.12.3
aws-cli 1.15.50
Ansible 2.6.0

テンプレートリポジトリ

Github

DockerでRails環境構築

以前に記事書いたのでこちらを参考に構築してください。 :pencil:
RubyをインストールせずにDockerでRails環境を構築する

ここまで完了した方は先ほどのテンプレートリポジトリから
terraformansibleフォルダをコピーし、個人のリポジトリを作成してpushしておいてください。

TerraformでAWSにインフラ準備

EC2, RDS, LBの構成で進めます。

terraform.tfstate格納用のS3バケットとAWSクレデンシャルは各自で用意してください。
S3バケット名:tf-bucket
AWSクレデンシャルprofile名:tf-profile

~/.aws/credentials
[tf-profile]
aws_access_key_id = XXXXXXXXXXXXXXXXX
aws_secret_access_key = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
terraform/security_group/main.tf
provider "aws" {
  region  = "ap-northeast-1"
  version = "2.12.0"
  profile = "tf-profile"
}

terraform {
  required_version = ">= 0.12"

  backend "s3" {
    bucket  = "tf-bucket"
    region  = "ap-northeast-1"
    key     = "sample/sg/terraform.tfstate"
    encrypt = true
    profile = "tf-profile"
  }
}

# EC2に設定するセキュリティグループ
resource "aws_security_group" "ec2_security_group" {
  name   = "${var.app_name}-sg"
  vpc_id = var.vpc_id

  ingress {
    from_port   = "22"
    to_port     = "22"
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port       = "80"
    to_port         = "80"
    protocol        = "tcp"
    security_groups = [aws_security_group.lb_security_group.id]
  }

  egress {
    from_port   = "0"
    to_port     = "0"
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.app_name}-sg"
  }
}

output "ec2_sg_id" {
  value = aws_security_group.ec2_security_group.id
}
//...これより後ろの設定はそのまま残しておいてください。
terraform/security_group/variable.tf
variable "vpc_id" {
  default = "your-vpc-id"
}

variable "app_name" {
  default = "sample-app"
}

ファイルの修正が完了したら下記コマンドを順番に実行してみてください。

terraform/security_group/
terraform init
terraform plan
terraform apply

最後まで実行出来たらセキュリティグループが作成されているはずです。 :tada:
output メソッドで最終的に作成されたセキュリティグループIDを出力していることを覚えて追いてください。これを後ほどEC2作成時に利用します。

次にEC2の作成です。
Keypairは事前に作成し、サブネットも各自のサブネットIDを指定してください。
サブネット:subnet-000111222、subnet-333444555
Keypair:tf-keypair

terraform/ec2/main.tf
provider "aws" {
  region  = "ap-northeast-1"
  version = "2.12.0"
  profile = "tf-profile"
}

terraform {
  required_version = ">= 0.12"

  backend "s3" {
    bucket  = "tf-bucket"
    region  = "ap-northeast-1"
    key     = "sample/ec2/terraform.tfstate"
    encrypt = true
    profile = "tf-profile"
  }
}

data "terraform_remote_state" "security_group" {
  backend = "s3"

  config = {
    bucket  = "tf-bucket"
    region  = "ap-northeast-1"
    key     = "sample/sg/terraform.tfstate"
    profile = "tf-profile"
  }
}

resource "aws_instance" "ec2" {
  count                  = var.instance_count
  ami                    = var.ami_id
  instance_type          = var.instance_type
  key_name               = var.key_pair
  subnet_id              = lookup(var.subnets, count.index % 2)
  associate_public_ip_address = "true"
  vpc_security_group_ids = [data.terraform_remote_state.security_group.outputs.ec2_sg_id]

  tags = {
    Name = "${var.app_name}_${count.index + 1}"
  }
}

output "instance_ids" {
  value = {
    for instance in aws_instance.ec2 :
    instance.id => instance.private_ip
  }
}

output "instance_count" {
  value = var.instance_count
}
terraform/ec2/variable.tf
variable "ami_id" {
  default = "ami-0b898040803850657"
}

variable "instance_count" {
  default = 1
}

variable "subnets" {
  default = {
    "0" = "subnet-000111222"
    "1" = "subnet-333444555"
  }
}

variable "instance_type" {
  default = "t3.micro"
}

variable "key_pair" {
  default = "tf-keypair"
}

variable "app_name" {
  default = "sample-app"
}

ファイルの修正が完了したら下記コマンドを順番に実行してみてください。

terraform/ec2/
terraform init
terraform plan
terraform apply

最後まで実行出来たらEC2が作成されているはずです。 :tada:
作成したKeypairにてssh接続出来るか各自確認をお願いします。

RDSとLBの手順は割愛しますが、似たような感じでテンプレとして用意していますのでご活用頂けたらと思います。 :bow:

AnsibleでRuby環境構築

以前に記事を書いています。→ AnsibleでRuby環境構築

今回はこれに+αとして実際にGitサーバからソースをCloneし、CapistranoでRailsアプリをデプロイするテンプレまで作成しました。

まずはansibleユーザーがssh接続するための設定をします。

ansible/development
---
[server]
先ほどTerraformで作成したEC2のIP

[server:vars]
ansible_user=ec2-user
ansible_ssh_private_key_file=~/.ssh/tf-keypair.pem

[server_secret:children]
server

[development:children]
server

次にアプリ名やデプロイ先のフォルダなどを指定をします。
ここで先ほどcommitした個人のリポジトリを指定してください。
あと、sshでClone出来るように各自で公開鍵と秘密鍵を作成してGitサーバ側に公開鍵を設定してください。
GitHubでssh接続する手順~公開鍵・秘密鍵の生成から

ansible/group_vars/server.yml
---
ruby_version: 2.6.3
bundler_version: 2.0.2
git_url: git@github.com:hoge.git
app_user: sample-app
project_name: sample
deploy_directory: /opt/{{ project_name }}

先ほど作成した秘密鍵やその他暗号化が必要な情報はansible-vaultで暗号化してください。
vault_pass_samplevault_passへリネームして下記コマンドを実行してください。

ansible-vault edit group_vars/server_secret.yml

ここにrailsのmaster_keyや先ほど作成した秘密鍵や公開鍵などを設定してください。

ansible/group_vars/server_secret.yml
rails_master_key: xxx
ssh_authorized_keys: 'ssh-rsa XXX'
ssh_private_key: |
  -----BEGIN RSA PRIVATE KEY-----
  XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  ...
  -----END RSA PRIVATE KEY-----

最後にgitブランチやcapistrano実行時のenvをgroup_vars/{env}.ymlに設定して、
下記コマンドを実行するとEC2インスタンス環境にRuby環境の構築からGitリポジトリのCloneまで一通り行ってくれます。

# 初回のみ実行
ansible-playbook init.yml -i development
# 2回目以降デプロイ用
ansible-playbook deploy.yml -i development

nginx.conflogrotate.confなどもテンプレとして配置してますので各プロジェクトの設定に応じてご利用ください。

Capistrano

参考までにCapistrano設定ファイル群をcapistranoフォルダに置いてますので興味のある方は覗いてみてください。

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

link_toメソッドの具体的な仕組み

Scaffoldでプロジェクトを作ると下記のようなリンクがindexなどに生成されるとおもうが、link_toメソッドの中に何があるのかいまいち理解せずに進めてしまっていた。

<% @products.each do |product| %>

----省略ーーー
<span><%= link_to 'Show', product %></span> 
<span><%= link_to 'Edit', edit_admin_product_path(product) %></span> 
<span><%= link_to 'Destroy', admin_product_path(product), method: :delete, data: { confirm: 'Are you sure?' } %></span>

Showは表示される名前であることはわかると思います。

しかしproductってなんだ??

多分eachメソッドのproducなんだろうけどproduct単体でいるのがよくわからない。。。

実はこれ省略されているんです!!
参考

<span><%= link_to 'Show', product_path(product) %></span>

これが省略せずに書いたものです。productはパラメーターとして渡されているんですね。
下記の流れをちゃんと把握しているとよく理解できると思います!

  • 画面からリンク押下してURLとHTTPメソッドがリクエストされる
    • それが ターミナルにログとして出る
  • コントローラーで想定したアクションが呼び出される
    • どのコントローラー、アクションが選択されたかログとして出る
    • アクションの途中で値が見たくなれば pやputs でできる
  • レスポンスして画面が表示される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Devise を使って Active Directory から情報を取得する方法

はじめに

この記事も Qrunch で書いた記事ですが、技術的なフィードバックがもらえると嬉しいので、Qiita に再掲します。
なお、Qiita 版の前回の記事はこちらです。

ここから本文

前回はActive Directory(AD)に登録されたユーザーで、ログインできるところまで実装しました。
認証だけならこれでいいのですが、実際は AD に登録されている氏名やメールアドレスをとってきたりすると思います。
今回はそうした関連情報の取得をやってみたいと思います。

なお、今回のリポジトリはこちらです

前提

今回は以下のユーザーを AD 上に作成して、AD に登録されている情報を取得したいと思います。

項目 設定値 属性名
ユーザー名 test_taro sAMAccountName
テスト sn
太郎 givenName
電話番号 070-1234-5678 telephoneNumber
役職 係長 title
メールアドレス taro-test@example.com mail
社員番号 00123 employeeID

マイグレーションファイルの作成とマイグレート

AD のユーザーでログインできるようになった前回の続きから、という体で書いていきたいと思います。
まずは、それぞれの項目を User テーブルに格納できるように、カラムを追加します。

$ bundle exec rails g migration AddColumnToUser last_name:string first_name:string phone_number:string title:string mail:string employee_number:string
Running via Spring preloader in process 42345
      invoke  active_record
      create    db/migrate/2019MMDDhhmmss_add_column_to_user.rb

こんな感じのマイグレーションファイルが作成されます。

class AddColumnToUser < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :last_name, :string
    add_column :users, :first_name, :string
    add_column :users, :phone_number, :string
    add_column :users, :title, :string
    add_column :users, :mail, :string
    add_column :users, :employee_number, :string
  end
end

マイグレートします。

$ bundle exec rails db:migrate
== 20190926141900 AddColumnToUser: migrating ==================================
-- add_column(:users, :last_name, :string)
   -> 0.0020s
-- add_column(:users, :first_name, :string)
   -> 0.0006s
-- add_column(:users, :phone_number, :string)
   -> 0.0005s
-- add_column(:users, :title, :string)
   -> 0.0004s
-- add_column(:users, :mail, :string)
   -> 0.0003s
-- add_column(:users, :emaployee_number, :string)
   -> 0.0004s
== 20190926141900 AddColumnToUser: migrated (0.0049s) =========================

各種情報を取得する

各種情報の取得は User モデルにコールバックとして実装します。

class User < ApplicationRecord
  before_save :get_last_name, :get_first_name, :get_phone_number, :get_title, :get_mail, :get_emplpyee_number
  devise :ldap_authenticatable, :rememberable, :trackable

  private

  def get_last_name
    last_name = Devise::LDAP::Adapter.get_ldap_param(username, 'sn')
    self.last_name = last_name.first.force_encoding('UTF-8') unless last_name.nil?
  end

  def get_first_name
    first_name = Devise::LDAP::Adapter.get_ldap_param(username, 'givenName')
    self.first_name = first_name.first.force_encoding('UTF-8') unless first_name.nil?
  end

  def get_phone_number
    phone_number = Devise::LDAP::Adapter.get_ldap_param(username, 'telephoneNumber')
    self.phone_number = phone_number.first.to_s unless phone_number.nil?
  end

  def get_title
    title = Devise::LDAP::Adapter.get_ldap_param(username, 'title')
    self.title = title.first.force_encoding('UTF-8') unless title.nil?
  end

  def get_mail
    mail = Devise::LDAP::Adapter.get_ldap_param(username, 'mail')
    self.mail = mail.first.to_s unless mail.nil?
  end

  def get_emplpyee_number
    employee_number = Devise::LDAP::Adapter.get_ldap_param(username, 'employeeID')
    self.employee_number = employee_number.first.to_s unless employee_number.nil?
  end
end

日本語の値を取得する処理についてはこちらの記事を参考にさせていただきました。
ActiveDirectoryの日本語ユーザ名(DisplayName)をRailsのDeviseで扱う
ありがとうございました。

一旦この状態で、bundle exec rails server をして、Rails アプリを起動します。
起動後に test_taro でログインして、正常にログインしたら、Rails のコンソールからレコードを確認します。

irb(main):001:0> User.first
  User Load (1.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, username: "test_taro", created_at: "2019-09-26 14:52:03", updated_at: "2019-09-26 15:02:45", last_name: "テスト", first_name: "太郎", phone_number: "070-1234-5678", title: "係長", mail: "taro-test@example.com", employee_number: "00123">

きちんと AD に登録した情報が取れていました。

ビューの編集

あとは、ビューで表示してあげるだけですね。
ログイン後にリダイレクトされる home 画面で表示したいと思います。

<h1>StaticPages#home</h1>
<p>Find me in app/views/static_pages/home.html.erb</p>
<p>Welcome <%= current_user.username %> !</p>

<li>ユーザー名: <%= current_user.username %></li>
<li>姓: <%= current_user.last_name %></li>
<li>名: <%= current_user.first_name %></li>
<li>電話番号: <%= current_user.phone_number %></li>
<li>役職: <%= current_user.title %></li>
<li>メールアドレス: <%= current_user.mail %></li>
<li>社員番号: <%= current_user.employee_number %></li>
<%= link_to  "ログアウト", destroy_user_session_path %>

うまく表示されました。
44d29b1ffb6a72bd675aebff9a47b2ff.png

まとめ

AD には様々な情報が登録されていますが、devise_ldap_authenticatable を使って比較的かんたんに情報を取得できると思います。
まだ試せてはいませんが、devise_ldap_authenticatable では、Rails アプリ側でパスワードを変更し、変更したパスワードを AD 側に書き戻すこともできるようなので、こちらにもトライしてみたいと思います。

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

Devise を使って Active Directory 認証を実装する

はじめに

この記事は Qrunch に投稿した記事です。
普段の業務でも AD 認証を使うことが多いのですが、きちんと Devise の使い方等がわかっていない部分があり、技術的なツッコミをいただけるとありがたいなーと思って、Qiita に再掲しました。

ここから本文

社内の認証基盤で、Active Directory(AD) を使っている企業は多くありますが、業務システムの認証基盤として、 社内の AD を使用したいというニーズもあると思います。
今回は、Rails で業務システムを作ることを前提に、Devise を使用して AD 認証を実装してみます。

Ruby と Rails のバージョンは↓のとおりです。

$ ruby -v
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-linux]

$ bundle exec rails -v
Rails 5.2.3

Gemfile の編集

AD 認証を実装するために、Devise と、Devise LDAP Authenticatable(Devise LDAP) を使用します。
ちなみに、Devise の詳しい解説については、よくわかってないのでこの記事では取り扱いませんのであしからず。

gem 'devise'
gem 'devise_ldap_authenticatable'

編集したら、bundle install でインストールします。

Devise と Devise LDAP のセットアップ

はじめに、Devise をセットアップしUserモデルを作成します。
その後で、Devise LDAP のセットアップを行います。

bundle exec rails g devise:install ## Devise のインストール
bundle exec rails g devise User ## User モデルの作成
bundle exec rails g devise_ldap_authenticatable:install ## Devise LDAP のインストール

User モデルの編集

生成された User モデルを編集します。
トラブルシュートなどでの利用を想定して、ログインしたPCのIPアドレスを記録したり、ログイン回数などを記録できる :trackable を追記しました。

app/models/user.rb
class User < ApplicationRecord
  devise :ldap_authenticatable, :rememberable, :trackable
end

マイグレーションファイルの修正

修正した User モデルに合わせて、マイグレーションファイルを修正します。
また、デフォルトではメールアドレスとパスワードで認証を行うのですが、メールアドレスを持っていなかったり、AD の mail 属性を使っていない場合を考慮して、ログインユーザー名sAMAccountName でログインできるようにします。

db/migrate/XXXXXXXXXX_devise_create_users.rb
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|

      ## LDAP authenticatable
      t.string :username, null: false, default: "", unique: true

      ## 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

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :username,                unique: true

    # add_index :users, :email,                unique: true
    # add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

マイグレーションファイルを修正したら、bundle exec rails db:migrate でマイグレートします。

Devise の初期設定

Devise の初期設定を行います。変更した箇所だけを抜粋します。
今回は、アプリ側でユーザー登録しなくても、AD にユーザーがいればログイン可能なようにしたいと思います。

config/initializers/devise.rb
config.ldap_logger = true  # LDAP クエリを Rails のログに記録する。
config.ldap_create_user = true  # すべての有効な AD ユーザーがログイン可能になり、自動的にユーザーのレコードが登録される。
config.ldap_update_password = false  # パスワード変更を AD 側に書き戻さないようにする。
config.ldap_use_admin_to_bind = true  # LDAP認証時のバインドで Administrator を利用する。
config.authentication_keys = [:username]  # 認証で利用するキー。今回は、User モデルの username を利用する。
config.case_insensitive_keys = [:username]  # キーの大文字小文字を区別しない
config.strip_whitespace_keys = [:username]  # キーに含まれる空白を削除する

LDAP の設定

AD の接続情報などを設定します。

config/ldap.yml
development:
  host: <ドメインコントローラ名>
  port: 389
  attribute: sAMAccountName
  base: CN=Users,DC=example,DC=local
  admin_user: CN=Administrator,CN=Users,DC=example,DC=local
  admin_password: <admin_user のパスワード>
  ssl: false

View の作成

Devise の View を作成します。今回は、ユーザー登録やメールでの登録確認等は行わないので、ログイン画面だけを作成します。
config/initializers/devise.rb でログイン時に自動的にアカウントが作成されるようにしたので、AD にアカウントさえあれば、ユーザー登録は特にいらないかもしれません。
ここはアプリの仕様に左右される部分だと思います。

bundle exec rails generate devise:views users -v sessions

View の修正

作成したログイン画面を修正します。
デフォルトではメールアドレスの入力欄になっているので、これを Windows アカウント名の入力欄に変更します。

app/views/users/sessions/new.html.erb
<h2>Log in</h2>

<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
  <div class="field">
    <%= f.label :username, "Windows アカウント名" %><br />
    <%= f.text_field :username, autofocus: true, autocomplete: "username" %>
  </div>

  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password, autocomplete: "current-password" %>
  </div>

  <% if devise_mapping.rememberable? %>
    <div class="field">
      <%= f.check_box :remember_me %>
      <%= f.label :remember_me %>
    </div>
  <% end %>

  <div class="actions">
    <%= f.submit "Log in" %>
  </div>
<% end %>

<%= render "users/shared/links" %>

Controller の生成

次にコントローラを作成します。
ログイン周りのハンドリングができれば良いので、Sessions のコントローラのみを生成します。

bundle exec rails generate devise:controllers users -c=sessions

Controller の修正

生成したコントローラはすべてコメントアウトされているので、適宜コメントを外します。

app/controllers/users/sessions_controller.rb
# frozen_string_literal: true

class Users::SessionsController < Devise::SessionsController
  before_action :configure_sign_in_params, only: [:create]

  # GET /resource/sign_in
  def new
    super
  end

  # POST /resource/sign_in
  def create
    super
  end

  # DELETE /resource/sign_out
  def destroy
    super
  end

  protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_in_params
    devise_parameter_sanitizer.permit(:sign_in, keys: [:username])
  end
end

ログイン後のページを作成

今の状態だと、ログイン画面はありますがログイン後に表示するページがありません。
静的なページ homeを作って、ログイン後は home に遷移するようにします。

bundle exec rails g controller StaticPages home

デフォルトで生成されるページだとあまり面白くないので、username を表示させたいと思います。
また、どこからもログアウトできないと困るので、ログアウトのリンクを追記します。

app/views/static_pages/home.html.erb
<h1>StaticPages#home</h1>
<p>Find me in app/views/static_pages/home.html.erb</p>
<p>Welcome <%= current_user.username %> !</p>
<%= link_to  "ログアウト", destroy_user_session_path %>

routes の編集

コントローラを作成した際にいくつか自動で追加されているルートもありますが、今回のアプリではログイン画面を URL の ルートにしたいと思います。
こんな感じで routes.rb を編集します。

config/routes.rb
Rails.application.routes.draw do  
  get 'home', to: 'static_pages#home'

  devise_for :users, controllers: {
    sessions: 'users/sessions'
  }

  devise_scope :user do
    root to: 'users/sessions#new'
    get 'sign_in', to: 'users/sessions#new'
    get '/users/sign_out', to: 'users/sessions#destroy'
  end
end

デフォルトの動作では、ログインしたあとに URL のルートに遷移するため、ログイン画面が無限ループします。
それを回避するために、application_controller.rb に、after_sign_in_path_for メソッドを追記して home に遷移させます。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  rescue_from DeviseLdapAuthenticatable::LdapException do |exception|
    render :text => exception, :status => 500
  end

  def after_sign_in_path_for(resource)
    home_path
  end
end

ログインしてみる

ここまでできたら bundle exec rails s でサーバーを起動して、実際にログインしてみましょう。
localhost:3000 にアクセスしたら、こんなログイン画面が表示されるはずです。
dacaa2e9f99ca25102e983ef5e8d4d0a.jpg

AD に登録されているユーザー名とパスワードを入力してログインすると、
a975f732e8191362a26271d357cb8e30.jpg

アプリ側で特にユーザー登録をしていませんが、ADで認証されたユーザーが Rails のデータベースに登録されているので、無事にログインできました。

最後に

ところで、ひとつバグがあります。
今の状態だとログインしていなくても home にアクセスできてしまいます。
そこで、home は認証必須にしたいと思います。

before_action フィルタで、認証済みのユーザーのみが home にアクセスできるようにします。

app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
  before_action :authenticate_user!

  def home
  end
end

これで、ダイレクトに/homeにアクセスしようとすると、ログイン画面にリダイレクトされるようになりました。

今回作成したアプリのリポジトリはGithubに登録しています。
ad_auth

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

railsでのデータの保存、更新、削除の方法 Create,Update,Delete

はじめに

今回はrailsでのデータの新規作成、更新、削除の方法をまとめていきます。

データの新規保存 

newメソッド+saveメソッドかcreateメソッドでレコードを新規作成します。

newメソッド+saveメソッドの例
#モデル名.new(カラム名: データ)+モデル名.saveのセットで保存されます。
#saveメソッド忘れに注意ください。これがないと保存されません。

#以下が例です。
post = Post.new(name: "記事1", title: "hogehoge") 
post.save
createメソッドの例
#モデル名.create(カラム名: データ)で保存されます。
#createメソッドはnewとsaveメソッドを一度に実行できます。

#以下が例です。
Post.create(name: "記事1", title: "hogehoge") 

データの更新

saveメソッドかupdateメソッドでレコードの更新を行います。
自身はupdateメソッドをよく使います。

updateメソッドの例
#モデル名.update(カラム名: データ)で保存されます。
#update前にfindでupdateをするデータを取得します。

#以下が例です。
post= Post.find(1)
post.update(name: "記事2", title: "higehige") 
saveメソッドの例
#モデルの各項目に値を入れていき、最後にsvaeメソッドで更新をかけます。

#以下が例です。
post= Post.find(1)
post.name = "記事2"
post.title = "higehige"
post.save

データの削除

destroyメソッドかdeleteメソッドを使いデータを削除します。

destroyメソッドの例(モデルクラスでdependent: :destroyを指定して関連付けたデータも自動的に削除されます)
#モデル名.find(削除したいid).destroy(カラム名: データ)でデータを削除します。

#以下が例です
Post.find(1).destroy
#ちなみにレコード全消去したい場合は以下になります。
Post.destroy_all
deleteメソッドの例(関連するデータは削除されません)
#モデル名.find(削除したいid).delete(カラム名: データ)でデータを削除します。

#以下が例です
Post.find(1).delete
#ちなみにレコード全消去したい場合は以下になります。
Post.delete_all

おわりに

基本的なrailsの新規作成、更新、削除について紹介いたしました。
データ新規登録時のsaveメソッドを忘れてはまらないように注意ください!!

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

Rails scaffoldで3回のコピペで快適に投稿モッグを作成

Rails scaffoldで3回のコピペで快適に投稿モッグを作成

rails new から bundle install までを一発コマンドで実行!

$ rails _5.2.3_ new sample-app -d postgresql && cd sample-app && bundle install --path vendor/bundle
/config/application.rb
module SampleApp
  class Application < Rails::Application
    # ここから下を追加
    config.generators do |g|
      g.javascripts false
      g.helper false
      g.test_framework false
    end
  end
end

scaffold から rails s までを一発コマンドで実行!

$ rails g scaffold post title:string details:string && rails db:create && rails db:migrate && rails s

終了です。

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

Railsコンソールで日本語入力ができない現象【Docker】

はじめに

Railsの参考書を読みながら勉強を進めているときに、

DockerのRailsコンテナ内で、Railsコンソールを起動し、日本語文章を入力しようとしたら、
入力はできるのに、エンターキーを押すとターミナルに表示されないという現象が起きたので、

その解決方法についてです。

エンターキーを押す前
image.png

押した後
image.png

解決方法

DockerFileに、

ENV LANG C.UTF-8

を記入して

docker-compose up --buildで再起動

これでコンソールに日本語を入力できるようになります。

エンターキーを押す前
image.png

押した後
image.png

参考にしたページ:Docker / rails console で日本語入力できない問題
https://gist.github.com/tasiyo7333/2163a09129ed36639645145a0146d8d3

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

2億件の本番データを無事に変換した話

この記事はクラウドワークス Advent Calendar 2019 の11日目の記事です。

昨日は@lemtosh469による「アジャイル開発のスケーリングの方法」でした。

はじめに

みなさんこんにちは、クラウドワークスでサーバーサイドエンジニアをしている@shimopataです。普段はRailsを中心に機能開発したり改修作業とかに勤しんでいます。
先日、大量のデータ変換を伴うテーブルの差し替え作業をしました。データ量も多く、大変な作業でしたが計画、検証からやらせてもらい貴重な経験だったので、公開できる範囲で共有させて頂こうと思います。

やりたいこと

あるテーブルに保存されているデータの形式を変換し保存し直す。
対象は以下のテーブル

テーブル名 レコード数
テーブルA 65,000件
テーブルB 4,300,000件
テーブルC 200,000,000件

2億件もレコードあるのかー:innocent:

やったこと

調査と方針決め

まずはじめに、対象のテーブルがどのように使用されているのかについて調べました。一度登録されたデータがユーザの操作等によって更新されるかどうかで、対応方針が大きく異なると思い、調査し始めました。
結果、一度登録されたデータはユーザーによって、変更・削除されないことが分かり、以下の計画で変換することにしました。

【計画】
1. 変換後の値を保存するテーブルを作成
2. データを変換するスクリプトを実行し、1で作成したテーブルに保存
3. テーブル名をリネームして差し替える
4. 不要なモデル、テーブルを削除

図に示すと次のような感じです。
データ変換の図 (1).png

メリットとしては次の通り、安心安全にデータの変換が行えるのでこの方式を採用しました。

  • 書き込みを行うテーブルはユーザが直接触るテーブルではないため、書き込み時にテーブルをロックしてもユーザ影響はなく、日中帯に作業ができる。
  • 変換前のデータがテーブルに残るので何かあっても、そこからリカバリーができる。

スクリプトの作成について

データ件数が多く、処理時間が長くなってしまうことから、少しでも短くするために以下の対応を行いました。

find_in_batchesを使用して、変換速度の底上げ

繰り返し処理をする場合、データ数が少なければ、eachなどを事足りるのですが、今回は、データ数が多いのでfind_in_batchesを使用しました。find_eachとの違いは

  • find_eachはデフォルトで1000個単位(引数batch_size:で指定可能)でデータを取得し、一件ずつをeachで処理していく
  • find_in_batchesはデフォルトで1000個単位(引数batch_size:で指定可能)でデータを取得し,1配列毎に処理していく

どのくらい違いが出るかを手元にあるテストデータで試してみます。

find_each
Benchmark.realtime do
  User.find_each(batch_size: 1000) { |user| p user }
end

-------以下、実行結果-----------

<User id: 1, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">
<User id: 2, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">
...
<User id: 2000, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">
=> 1.006721000012476
find_in_batches
Benchmark.realtime do
  User.find_in_batches(batch_size: 1000) { |user| p user }
end

-------以下、実行結果-----------

[<User id: 1, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">,
・・・
<User id: 1000, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">]

[<User id: 1001, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">,
・・・
<User id: 2000, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">]
=> 0.4186677999678068

2000件程度のデータで約二倍の速度の違いが出ていますね。対象となるデータはこの1000000倍なので、find_in_batchesを使うしかないですね。

また、バッチサイズのサイジングもこのスクリプトのキーポイントでした。この数値を大きくする事で一度に処理できるデータの数を増やすことができ、その分、速度が早くなるのですが、当然、メモリを多く使用してしまいます。安全策をとるのであれば、夜に少しづつ変換を進めていくこともできたのですが、作業締め切りもあるため日中帯にスクリプトを流す必要がありました。

速度を出すために、バッチサイズを大きくしたい、でも、サービスに負荷をかけれない。。。

そのため、検証環境に大量のダミーデータを用意し、検証を行いました。バッチサイズの大きさを変更し実行しては、AWSコンソールメトリクスを確認を繰り返し、最適値を探しました。速度面、リソースの安全性からバッチサイズは5000としました。

activerecord-importを使用して書き込み速度の底上げ

activerecord-importというgemを使用しました。これは、一括でINSERT処理やUPDATE処理を行えるようにするgemで、スクリプト実行時間の大幅な短縮をすることができました。
使い方としては次のような感じです。

sample.rb
books = []
10.times do |i|
  books << Book.new(name: "book #{i}")
end
Book.import books 

modelにimportというメソッドが追加されるので、引数に取り込みたいデータを入れてあげるだけで、簡単にINSERT処理が行えます。また、オプションもたくさんあるため(特定のカラムだけ取り込む、取り込み時にバリデーションを張るなど)、興味があったら、gemのREADMEをみてみてください。

こちらも、どのくらい違いが出るかを手元にあるテストデータで試してみます。

sample_bulk_import.rb
users = []
Benchmark.realtime do
  1000.times do |i|
    users << TestUser.new(id: i,name: "test")
  end
  TestUser.import users
end

------------以下実行結果-----------------
0.1599934000405483

sample_import.rb
Benchmark.realtime do
  1000.times do |i|
    TestUser.create!(name: "test")
  end
end

------------以下実行結果-----------------
5.3943878000136465

圧倒的に早そうですね。

変換スクリプトの実施

事前検証も終え、負荷なども問題ないことを確認した上で、本番サーバーに対して変換作業を行いました。いくら使用していないテーブルとはいえ、本番環境に対する変更のため、二週の間、対象となるデータを約1000万件単位に分割し勤務時間帯にのみスクリプトを実行していたのですが、とにかくデータ量が多く、時間がかかりました。リソースの監視はAWSのマネジメントコンソールの情報を中心に確認していました。

image.png

いつ、どのような不具合が発生するのか分からないという状況で長時間作業していくのはなかなか体力と精神を削られる作業で大変でした。:cold_sweat:

テーブルの差し替え

変換作業が完了後、テーブル名を変換するmigrationを作成し、テーブルの差し替えを行いました。

xxxx_rename_table.rb
def up
 rename_table :target_table, :backup_target_table
 rename_table :converted_table, :target_table
end

作業を初めて見ると、なかなかmigrationが終わらず、原因を探るために、migration実行中に発行しているクエリを確認しました。
次のクエリを実行して見てみると、インデックスの再作成で時間かかっている事が分かりました。
SELECT * FROM information_schema.PROCESSLIST WHERE Time >= 10 AND command = 'Query' ORDER BY Time DESC;

クエリ自体は問題なく発行されているので、ただ待つしかなく、結果として、作業時間を超えてしまったのですが、大量のデータ取り扱ってく上での学びになりました。

終わりに

テーブルの差し替え作業のタイミングでチームを移ってしまい、先輩エンジニアに作業を引き継ぎました。この作業のタイミングで、tableBのデータについて、特定の条件下で登録したデータがユーザによって削除であることが分かりました。1が、なんとかこれも対応してもらい、ほんと、ありがとうございました。

データ変換をやり始めてからでないと分からないリスクとして、

  • バリデーションなどの制約から外れているデータ
  • 過去に何らかの理由で手動でデータ修正をして、想定していない形で入っているデータ

などを想定していたのですが、実際にやってみると意外に該当するものは少なく、よかったです。

振り返ってみるとやっていること自体は単純だったりするのですが、やっていた当時は「本当に大丈夫なんだろうか、問題ないのだろうか」とリスクがないことを証明する悪魔の証明をずっとしながら、作業工程を組み立てていたので、非常に不安になりながら作業を進めていました。
そんな状況の中でも、相談に乗ってくれたり、作業に協力してくれたエンジニアに支えられてなんとかゴールまで行けたのかなと思います。

ゴールに辿り着くまでに色々失敗をしたりしたのですが、学びとして得られるものも大きく、今後に生かしていきたいと思います。


  1. 最初の調査の時に気づけなかったのは反省です。。。 

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

VMWareの共有フォルダ上でRailsプロジェクトを作成しようとしてハマった

問題

新しいrailsプロジェクトを作ろうとして、rails newしたら下記のようなエラーが出た。

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/ext/bootsnap
/home/n/.rbenv/versions/2.6.5/bin/ruby -I /home/n/.rbenv/versions/2.6.5/lib/ruby/site_ruby/2.6.0 -r ./siteconf20191209-2795-4ulozw.rb extconf.rb
creating Makefile

current directory: /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/ext/bootsnap
make "DESTDIR=" clean

current directory: /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/ext/bootsnap
make "DESTDIR="
compiling bootsnap.c
bootsnap.c: In function ‘get_ruby_platform’:
bootsnap.c:240:3: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
   struct utsname utsname;
   ^~~~~~
bootsnap.c: In function ‘bs_cache_path’:
bootsnap.c:266:39: warning: format ‘%llx’ expects argument of type ‘long long unsigned int’, but argument 5 has type ‘uint64_t {aka long unsigned int}[-Wformat=]
   sprintf(*cache_path, "%s/%02x/%014llx", cachedir, first_byte, remainder);
                                 ~~~~~~^
                                 %014lx
bootsnap.c: In function ‘bs_rb_fetch’:
bootsnap.c:307:3: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
   char * cachedir = RSTRING_PTR(cachedir_v);
   ^~~~
bootsnap.c:653:13: warning: ‘output_data’ may be used uninitialized in this function [-Wmaybe-uninitialized]
     else if (!NIL_P(output_data))             goto succeed; /* fast-path, goal */
             ^
bootsnap.c:624:9: note: ‘output_data’ was declared here
   VALUE output_data;  /* return data, e.g. ruby hash or loaded iseq */
         ^~~~~~~~~~~
bootsnap.c: At top level:
cc1: warning: unrecognized command line option ‘-Wno-self-assign’
cc1: warning: unrecognized command line option ‘-Wno-parentheses-equality’
cc1: warning: unrecognized command line option ‘-Wno-constant-logical-operand’
cc1: warning: unrecognized command line option ‘-Wno-cast-function-type’
linking shared-object bootsnap/bootsnap.so

current directory: /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/ext/bootsnap
make "DESTDIR=" install
/usr/bin/install -c -m 0755 bootsnap.so ./.gem.20191209-2795-1foym7j/bootsnap

No such file or directory @ rb_file_s_rename - (./.gem.20191209-2795-1foym7j/bootsnap,
/mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/extensions/x86_64-linux/2.6.0/bootsnap-1.4.5/bootsnap)

Gem files will remain installed in /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5 for inspection.
Results logged to /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/extensions/x86_64-linux/2.6.0/bootsnap-1.4.5/gem_make.out

An error occurred while installing bootsnap (1.4.5), and Bundler cannot continue.
Make sure that `gem install bootsnap -v '1.4.5' --source 'https://rubygems.org/'` succeeds before bundling.

どうやらoutputファイルの書き込みに失敗しているらしい。

環境

  • Windows 10 (ホストOS)
  • Ubuntu 18 LTS (ゲストOS)
  • VMWare Workstation 15
  • Rails 6

原因

VMWareの共有フォルダ機能を使用して、ホストOSのフォルダをゲストOSに共有して、この上で開発をしようとしていたのだがこれが良くなかった。
共有フォルダのパーミッションを確認すると、所有権が全てrootになっており、変更ができない。
このために書き込みに失敗したらしい。

/mnt/hgfs/Dev/rail_project $ ls -la
合計 37
drwxrwxrwx 1 root root 4096 12月  9 11:13 .
drwxrwxrwx 1 root root 4096 12月  9 11:10 ..
drwxrwxrwx 1 root root    0 12月  9 11:10 .bundle
drwxrwxrwx 1 root root 4096 12月  9 11:13 .git
-rwxrwxrwx 1 root root  681 12月  9 11:13 .gitignore
-rwxrwxrwx 1 root root    6 12月  9 11:13 .ruby-version
-rwxrwxrwx 1 root root 1962 12月  9 11:13 Gemfile
-rwxrwxrwx 1 root root 3337 12月  9 11:12 Gemfile.lock
-rwxrwxrwx 1 root root  374 12月  9 11:13 README.md
-rwxrwxrwx 1 root root  227 12月  9 11:13 Rakefile
drwxrwxrwx 1 root root 4096 12月  9 11:13 app
drwxrwxrwx 1 root root    0 12月  9 11:13 bin
drwxrwxrwx 1 root root 4096 12月  9 11:13 config
-rwxrwxrwx 1 root root  130 12月  9 11:13 config.ru
drwxrwxrwx 1 root root    0 12月  9 11:13 db
drwxrwxrwx 1 root root    0 12月  9 11:13 lib
drwxrwxrwx 1 root root    0 12月  9 11:13 log
-rwxrwxrwx 1 root root  222 12月  9 11:13 package.json
drwxrwxrwx 1 root root 4096 12月  9 11:13 public
drwxrwxrwx 1 root root    0 12月  9 11:13 storage
drwxrwxrwx 1 root root 4096 12月  9 11:13 test
drwxrwxrwx 1 root root    0 12月  9 11:13 tmp
drwxrwxrwx 1 root root    0 12月  9 11:13 vendor

解決

共有フォルダ機能を使うのを止めて、ゲストOS上のディレクトリをsambaでホストOSと共有することにしました。
なおゲストOSとホストOSでsambaを使うためには、VMWareのネットワークアダプタの設定をブリッジ接続にする必要があります。

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

rails s で(http://localhost:3000)が立ち上がらない解決方法

新規アプリ作成に入り、 $ rails new  のあと
.....

$ rails s にて localhost:3000 が立ち上がらない場合に解決した方法。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
結論:初心者のため、 「rails s 立ち上がらない 」等の検索を先に行ってみたが、
結局の解決方法は(実行するべきコマンドすらもが)書いてある,という事で、今後の勉強になりました。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

............
Webpacker configuration file not found /Users/..../config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - .../config/webpacker.yml (RuntimeError)

エラー文の中に、webpackerファイルが無い、インストールするように出た為、

$ rails webpacker:install

Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/

すると、今度は https://yarnpkg.com/lang/en/docs/install/ 
からダウンロードするように出る。
$ brew install yarn  で出来るとサイト内にある為、コマンド実行。

You should change the ownership of these directories to your user.sudo chown -R $(whoami) /usr/local/share/man/man5 /usr/local/share/man/man7

すると今度は、権限を変えるように出た為、書いてある通りにコマンド実行。
($ ls -lで 現在の確認を一応行い)

$ sudo chown -R $(whoami) /usr/local/share/man/man5 /usr/local/share/man/man7

(パスワード:入力成功)

$ rails s 成功
無事にいつもの画面が出た。

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

【Rails】modelやhelperに無駄なmethodを増やさないpresenter

Presenterという層を導入してみて、良かったなあってこと、書こうと思います?

正直 n番煎じで、どこにでもある記事なのですが、Presenterを使う時のルールから考えれば、感じたメリットがやや違うなあと思ったので自分なりに書きます?
(実は、導入したのは弊社の別のエンジニアなのですが、もういないので勝手にいただきます?)

こんなRailsの開発を回避したい!

そもそも、どういう開発でPresenterが使われるのか?
僕なりに、問題提起していこうかと思います。

Viewのロジックをどんどんモデルに追加してしまう

厳密には以下のようなことはありえないですが、
それview以外じゃ使わないでしょう的なことを察してほしいです。?

class User < ApplicationRecord
  def self.name_array(users)
    users.map { |e| e.name }
  end
end

以下の通りですが、viewのロジックばっかりのモデルは少し嫌ですね。

モデルとは、データベースや外部サービスへのアクセスをはじめ、ビジネスロジック全般を担当するコンポーネントのことです。
出典 : モデルの基本

controllerのアクションmethod の外でインスタンス変数を作ってしまう

別に問題はないけど「え、こんなインスタンス作ったっけ?」ってなります。
(気づかずに同じインスタンスを作ってしまうとかも嫌ですね。)

class UsersController < ApplicationController
  before_action :set_greate_users

  def index
    @target_users = current_user.target_users
  end

  private

  def set_greate_users
    @great_users = current_user.great_users
  end
end

以下の記事でも、謎の場所でインスタンスを定義するな!と言ってる過激派もいます。
Controller Best Practices: Don’t Hide Instance Variables

神helperにしてしまう

そもそもviewのロジックはhelperに追加しようというのが基本的だと思うのですが、危険性を書きます。

  • helperは、どこでも読み込まれるので、メソッド名が衝突する可能性がある
  • helperの修正を行うと、意図しない箇所も変更されてしまう
  • 色々なhelperから呼び出されると、ロジックを追うのだけで大変になる
module UserHelper
  def great_users
    current_user.great_users
  end
end

ほぼ以下の記事の受け売りなのは秘密です。
Decorator と Presenter を使い分けて、 Rails を ViewModel ですっきりさせよう

なぜそんな開発になったの?

僕も開発していて、ハッとしたので、書き残します。

rubocopのAbcSizeが怒るから

ありがちですね。
そもそもアクションmethodを起点に何かが起こるので、アクションmethod内で色々したくなります。
でも、ロジックが多く書けない以上、インスタンス変数から呼び出せるようにモデルにロジックを追加するしかない気持ちもわかります。

Metrics/AbcSize: Assignment Branch Condition size for show is too high. [16.31/15]

AbsSizeを追加されたプルリク
- New cop Metrics/AbcSize

AbcSizeのデフォルトを考え直そうという提案がされたプルリク
- Relax metrics cops threshold
↓ちゃんと明文化することが大事って書いてありました。

I think that at this point is way more important to document some of the defaults and why they were picked than relaxing them. Not to mention that default values like 18 and 14 seem extremely random.

rails_best_practicesに言われたから

rails_best_practicesがアクションmethod内部で変数に対して4回以上methodを呼び出すと、以下のようなエラーが出力されます。
2~3個のインスタンスを作成していたら、確かに出がちなエラーだと思います。

/Users/.../app/controllers/users_controller.rb:5 - 
move model logic into model (current_user use_count > 4)

Presenterを導入しよう

ここからは n番煎じ、presenterの導入方法なので、どの記事を読んでも同じです。
他の記事読んでも大差ないです。

Presenterを使う時のマイルール

何でもPresenterを使えってことではありません。
僕なりの使い方を書きます。

  • インスタンス変数が4個以上になるならPresenter
    *4という数字に意図はありません、ノリです。
  • どこでも使うviewのロジック(URLの置換)などはhelperを使う
  • Decoratorという考え方は入れない(混乱するので)

Presenterのコード

弊社ではPresenterにいくつもの引数を渡せるように、BasePresenterを作っています。

app/presenters/base_presenter.rb
require 'delegate'

class BasePresenter < SimpleDelegator
  attr_reader :resource, :args

  def initialize(resource, **args)
    @resource = resource
    @args = args
    super(resource)
  end
end

クラス名は、どのコントローラーのどのアクションmethodなのかわかりやすいようにしています

app/presenters/users/index_presenter.rb
module Users
  class IndexPresenter < BasePresenter
  # arg[:taregt_id]で、第2引数のhashの中身を呼び出せます
  # ↓ロジックを書く
  def target_name
    User.find(arg[:target_id]).name
  end

  alias_method :current_user, :resource
 # ↑resourceでも呼び出せますが、current_userで呼び出せるようにしています
  end
end

controllerで呼び出すだけです

class UsersController < ApplicationController
  def index
    @presenter = Users::IndexPresenter.new(current_user, target_id: params[:id])
  end
end

あとは@presenter.target_nameで呼び出せます!

DecoratorとPresenterの違いって?

僕もよくわかりません?
あえて載せませんが、rails presenterで検索してヒットする記事は全てDecorator/Presenterの解釈が違いました。
弊社では Decorator/Presenter の記事に書いてあった Exhibit vs Presenter が一番近いなと思いました。

Decorator パターンは定義上、1個のオブジェクトしか内包しないので、「Presenter」のように2個のオブジェクトに委譲を行うような設計は厳密には Decorator パターンではありません。

弊社ではPresentercurrent_userという基本的なオブジェクトと、その他のhashを渡しています。
内部で複数のインスタンス変数が定義されたりするので、ちょっと特殊な使い方です。

所感

そういえば、Presenterを導入した記事を読むと、昔の人はrailshtmlを返却していたみたいですね。
弊社はrailsjsonを返却するAPIで、Vueでフロントを制御しています。
昔の人はどんな開発をしていたんだろうか、、、あまり想像できていない??

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

docker + #rails 環境の bundle install で `No such file or directory (needed by /bundle/gems/mysql2-x.x.x/lib/mysql2/mysql2.so`

bootsnapが悪さをしているだろうか?(bootsnapってなんだっけ?

/bundle/gems/bootsnap- y.y.y/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require': Error loading shared library libmysqlclient.so.18: No such file or directory (needed by /bundle/gems/mysql2-x.x.x/lib/mysql2/mysql2.so) - /bundle/gems/mysql2-x.x.x/lib/mysql2/mysql2.so (LoadError)

対策

bundle ディレクトリを全て削除してからinstallし直し

rm -rf .bundle
bundle install

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2825

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

Rails6.0.2の変更点まとめ

Ateam Finergy Inc. Advent Calendar 2019の10日目です。

11月5日にRails6.0.1がリリースされましたが、11月27日には早くも6.0.2rc1がリリースされています。
Rails6.0.1から6.0.2への変更点をまとめました。

1. Active Support

1-1. initialize時に訳文(i18n)をeager loadする

Rails6.0.1までは、i18nの読み込みによってアプリケーションの初期レスポンスが遅くなっていました。そこでRails6.0.2からは、アプリケーションのinitialize時にi18neager loadすることで、レスポンス速度の低下を避けるようになりました。

具体的には、以下の変更が追加されています。

activesupport/lib/active_support/i18n_railtie.rb
config.eager_load_namespaces << I18n

config.eager_load_namespacesは元々Railsに存在しているオプションで、ここに名前空間を設定するとconfig.eager_loadオプションをtrueにしている場合に読み込まれます。

1-2. ActiveSupport::NotificationsでスレッドごとのCPUタイムクロックを使用するように

ActiveSupport::Notifications::Instrumentクラスで、CPU時間の取得にProcess::CLOCK_THREAD_CPUTIME_IDを使用するようになりました。
Rails6.0.1まではProcess::CLOCK_PROCESS_CPUTIME_IDを使用していましたが、これでは一般的によく使われているマルチスレッドのWebサーバにおいて、現在のリクエストだけでなく他のリクエストにも時間を費やしてしまうリスクがあります。そこで、Process::CLOCK_THREAD_CPUTIME_IDに変更されました。
またこの変更に伴い、Process::CLOCK_THREAD_CPUTIME_IDが定義されているかをチェックするようになっています。

2. Active Job

2-1. SidekiqActive Jobクラスのsidekiq_optionsにアクセスできるように

SidekiqActive Jobクラスにアクセスすることにより、そのActive Jobタイプに設定されているsidekiq_optionsにアクセスすることができるようになりました。
Rails6.0.1まではActive Jobのクラス名をto_sしていたため文字列型になってしまい、sidekiq_optionsにはアクセスできませんでした。Rails6.0.2からはto_sを無くしたことで、アクセスできるようになりました。

3. Action Pack

3-1. Railsエンジンにマウント可能なルーティングヘルパーを、システムテストで使えるように

Rails6.0.1のシステムテストにおいて、Railsエンジンにマウント可能なルーティングヘルパー(ex: engine_name.root_url)を呼び出すと、未定義のエラーが発生します。
Rails6.0.2では、このエラーを解消しています。

4. Railties

4-1. Scaffoldジェネレーターの衝突チェックを修正

Scaffoldで新たに生成したクラスと既に存在するクラス名が衝突した場合のチェックの処理が、修正されています。

4-2. 各環境ごとにautoload_pathsautoload_once_pathseager_load_pathsが設定できるように

各環境の設定ファイル(config/environments/*.rb)で、autoload_pathsautoload_once_pathseager_load_pathsの値が設定できるようになりました。

5. 最後に

今回はマイナーバージョンアップなので、細かなバグ修正やパフォーマンス向上がほとんでしたね。
今後もRails6の品質とパフォーマンスが、より向上していくことを期待しています。

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

Railsエンジニアの視点でHanamiを解説してみた

はじめに

新しい職場に来てもう少しで2ヶ月が経過します。現在の業務でHanamiを書き始め、何となく慣れてきたので、アドベンドカレンダーの機会にHanamiについて書いてみたいと思います。しかし、Hanamiはまだまだマイナーなフレームワークであるため、Railsエンジニアの視点で分かりやすいように解説していきます。

Hanamiとは

2017年4月にバージョン 1.0.0 がリリースされたばかりの比較的新しいRubyのフレームワークです。Railsとの違いで代表的なものはこんなところです。

長期的なメンテナンスに向いたフレームワーク

Rails はMVCやActiveRecordに仕様の大部分が依存したフレームワークになっています。 一方でHanami は DDD (ドメイン駆動設計) をベースにしつつ、ある程度柔軟性を残した状態で開発出来るフレームワークとなっています。 もう少し具体的に言うと、Rails はサービスを素早くローンチすることに向いている一方で Hanami は長期的な保守を念頭においた開発に向いているといった感じです。

ただし、Railsと同様にMVC全体がサポートされたフルスタックなフレームワークなので、必ずしも初期ローンチを犠牲にしている訳ではありません。最近では、マイクロサービスを念頭に置いた開発を行うようになってきたこともあり、初期ローンチとサービス成長の両方を見据えた選択肢としては良さそうなフレームワークです(ここはあくまでも2ヶ月間書いてきた人間の初感ですが。。)。

マジックが少ない

マジックと言うと少し分かりにくいですが、Rubyはメタプログラミングを上手く活用することで、プログラムを書くプログラムを書くことが可能です(初学者を混乱させてしまう説明かも。。)。もう少し具体的に言うと、実行時に与えられた引数に合わせて処理を柔軟に変化させるクラスやメソッドが書けるといった感じです(さらに分らなかったらごめんなさい。。)。メタプログラミングで書かれたクラスやメソッドは内部処理を知らなくても柔軟に使用出来てしまうことから、このような言われ方がされます。

特に、Railsは、ActiveRecordやActiveSupport系などの強力なGemによりマジックが生み出され、開発者が詳細を意識しなくても書けてしまうフレームワークになっています。このあたりがRailsは書けるけど、Rubyは書けないみたいなエンジニアが生まれてしまう原因だったり。。しかし、ビジネス的な視点で見ると、早くローンチ出来た方がビジネスでのトライ&エラーが出来るため、Railsが広まった大きな理由でもあり、問題というよりは利点だと思われます。

一方、Hanamiの場合は、一部で特定のGemに依存している部分は見られるものの、マジックが少なく、 ほぼPure Ruby に近いフレームワークとなっています。ちなみに、ActiveSupportが使えないため、一番最初に困るのが、present?blank?メソッドが使えなくなる点です。オブジェクトの種類を意識しないと無意識にNoMethodErrorを引き起こすコードを書きます。

あとは、長くなりそうなので、技術的な特徴などは公式ホームページをご覧下さい。

公式ホームページはこちら

hanamiコマンド

Hanamiでは、Railsと同様にhanamiコマンドが用意されています。細かいところで違いはあるものの、ほとんどrailshanamiになっただけです。

プロジェクトの作成
bundle exec hanami new . --database=mysql
Hanami console
bundle exec hanami console

ディレクトリ構造

上記のコマンドを実行した後のディレクトリ構造はこんな感じです。

今回、環境にdocker-composeを利用しています。dbディレクトリとDocker関係のファイルは気にしないで下さい。

apps配下にRailsでも馴染みがあるcontrollerstemplatesviewsが存在します。

そして、Railsと大きく違う点は、lib配下にDDDらしさが伺えるentitiesrepositoriesが存在する点です。簡単に説明すると、この2つがModelの役割を果たします。

.
|-- Dockerfile
|-- Gemfile
|-- Gemfile.lock
|-- README.md
|-- Rakefile
|-- apps
|   `-- web
|       |-- application.rb
|       |-- assets
|       |   |-- favicon.ico
|       |   |-- images
|       |   |-- javascripts
|       |   `-- stylesheets
|       |-- config
|       |   `-- routes.rb
|       |-- controllers
|       |-- templates
|       |   `-- application.html.erb
|       `-- views
|           `-- application_layout.rb
|-- config
|   |-- boot.rb
|   |-- environment.rb
|   `-- initializers
|-- config.ru
|-- db
|   |-- migrations
|   |-- mysql
|   |   `-- volumes
|   `-- schema.sql
|-- docker-compose.yml
|-- lib
|   |-- app
|   |   |-- entities
|   |   |-- mailers
|   |   |   `-- templates
|   |   `-- repositories
|   `-- app.rb
|-- public
`-- spec
    |-- app
    |   |-- entities
    |   |-- mailers
    |   `-- repositories
    |-- features_helper.rb
    |-- spec_helper.rb
    |-- support
    |   `-- capybara.rb
    `-- web
        |-- controllers
        |-- features
        `-- views
            `-- application_layout_spec.rb

Generater

これは、users#showというアクションを作成するコマンドです。もう、なんとなく分かると思いますが、RailsでいうところのControllerのActionです。

Hanamiの大きな特徴として、ControllerはAction毎にファイルを作ります。詳しくは、Controllerの章で解説します。

# bundle exec hanami generate action web users#show
      create  apps/web/controllers/users/show.rb
      create  apps/web/views/users/show.rb
      create  apps/web/templates/users/show.html.erb
      create  spec/web/controllers/users/show_spec.rb
      create  spec/web/views/users/show_spec.rb
      insert  apps/web/config/routes.rb

Routing

Railsと全く同じに書けます。詳細に調べて行くと、セッションの有無に応じてルーティングを行うような高度なルーティングも出来ますが、Railsと同じ感覚でこのあたりさえ覚えておけば、ほとんど使えてしまいそうです。

apps/web/config/route.rb
get '/',  to: "users#show"
post '/new', to: "users"

または

apps/web/config/route.rb
resources :users, only: [:show, :create]

Model

まずは、hanamiコマンドでModelを作成します。以下のコマンドを実行すると、entitiesrepositoriesが作成されます。

bundle exec hanami generate model user
      create  lib/src/entities/user.rb
      create  lib/src/repositories/user_repository.rb
      create  db/migrations/20191208133857_create_users.rb
      create  spec/src/entities/user_spec.rb
      create  spec/src/repositories/user_repository_spec.rb

Migration

そして、マイグレーションファイルもRailsとそれほど変わりません。

db/migrations/20191207110038_create_users.rb
Hanami::Model.migration do
  change do
    create_table :users do
      primary_key :id

      column :name,  String, null: false
      column :email, String, null: false

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end

マイグレーションを実行するコマンドはこんな感じです。

bundle exec hanami db prepare

Entity

特殊な要件がなければ、これだけで使用出来ます。RailsのModelと同じように、Hanami::Entityを継承しているだけですが、面白い違いがあります。

class User < Hanami::Entity
end

Hanamiのconsoleで試して見ると、違いが分かります。Userクラスをインスタンス化する時に値は入れられるものの、後から代入しようとすると値が入りません。

つまり、Entityはイミュータブルな設計になっており、initializeの段階でしか値を入力することが出来ません。

つまり、Entityはデータをオブジェクトとして扱うためのただの入れ物です。

user = User.new(email: 'test@gmail.com')
user.email
#=> "test@gmail.com"
#=>
user.email = 'hoge@gmail.com'
#=> NoMethodError: undefined method `email='

Repository

それでは、どうやって値を代入するかですが、その役割はRepositoryが担います。つまり、DBへの更新はRepositoryの役割であり、SQLを発行するのもここが担います。

createupdatedeleteallfindなどの基本メソッドは、Hanami::Repositoryを継承するだけで予めサポートされます。他に必要なメソッドは以下のように自作する必要があります。

また、ここに複雑なロジックを噛ませることも可能です。しかし、ここにトリッキーなメソッドを定義するのは自殺行為なのでやめた方がいいです。ここでバグを仕込むとDBに保存されるデータが汚染され、恐ろしいことになるので、ロジックは出来るだけ別階層に書くべきです。遊びで試したところ、思い切ったクエリも実行出来るので下手なことをすると地獄を見そうで怖いです。

lib/src/repositories/user_repository.rb
class UserRepository < Hanami::Repository
  def find_by_email(email)
    users.where(email: email).first
  end
end

ちなみに使うときはこんな感じです。

user_repository = UserRepository.new
@user = user_repositor.find_by_email("test@gmail.com")

View

ViewはRailsとほとんど同じです。デフォルトのTemplateに、eRuby(.erb)が使用されています。
大きな違いはViewクラスが用意されている点です。

apps/web/views/users/show.rb
module Web
  module Views
    module Users
      class Show
        include Web::View

        def title
          'あるユーザーの情報'
        end
      end
    end
  end
end

あとはerbから呼び出すだけで使用できます。Rails経験者は勘が働くと思いますが、<h2>タグで呼び出されているコードはControllerで説明します。

apps/web/templates/users/show.html.erb
<h1><%= title %></h1>
<h2><%= user.name %></h2>
<h2><%= user.email %></h2>

どうやら、Viewクラスが存在することで、パーシャル化を行う際に非常に重宝するとの解説が見られますが、業務であまりいじらない箇所なので詳細な知見までは把握出来ていません。

Controller

Generaterの章で少し触れますが、HanamiのContorollerはAction毎に別ファイルに記載します。RailsだとControllerクラス配下のメソッドで定義されていたものが、Actionクラス毎に定義されるのだと理解すると分かりやすいと思います。

apps/web/controllers/users/show.rb
module Web::Controllers::Users
  class Show
    include Web::Action

    params do
      required(:id).filled(:str?)
    end

    expose :user

    def call(params)
      if params.valid?
        redirect_to '/'
      else
        self.status = 422
        return
      end

      @user = UserRepository.new.find(params(:id))
    end
  end
end

フォームから送信された値がcallメソッドに引数として渡され、処理が行われます。このとき、params.valid?で以下のように定義されたValidationが実行されます。

今回は文字列かどうかのチェックだけが行われていますが、オリジナルのメソッドを定義すれば、有効なEmailアドレスの文字列が不正ではないかなどのチェックもここで可能です。

params do
  required(:id).filled(:str?)
end

そして、Viewの章で少し触れていますが、インスタンスメソッドを以下のように定義することで、View側に渡すことが出来ます。そして、Railsと同様にテンプレート内で使用が可能です。

ちなみに、Railsと違い、ここで定義しないとインスタンス変数は外に出ませんので注意が必要です。

expose :user

Interactor

最後に、一番馴染みがない名前が登場します。この用語の解説を始めると、ドメイン駆動設計の話やクリーンアーキテクチャの話に広がってしまうため、今回は省略します(私も勉強中のため、説明出来るほど理解が進んでいない。。)。

そこで、ここでは、RepositoryとControllerの間に入る層だと思えば、少し理解しやすいと思います。

また、Repositoryの章で複雑なロジックは、Repositoryクラスに書かないように注意を促しましたが、HanamiのビジネスロジックはこのInteractorに書くことになります。

例えば、Controllerの章で例にしたActionのロジックを移設するとこんな感じです。

lib/bookshelf/interactors/user/show.rb
require 'hanami/interactor'

module UserInteractor
  class Show
    include Hanami::Interactor

    expose :user

    def call(id)
      @user = UserRepository.new.find(id)
    end
  end
end

そして、Controllerはこのようになります。

apps/web/controllers/users/show.rb
module Web::Controllers::Users
  class Show
    include Web::Action
    include Hanami::Interactor

    params do
      required(:id).filled(:str?)
    end

    expose :user

    def call(params)
      if params.valid?
        redirect_to '/'
      else
        self.status = 422
        return
      end

      @user = UserInteractor::Show.new.call(params[:id])
    end
  end
end

業務上の経験でしかありませんが、基本的にビジネスロジックはInteractorに詰め込んでいくため、ちょっとした仕様変更だとここを弄るだけで済んでしまうことがあり、個人的にはHanamiの良さだと思っています。

最後に

長い解説になってしまいすみません。。もし、少しでも興味を持っていただけるのなら、Hanamiに触れてみてください。

また、今回は、Advent Calendar に間に合わせるために、サンプルを用意出来ませんでしたが、検証用のdocker-composeとサンプルリポジトリをあとで追加したいと思います。

以上です。

参考文献

HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか
Hanami Getting Started guide

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

dbの指定を忘れてrails newしてしまった。しにたい。

症状

アプリを作成している時、いつもの如くSequel Pro確認したらdbがない

原因

いつもはrails newする時に

rails new -d mysql

としていたのだが

rails new

というやり方でディレクトリを作成してしまった。

処置

Gemfile
gem 'mysql'
#gem 'sqlite3'

上記のようにgemfileにmysqlを追記。
sqlite3は念のためコメントアウトして置いて

bundle install

そしてdatabase.ymlを下記のように編集

database.yml
default: &default
  adapter: sqlite3
  pool: 5
  timeout: 5000

development:
  adapter: mysql2
  encoding: utf8
  database: [データベース名]
  pool: 5
  username: root
  password:
  host: localhost

その後

rake db:create

でデータベースを作成。これでなんとかデータベースはmysqlになった。

まとめ

色々中途半端な対策かもしれません。今回はまだデータベースにデータを入れてない状態だったのでこれで済んだ。あとアプリを仕上げて本番環境とかになったら後遺症見たいのが出てくるのかもしれないがそれはその時対策します。
備忘録としてですが、
ご指摘あればぜひよろしくお願いします。

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