- 投稿日:2020-02-06T23:49:56+09:00
Rails データの取得方法
ActiveRecord クラス
テーブルから情報を取得するために必要なメソッドを兼ね備えたクラス。all(テーブルの全てのデータを取得)、find(テーブルのレコードの内、ある1つのデータを取得)、new(クラスのインスタンス(レコード)を生成)、save(クラスのインスタンス(レコード)を保存)がある。
例 変数@ postに1番目のレコードのデータを代入
しかし、このままだと以下のような表示になる。
修正するためにビューファイルを編集する。
これでcontentカラムのデータだけを表示全てのデータを取得
しかしこのままだとエラー。
その時に解決してくれるのがeachメソッド。
しかし、このままだと横並びになる。index.html.erbを以下のように書き換える。
app/assets/stylesheets/posts.scssを以下のように書き換える。そうすると横並び解決。
- 投稿日:2020-02-06T23:23:05+09:00
Rails モデル
- 投稿日:2020-02-06T23:20:09+09:00
【Rails】Sorceryでfacebook認証 エラーと解決法
はじめに
Sorceryによるfacebookログイン認証をしようとして、盛大なエラー祭りになりました。
誰かのための逆引き辞典になればいいなと思い、記録します。
ざっくりとした流れについては別記事で書いています。動作環境
ruby 2.6.5 Rails 5.2.3 sorcery 0.14.0 mkcert 1.4.1エラーとその解決法
ArgumentError in Hogehoge
ArgumentError in BoardsController#index
No association found for name `authentications'. Has it been defined yet?原因と解決法
Userモデルにおける
accepts_nested_attributes_for :authentications
は:authentications
とのアソシエーションが存在することが前提となっている。
そのため、has_many :authentications, dependent: :destroy
より下に書く必要がある。Sorry, something went wrong.
Sorry, something went wrong.
we're working on getting this fixed as soon as we can.解決法
sorcery.rbconfig.facebook.user_info_path = 'me?fields=email' config.facebook.access_permissions = %w[email]ここに不適な値を入れていた場合に起こるので、一旦デフォルトに戻してみる。
URLはブロックされています
URLはブロックされています: リダイレクトURIがアプリのクライアントOAuth設定でホワイトリストに追加されていないため、リダイレクトできませんでした。クライアントとウェブOAuthログインをオンにして、すべてのアプリドメインを有効なOAuthリダイレクトURIとして追加してください。
解決法
https://localhost:3000
とhttps://localhost:3000/oauth/callback?provider=facebook
を有効なOAuthリダイレクトURIに登録する。
リダイレクトURIはSorceryのcallback_url
のことです。接続はプライベートではありません
原因と解決法
リダイレクトURIを
https://0.0.0.0:3000/oauth/callback?provider=facebook
のままにしているとこのような警告が出る。
mkcertの設定ではhttps://0.0.0.0
は証明書の対象ではないので、HTTPS化していない。
https://localhost:3000/oauth/callback?provider=facebook
に書き換える。おわりに
と言う名の反省。
最初にいろんな作業を中途半端に進めた結果、エラーが出たときにどこが原因なのかわからなくなってしまいました。
みなさんは、まずはwikiに従って、その後自分で設定を追加していくようにしてください。
くれぐれも、よくわかってもいないのにリファクタリングとか設定の追加をしながら書き進めていかないように。
- 投稿日:2020-02-06T23:19:48+09:00
【Rails】Sorceryでfacebook認証 Sorceryの設定
はじめに
mkcertとSorceryの拡張機能を用いて、facebook認証機能を追加しました。
Sorceryのwikiが少し古かったり、facebook for developersとの連携に苦戦したので、備忘録がてら書いておこうと思います。
また、設定に不備があった場合のエラーについては別記事で書いています。動作環境
ruby 2.6.5 Rails 5.2.3 sorcery 0.14.0 mkcert 1.4.1前提
- sorceryによるログイン機能
- mkcertでのHTTPS通信を許可
- facebook for developersで「facebookログイン」の作成
は済ませていることとします。
この辺はggれば死ぬほど出てくるので問題ないと思います。簡単な流れ
Sorceryのwikiにチュートリアルがあるので、基本的にはそれに従っていきます。
authenticationsテーブルを作成
$ rails g sorcery:install external --only-submodules gsub config/initializers/sorcery.rb insert app/models/user.rb create db/migrate/20200203110653_sorcery_external.rbコマンドを打つと、外部認証に必要なauthenticationsテーブルを作成するためのマイグレーションファイルが作られます。
db/migrate/20200203110653_sorcery_external.rbclass SorceryExternal < ActiveRecord::Migration def change create_table :authentications do |t| t.integer :user_id, :null => false t.string :provider, :uid, :null => false t.timestamps end add_index :authentications, [:provider, :uid] add_index :authentications, :user_id # user_idにindexを貼る場合は追加 end end$ bundle exec rails db:migrateAuthenticationモデルの設定
$ rails g model Authentication --migration=false先ほど既にマイグレーションファイルは作成しているので、
--migration=false
オプションを付けています。app/models/authentication.rbclass Authentication < ActiveRecord::Base belongs_to :user endapp/models/user.rbclass User < ApplicationRecord authenticates_with_sorcery! has_many :authentications, dependent: :destroy accepts_nested_attributes_for :authentications # has_many :authenticationsより下に書く endここまではfacebook認証に限らず、外部認証共通の処理となります。
sorcery.rbの設定
sorcery.rbの以下の部分のコメントアウトを外します。
config/initializers/sorcery.rbRails.application.config.sorcery.submodules = [:external, blabla, blablu, ...] # コマンドを打つと自動的に追加される Rails.application.config.sorcery.configure do |config| ... config.external_providers = [:facebook, blabla, ...] # 外部認証に使用するものを追加 ... config.facebook.key = Rails.application.credentials.dig(:sorcery, :facebook, :key) # 1.で解説 config.facebook.secret = Rails.application.credentials.dig(:sorcery, :facebook, :secret) # 1.で解説 config.facebook.callback_url = 'https://localhost:3000/oauth/callback?provider=facebook' # 2.で解説 config.facebook.user_info_path = 'me?fields=email' # 3.で解説 config.facebook.user_info_mapping = { email: 'email' } # 3.で解説 config.facebook.access_permissions = %w[email] # 3.で解説 config.facebook.display = 'page' config.facebook.api_version = 'v6.0' # 4.で解説 config.facebook.parse = :json ... # --- user config --- config.user_config do |user| ... # -- external -- user.authentications_class = Authentication ... end ... end
config.facebook
に関する最初の3行は、環境によって値を変更することになると思いますが、ここでは省略します。Configなどを使ってください。1. facebook keyとsecret
facebook keyとsecretは外部には漏らしたくないため、credentialsで管理します。
$ rails credentials:editcredentials.ymlsorcery: facebook: key: 'アプリIDの値' secret: 'app secretの値'facebook for developersからマイアプリにアクセスし、「ダッシュボード」下の「設定」→「ベーシック」の画面から「アプリID」と「app secret」を確認し、記述してください。
IDは15桁の数字、secretは英数字のハッシュとなっています。
Rails5.2から追加された credentials.yml.enc のキホン
2. callback_url
HTTPS化をしているため、デフォルトの
"http://0.0.0.0:3000/oauth/callback?provider=facebook"
ではエラーが出ます。
SSL証明書の発行されているhttps://localhost:3000/oauth/callback?provider=facebook
をここに記述します。
そして、「Facebookログイン」「設定」の「有効なOAuth リダイレクトURI」にも同じものを登録します。
3. facefookから取得するデータ
デフォルトでfacebookから取得できるデータは以下の通りです。
id
first_name(名)
last_name(姓)
middle_name(ミドルネーム)
name(フルネーム)
name_format(デフォルトは{last}{first}
)
picture(プロフィール画像)
short_name(設定されていない場合はフルネーム)
アクセス許可のリファレンスそれ以外のデータを取得したい場合は、
config.facebook.access_permissions
に記述します。
emailを取得するためにはfacebook側の手続きは要りませんが、それ以外にはアプリレビューが必要です。Email、姓名、プロフィール画像を取得してUserに入れる場合はこのようになります。
config.facebook.user_info_path = 'me?fields=email,first_name,last_name,picture.type(large)' # facebookから取得するデータの受け取り方 config.facebook.user_info_mapping = { email: 'email', first_name: 'first_name', last_name: 'last_name', remote_avatar_url: 'picture/data/url' } # facebook側の属性名とUserモデルの属性を対応させる config.facebook.access_permissions = %w[email] # デフォルト以外のデータを取得する場合はここに書くプロフィール画像の取得方法は他と少し違うので注意が必要です。
参考:Sorcery + CarrierWave で Facebook 認証時に大きめのアイコン画像を保存する4. APIのバージョン
facebook APIのバージョンは「Facebookログイン」の「クイックスタート」から「ウェブ」「2. JavaScript用Facebook SDKを設定する」で確認できます。
Oauth処理を行うコントローラを作成
$ rails g controller Oauths oauth callbackapp/controllers/oauths_controller.rbclass OauthsController < ApplicationController skip_before_action :require_login # applications_controllerでbefore_action :require_loginを設定している場合 def oauth login_at(auth_params[:provider]) end def callback provider = auth_params[:provider] if (@user = login_from(provider)) redirect_to root_path, notice: "#{provider.titleize}でログインしました" else begin @user = create_from(provider) reset_session auto_login(@user) redirect_to root_path, notice: "#{provider.titleize}でログインしました" rescue StandardError redirect_to root_path, alert: "#{provider.titleize}でのログインに失敗しました" end end end private def auth_params params.permit(:code, :provider) end end基本的にはチュートリアルの通りです。多少、Rails5の書き方に直します。
ログインボタンを追加
login_page.html.erb<%= link_to 'Login with Facebook', auth_at_provider_path(provider: :facebook) %>ルーティングの追加
config/routes.rbpost "oauth/callback", to: "oauths#callback" get "oauth/callback", to: "oauths#callback" # Github, Facebookを使う場合は追加 get "oauth/:provider", to: "oauths#oauth", as: :auth_at_provideroauth_callback POST /oauth/callback(.:format) oauths#callback GET /oauth/callback(.:format) oauths#callback auth_at_provider GET /oauth/:provider(.:format) oauths#oauth
- 投稿日:2020-02-06T23:17:06+09:00
Rails ビュー
ビューファイル
レスポンスとして返す見た目を設定する。アプリケーションの見た目を定義するファイルのこと。
ビューファイルはapp/views/コントローラー名ディレクトリに、アクション名.html.erbというファイル名で作成される。
○○.html.erb ファイル
HTMLの記述方法に加え、Rubyのコードを埋め込むことができるファイルのこと。
ビューファイル作成
今回はindexアクションに対応するファイルを作成する前提で進めます。なのでindex.html.erbとする。index.html.erbの中身に記述するとその内容が表示される。
コントローラーにインスタンス変数を定義
コントローラーのアクション内にインスタンス変数を定義すると、その内容をビューファイルで表示できる。
例
ブラウザをリロードすると以下のようになる。
ビューファイルでインスタンス変数などのRubyの記述を用いるには、ERBタグ(<%= %>)を使う。フォームを追加する場合
例:新規投稿ページにアクセスするとリクエストがあった場合
app/views/posts/new.html.erbを作成する。
- 投稿日:2020-02-06T22:20:46+09:00
【Rails】DBのカラムにインデックスを付与する/しない場合での速度比較をしてみた
はじめに
RailsでDBにインデックスを付与するメリットを体感するために、試験的に1万件のレコードを作成し、インデックスの有無で実行速度の比較をしてみました。
今回のケースではおよそ18%の速度UPという結果が出ています
※サクッと作成するため、DBはRailsのデフォルトであるSQLite3を使用しました。2020/02/07追記
10万件のレコードだと20倍の差が出ていることを確認し、結果を追記しました。
@error_401 さんありがとうございます。環境
OS: macOS Catalina 10.15.3 zsh: 5.7.1 Ruby: 2.6.5 Rails: 6.0.2.1事前準備
rails_newする$ rails new sampleapp
sampleappディレクトリに移動$ cd sampleappdb_createする$ rails db:create
Postモデルの作成$ rails g model Post name:string description:string
db_migrateする$ rails db:migrate
事前に1万件のデータを登録irb(main):001:0> 10000.times do |p| Post.create(name: "sample#{p}", description: "this is sample!#{p}") end準備が整ったら、実験開始です!
実行するコマンド
nameカラムを検索対象にし、
find_by
メソッドでランダムな名前のレコードを1万回変数post
に代入するという内容です。
test.rb
を作成します。sampleapp/test.rbrequire 'benchmark' num_iteration = 10000 Benchmark.bm 10 do |r| r.report "no-index" do num_iteration.times do post = Post.find_by(name: "sample#{rand(1..1000)}") end end end1.インデックスなしの場合
rails_consoleで実行irb(main):001:0> require './test.rb'インデックスなしuser system total real 2.400283 0.353093 2.753376 ( 2.840555) # 1回目 2.456253 0.352958 2.809211 ( 2.914810) # 2回目 2.527288 0.383071 2.910359 ( 2.999960) # 3回目測定の対象はuserの数値にします。
3回の平均はおよそ2.46秒でした。
2.インデックスありの場合
ターミナル$ rails g migration add_index_to_post
XXXXXXXXXXXXXXXX_add_index_to_post.rbclass AddIndexToPost < ActiveRecord::Migration[6.0] def up add_index :posts, :name end def down remove_index :posts, :name end endターミナル$ rails db:migrate
これでインデックスが
name
カラムに付与されたので、効果測定です。再度
test.rb
を実行します。rails_consoleで実行irb(main):001:0> require './test.rb'レコードありuser system total real 2.016621 0.349745 2.366366 ( 2.455184) # 1回目 2.037209 0.333974 2.371183 ( 2.465041) # 2回目 2.039167 0.327804 2.366971 ( 2.471504) # 3回目3回の平均はおよそ2.03秒!
結論:およそ18%の速度UP
今回の単純なケースでは、およそ18%検索速度が上がりました!
もちろん
test.rb
の内容によって異なることもあると思いますが、指標の一つとして使えるのではないかと思います。以上です!
2020/02/07追記 レコード10万件の場合
@error_401 さんよりコメントを頂き、早速レコードを増やした場合にどうなるか検証してみました!
検索条件の変化
- レコード数:10万件
- 検索条件:
name
カラムの検索条件をsample1
〜sample100000
へ変更。(回数は1万回で変更なし)結果:インデックスなし
インデックスなしuser system total real 39.969663 14.484736 54.454399 ( 54.855391) # 1回目 40.167824 14.695771 54.863595 ( 55.215927) # 2回目 40.057479 14.542640 54.600119 ( 54.955958) # 3回目3回の平均はおよそ40.05秒
結果:インデックスあり
インデックスありuser system total real 2.007433 0.391915 2.399348 ( 2.546632) # 1回目 1.989761 0.376209 2.365970 ( 2.450146) # 2回目 2.029753 0.386575 2.416328 ( 2.566732) # 3回目3回の平均はおよそ2.00秒!
10万件のレコード数だとなんと20倍速度が早くなりました!
ビックリしたのは、インデックスありだと1万件のときとほとんど数字が変わらないこと。
これはすごいおわりに
最後まで読んで頂きありがとうございました
どなたかの参考になれば幸いです
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2020-02-06T21:35:22+09:00
Rails コントローラー
コントローラー
ルーティングで振り分けられたリクエストを実際に処理する。
① ルーティングがリクエストを受け取った際、対応するアクションを動かす
② 必要であればデータベースとやり取りをする
③ レスポンスとして返すビューを決める
アクション名は主に7つ。index(一覧表示ページを表示する)、new(新規投稿ページを表示する)、create(データの投稿を行う)、show(個別詳細ページを表示する)、edit(投稿編集ページを表示する)、update(データの編集を行う)、destroy(データの削除を行う)。コントローラーの作成
rails g controller コントローラー名(今回はposts)で作成できる。
作成されたら、app/controllers/posts_controller.rbができる。
もし、ページを表示するならindexをposts_controller.rbに記述する。
class PostsController < ApplicationController
def index # indexアクションを定義した
end
end
これでルーティングに対するアクションを記述することができた。ActionController::UnknownFormat in PostsController#indexと表示されたら、ビューファイルが設定されていないということ。フォームを追加する場合
- 投稿日:2020-02-06T21:33:26+09:00
Rails ルーティングについて
ルーティング
ルーティングの記述
configディレクトリの、routes.rbに記述する。
以下のように記述する。
Rails.application.routes.draw do
[HTTPメソッド] '[URIパターン]', to: '[コントローラー名]#[アクション名]'
endHTTPメソッド
HTTPメソッドにはGET(ページの表示)、POST(データ登録)、PUT(データ変更)、DELETE(データ削除)がある。URIパターン
URLのようなもの例えばhttp://localhost:3000/postsならURIにpostsを指定する。
コントローラー
ルーティングの次に行う処理。
アクション
コントローラー内における、処理のカテゴリーのこと。
実際にルーティングを設定する。
例 get 'posts', to: 'posts#index'
リクエスト
GETのHTTPメソッド(ただ単にトップページを表示するため)
URLはhttp://localhost:3000/posts
行き先
postsコントローラーという名前のコントローラー
indexアクションという名前のアクションrails routes
ルーティングが設定されているかどうか確認できる。
上記のような結果が出たら、ルーティングが設定できてることになる。
もし、ルーティングが設定できてなければ、Routing Errorが発生する。
以下のようにルーティングのみしか設定していない状態。
フォームを追加する場合
例 新規投稿ページを表示したいというリクエストがあった場合
ルーティングを以下のように設定する。
rails routesを実行
- 投稿日:2020-02-06T21:14:53+09:00
Railsの基礎
基礎を学習する方にオススメです。
1.rails new
Railsで新規アプリケーションを作成する際に使用する。
rails new アプリケーション名 -オプション名
仮にオプションに-d mysqlを使用したら、MySQLというデータベースに最適化された設定でアプリケーションが生成される。
例
rails 5.2.3 new my_sample -d mysql
これでRailsのバージョン5.2.3を用いて、「my_sample」を「-d」オプションでMySQLを指定して作成。正しくファイルが読み込まれているか確認
pwdで現在のディレクトリパスを指定
bundle installで関連ファイルが読み込まれているか確認。2.データベースを作成する
現在の状態は、データベースが無い状態なので作成する。
rails db:createで作成できる。
このコマンドはdatabase.ymlというファイルの内容に基づいてデータベースを新規作成する。database.yml(データベース・ヤムル) ファイル
Railsは運用環境ごとにデータベースを持つので、運用環境の分だけデータベースの設定を記述する。開発環境、テスト環境、本番環境。Sequel Pro(シークエル・プロ)というアプリケーションを使って、データベースが正しく作成できているかを確認する。
ローカルサーバーの起動
rails s
localhost:3000
localhostとは自分のpcという意味。
- 投稿日:2020-02-06T21:13:26+09:00
発言(post)をroleカラムを使い分ける
トークルームにて発言を分ける際にやった事のメモです。
今回作ったのが、自作自演でLINEのようなチャットルームにて一人で交互に発言出来るアプリです。
その際にやった事は、roleカラムを作り・role 0 なら Aさん
・role 1 なら Bさんと分ける事で実装しました。
では実際にコードなどをみていきましょう。
まず発言を投稿するformです。
memo_room_idに紐づいた、memo_room_postとなるように、
memo_room_idを取れるようにmemo_room_postをルーティングでネストしてあります。def new
@memo_room = MemoRoom.find(params[:memo_room_id])
@memo_room_post = MemoRoomPost.new
@memo_room_posts = @memo_room.memo_room_posts MemoRoomインスタンスのデータに紐づいた、memo_room_postsのデータ
@memo_room_post.memo_room_id = @memo_room.id 上記にmemo_room_idの値を入れてあげます。
end<%= form_with(model: @memo_room_post, url: memo_room_memo_room_posts_url(@memo_room), local: true) do |f| %> <%= render 'layouts/error_messages', model: f.object %> <div class="form-group"> <%= hidden_field_tag :memo_room_id, @memo_room.id %> <%= f.text_area :content, class: 'form-control chat-form', placeholder: "メモの内容を入れて投稿してください" %> </div> <%= button_tag :type => "submit", :class =>"btn btn-default btn-s-md" do %><i class="fas fa-paper-plane"></i> <% end %> <% end %> </div> <% else %>ここからが今回のお話の中心です。
role分けをする際の主となる部分です。
def create @memo_room_post = current_user.memo_room_posts.build(memo_room_post_params) @memo_room = MemoRoom.find(params[:memo_room_id]) @memo_room_post.memo_room_id = @memo_room.id #@memo_roomを使い、紐づいたMemoRoomPostインスタンスの最後の発言データを取得 @lastpost = @memo_room.memo_room_posts.last if @lastpost == nil #MemoRoomに紐付いたMemoRoomPostの最後のデータ がnilなら @memo_room_post.role = 0 #memo_room_postのrole を 0 にする。 開始位置 role 0 elsif @lastpost.role == 0 #MemoRoomに紐付いたMemoRoomPostの最後のデータのroleの値が0なら @memo_room_post.role = 1 #memo_room_postのrole の値を 1にする。 elsif @lastpost.role == 1 ##MemoRoomに紐付いたMemoRoomPostの最後のデータのroleの値が 1なら @memo_room_post.role = 0 #memo_room_postのroleの値を 0にする。 endまずはconsoleにて・・・試すのですが。貼れるものがないので簡単に。
user = User.first
category = Category.first
@memo_room = user.memo_rooms.build(title: 'a', category_id: category.id) カテゴリとメモルームが紐づいてます。
@memo_room_post = current_user.memo_room_posts.build(content: 'a', memo_room_id: @memo_room.id)こんな感じで@memo_room_post作ります。ルームのidは直接コンソールでは入れ込みます。
そして、@lastpost(わかりやすく変数名しただけに) = @memo_room.memo_room_posts.last
MemoRoomに紐づいたMemoRoomPostの最後の発言データを取得します。
トークルームの中で発言がない場合は当然nilになります。なので・・・
@lastpost == nil 最後の発言データがなくnilであるなら
@memo_room_post.role = 0 roleの値を0に。以下略 上記のコード参照
これで、発言がない場合、roleが0の場合 roleが1の場合の記載が出来ました。
<% if memo_room_post.role == 0 %>
そしてview側では、roleの値 が一致するかしないかで、発言を分けるIF文を使い、表示分けを行いました。
実際のコードです。html.erb <div class="line-bc col-sm-12"> <!-- タイトル --> <div class="line__title"> ルーム名:<%= @memo_room.title %> </div> <% @memo_room_posts.each do |memo_room_post| %> <% if memo_room_post.role == 0 %> ここ ← -------- <div class="col-sm-12 balloon6"> <div class="faceicon"> <%= image_tag current_user.post_a_picture.to_s %> </div> <div class="chatting"> <div class='says'> <p><%= memo_room_post.content %></p> </div> </div> </div> <% elsif memo_room_post.role == 1 %> ここ← ------- <div class="offset-sm-6 col-sm-6 mycomment"> <div class="faceicon"> <p><%= memo_room_post.content %></p><%= image_tag current_user.post_b_picture.to_s %> </div> </div> <% end %> <% end %>メモ書きなので、省略してるところも多いと思いますが、似たような事をする際に
役に立てばと思います。以上
- 投稿日:2020-02-06T20:12:15+09:00
中規模以上レガシーRailsシステムのリファクタリング方針
前置き
ここ一年、現職において0→1フェーズからすくすく育ってきたRailsプロジェクトのバックエンド全般を任されており、リファクタリング方針において、ある程度いい感触を掴みつつあるので、その知見を共有してみようってことで書いてみました。
現状
- 一連のビジネス要求に対する判断や分岐、データアクセスといったドメインロジックが、controllerや、方々のmodelに跨って定義されていることによるドメインロジックの見通しの悪さ
- データアクセスと判断や分岐といった処理がmodelに一緒くたに実装されているため、テストコードの実装が難解である
- テストコードの実装が難解であるため、テストの実装を諦め、人力動作確認でリリースまで進めるが、高確率で予期せぬデグレが発生する
- リリース後にデグレ対応に追われるため、スムーズに次の開発へと移行できない
考察
テストがないことに起因した既存実装の考慮漏れによるリリース後のデグレに一番時間が割かれてしまうため、まずはテストコードを書こうと思い辺りました。
ただ、既存の実装のままだと、そのテストコードが書きづらいため、テストを実装していく前に、アプリケーションコードの設計改善が必要だと感じ、今のプロダクトの規模に合わせて、中規模以上でのRailsシステムにおけるテストが書きやすい設計を模索していく必要があります。設計改善
ここでは具体的にどう改善していったのか(改善していこうとしているのか)を説明していきます。
現状は基本的には説明しやすくするために Skinny(かもしれない?) Controller, Fat Model の基本的なRails wayに則って実装されているとします。
それを以下の図のようなアーキテクチャへと変更していきました(一部まで適用できていない願望も含まれています)。
基本的には上図の通りに特に変更が発生しやすかったり、ビジネス的に重要度の高い部分からこちらの設計に実装を改善している段階です。
各層の役割
ここからは各層の役割について説明していきます。
上の図からなんとなく察している方もいらっしゃると思いますが、クリーンアーキテクチャの思想をRailsに合わせた形で表現していこうと試みています。Presentation層
Controller
ここは、PCやスマホの画面といった各Clientからのリクエストを受け取り、後述するusecaseにデータを渡します。
usecaseにデータを渡し、usecase以降で行われた処理の結果をレスポンスとしてて返却することと、渡されてきたデータのバリデーションにのみ責任を負ってもらうようにしています。Domain層
ビジネス要求にたいする判断や分岐のロジックを実装している層です。
usecase
controllerから渡されてきた値に対し、domainとdaoを組み合わせてビジネスロジックを表現しています。
ここで一番注意したいのは、判断や分岐、データアクセスといった処理は調節定義せず、
判断や分岐→domainで定義
データアクセス→daoで定義
といったように処理の実態はそれぞれ別の層で定義することを鉄則としています。なぜそうしたを説明しますと、usecaseで判断や分岐、データアクセスを一緒くたに実装してしまうと、今までmodelに定義されていた、データアクセスと密結合になってしまったビジネスロジックの実装箇所が、ただmodelからusecaseに移動されただけで、データアクセスと密結合になっているが故に、テストを書くことの困難さの解決にならなくなってしまうからです。
domain
ここにビジネス要求における判断や分岐といった、データアクセスを伴わないビジネスロジックをピュアなRubyのクラスとして実装していきます。
もちろん、手続き型ではなく、OOPによる再利用性や、変更容易性といったメリットを享受した形での実装が行っていくため、今後システムとして質の良い実装状態を担保していくためには、ここでの実装力の勝負になってくるかと思っています。dao
DAOとは(Data Access Object)の略です。
ビジネス要求に基づく、データ(主にRDB)への参照や、永続化を表現していく層になります。データの参照や永続化は、modelでも定義してくことが出来るのですが、modelはRDBのテーブルと1対1で紐づくため、システムの中心となるmaster系のテーブルに紐づくmodelがデータの参照や更新のみでみるみる太っていくことが予想されるため、こういった層に切り出そうといった考えに落ち着くました。
では、modelは何をすればいいのかと言いますと、ActiveRecordの場合、リレーションを定義しないと、複数テーブルをjoinするようなデータ参照が行えないため、各modelへのリレーションの定義と、そのmodel単体で済むデータアクセスであればmodelに持たせてもいいのかな? と思っています。
Data層
model
上述した通り、各modelへのリレーションの定義と、そのそのmodel単体で済むデータアクセスのみを表現させます。
other data source
外部で持つデータとのやり取りをこの層に閉じ込めます。
よくある部分としては、DynamoDBやRedisといったデータソースとのやり取りがあるかと思います。
後は、非同期で処理させたいjobの起動や、イベント駆動での連携があるシステムでは、イベントの発火等もこの層に閉じ込められるかと思います。今後の課題
現在はまだまだ既存の辛いままの実装が多く残っているのですが、全ての作業を止めてこの改善にだけ時間を費やすことはできないので、一番故障率が高く、ビジネス的にも重要度の高いところから順次適用を進めています。
インパクトの大きい部分から適用を進め、変更コストが下がり、デグレの発生頻度も下がってきた暁には、そこで空いたリソースを費やして、改善活動を加速していけるのではと考えています。
後、割とリファクタしたと思っても、既存実装の考慮漏れがあって事故ります。
変更しやすく実装し直しているはずなので、事故っても安易に切り戻しに走らず、例え一旦は切り戻したとしても、事故った部分を考慮した変更をリファクタ先の実装に加えていけばよい、加えていくべきだと思っています。
また、改善スピードを増すためには、同じように改善が行えるメンバーの育成についても考えていかなくていけないなと感じています。最後に
本来であれば、中規模以上に成長したシステムは、Railsから卒業して中長期での開発手法であるDDD等と親和性の高い言語やFWに乗り換えていくべきだと思うのですが、スタートアップのような、小さな組織で効率よく開発を進めたいといったニーズと、小さいサービスを最大速度で実装できるためのRailsの犠牲的アーキテクチャの親和性が高く、そのままプロダクトも当たって成長してきたが、リプレースのためのリソースが取れず、Railsで保守や追加開発をせざるを得ないといった組織は、少なからず存在していると思います。
今回のリファクタリング方針が、そういったエンジニア達の助けに少しでも貢献できれば幸いです。
- 投稿日:2020-02-06T19:59:19+09:00
【Rails】railsブログサイトの練習で躓いたところメモ
railsチュートリアル+αでブログサイトを作成していて躓いたところをまとめました。
環境
- ruby on rails
- mysql
- redis
- docker
- macOS
作ってみて
railsのブログアプリを作ること自体はそこまで難しいものではありませんでした。
ただデータベース周りがややこしくてそこにかなり時間をとられてしまったことが今回の反省点です。Rails deviceを使ってユーザー関連機能を作る
ブログアプリにまず必要なのはユーザー関連のあれこれです。Railsはその辺かなり楽に作れるdeviceというgemがあります。
deviceを利用するとログイン認証やアクセス制限などが簡単に実装出来ます。今まで練習も兼ねてユーザー関連は自分で作ってたのですが、一度こういった機能を知ってしまうと戻れなくなってしまいますね。
ここはそこまで詰まらなかったので以下の参考サイトを見れば大丈夫だと思います。
view関連なども「Rails device view」とかで調べたらたくさん出てきます。
【https://qiita.com/Hal_mai/items/350c400e8763ce0487a3】
【https://www.pikawaka.com/rails/devise】
【https://qiita.com/Hal_mai/items/350c400e8763ce0487a3】
Rails scaffoldを使って投稿機能を作る
投稿機能はRailsのscaffoldが便利です。勝手にMVCを作ってくれます。
これも以下の記事を見たら出来るので割愛します。
【https://techacademy.jp/magazine/7204】
注意点
ここで1つ注意点と言いますか、知っておいた方が良いなと個人的に思ったことを書きます。
先にあげたdeviceとscaffoldは大変便利な代物で、私みたいな初心者でも簡単に実装できてしまうものなのですが、これらはあくまで時短や効率化のためにあるものだと思うので初心者が最初から多用するのは危ないと思いました。
私はこのブログ練習サイトの前に3つほどscaffoldとdeviceなしで1から似たようなものを作って練習しているのですが、その練習がなかったらいきなりこれを使っても結局何をしてくれてるのか分からずじまいだったと思います。
分からないのに実装出来ちゃうからわかった気になってしまう危険性があるので、まずはMVCの理解を深めるためにもプロゲートやドットインストールなどの基本的なサイトを見て仕組みをなんとなくでも理解しながら進めていくのがいいのではないかと思いました。
現在のユーザーとアクセス制限
current_user
deviceにはオプションとしてcurrent_user(現在のユーザー)なるものがついています。
例えば「投稿者が他の投稿者の記事の編集や削除を出来ないようにする」といった時に便利な機能です。
今回は投稿の編集と削除を投稿者以外できないようにしたかったので、posts_controller.rbに以下のように記述しました。
posts_controller.rbbefore_action :ensure_correct_user, only: [:edit, :update, :destroy] 省略 private 省略 def ensure_correct_user if current_user.id!=@post.user.id flash[:notice]="Not yours" redirect_to(posts_path) end endprivate以下でensure_correct_userを定義して、before_actionでcurrent_user以外が使えないようにアクションに対して適応させています。
@post.user.idは投稿者のユーザーidのことです。これとcurrent_user.idが違ったら編集(update,edit)も削除(destroy)もできないですよって感じです。
user_signed_in?
user_signed_in?はユーザーがログインしているかどうかを確かめます。
例えば「ログインしている時としていない時で表示内容を変えたい」といった時に使える機能です。
今回はviewにこんな感じで書きました。
show.rb<% if user_signed_in? %> <li class="nav-item active"><%= link_to("新規投稿", new_post_path,{class:"nav-link"}) %></li> <li class="nav-item active"><%= link_to("ログアウト", destroy_user_session_path,{method: :delete,data:{confirm: "ログアウトしますか?"},class:"nav-link"}) %></li> <% else %> <li class="nav-item active"><%= link_to("新規登録", new_user_registration_path,{class:"nav-link"}) %></li> <li class="nav-item active"><%= link_to("ログイン", new_user_session_path,{class:"nav-link"}) %></li> <% end %>user_signed_in?していたら新規投稿・ログアウトを表示、していなかったら新規登録とログインを表示、という単純なものです。
これらもdeviceを使った機能なので基本的に書くだけで簡単に実装出来てしまうのですが、もし使わない場合だとインスタンス変数@current_userを定義するところから始めなければいけません。今回はそこは割愛しますが、その辺の仕組みもまた理解を深めるためにも記事にできたらと思います。
ユーザーと投稿の紐付け
最初に躓いたのはここでした。ユーザーと投稿を用意できたはいいがこれをどうやって紐付けるかが問題です。
私はプロゲートに倣ってPostモデルにuser_idカラムを追加しました。記事を投稿するときにuser_idカラムにcurrent_user.idを入れて紐づけるといった感じです。
posts_controller.rbでscaffoldで自動生成されたpost_paramsのpermitにuser_idを追加します。
posts_controller.rbdef post_params params.require(:post).permit(:title, :content, :user_id) endこれでuser_idに値が届くようになります。
続いてapp/views/posts/_form.html.erbではuser_idを送信できるように以下のように変更します。
app/views/posts/_form.html.erb<div class="field"> <%= form.label :記事内容 %> <%= form.text_area :content,value:@post.content %> <%= form.hidden_field :user_id, value: current_user.id %> </div>form.hidden_fieldを設置して、こっそりuser_idテーブルにvalueに設定したcurrent_user.idを送ります。これで投稿とユーザーの紐付けはおkです。
redisの導入
ブログの形になったので最後にランキングを作ります。
正直ここが1番躓きました。というのも、redisの基本コマンドとかはなんとなく分かっていたのですが具体的にどういった時に使うのかわからなくてそもそもイメージがちゃんと出来てなかったからです。
今回使うredisはブログサイトでよくあるランキングを表示するために使います。
他にはセッション管理などに使っているのが調べてたら多く見られました。
まずRailsとredisの接続からしないといけないのですが、ここで2日くらい躓きました。色んなサイトを見まくって試したけど全然繋がらない状態が続き地獄でした。
結論から申し上げると、基本的にredisはlocalhost:6379に繋ぐのが普通なのですが、開発段階だとRails自体をlocalhostに繋いでるので混同しちゃって上手く繋がらなくなってしまっていたということでした。
なのでredisの設定をlocalhost→redisといった感じに名前を変更して行えばすんなり上手くいった感じです。
出来てしまえば簡単なことだったと思うのですが、やはり初心者には結構辛いところでした。コード打っててエラーならまだしも繋がらなくてエラーは精神的にかなりきます。
以下は変更点と参考サイトです。
【https://qiita.com/sibakenY/items/0fff6398b8f832fb40a6】
【https://teratail.com/questions/115631】
redisでランキング機能の実装
無事redisは導入出来ましたが、「導入出来てしまえばこっちのもん!」というわけではありません。
ランキングを表示しないといけないのでこれまたredisの基礎とcontroller、viewを見直さないといけません。
これも「Rails redis ランキング」と調べたら結構参考サイトは出てくるのですが、仕組みの理解が乏しいので基礎の見直しが必要でした。
redisの特徴としては以下のような感じです。
- インメモリアルデータベース(すごく早い!)…ランキングなどに向いてる
- 永続化(定期的にディスクに書き出す)
- データ構造サーバー
そんなredisをRailsで使うには、methodを利用する必要があります。
今回はredisのソート済みセットを使ってランキングを実装していきました。
この辺は以下のサイトが非常に参考になったので貼っておきます。
【https://qiita.com/yokozawa/items/aae59b53897ca12f7064】
【https://qiita.com/sibakenY/items/0fff6398b8f832fb40a6】
【https://blog.seishin55.com/entry/2016/05/02/214513】
また、pv数の表示はviewに直接以下のように書けば表示されます。
index.rb<ul> <% @ranking_posts.each do |ranking_post| %> <li> <%= link_to(ranking_post.title,"/posts/#{ranking_post.id}") %> (<%= REDIS.zscore("posts/daily/#{Date.today.to_s}", ranking_post.id).to_i %>PV) </li> <% end %> </ul>アクションに設置する方法がないか考えたのですが、これしか方法がわからなかったです。ちょっと見苦しいですがとりあえずこれでPV数が表示されます。
まとめ
以上今回作ったブログサイトの大雑把なまとめでした。
初心者のメモ程度のものなので間違ってたり足りないところとかたくさんあると思いますが、初心者の方とかの参考になれば嬉しいです。
また、アドバイスなどあればコメントなどしてくれたら嬉しいです。お付き合い頂きありがとうございました。
- 投稿日:2020-02-06T19:51:09+09:00
Heroku + Rails + S3でサイトマップを設置
Herokuを使うと、サイトマップの設置がめんどいです。
レンタルサーバー時代はweb上の適当なツールで自動生成して、FTPでアップロードするだけの超単純な作業だったのですが、今回は色々と大変でした。とは言えやり方がわかってしまえばそこまで大変な作業ではないので、今後のために手順をまとめておきます。
全体の流れ
- AWSのアカウントを作る
- S3のバケットを作る
- アクセスキー、シークレットアクセスキーを取得する
- 環境変数を登録する
- gemをインストールする
- サイトマップ用の設定ファイルを作る
- ルーティングを追加する
- サイトマップをS3にアップロードする
- robots.txtを編集する
- Google Search Consoleにサイトマップの場所を登録する
環境変数登録の部分から説明していきます。
アクセスキーの取得までは、「S3 バケット作成」等でググって頑張ってください。環境変数を登録する
ローカル、Herokuそれぞれに、以下の3つの環境変数を登録します。
AWS_ACCESS_KEY_ID
:AWSのアクセスキー
AWS_SECRET_ACCESS_KEY
:AWSのシークレットキー
S3_BUCKET_NAME
:作成したバケットの名前私の場合、ローカルは
dotenv-rails
を使い、Herokuは管理画面上から登録しました。gemをインストールする
Gemfilegem 'sitemap_generator' #サイトマップ作成用 gem 'aws-sdk' #AWS接続用$ bundle installサイトマップ用の設定ファイルを作る
$ rails sitemap:install以下のように書き換える
config/sitemap.rbSitemapGenerator::Sitemap.default_host = 'https://example.com' SitemapGenerator::Sitemap.sitemaps_host = "https://s3-ap-northeast-1.amazonaws.com/#{ENV['S3_BUCKET_NAME']}" SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new( ENV['S3_BUCKET_NAME'], aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'], aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], aws_region: 'ap-northeast-1', ) SitemapGenerator::Sitemap.create do #ここからサイトマップに登録したいページのパスを書いていく add root_path add mcs_genre_path mcs = Mc.all mcs.each do |mc| add mcs_detail_path(mc_id: mc.id) end add rankings_p_line_path add comments_path . . endルーティングを追加する
config/routes.rbget '/sitemap', to: redirect("https://s3-ap-northeast-1.amazonaws.com/#{ENV['S3_BUCKET_NAME']}/sitemaps/sitemap.xml.gz")サイトマップをS3にアップロードする
ここまでの変更をHerokuにデプロイした後、以下のコマンドを打ち込むと、サイトマップが作成されS3に保存されます。
$ heroku run rails sitemap:refresh※S3のパブリックアクセスの設定が上手くできていないとエラーが発生するので注意。
AWS S3で「Access Denied」を解決するrobots.txtを編集する
以下の1行を追加
public/robots.txtSitemap: https://example.com/sitemapGoogle Search Consoleにサイトマップの場所を登録する
Search Consoleにログイン後、画面左のサイドバーから「サイトマップ」を選択し、それっぽいところにURLを入力して送信します。
参考
- 投稿日:2020-02-06T19:08:54+09:00
Railsチュートリアル 第14章 ユーザーをフォローする - followingアクションおよびfollowersアクションの統合テストに存在する不具合の修正
following
アクションおよびfollowers
アクションの統合テストにおける、Railsチュートリアル本文記載のテストの不具合実は、Railsチュートリアル本文のリスト 14.29に記述されているテストには、1つの不具合があります。
例えば、
app/views/users/show_follow.html.erb
に以下の欠落がある場合を考えてみましょう。app/views/users/show_follow.html.erb<% provide(:title, @title) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> ...略 </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>
の場合、例えば users/1/following のWebブラウザにおける表示は以下のようになります。
ページ右側にフォローしているユーザー一覧が描画されていません。明らかに意図した表示内容ではないですね。
しかしながら、Railsチュートリアル本文のリスト 14.29に記述されているテストの場合、この状態でもテストは成功してしまうのです。
# rails test test/integration/following_test.rb Running via Spring preloader in process 1248 Started with run options --seed 41256 2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.66436s 2 tests, 10 assertions, 0 failures, 0 errors, 0 skips同様に、
app/views/users/show_follow.html.erb
に以下の欠落がある場合でも、Railsチュートリアル本文のリスト 14.29に記述されているテストは成功してしまいます。app/views/users/show_follow.html.erb<% provide(:title, @title) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> ...略 </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>
不具合の原因は、テストの実装が「ユーザーのプロフィールページへのリンクが1つ以上あればOK」という内容になっているためです。単一ユーザーのプロフィールページへのリンクは、「サイドバーのアイコンで1つ、FollowingまたはFollowersの一覧で1つ〜2つ1」存在します。そのため、「リンクが1つ以上」というテストの実装では、「サイドバー」「FollowingまたはFollowersの一覧」いずれか片方の欠落ではテストをすり抜けてしまうのです。
following
アクションおよびfollowers
アクションの統合テストの不具合を修正するユーザーのプロフィールページへのリンクの存在によって「サイドバー」「FollowingまたはFollowersの一覧」両方が正しく描画されていることを確認するためには、「ユーザーのプロフィールページへのリンクが2つ以上存在すること」をテストする必要があります。
assert_select
で「要素が2つ以上存在すること」をテストするためには、オプションハッシュにminimum: 2
という設定を与えればOKです。上記を踏まえ、
test/integration/following_test.rb
の修正内容は以下のようになります。require 'test_helper' class FollowingTest < ActionDispatch::IntegrationTest def setup @user = users(:rhakurei) 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) + assert_select "a[href=?]", user_path(user), minimum: 2 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) + assert_select "a[href=?]", user_path(user), minimum: 2 end end end上記修正は本当に正しいのか
上記修正を行った
test/integration/following_test.rb
を対象に、改めて以下のコードのテストを行ってみます。app/views/users/show_follow.html.erb<% provide(:title, @title) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> ...略 </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>
結果は以下のようになります。
# rails test test/integration/following_test.rb Running via Spring preloader in process 1235 Started with run options --seed 28029 FAIL["test_followers_page", FollowingTest, 2.6110920999926748] test_followers_page#FollowingTest (2.61s) Expected at least 2 elements matching "a[href="/users/919532091"]", found 1.. Expected 1 to be >= 2. test/integration/following_test.rb:23:in `block (2 levels) in <class:FollowingTest>' test/integration/following_test.rb:22:in `block in <class:FollowingTest>' FAIL["test_following_page", FollowingTest, 2.699444500001846] test_following_page#FollowingTest (2.70s) Expected at least 2 elements matching "a[href="/users/314048677"]", found 1.. Expected 1 to be >= 2. test/integration/following_test.rb:14:in `block (2 levels) in <class:FollowingTest>' test/integration/following_test.rb:13:in `block in <class:FollowingTest>' 2/2: [===================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.71399s 2 tests, 8 assertions, 2 failures, 0 errors, 0 skips
Expected 1 to be >= 2.
というのがポイントですね。「ユーザーのプロフィールページへのリンクが2つ以上存在すること」に対する正しいテストが書けているようです。
ログインユーザーがAdmin属性である場合、ユーザーを削除するためのリンクも、リンク先のURLは当該ユーザーのプロフィールページへのリンクのURLと同じになります。違うのは、発行されるアクションが
GET
であるかDELETE
であるかです。 ↩
- 投稿日:2020-02-06T18:06:48+09:00
[rails,html]文字省略〜続きを読む実装〜
- 投稿日:2020-02-06T16:19:05+09:00
【Raila】SorceryのExternalを使用してSNSログインを実装(Facebook認証)
環境
注)) ローカル環境で動作確認するための実装です。
Ruby 2.6.5
Rails 5.2.3
mysql 5.7.28
gem sorcery
gem font-awesome-sass
gem config
mkcert
credentials
実装
Sorceryの導入、email・パスワードログイン機能は実装済みであることが前提です。
gemのbundleを済ませておいて下さい。
- Facebook For Developersの設定
- ローカル環境の設定
2つに分けて実装していきます。
Facebook for developersの設定
1.Facebookアカウント作成
2.Facebook For Developersに登録・アプリ作成
下記資料を参考にしました。
参考資料:Fantastech!!
アプリID、app secret
は後ほど使用しますので記載場所を確認しておいて下さい。
マイアプリ→設定→ベーシックに記載されています。3.Facebookログインの設定
ダッシュボード上にある
Facebookログイン
の設定をクリック。
設定のクライアントOAuth設定
の下記部分を確認。
・クライアントOAuthログイン→はい
・ウェブOAuthログイン→はい
・リダイレクトURIに制限モードを使用→はい
・有効なOAuthリダイレクトURI→https://localhost:3000/oauth/callback?provider=facebook
リダイレクトURIにはHTTPSが強制になるので後ほどローカル環境をSSL化します。設定 → ベーシック → プライバシーポリシーのURLの指定。
プライバシーポリシーとは、収集した情報をこれこれこういう目的で使いますよという旨が書かれた文書のことです。とりあえず開発時にはアクセスできるサイトであればなんでもよいです。アプリケーションを公開する段階になったら、忘れずに自分のアプリケーション内にプライバシーポリシーを載せたページを作り、それを指定しましょう。
OAuthとは
この図は自分用にまとめたものです。
OAuthに関しては一番分かりやすい OAuth の説明が最強です。ローカル環境の設定
基本はwikiに沿って進めていきますが、流れを意識して記述するので順番が前後します。
1.externalインストール、DB反映
external
をインストールターミナル$ rails g sorcery:install external --only-submodules
migrationファイル
が生成されるのでdb/migrate/xxxxxxxx_sorcery_external.rbclass SorceryExternal < ActiveRecord::Migration def change create_table :authentications do |t| t.integer :user_id, null: false t.string :provider, :uid, null: false t.timestamps end add_index :authentications, [:provider, :uid, :user_id] end end外部キーの
user_id
にindex張るのを忘れないように追加しておきましょう。ターミナル$ rails db:migrate2.Authenticationモデルの生成
authentications
テーブルにはFacebook認証ログインしたユーザーデータが入ります。ターミナル$ rails g model Authentication --migration=falseアソシエーション
User
モデルとAuthentication
モデルの関連付けを行います。user.rbclass User < ActiveRecord::Base has_many :authentications, dependent: :destroy accepts_nested_attributes_for :authentications endauthentication.rbclass Authentication < ActiveRecord::Base belongs_to :user end3.oauthsコントローラー作成
ターミナル$ rails g controller Oauths oauth callback --skip-template-engine
--skip-template-engine
→ viewファイルスキップoauths_controller.rbclass OauthsController < ApplicationController skip_before_action :require_login def oauth login_at(params[:provider]) end def callback provider = params[:provider] if (@user = login_from(provider)) redirect_to root_path, success: 'フェイスブックでログインしました' else begin @user = create_from(provider) reset_session auto_login(@user) redirect_to root_path, success: 'フェイスブックでログインしました' rescue StandardError redirect_to root_path, danger: 'ログインに失敗しました' end end end endほぼwiki通りですがフラッシュメッセージにBootstrapを使用していますのでgemをいれてない方はwiki通りに進めて下さい。
4.ルーティング設定
routes.rbpost 'oauth/callback', to: 'oauths#callback' get 'oauth/callback', to: 'oauths#callback' get 'oauth/:provider', to: 'oauths#oauth', as: :auth_at_provider5.viewにFacebook認証ボタン配置
ボタンを表示させたい場所へ記述して下さい。
xxxx.html.erb<%= link_to auth_at_provider_path(provider: :facebook), class: 'facebook-btn' do %> <i class="fab fa-facebook-f"></i> Facebookログイン <% end %>6.サブモジュール(external)と設定の追加
その前にやるべきことがあります。
・ローカル環境でSSL暗号化通信を可能にするためmkcert
を使用する
・keyとsecretの暗号化のためにcredentials
に記述
・keyとsecretを定数管理するためconfig
を使用するSSL暗号化通信以外は設定しなくても動作しますが、設定することをおすすめします。
mkcertでSSL暗号化通信を可能にする
SSL暗号化通信についてはこちらにまとめましたのであやふやな人はのぞいてみてください。
mkcertの使い方は下記資料の中にある手順で進めます。
やることはmkcertを使用してSSL証明書を発行して、httpsでアクセスできるように設定します。
【Rails】Facebookでユーザー認証する一部修正。
開発環境下でのみhttpsアクセスできるよう制限をかけます。config/puma.rbif Rails.env.development? ssl_bind "0.0.0.0", "3000", { cert: "config/certs/localhost.pem", key: "config/certs/localhost-key.pem" } endcredentialsを使用してkeyとsecretを暗号化
ターミナル$ EDITOR=vim bin/rails credentials:editcredentials.yml.encがvimで開くので
credentials.yml.enc# 追記 facebook_key: facebook for developersから 'アプリID' を参照して記述 facebook_secret: facebook for developersから 'app secret' を参照して記述保存して再起動。
configを使用してkeyとsecretを定数管理
config/settings/development.ymlfacebook: key: <%= Rails.application.credentials.facebook_key %> secret: <%= Rails.application.credentials.facebook_secret %> callback_url: "https://localhost:3000/oauth/callback?provider=facebook"ポイントはcredentialsを呼び出す際にerb記法で記述すること。
<%%>
で囲まないと動作しません。これで3つの準備が終わったのでサブモジュールと設定の追加をしていきます。
サブモジュール(external)の追加
config/initialzers/sorcery.rbRails.application.config.sorcery.submodules = %i[external]設定の追加
config/initialzers/sorcery.rbRails.application.config.sorcery.configure do |config| ... config.external_providers = [:facebook] ... config.facebook.key = Settings.facebook.key config.facebook.secret = Settings.facebook.secret config.facebook.callback_url = Settings.facebook.callback_url config.facebook.user_info_mapping = { email: 'email', first_name: 'first_name', last_name: 'last_name' } config.facebook.user_info_path = 'me?fields=email,first_name,last_name' config.facebook.display = 'page' config.facebook.api_version = 'v2.3' ... config.user_config do |user| ... user.authentications_class = Authenticationここの設定でFacebookからどんなユーザー情報が欲しいのかなど設定します。
実装終了。まとめ
wiki通り動かないとしんどい 笑
Deviseとの比較:Rails でアカウントロジックを扱うなら sorcery が良いかも
- 投稿日:2020-02-06T15:57:14+09:00
herokuにデプロイした際にCSSで指定した画像が表示されない
backgroundで指定した画像が消えてる!?
Googleで検索しても・・・中々望んでる解答がない。
[そこで検索してるうちにようやく見つけた解決策]
http://kgmx.hatenablog.com/entry/2014/06/26/085304
CSS .xxx { background-image: url("heart.png"); }これだと、デプロイしても画像は表示されません。
上手く画像を見つけ出してくれないらしいです。.xxx { background-image: image-url("heart.png"); }image-urlとすることで、app/assets/images配下から画像を探してくれる。
との事で試した結果。すんなりと直りました。という事で、今回のメモでした。
- 投稿日:2020-02-06T15:07:53+09:00
【Rails】フォームにフォーム外のファイルを追加しようとしたらJavaScriptをめっちゃ書くことになった
概要
Railsのフォーム送信は色々いい感じにやってくれるが、少し変えたい時もある。
僕はフォーム外からファイルを追加したかった。そういう時は、rails-ujsのajaxイベントハンドラを使うとフォーム送信に要所で割り込める。
Working with JavaScript in Rails#3.5 Rails-ujs event handlers ※ Rails 5.1以降の機能以下、これを使ってフォーム外のファイルをフォーム内容と同時送信する例を紹介する。
やりたいことの例
本来フォーム内の
name
だけ送信されるところ、フォーム外からファイルを追加送信したい。
ここでは仮に、フォーム外のnewImageFiles
に画像ファイルを入れてあり、フォーム内容と一緒に送信したいという前提で話を進める。<%= form_with model: [:user, @config] do |f| %> <div> <%= f.label :name, 'お名前' %> <div> <%= f.text_field :name %> </div> </div> <div> <%= f.submit '保存' %> <%#= 通常、フォームの内容(今回はname)のみ送信される %> </div> <% end %> <script> var newImageFiles = [] // ← フォーム外のファイルを送信に含めたい </script>※
form_with
にlocal
オプションを付けないでください。
参考:【Rails】form_withのlocalオプション実装
// ...前略 <script> var newImageFiles = [] // ← フォーム外のファイルを送信に含めたい document.body.addEventListener('ajax:beforeSend', function(event) { // [1] var detail = event.detail var xhr = detail[0], options = detail[1] // [2]コールバック options.success = function() {} options.error = function() {} options.complete = function() {} newImageFiles.forEach(image => { options.data.append('config[images][]', image, image.name) // [3] }) xhr.onreadystatechange = function () { // [4] if (this.readyState === XMLHttpRequest.DONE) { if (this.status === 200) { // [4-1] 200の時、リロード window.location.href = location.href } else { // [4-1] 200以外の時、失敗表示 alert(`フォーム送信に失敗しました`) } } } xhr.send(options.data) // [5] event.preventDefault() // [6] その後の通常処理を行わない })[1]
ajax:beforeSend
イベントから、フォームから送信予定のxhrとoptionsがとれる。これをカスタマイズするdocument.body.addEventListener('ajax:beforeSend', function(event) { // [1] var detail = event.detail var xhr = detail[0], options = detail[1][2] optionsにコールバック設定がある。そのまま利用してもよいが今回は雑に全部削除
// [2]コールバック options.success = function() {} options.error = function() {} options.complete = function() {}[3] options.dataにフォーム外から含めたいデータをappend。
newImageFiles.forEach(image => { options.data.append('config[images][]', image, image.name) // [3] })[4] onreadystatechangeを改めて書く。これも今回は最低限。
xhr.onreadystatechange = function () { // [4] if (this.readyState === XMLHttpRequest.DONE) { if (this.status === 200) { // [4-1] 200の時、リロード window.location.href = location.href } else { // [4-1] 200以外の時、失敗表示 alert(`フォーム送信に失敗しました`) } } }[5] xhrを送信。
xhr.send(options.data) // [5][6] その後の通常処理をキャンセル。Rails5以前のjquery-ujsではこういうところで
return false
と書くらしいので注意event.preventDefault() // [6] その後の通常処理を行わない感想
Railsの話だけどJavaScriptしか書いていない。
rails-ujsのこの機能は、ほかにも色々な用途で使えると思います。
- 投稿日:2020-02-06T14:53:13+09:00
rails new からrails serverまでの流れ
この記事は
rails new
したい時に色々忘れていたりするので自分用のメモです。
都度更新していくかもしれません。rails newまで
gemはvendor/bundleで管理したい
- フォルダ作成からAtomへ移動まで
~ ❯ cd MyApp ~/MyApp ❯ mkdir portfolio ~/MyApp ❯ cd portfolio ~/MyApp/portfolio ❯ bundle init Writing new Gemfile to /Users/kn428/MyApp/portfolio/Gemfile ~/MyApp/portfolio ❯ atom .
- Gemfileの
gem "rails"
のコメントアウトを外す- bundler経由でrailsをインストールする
~/MyApp/portfolio ❯ bundle install --path vendor/bundle --jobs=4
bundle exec
経由でrails new
にopを付けて実行する
.
は現在のディレクトリの意味
opはrails new -h
で確認またはググる~/MyApp/portfolio 59s ❯ bundle exec rails new . -B -d mysql --skip-test exist create README.md create Rakefile create .ruby-version create config.ru create .gitignore conflict Gemfile Overwrite /Users/kn428/MyApp/portfolio/Gemfile? (enter "h" for help) [Ynaqdhm] YGemfileを上書きしていいか聞かれたら
Y
で続行参考 :
新規Railsプロジェクトの作成手順まとめ
rails new 手順書Gemfileに必要なGemを追加
- gemfileに下記をコピペする(※私の場合)
汎用性のためSLIM等は外しておくsource 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.6.4' gem 'rails', '~> 5.2.1' gem 'bootsnap', require: false gem 'mysql2', '~> 0.5.2' gem 'puma', '~> 3.7' gem 'sass-rails', '~> 5.0' gem 'uglifier', '>= 1.3.0' gem 'jquery-rails', '~> 4.3' gem 'turbolinks', '~> 5.0' gem 'coffee-rails', '~> 4.2' gem 'jbuilder', '~> 2.5' group :development, :test do gem 'sqlite3', '~> 1.3.6' gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'rspec-rails' gem 'factory_bot_rails' gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' gem 'spring-commands-rspec' gem 'pry-rails' gem 'pry-doc' gem 'pry-byebug' gem 'rails-erd' gem 'annotate' end group :development do gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' gem 'rubocop-airbnb' gem 'bullet' end group :test do gem 'capybara' gem 'webdrivers' end gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]これで
$ bundle install
したところ下記のエラーが発生An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.様々な対処法があるらしいが私は下記で対処できた
1.$ brew info openssl
を実行し
export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"
この箇所の""
内の部分をコピーしてメモ帳などで貼っておく(環境によるのでちゃんとコマンドを実行して確認すること)
2. それを下記のコマンドに繋げる
--with-cppflags
--with-ldflags
3. 最後にbundle config
コマンドに繋げる
$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
すると.bundle/config
内に追加されている(--with-cppflags
のほうが上書きされている気がするが気にしないでおく).bundle/config--- BUNDLE_PATH: "vendor/bundle" BUNDLE_JOBS: "4" BUNDLE_BUILD__MYSQL2: "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"これで
$ bundle install
したら無事全てインストールできた
- 最後に
config/application.rb
内を下記の状態に変更する
requireをrequire "rails/all"
にする
config.load_defaults
を5.2(railsのver)にする参考 :
mysql2 gemインストール時のトラブルシュート
Bundlerでビルドオプションを指定するinstallしたGemの設定
Rspec関連を設定する
$ bundle exec rails generate rspec:install
.rspec
のフォルダの中に--format documentation
を追加する
config/application.rb
内を下記の状態に変更する.config/application.rbmodule Portfolio class Application < Rails::Application # ...省略... config.time_zone = 'Tokyo' config.generators do |g| g.test_framework :rspec, view_specs: false, helper_specs: false, controller_specs: false, routing_specs: false, request_specs: false end config.generators.system_tests = false config.generators.stylesheets = false config.generators.javascripts = false config.generators.helper = false end end
spec/rails_helper.rb
に以下を追記RSpec.configure do |config| # ...省略... config.include FactoryBot::Syntax::Methods # 追加 endテスト用DBをマイグレーションしておく
$ bundle exec rails db:migrate RAILS_ENV=test
- Capybaraの初期設定
$ mkdir spec/supports
$ touch spec/supports/capybara.rb
spec/supports/capybara.rb
内に下記を実装RSpec.configure do |config| config.before(:each, type: :system) do driven_by :selenium_chrome_headless end end
spec/spec_helper.rb
に下記を追加
require 'supports/capybara'
SystemとRequest Specの追加
$ mkdir spec/system
$ mkdir spec/requests
Rspecの高速化
$ bundle exec spring binstub --all
$ bin/rspec spec/
のコマンドで動けばok。direnvの設定は次回の機会にまわす参考 :
RSpecを導入してテストを書いてみる
spring と direnv を使って Rails と rspec を高速起動。快適開発はじめるrails server
$ bundle exec rails db:create
$ bundle exec rails db:migrate
$ bundle exec rails s
無事(http://localhost:3000/) に「Yay! You’re on Rails!」が表示されれば完了git init ~ push
使いまわせたら嬉しいのでここまでをgithub等にpushしておく
まず.gitignore
の中に/vendor/bundle
を入れておく
$ echo '/vendor/bundle' >> .gitignore
$ git init $ git add . $ git commit -m "first commit" $ git remote add origin https://github.com/ユーザー名/リポジトリ名.git $ git push -u origin master以上です。何かアドバイス等ありましたらコメントいただけると嬉しいです。
- 投稿日:2020-02-06T13:54:22+09:00
【rails】開発環境のデータをseed_dumpを使ってseedファイルとして書き出す
はじめに
初学者がポートフォリオ作成でハマったことをメモします。
以前書いた記事でwebサイトからデータをスクレイピングして開発環境用のデータベースに保存した。
https://qiita.com/tsubasaweb1/items/5be78cefed67020ac483今回は本番環境のデータベースへの保存を試みた。
しかし、herokuの「Cleardb」というアドオンを使ってMySQLを使用している為、時間当たりのクエリ数に制限があり、全てのデータを保存することができなかった。
そこで今回はseed_dumpというgemを使って開発環境のデータを分割して2つのseedファイルに書き出した。
その後2つのseedファイルを使って本番環境にデータを流し込んだ。環境
Ruby 2.6.3 ,Rails 5.2.4
インフラ:heroku手順
・ gemをインストール
Gemfilegem 'seed_dump'ターミナル$ bundle install
・ db/seedsディレクトリを作成 (seed_dumpをデフォルトで使用するとdb/seeds.rbを上書きしてしまう)
・以下のコマンドを実行ターミナル$ bundle exec rails db:seed:dump MODELS=Fish FILE=db/seeds/20200101_fish.rb
MODELS=Fish
で書き出すテーブルを指定。FILE=db/seeds/20200101_fish.rb
で作るファイル名を指定。今回はファイル名を変えて2回行い、お互いのデータを補完するように半分ずつデータ消した。
・db:seed用のrakeタスクを作成lib/tasks/seed.rakeDir.glob(File.join(Rails.root, 'db', 'seeds', '*.rb')).each do |file| desc "Load the seed data from db/seeds/#{File.basename(file)}." task "db:seed:#{File.basename(file).gsub(/\..+$/, '')}" => :environment do load(file) end end
・heroku にpushする・データを追加したいテーブルに既にデータがある場合以下を実行してレコードを消去
ターミナル$ heroku run rails db:migrate:reset DISABLE_DATABASE_ENVIRONMENT_CHECK=1 #全てのテーブルのレコードを消去 $ heroku run rails db:seed #db:seeds.rbのデータを入れる
・dumpしたファイルを指定し、本番環境にデータを流す(今回は2つファイルを作ったのでファイル名を変えて2回行った。)ターミナル$ heroku run rails db:seed:20200101_fish RAILS_ENV=production最後に
誰かのお役にたてれば幸いです。
間違えている部分があればコメントお願いいたします。参考にした記事
https://gist.github.com/seak0503/5aa5db45abfac5c42a06
http://gre.hacca.jp/2018/09/12/rails-db%E3%83%80%E3%83%B3%E3%83%97%E3%81%A8%E3%83%AA%E3%82%B9%E3%83%88%E3%82%A2%EF%BC%88%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%A8heroku%EF%BC%89/
https://github.com/rroblak/seed_dump
- 投稿日:2020-02-06T13:53:34+09:00
rails migrations
remove
rails generate migration RemoveFieldNameFromTableName field_name:datatype
class RemoveFieldNameFromTableName < ActiveRecord::Migration[6.0] def change remove_column :table_names, :field_name, :datatype end endadd
rails generate migration AddFieldNameToTableName field_name:datatype
class AddFieldNameToTableName < ActiveRecord::Migration[6.0] def change add_column :table_names, :field_name, :datatype end end
- 投稿日:2020-02-06T13:14:41+09:00
中間テーブルとは??
プログラミング初学者が学習する中間テーブルについて
Railsなどのフレームワークを学習していて『中間テーブル』という言葉がよくわからなかったので備忘録も兼ねて解説します! (初投稿なので温かく見守ってください泣)
中間テーブルってなんなの??
具体的な使い方
中間テーブルってなんなの??
中間テーブルとは・・・データベースで、テーブルとテーブルの多対多の関係を表すテーブルのこと。
これだけ言われてもパッとしないかもしれません。イメージしてみましょう!
データベースにuserテーブルとgroupテーブルというのが存在すると仮定します。
userテーブル
id name 1 山田 2 佐藤 3 鈴木 4 田中 〜 〜 groupテーブル
id group-name 1 Aグループ 2 Bグループ 3 Cグループ 4 Dグループ 〜 〜 以上二つのテーブルから誰がどのグループに属しているかを表現してみると、、
id name group 1 山田 Bグループ 2 佐藤 Cグループ 3 鈴木 Aグループ 4 田中 Cグループ 〜 〜 〜 このようにuserテーブルにgroupカラムを追加することで解決できます。
しかし!
userがいくつものgroupに所属できるようにしたい!と考えました
*例えば通話アプリの『LINE』でも複数の『グループ』に所属できますよね!すると、、
userテーブル
id name group1 group2 group3 group4 1 山田 Bグループ Cグループ Aグループ 2 佐藤 Cグループ Aグループ Dグループ Bグループ 3 鈴木 Aグループ 4 田中 Cグループ Bグループ これじゃ、空のカラムが大量発生するじゃないか!!!(親父ギャグ)
- カラムどんだけ追加しないとダメなんだ・・・
- DB設計時にどれだけカラムを用意しなければならないんだろう・・・予測できない・・
- 空のカラムはエラーの元じゃないか・・・
ここで登場するのが 中間テーブル です
中間テーブルの具体的な使い方
userテーブル
id name 1 山田 2 佐藤 3 鈴木 4 田中 〜 〜 groupテーブル
id group-name 1 Aグループ 2 Bグループ 3 Cグループ 4 Dグループ 〜 〜 新たに以下のテーブルを追加!
group_userテーブル
id user group 1 山田 Bグループ 2 山田 Cグループ 3 山田 Aグループ 4 佐藤 Cグループ 5 佐藤 Aグループ 6 佐藤 Dグループ 7 佐藤 Bグループ 8 鈴木 Aグループ 〜 〜 〜
このように中間テーブルを追加することで空のカラムもなくなり、その都度追加する必要があったカラムも追加せずに済むことができます。さらに誰がどのグループに所属しているかが見やすくなりました。
データベースの設計をしていく際には空のカラムが発生しないか、チームとして開発を進めていく中で見やすいDBができているかを考える必要があると思うので覚えておきましょう!!!
初投稿となりましたが、何かわかりにくい点や誤って理解してしまっている点などあればご教授願います。では!
- 投稿日:2020-02-06T12:43:11+09:00
RailsにてPostgreSQL使用の共同アプリ開発の導入手順
共同開発用アプリを
git clone
、bundle install
、bundle exec rails db:create
した際にcould not connect to server: No such file or directory Is the server running locally and accepting connections on Unix domain socket "/tmp/.s.PGSQL.5432"? Couldn't create database for {"adapter"=>"postgresql", "encoding"=>"unicode", "pool"=>5, "database"=>"XXX"} rails aborted! PG::ConnectionBad: could not connect to server: No such file or directory Is the server running locally and accepting connections on Unix domain socket "/tmp/.s.PGSQL.5432"?のエラーが発生。ローカルで共同アプリ用のPostgreSQLを構築するため、以下対応が必要。
# PostgreSQLのインストール(PostgreSQLがインストールされていない場合 ) brew install postgresql# macに初期データが有る場合があるので一度削除、初期化 rm -rf /usr/local/var/postgres initdb /usr/local/var/postgres -E utf8# PostgreSQLの起動 brew services start postgresql# PostgreSQLの動作確認 psql -l問題なければ以下を実行し、完了。
bundle exec rails db:create bundle exec rails db:migrate参考記事
・Railsのプロジェクトに途中から参加するとき
https://uncode.co.jp/web/rails%E3%81%AE%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AB%E9%80%94%E4%B8%AD%E3%81%8B%E3%82%89%E5%8F%82%E5%8A%A0%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D/・macOS SierraへのPostgreSQLインストール
https://morizyun.github.io/database/postgresql-install-mac.html
- 投稿日:2020-02-06T12:35:26+09:00
Rails ユーザーごとに複数の一覧ページのフラグメントキャッシュを作成する
始めに
プログラミング初学者のQiita初投稿です。
至らない点も多くあると思いますが、頑張って最近やったことを書いてみようと思います。なにをするか
まず、ユーザーが投稿した記事を一覧表示したページが複数ある
今回は全ての記事一覧と、ユーザーがお気に入り登録した記事一覧を作成この2つのページのフラグメントキャッシュをユーザーごとに分けて作成する
キャッシュを通して全てのユーザーに同じページが表示されてしまうのを防ぐため、ユーザーごとに分けます。環境
- Ruby 2.6.2
- Rails 5.2.2
- devise 4.7
- Redis 4.1
ModelとControllerの設定
Modelは
User, Post, Likeの3つ
ユーザーと、ユーザーが投稿する記事と、その記事をお気に入り登録するためのモデルです。
ユーザーはdeviseを使って作成しました。app/models/user.rbclass User < ApplicationRecord has_many :posts has_many :likes has_many :like_posts, through: :likes, source: 'post' endapp/models/post.rbclass Post < ApplicationRecord belongs_to :user has_many :likes has_many :like_users, through: :likes, source: 'user' endapp/models/like.rbclass Like < ApplicationRecord belongs_to :user belongs_to :post endControllerはPostsControllerに2つのページのアクションを用意します
app/controllers/posts_controllers.rbclass PostsController < ApplicationController def index @posts = Post.all end def like_index @posts = current_user.like_posts end記事を投稿してお気に入り登録するまでの流れは、今回の本題とは離れるので割愛します。
記事一覧ページの作成
一覧表示部分は部分テンプレートで共通化します
app/views/posts/index.html.erb<h1>記事一覧</h1> <%= render partial: 'posts_index', locals: { posts: @posts } %>app/views/posts/like_index.html.erb<h1>お気に入り記事一覧</h1> <%= render partial: 'posts_index', locals: { posts: @posts } %>app/views/posts/_posts_index.html.erb<% posts.each do |post| %> <%= post.title %> <%= post.user.name %> ↓お気に入り登録ボタン↓ <% unless post.like_users.include?(current_user) %> <%= link_to likes_path(user_id: current_user.id, post_id: post.id), method: :post do %> <p>お気に入り登録</p> <% end %> <% else %> <%= link_to like_path(id: post.id), method: :delete do %> <p>お気に入り解除</p> <% end %> <% end %> ↑お気に入り登録ボタン↑ <% end %>一覧表示部分には、ワンクリックで記事をお気に入り登録・解除できるリンクを記述しました。
キャッシュの作成
ここからキャッシュを作っていきます。
キャッシュストアにはRedisを使用しています。
Railsはデフォルトでキャッシュがオフになっているので、以下のコマンドでキャッシュをオンにします。rails dev:cache設定ファイルで有効期限を設定できます。
config/environments/development.rbconfig.cache_store = :redis_store, { expires_in: 1.hour }フラグメントキャッシュを作成するのは、以下のようにして簡単にできます。
app/views/posts/_posts_index.html.erb<% cache 'post_index' do %> 追加 <% posts.each do |post| %> <%= post.title %> <%= post.user.name %> ↓お気に入り登録ボタン↓ <% unless post.like_users.include?(current_user) %> <%= link_to likes_path(user_id: current_user.id, post_id: post.id), method: :post do %> <p>お気に入り登録</p> <% end %> <% else %> <%= link_to like_path(id: post.id), method: :delete do %> <p>お気に入り解除</p> <% end %> <% end %> ↑お気に入り登録ボタン↑ <% end %> <% end %> 追加これで、'post_index'というキーで指定した範囲をキャッシュできます。
しかし、このままでは2つのページのキャッシュキーが同じなため、ページ内容が同じになってしまいます。
ここでは、アクションで別々のキャッシュキーを作成し、変数に入れることで対処します。ページごとにキャッシュを分ける
app/controllers/posts_controllers.rbclass PostsController < ApplicationController def index @cache_key = 'index' @posts = Post.all end def like_index @cache_key = 'like_index' @posts = current_user.like_posts endapp/views/posts/_posts_index.html.erb<% cache @cache_key do %> 変更 <% posts.each do |post| %> <%= post.title %> <%= post.user.name %>これで、ページごとにキャッシュを分けることができます。
続いてユーザーごとにキャッシュを分けられるようにしましょう。
今のままだと全ユーザーに同一のページが見えてしまいます。
お気に入りに登録した記事を見ようとしたら他人がお気に入りにした記事一覧が表示された、なんてことになります。
もしくは自分のお気に入り記事一覧ページがキャッシュされた場合、他の全ユーザーにそれが行き渡ります。ユーザーごとにキャッシュを分ける
app/controllers/posts_controllers.rbclass PostsController < ApplicationController def index @cache_key = ['index', current_user.id] @posts = Post.all end def like_index @cache_key = ['like_index', current_user.id] @posts = current_user.like_posts endユーザーのIDをキャッシュのキーに含ませることで、ユーザーごとに違うキャッシュが作成されるようになります。
ここまでで、ユーザーごとに複数の一覧ページのキャッシュを作成できましたが、まだ深刻な問題があります。
新しく記事が投稿されたり、ユーザーや記事の名前が変更された場合、キャッシュがあるせいでそれがビューに反映されません。
データが変わった時にはキャッシュのキーも変更することでこれを回避できるので、キーにはデータの最新情報を含ませるようにします。最新のデータを反映できるようにする
app/models/application_record.rbscope :latest, -> { order(updated_at: :desc).first }app/controllers/posts_controllers.rbclass PostsController < ApplicationController def index @cache_key = ['index', current_user.id, User.latest.update_at, Post.latest.update_at, Like.latest.update_at] @posts = Post.all end def like_index @cache_key = ['like_index', current_user.id, User.latest.update_at, Post.latest.update_at, Like.latest.update_at] @posts = current_user.like_posts endこれで、データの変更時には新しくキャッシュが作成されるようになりました。
削除が反映されない問題
ここまでで基本的なフラグメントキャッシュを作成することができました。
しかしまだ問題はあります。
データの変更時にキャッシュを新しく作成することはできましたが、
データを削除した時はキャッシュは変わりません。(最新のデータであれば変わります)例えば、
記事1を投稿する キャッシュ1ができる
記事2を投稿する キャッシュ2ができる
ここで記事1を削除する 最新の更新時間は変わらないためキャッシュはできない
記事一覧ページのロードには最新のキャッシュ2が使われる
キャッシュ2ができた時は記事1は存在したため、記事1は表示されるというように、
削除された記事1が、削除後も表示されてしまいます。
データ変更時の時間を、削除された時間で上書きする必要がありそうです。
そこでデータが削除された時間を取得する方法を探してみたら、論理削除というものを発見しました。
論理削除はデータが削除された時、レコードを消去せずに削除時間を入力して削除されたものとみなすもののようです。
この削除時間が入力された時、更新時間(updated_at)も変わるため、今回の問題にはこれが使えそうです。
もっといい方法があるかもしれませんが、今回は論理削除を活用してみようと思います。論理削除
Userは削除されないものとして、
PostとLikeモデルに論理削除を適用します。
まずパラノイアというGemをインストールします。gem 'paranoia'$ bundle install論理削除を使うモデルに、deleted_atカラムを追加します。
$ rails g migration AddDeletedAtToPosts deleted_at:datetime$ rails g migration AddDeletedAtToLikes deleted_at:datetimeclass AddDeleteAtToPosts < ActiveRecord::Migration[5.2] def change add_column :posts, :deleted_at, :datetime end endclass AddDeleteAtToLikes < ActiveRecord::Migration[5.2] def change add_column :likes, :deleted_at, :datetime end end$ rails db:migrateモデルファイルにacts_as_paranoidを記述します。
app/models/post.rbclass Post < ApplicationRecord acts_as_paranoid belongs_to :user has_many :likes has_many :like_users, through: :likes, source: 'user' endapp/models/like.rbclass Like < ApplicationRecord acts_as_paranoid belongs_to :user belongs_to :post endこれだけで論理削除が適用されるようになりました。
これで記事やお気に入りが削除された際、レコードは実際には消えず
削除時間が追加されるだけになりました。データ検索の際に従来の、
Post.allだと、削除されたデータは除外されますが
Post.with_deleted.allとすると、削除されたデータも含めて検索できます。
改めて、
app/controllers/posts_controllers.rbclass PostsController < ApplicationController def index @cache_key = ['index', current_user.id, User.latest.update_at, Post.with_deleted.latest.update_at, Like.with_deleted.latest.update_at] @posts = Post.all end def like_index @cache_key = ['like_index', current_user.id, User.latest.update_at, Post.with_deleted.latest.update_at, Like.with_deleted.latest.update_at] @posts = current_user.like_posts endとすることで、
記事1を投稿する キャッシュ1ができる
記事2を投稿する キャッシュ2ができる
記事1を削除する 最新の更新時間が変わったためキャッシュ3ができる
記事一覧ページのロードには最新のキャッシュ3が使われる
キャッシュ3ができた時は記事1はいないため、記事1は表示されないというように、
削除された記事1を一覧表示から消すことができました。最後にキャッシュキー作成コードを、DRYにして終わりです。
app/controllers/posts_controllers.rbclass PostsController < ApplicationController def index @cache_key = make_cache_key('index') @posts = Post.all end def like_index @cache_key = make_cache_key('like_index') @posts = current_user.like_posts end def make_cache_key(action) [action, current_user.id, User.latest.update_at, Post.with_deleted.latest.update_at, Like.with_deleted.latest.update_at] end後になって気づいたこと
さて、ここまでやってきた最中で、
今までのことの多くが無駄だったことに気づきます。app/views/posts/_posts_index.html.erb<% cache @cache_key do %> これを <% posts.each do |post| %> <%= post.title %> <%= post.user.name %>app/views/posts/_posts_index.html.erb<% posts.each do |post| %> <% cache post.updated_at do %> こうする <%= post.title %> <%= post.user.name %>これだけでよかった!
eachで回される1つ1つのオブジェクト自体をキャッシュすることで、
自然とユーザーやページごとに別の内容になります。
コントローラーにも何も書かなくていいです。
今思えばこういう風に書いてるサイトが多かったのになぜこうしなかったのか、、、ただ、今回に関してはこれだけではダメです。
今回は一覧表示されている記事1つ1つに、お気に入りボタンがありました。
上記のコードは記事1つ1つの更新時間をキャッシュキーにしているだけなので、お気に入り登録の変更は感知してくれません。
記事作者の情報が変更されても感知しません。
それに、お気に入りボタンは人によって見え方が違うはずなので、
ユーザーを識別するためにユーザーIDをキャッシュキーに含める必要が出てきます。
つまり今までやったようなことが結局必要になります。app/controllers/posts_controllers.rbclass PostsController < ApplicationController def index @cache_key = make_cache_key @posts = Post.all end def like_index @cache_key = make_cache_key @posts = current_user.like_posts end def make_cache_key [Like.with_deleted.latest.update_at] endapp/views/posts/_posts_index.html.erb<% posts.each do |post| %> <% cache [current_user.id, room.updated_at, room.user.updated_at, @cache_key] do %> <%= post.title %> <%= post.user.name %>大体こんな感じになるでしょうか。
結局最初にやったのと同じような感じにはなりました。
一覧表示全体を1つのキャッシュにするか、一覧内容1つ1つをキャッシュするか。
どちらの方が良いかは時と場合によるでしょうか?最後に
今回、お気に入りボタンなどもキャッシュしてみましたが、
そもそもユーザーによって表示が変わる、いわゆる動的な部分はキャッシュしないのが普通なのかもしれません。
論理削除も積極的に使うべきではないと思うので、全体としてあまり有意義なことはできなかったかもです。
それでも今回得られたキャッシュについての知見は、どこかで必ず役に立つとは思います。参考資料
- 投稿日:2020-02-06T11:43:25+09:00
Rails コーティング規約について 1
はじめに
Ruby、Railsの基礎を学習中の方に向けて記載致します。
Rubyのコーティング規約はコチラをクリック願います。
私自身これからチーム開発を行う上で大事にしたい。知っておきたいことをOutputします。Routing
ActiveRecordのモデル間の関連を表現するには、入れ子型でルートを定義する。
qiita.rbclass Post < ActiveRecord::Base has_many :comments end class Comments < ActiveRecord::Base belongs_to :post end # routes.rb resources :posts do resources :comments endActiveRecord
なるべくhas_and_belongs_to_manyよりhas_many :throughを利用する。
qiita.rb# あまり良くない例 class User < ActiveRecord::Base has_and_belongs_to_many :groups end class Group < ActiveRecord::Base has_and_belongs_to_many :users end # 良い例 class User < ActiveRecord::Base has_many :memberships has_many :groups, through: :memberships end class Membership < ActiveRecord::Base belongs_to :user belongs_to :group end class Group < ActiveRecord::Base has_many :memberships has_many :users, through: :memberships endさいごに
コーティング規約については毎日更新します。
皆様の復習等にご活用頂けますと幸いです。
- 投稿日:2020-02-06T10:06:15+09:00
【Rails6】DEPRECATION WARNING: Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
TL;DR
uniqueness
で grep して、ひたすらcase_sensitive: true
を付けていきましょう。
※既存の動作を維持する場合はこれでOK。ただ、この機会に見直すことを推奨します。環境
- Ruby: 2.6.3
- Rails: 5.2.4.1 => 6.0.2.1 へアップデート
なんだこのWarning
DEPRECATION WARNING: Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1. To continue case sensitive comparison on the :destination_type attribute in PublicReference model, pass `case_sensitive: true` option explicitly to the uniqueness validator.Uniqueness validator を使っているところでこんなWarningが出ました。
- Rails 6.0 までは
case_sensitive: true
(大文字小文字を区別する)がデフォルト- Rails 6.1 からは
case_sensitive: false
(大文字小文字を区別しない)がデフォルト- 今までのデフォルト通り、大文字小文字を区別したチェックにしたければ、
case_sensitive: true
を明示的に指定してねと言われています。
今まで通り、大文字小文字を区別したいケースが大半かなと思いますので、
case_sensitive: true
を指定しておきましょう。修正例class Post < ApplicationRecord - validates :uuid, uniqueness: true + validates :uuid, uniqueness: { case_sensitive: true } endこれでWarningを解決できました。
参考
- 投稿日:2020-02-06T05:45:43+09:00
Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #18.5 環境変数, Gmail送信設定編
こんな人におすすめ
- プログラミング初心者でポートフォリオの作り方が分からない
- Rails Tutorialをやってみたが理解することが難しい
前回:#18 EC2環境構築, Nginx+Puma+Capistrano編
次回:準備中今回の流れ
- Railsの環境変数を設定する
- 本番環境でのGmailの送信を設定する
Railsの環境変数を設定する
Rails5.2以降の環境変数の設定には、credentials.yml.encを使います。
デフォルトで.gitignoreになり暗号化されているのでおすすめです。
以下のように設定します。shellEDITOR=vim rails credentials:editcredentials.yml.enc# aws: # access_key_id: XXXX # secret_access_key: XXXX # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. secret_key_base: XXXXおそらくこんな感じかと思うので、以下のように環境変数を追加します。
credentials.yml.enc# 省略 a: b: bbb c: ccc # 省略以上で設定が終わりました。
試しにコンソールで値を引き出してみます。
値を引き出すには、以下のように行います。shell$ rails c
> Rails.application.credentials.a[:b] => "bbb"この後のGmailの送信の設定には、このcredentials.yml.encを使います。
本番環境でのGmailの送信を設定する
前提として本番環境には、EC2を使っています。
またActionMailerの生成はすでに行なっているものとします。
(お済みでない方は#12などをご確認ください。)それではGmailを送信の設定をします。
ここでの手順は以下の通りです。
- Googleアカウントのアプリパスワードを有効にする
- production.rbの設定を変更する
- 環境変数を設定する
Googleアカウントのアプリパスワードを有効にする
お手持ちのGoogleアカウントのアプリパスワードを有効にします。
有効にするまでに、いくつかの手順を踏みます。
- googleアカウント → 画面左ダッシュボード『セキュリティ』 → Googleへのログイン『2段階認証プロセス』 → 有効にする
- googleアカウント → 画面左ダッシュボード『セキュリティ』 → Googleへのログイン『アプリパスワード』 → 16桁のパスワードをコピーする
これで次の手順に使うパスワードを手に入れました。
production.rbの設定を変更する
Gmail用に設定を変更します。
config/environments/production.rbconfig.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :smtp config.action_mailer.default_url_options = { host: Rails.application.credentials.host_server[:ip] } ActionMailer::Base.smtp_settings = { :address => 'smtp.gmail.com', :port => '587', :authentication => :plain, :user_name => Rails.application.credentials.gmail[:user_name], :password => Rails.application.credentials.gmail[:password], :domain => 'gmail.com', :enable_starttls_auto => true }先ほど見かけた、環境変数を引き出す記述があります。
最後はこれが動作するよう、環境変数を設定します。環境変数を設定する
環境変数の設定です。
送信に使うアドレスやアプリパスワードはここに記述します。credentials.yml.enchost_server: ip: XX.XX.XX.XX # EC2のIPアドレス gmail: user_name: hogehoge@gmail.com # Gmailアドレス password: hogehoge # コピーしたアプリパスワードこれで送信の設定が完了しました。
参考になりました↓
【Rails】メール送信設定 〜gmail利用〜
前回:#18 EC2環境構築, Nginx+Puma+Capistrano編
次回:準備中
- 投稿日:2020-02-06T02:05:31+09:00
リファクタリングでモブプロみたいなことをしたお話
弊社内でエンジニア3人が集まって楽しくリファクタリングをした話です。
背景
社内のプロダクトで「手が空いてるならリファクタリングやってくれると嬉しい。手が足らない」というようなSOSを受け取り、
そのプロダクトをある程度知っている人 1人 + 知らない人 2人 の3人でリファクタリング部隊を作ってリファクタリングしていこうという話になりました。今回は、モブプロ「みたいなこと」です。厳密なモブプロではないです。
なので、
- ドライバーも考える (みんなで考える)
- ドライバーは変わらない
という感じでゆるく進行しました。
プロダクトについて
Railsで動いています。Rails3から始まり、今Rails5です。
- 7年ぐらい前から動いているプロダクト
- 新しく書いたところは新しく、古いものは古いという歴史が積み重なっている
- Rails3からのコードが残っている
- Rails何それ?Ruby何それ?から始まったコードが存在している
- ただファイルが分割されただけのコードがConcernsとして存在している
- でもテストはちゃんと書かれている
という感じで、とても歴史のあるプロダクトです。
今回やった人について
A: このプロダクトわかる。設計強い。DDD好き。Ruby, Railsともに得意。
B: このプロダクトわからない。インフラ強い。Rubyそんなに得意じゃない。 (本人談)
C: このプロダクトわからない。設計好き。DDD好き。Ruby得意。型を欲しがる。という感じで思想やスキルの差が若干ありつつ、
- 仲が良いので思想のぶつけ合いになっても険悪にはなりづらい
- 誰か一人が発言しづらいということはない
- お互いの技術に信頼/リスペクトしている
というメンバーです。今回はBさんがドライバーをしました。
今回の流れ
実際にどんな感じでやったのか?ですが、基本は
- つっつき会
- メシ
- 修正会
という3つのフェーズで行いました。
会議室で大画面を使用してみんなで唸ります。つっつき会
今回のメインはモブプロをやることではないので、まずはリファクタリング対象(ディレクトリ/ファイル単位)を決め、
コードを読んでみてよくなさそうなところをつっついて、方針決めをします。大体4時間ぐらい。このつっつきは
TODO: つっつき serviceとして切り出す
というようなTODOコメントとして残し、コミットします。今回は、以下のような観点でつっついていきました。
- そのファイルの構造、ネームスペースが正しいか(分割されただけのファイルがconcernsとして存在しているため)
- そのクラスの責務を逸脱したメソッドがあるか(↑と大体一緒)
- 冗長(わかりづらい)な書き方をしていないか
- これらを大幅な変更せず綺麗にできるか
この時点でも、3人分の目と知識があるので、誰かが「この書き方はなんだ?」「これどうなってるんだ」となっても、知ってる/わかってる人が説明するということもあり、一人でやるときよりもスムーズです。
また、
これらを大幅な変更せず綺麗にできるか
という判断も1人だと消極的な理由でやらないことを選択してしまいそうな部分も、誰かの「実は簡単に修正できるのでは」という知識の後押しにより、積極的な判断をすることもできます。もちろん、やらないという判断もあります。
特に、やるか/やらないか という部分以外にも判断材料を出せる人が増えるというのは多人数の有利な部分です。
メシ
修正会
実際につっつき会でつっついた部分をリファクタリングしていきます。大体2時間ぐらい
基本的につっつき会で方針が出ているのでその方針に沿ってリファクタリングするだけです。
とはいえ、この段階でも「あ、もっといい方法あるじゃん」というのはあるので、随時取り入れていきます。KPT
Keep
- 誰かの「これどうなるんだろう?」に対して「じゃあ、試してみるか」というラフな形でやってみることができる
- コードではわからないお互いの思考フローが知れる
- ドメインエキスパートが居ないことによる「名前から直感的に判断できるか」という部分に焦点をあてれる
Problem
- 仲が良いからこそ、脱線すると脱線し続けそうになる
- ドメインエキスパートが居ないので仕様がわからないのでパスというのがあった
- 複数人いるのでガッツリ集中して書くというのは難しい
Try
- ドメインエキスパートも呼んでみる
- 仕様についてをサクッと聞けるので時間短縮になりそう
- keepの部分とコンフリクトしそう
- 人数を増やしてみる
- 人が増えれば集合知というのは単純な加算になるのか
終わりに
ペアプロ、モブプロは人と人の知識をリアルタイムでつなぎ合わせる というのがコアだと思うので、
「ドライバーは10分」「ナビゲーターの指示に従う」といった感じで厳密にやる必要は別になく、人が集まってみんなでコードに向かうというのは大事だと感じました。
特にkeepでも書いた、コードではわからないお互いの思考フローが知れる
というのはお互いに勉強になります。
また、エンジニア間のコミュニケーションも取れるのでお互いに刺激になり、メインの業務にもハリが出ると思います。更に、人間誰しも見落としや勘違いもあるので、単純に人数が増えることにより、見落としや勘違いなどをし続けることがなくなりやすいです。 (ダブルチェックと一緒で単純に増えればいいわけではないですが)
と、いう感じで、定期/不定期に限らずこういうことをやるのはいい感じだと思います。
おわり。
- 投稿日:2020-02-06T01:35:42+09:00
初心者!current_userの使い方 〜ログイン中のユーザ名を表示〜
current_userでログイン中のユーザ名とメールアドレスの表示の仕方を紹介します。
書き始めるときは
current_user
の前に=
を忘れないでくださいね!ユーザー名を表示 〜current_user〜
.header__left-box__list = current_user.userメールアドレスを表示させたい↓
.header__left-box__list = current_user.email #nameからemailに変更なぜ、このように
= current_user.name
や= current_user
のようになるかというと、、、テーブルに注目。↓
○usersテーブル
id name
1 mirai mirai@mirai.com 2 : : 3 : :
name
カラムや
=current_user
の後ろには.
を忘れず書く事!!また、each doも同じです。
ではまた(^_^ )
おまけ
ユーザー名を表示 〜each do〜
.header__left-box__list - @group.users.each do |hhgg| = hhgg.nameメールアドレスを表示させたい↓
.header__left-box__list - @group.users.each do |hhgg| = hhgg.email #nameからemailに変更
- 投稿日:2020-02-06T00:05:06+09:00
【Rails】I18n.tはtに省略できるんじゃない、省略したほうが良い
I18n.tの省略記法
まだなんにもわかっていないRails触り始めて1週間くらいの頃、
「RailsはViewファイル内ではI18n.t('hoge')
って書かなくてもt('hoge')
って書けるよ、
先輩たちもそう書いてるから合わせてね」って言われてふむふむなるほど、
よくわからんがviewでは省略できるのかってなった経験はありませんか?実はI18n.tをtにすると文字が4文字節約できるってレベルではない変化が起きていた
つまりどういう事か
詳しい実相はこの辺
TranslationHelper
にtが実装されていたんですねrails/actionview/lib/action_view/helpers/translation_helper.rbdef translate(key, options = {}) options = options.dup if options.has_key?(:default) remaining_defaults = Array.wrap(options.delete(:default)).compact options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol) end # If the user has explicitly decided to NOT raise errors, pass that option to I18n. # Otherwise, tell I18n to raise an exception, which we rescue further in this method. # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default. if options[:raise] == false raise_error = false i18n_raise = false else raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations i18n_raise = true end if html_safe_translation_key?(key) html_safe_options = options.dup options.except(*I18n::RESERVED_KEYS).each do |name, value| unless name == :count && value.is_a?(Numeric) html_safe_options[name] = ERB::Util.html_escape(value.to_s) end end translation = I18n.translate(scope_key_by_partial(key), **html_safe_options.merge(raise: i18n_raise)) if translation.respond_to?(:map) translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element } else translation.respond_to?(:html_safe) ? translation.html_safe : translation end else I18n.translate(scope_key_by_partial(key), **options.merge(raise: i18n_raise)) end rescue I18n::MissingTranslationData => e if remaining_defaults.present? translate remaining_defaults.shift, options.merge(default: remaining_defaults) else raise e if raise_error keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) title = +"translation missing: #{keys.join('.')}" interpolations = options.except(:default, :scope) if interpolations.any? title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ") end return title unless ActionView::Base.debug_missing_translation content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title) end end alias :t :translateずいぶん色々やってますね
ざっくりいうとI18n.tに引数を渡す前に該当するロケールが存在しなかった場合の保険とか、
挙動にまつわるオプションの分岐とかががっつり書かれているわけですねというわけで今後I18n.tってviewに書くプルリクエストが来たら修正してもらいましょう