20200206のRailsに関する記事は30件です。

Rails データの取得方法

a.png

ActiveRecord クラス

テーブルから情報を取得するために必要なメソッドを兼ね備えたクラス。all(テーブルの全てのデータを取得)、find(テーブルのレコードの内、ある1つのデータを取得)、new(クラスのインスタンス(レコード)を生成)、save(クラスのインスタンス(レコード)を保存)がある。
a.png
例 変数@ postに1番目のレコードのデータを代入
a.png

しかし、このままだと以下のような表示になる。
a.png
修正するためにビューファイルを編集する。
a.png
これでcontentカラムのデータだけを表示

全てのデータを取得

a.png
a.png
しかしこのままだとエラー。
a.png
その時に解決してくれるのがeachメソッド。
a.png
しかし、このままだと横並びになる。index.html.erbを以下のように書き換える。
a.png
app/assets/stylesheets/posts.scssを以下のように書き換える。そうすると横並び解決。
a.png

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

Rails モデル

モデル

Railsの中でデータベースへのアクセスをはじめとする情報のやりとりに関する処理のこと。
あ.png

モデルの作成

rails g model モデル名でモデルを作成できる。
例 rails g model post # モデルを作成する
作成されたファイルはapp/models/post.rbにある。
また、これでマイグレーションファイルも作成される。

マイグレーションファイル

テーブルの設計図・仕様書のこと。
a.png
a.png
マイグレーションファイルにtext:content。

マイグレーションファイルを実行してテーブルを作成

rails db:migrateで実行。

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

【Rails】Sorceryでfacebook認証 エラーと解決法

はじめに

Sorceryによるfacebookログイン認証をしようとして、盛大なエラー祭りになりました。
誰かのための逆引き辞典になればいいなと思い、記録します。
ざっくりとした流れについては別記事で書いています。

動作環境

ruby 2.6.5
Rails 5.2.3
sorcery 0.14.0
mkcert 1.4.1

エラーとその解決法

ArgumentError in Hogehoge

スクリーンショット 2020-02-06 23.13.20.png

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.

スクリーンショット 2020-02-04 16.11.49.png

Sorry, something went wrong.
we're working on getting this fixed as soon as we can.

解決法

sorcery.rb
  config.facebook.user_info_path = 'me?fields=email'
  config.facebook.access_permissions = %w[email]

ここに不適な値を入れていた場合に起こるので、一旦デフォルトに戻してみる。

URLはブロックされています

スクリーンショット 2020-02-04 20.28.31.png

URLはブロックされています: リダイレクトURIがアプリのクライアントOAuth設定でホワイトリストに追加されていないため、リダイレクトできませんでした。クライアントとウェブOAuthログインをオンにして、すべてのアプリドメインを有効なOAuthリダイレクトURIとして追加してください。

解決法

https://localhost:3000https://localhost:3000/oauth/callback?provider=facebookを有効なOAuthリダイレクトURIに登録する。
リダイレクトURIはSorceryのcallback_urlのことです。

接続はプライベートではありません

スクリーンショット 2020-02-04 20.26.10.png
「OK」をクリックすると
スクリーンショット 2020-02-04 20.19.42.png
スクリーンショット 2020-02-04 20.31.19.png

原因と解決法

リダイレクト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に従って、その後自分で設定を追加していくようにしてください。
くれぐれも、よくわかってもいないのにリファクタリングとか設定の追加をしながら書き進めていかないように。

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

【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.rb
class 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:migrate

Authenticationモデルの設定

$ rails g model Authentication --migration=false

先ほど既にマイグレーションファイルは作成しているので、--migration=falseオプションを付けています。

app/models/authentication.rb
class Authentication < ActiveRecord::Base
  belongs_to :user
end
app/models/user.rb
class 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.rb
Rails.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:edit
credentials.yml
sorcery:
  facebook:
    key: 'アプリIDの値'
    secret: 'app secretの値'

facebook for developersからマイアプリにアクセスし、「ダッシュボード」下の「設定」→「ベーシック」の画面から「アプリID」と「app secret」を確認し、記述してください。
IDは15桁の数字、secretは英数字のハッシュとなっています。
スクリーンショット 2020-02-05 21.49.51.png

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」にも同じものを登録します。
スクリーンショット 2020-02-05 20.19.23.png

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を設定する」で確認できます。
スクリーンショット 2020-02-05 21.59.06.png

Oauth処理を行うコントローラを作成

$ rails g controller Oauths oauth callback
app/controllers/oauths_controller.rb
class 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.rb
post "oauth/callback", to: "oauths#callback"
get "oauth/callback", to: "oauths#callback" # Github, Facebookを使う場合は追加
get "oauth/:provider", to: "oauths#oauth", as: :auth_at_provider
          oauth_callback POST   /oauth/callback(.:format)                                                                oauths#callback
                          GET    /oauth/callback(.:format)                                                                oauths#callback
         auth_at_provider GET    /oauth/:provider(.:format)                                                               oauths#oauth
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails ビュー

ビューファイル

レスポンスとして返す見た目を設定する。アプリケーションの見た目を定義するファイルのこと。
ビューファイルはapp/views/コントローラー名ディレクトリに、アクション名.html.erbというファイル名で作成される。
a.png

○○.html.erb ファイル

HTMLの記述方法に加え、Rubyのコードを埋め込むことができるファイルのこと。

ビューファイル作成

今回はindexアクションに対応するファイルを作成する前提で進めます。なのでindex.html.erbとする。index.html.erbの中身に記述するとその内容が表示される。

コントローラーにインスタンス変数を定義

コントローラーのアクション内にインスタンス変数を定義すると、その内容をビューファイルで表示できる。

a.png

ビューファイルにインスタンス変数を記載

a.png

ブラウザをリロードすると以下のようになる。
a.png
a.png
ビューファイルでインスタンス変数などのRubyの記述を用いるには、ERBタグ(<%= %>)を使う。

フォームを追加する場合

例:新規投稿ページにアクセスするとリクエストがあった場合
app/views/posts/new.html.erbを作成する。

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

【Rails】DBのカラムにインデックスを付与する/しない場合での速度比較をしてみた

はじめに

RailsでDBにインデックスを付与するメリットを体感するために、試験的に1万件のレコードを作成し、インデックスの有無で実行速度の比較をしてみました。

今回のケースではおよそ18%の速度UPという結果が出ています:point_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 sampleapp
db_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.rb
require '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
end

1.インデックスなしの場合

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.rb
class 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カラムの検索条件をsample1sample100000へ変更。(回数は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万件のときとほとんど数字が変わらないこと。
これはすごい:dizzy_face:

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかの参考になれば幸いです:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

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

Rails コントローラー

コントローラー

ルーティングで振り分けられたリクエストを実際に処理する。
a.png
① ルーティングがリクエストを受け取った際、対応するアクションを動かす
② 必要であればデータベースとやり取りをする
③ レスポンスとして返すビューを決める
a.png
アクション名は主に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と表示されたら、ビューファイルが設定されていないということ。

フォームを追加する場合

例:新規投稿ページを表示とリクエストがあった場合
posts_controller.rbにnewアクションを定義する。
a.png

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

Rails ルーティングについて

ルーティング

リクエストの行き先を指定する。
a.png
a.png

ルーティングの記述

configディレクトリの、routes.rbに記述する。
以下のように記述する。
Rails.application.routes.draw do
[HTTPメソッド] '[URIパターン]', to: '[コントローラー名]#[アクション名]'
end

HTTPメソッド

a.png
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

ルーティングが設定されているかどうか確認できる。
a.png
上記のような結果が出たら、ルーティングが設定できてることになる。
もし、ルーティングが設定できてなければ、Routing Errorが発生する。
以下のようにルーティングのみしか設定していない状態。
a.png

フォームを追加する場合

例 新規投稿ページを表示したいというリクエストがあった場合
ルーティングを以下のように設定する。
a.png
rails routesを実行
a.png

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

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という意味。

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

発言(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 %>

メモ書きなので、省略してるところも多いと思いますが、似たような事をする際に
役に立てばと思います。以上

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

中規模以上レガシーRailsシステムのリファクタリング方針

前置き

ここ一年、現職において0→1フェーズからすくすく育ってきたRailsプロジェクトのバックエンド全般を任されており、リファクタリング方針において、ある程度いい感触を掴みつつあるので、その知見を共有してみようってことで書いてみました。

現状

  • 一連のビジネス要求に対する判断や分岐、データアクセスといったドメインロジックが、controllerや、方々のmodelに跨って定義されていることによるドメインロジックの見通しの悪さ
  • データアクセスと判断や分岐といった処理がmodelに一緒くたに実装されているため、テストコードの実装が難解である
  • テストコードの実装が難解であるため、テストの実装を諦め、人力動作確認でリリースまで進めるが、高確率で予期せぬデグレが発生する
  • リリース後にデグレ対応に追われるため、スムーズに次の開発へと移行できない

どうしよう
dousiyou.png

考察

テストがないことに起因した既存実装の考慮漏れによるリリース後のデグレに一番時間が割かれてしまうため、まずはテストコードを書こうと思い辺りました。
ただ、既存の実装のままだと、そのテストコードが書きづらいため、テストを実装していく前に、アプリケーションコードの設計改善が必要だと感じ、今のプロダクトの規模に合わせて、中規模以上でのRailsシステムにおけるテストが書きやすい設計を模索していく必要があります。

設計改善

ここでは具体的にどう改善していったのか(改善していこうとしているのか)を説明していきます。

現状は基本的には説明しやすくするために Skinny(かもしれない?) Controller, Fat Model の基本的なRails wayに則って実装されているとします。

それを以下の図のようなアーキテクチャへと変更していきました(一部まで適用できていない願望も含まれています)。
構成図-アプリケーション設計 (1).png

基本的には上図の通りに特に変更が発生しやすかったり、ビジネス的に重要度の高い部分からこちらの設計に実装を改善している段階です。

各層の役割

ここからは各層の役割について説明していきます。
上の図からなんとなく察している方もいらっしゃると思いますが、クリーンアーキテクチャの思想を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で保守や追加開発をせざるを得ないといった組織は、少なからず存在していると思います。
今回のリファクタリング方針が、そういったエンジニア達の助けに少しでも貢献できれば幸いです。

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

【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.rb
before_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
end

private以下で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.rb
def 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】

https://stackoverflow.com/questions/34729752/sidekiq-error-connecting-to-redis-on-127-0-0-16379-errnoeconnrefused-on-doc】

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数が表示されます。

まとめ

以上今回作ったブログサイトの大雑把なまとめでした。
初心者のメモ程度のものなので間違ってたり足りないところとかたくさんあると思いますが、初心者の方とかの参考になれば嬉しいです。
また、アドバイスなどあればコメントなどしてくれたら嬉しいです。

お付き合い頂きありがとうございました。

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

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をインストールする

Gemfile
gem 'sitemap_generator' #サイトマップ作成用
gem 'aws-sdk' #AWS接続用
$ bundle install

サイトマップ用の設定ファイルを作る

$ rails sitemap:install

以下のように書き換える

config/sitemap.rb
SitemapGenerator::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.rb
get '/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.txt
Sitemap: https://example.com/sitemap

Google Search Consoleにサイトマップの場所を登録する

Search Consoleにログイン後、画面左のサイドバーから「サイトマップ」を選択し、それっぽいところにURLを入力して送信します。

参考

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

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ブラウザにおける表示は以下のようになります。

スクリーンショット 2020-02-06 8.09.39.png

ページ右側にフォローしているユーザー一覧が描画されていません。明らかに意図した表示内容ではないですね。

しかしながら、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つ以上存在すること」に対する正しいテストが書けているようです。


  1. ログインユーザーがAdmin属性である場合、ユーザーを削除するためのリンクも、リンク先のURLは当該ユーザーのプロフィールページへのリンクのURLと同じになります。違うのは、発行されるアクションがGETであるかDELETEであるかです。 

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

[rails,html]文字省略〜続きを読む実装〜

簡単なブログサイトを作成中に、よくサイトで記事が一定の文字数を超えた時に「...続きを見る」を実装したかったので調べて実装してみた。

コード

<%= truncate(45, omission: "") %>

45文字以上は切り捨て。omissionは切り捨てた場合に表示させたい文字を書く。

続きを読む.png

無事45文字で切れた!!!

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

【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を済ませておいて下さい。

  1. Facebook For Developersの設定
  2. ローカル環境の設定

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とは

Image from Gyazo

この図は自分用にまとめたものです。
OAuthに関しては一番分かりやすい OAuth の説明が最強です。

ローカル環境の設定

基本はwikiに沿って進めていきますが、流れを意識して記述するので順番が前後します。

1.externalインストール、DB反映

externalをインストール

ターミナル
$ rails g sorcery:install external --only-submodules

migrationファイルが生成されるので

db/migrate/xxxxxxxx_sorcery_external.rb
class 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:migrate

2.Authenticationモデルの生成

authenticationsテーブルにはFacebook認証ログインしたユーザーデータが入ります。

ターミナル
$ rails g model Authentication --migration=false

アソシエーション

UserモデルとAuthenticationモデルの関連付けを行います。

user.rb
class User < ActiveRecord::Base
  has_many :authentications, dependent: :destroy
  accepts_nested_attributes_for :authentications
end
authentication.rb
class Authentication < ActiveRecord::Base
  belongs_to :user
end

3.oauthsコントローラー作成

ターミナル
$ rails g controller Oauths oauth callback --skip-template-engine

--skip-template-engine → viewファイルスキップ

oauths_controller.rb
class 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.rb
post 'oauth/callback', to: 'oauths#callback'
get 'oauth/callback', to: 'oauths#callback'
get 'oauth/:provider', to: 'oauths#oauth', as: :auth_at_provider

5.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.rb
if Rails.env.development?
  ssl_bind "0.0.0.0", "3000", {
    cert: "config/certs/localhost.pem",
    key:  "config/certs/localhost-key.pem"
  }
end

credentialsを使用してkeyとsecretを暗号化

ターミナル
$ EDITOR=vim bin/rails credentials:edit

credentials.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.yml
facebook:
  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.rb
Rails.application.config.sorcery.submodules = %i[external]

設定の追加

config/initialzers/sorcery.rb
Rails.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 が良いかも

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

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配下から画像を探してくれる。
との事で試した結果。すんなりと直りました。

という事で、今回のメモでした。

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

【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_withlocalオプションを付けないでください。
参考:【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のこの機能は、ほかにも色々な用途で使えると思います。

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

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] Y

Gemfileを上書きしていいか聞かれたら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.rb
module 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

以上です。何かアドバイス等ありましたらコメントいただけると嬉しいです。

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

【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をインストール

Gemfile
gem '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.rake
Dir.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

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

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
end

add

rails generate migration AddFieldNameToTableName field_name:datatype

class AddFieldNameToTableName < ActiveRecord::Migration[6.0]
  def change
    add_column :table_names, :field_name, :datatype
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

中間テーブルとは??

 プログラミング初学者が学習する中間テーブルについて

  • 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グループ
  1. このように中間テーブルを追加することで空のカラムもなくなり、その都度追加する必要があったカラムも追加せずに済むことができます。さらに誰がどのグループに所属しているかが見やすくなりました。

  2. データベースの設計をしていく際には空のカラムが発生しないか、チームとして開発を進めていく中で見やすいDBができているかを考える必要があると思うので覚えておきましょう!!!

初投稿となりましたが、何かわかりにくい点や誤って理解してしまっている点などあればご教授願います。では!

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

RailsにてPostgreSQL使用の共同アプリ開発の導入手順

共同開発用アプリをgit clonebundle installbundle 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

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

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.rb
class User < ApplicationRecord
  has_many :posts
  has_many :likes
  has_many :like_posts, through: :likes, source: 'post'
end
app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :likes
  has_many :like_users, through: :likes, source: 'user'
end
app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post
end

ControllerはPostsControllerに2つのページのアクションを用意します

app/controllers/posts_controllers.rb
class 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.rb
config.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.rb
class PostsController < ApplicationController

  def index
    @cache_key = 'index'

    @posts = Post.all
  end

  def like_index
    @cache_key = 'like_index'

    @posts = current_user.like_posts
  end
app/views/posts/_posts_index.html.erb
<% cache @cache_key do %>  変更
<% posts.each do |post| %>
  <%= post.title %>
  <%= post.user.name %>

これで、ページごとにキャッシュを分けることができます。

続いてユーザーごとにキャッシュを分けられるようにしましょう。
今のままだと全ユーザーに同一のページが見えてしまいます。
お気に入りに登録した記事を見ようとしたら他人がお気に入りにした記事一覧が表示された、なんてことになります。
もしくは自分のお気に入り記事一覧ページがキャッシュされた場合、他の全ユーザーにそれが行き渡ります。

ユーザーごとにキャッシュを分ける

app/controllers/posts_controllers.rb
class 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.rb
  scope :latest, -> { order(updated_at: :desc).first }
app/controllers/posts_controllers.rb
class 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:datetime
class AddDeleteAtToPosts < ActiveRecord::Migration[5.2]
  def change
    add_column :posts, :deleted_at, :datetime
  end
end
class AddDeleteAtToLikes < ActiveRecord::Migration[5.2]
  def change
    add_column :likes, :deleted_at, :datetime
  end
end
$ rails db:migrate

モデルファイルにacts_as_paranoidを記述します。

app/models/post.rb
class Post < ApplicationRecord
  acts_as_paranoid

  belongs_to :user
  has_many :likes
  has_many :like_users, through: :likes, source: 'user'
end
app/models/like.rb
class Like < ApplicationRecord
  acts_as_paranoid

  belongs_to :user
  belongs_to :post
end

これだけで論理削除が適用されるようになりました。
これで記事やお気に入りが削除された際、レコードは実際には消えず
削除時間が追加されるだけになりました。

データ検索の際に従来の、

Post.all

だと、削除されたデータは除外されますが

Post.with_deleted.all

とすると、削除されたデータも含めて検索できます。

改めて、

app/controllers/posts_controllers.rb
class 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.rb
class 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.rb
class 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]
  end
app/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つをキャッシュするか。
どちらの方が良いかは時と場合によるでしょうか?

最後に

今回、お気に入りボタンなどもキャッシュしてみましたが、
そもそもユーザーによって表示が変わる、いわゆる動的な部分はキャッシュしないのが普通なのかもしれません。
論理削除も積極的に使うべきではないと思うので、全体としてあまり有意義なことはできなかったかもです。
それでも今回得られたキャッシュについての知見は、どこかで必ず役に立つとは思います。

参考資料

Railsのフラグメントキャッシュについて調べてみた

Railsのキャッシュ機構

Railsでparanoiaを使って論理削除を実装する

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

Rails コーティング規約について 1

はじめに

Ruby、Railsの基礎を学習中の方に向けて記載致します。
Rubyのコーティング規約はコチラをクリック願います。
私自身これからチーム開発を行う上で大事にしたい。知っておきたいことをOutputします。

Routing

ActiveRecordのモデル間の関連を表現するには、入れ子型でルートを定義する。

qiita.rb
class Post < ActiveRecord::Base
  has_many :comments
end

class Comments < ActiveRecord::Base
  belongs_to :post
end

# routes.rb
resources :posts do
  resources :comments
end

ActiveRecord

なるべく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

さいごに

コーティング規約については毎日更新します。
皆様の復習等にご活用頂けますと幸いです。

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

【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 :thinking:

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を解決できました。

参考

Rails 6.0でDeprecatedになるActive Recordの振る舞い3つ - かみぽわーる

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

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #18.5 環境変数, Gmail送信設定編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#18 EC2環境構築, Nginx+Puma+Capistrano編
次回:準備中

今回の流れ

  1. Railsの環境変数を設定する
  2. 本番環境でのGmailの送信を設定する

Railsの環境変数を設定する

Rails5.2以降の環境変数の設定には、credentials.yml.encを使います。
デフォルトで.gitignoreになり暗号化されているのでおすすめです。
以下のように設定します。

shell
EDITOR=vim rails credentials:edit
credentials.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アカウントのアプリパスワードを有効にします。
有効にするまでに、いくつかの手順を踏みます。

  1. googleアカウント → 画面左ダッシュボード『セキュリティ』 → Googleへのログイン『2段階認証プロセス』 → 有効にする
  2. googleアカウント → 画面左ダッシュボード『セキュリティ』 → Googleへのログイン『アプリパスワード』 → 16桁のパスワードをコピーする

これで次の手順に使うパスワードを手に入れました。

production.rbの設定を変更する

Gmail用に設定を変更します。

config/environments/production.rb
  config.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.enc
host_server:
  ip: XX.XX.XX.XX # EC2のIPアドレス

gmail:
  user_name: hogehoge@gmail.com # Gmailアドレス
  password: hogehoge # コピーしたアプリパスワード

これで送信の設定が完了しました。

参考になりました↓
【Rails】メール送信設定 〜gmail利用〜


前回:#18 EC2環境構築, Nginx+Puma+Capistrano編
次回:準備中

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

リファクタリングでモブプロみたいなことをしたお話

弊社内でエンジニア3人が集まって楽しくリファクタリングをした話です。

背景

社内のプロダクトで「手が空いてるならリファクタリングやってくれると嬉しい。手が足らない」というようなSOSを受け取り、
そのプロダクトをある程度知っている人 1人 + 知らない人 2人 の3人でリファクタリング部隊を作ってリファクタリングしていこうという話になりました。

今回は、モブプロ「みたいなこと」です。厳密なモブプロではないです。
なので、

  • ドライバーも考える (みんなで考える)
  • ドライバーは変わらない

という感じでゆるく進行しました。

プロダクトについて

Railsで動いています。Rails3から始まり、今Rails5です。

  • 7年ぐらい前から動いているプロダクト
  • 新しく書いたところは新しく、古いものは古いという歴史が積み重なっている
  • Rails3からのコードが残っている
  • Rails何それ?Ruby何それ?から始まったコードが存在している
  • ただファイルが分割されただけのコードがConcernsとして存在している
  • でもテストはちゃんと書かれている

という感じで、とても歴史のあるプロダクトです。

今回やった人について

A: このプロダクトわかる。設計強い。DDD好き。Ruby, Railsともに得意。
B: このプロダクトわからない。インフラ強い。Rubyそんなに得意じゃない。 (本人談)
C: このプロダクトわからない。設計好き。DDD好き。Ruby得意。型を欲しがる。

という感じで思想やスキルの差が若干ありつつ、

  • 仲が良いので思想のぶつけ合いになっても険悪にはなりづらい
  • 誰か一人が発言しづらいということはない
  • お互いの技術に信頼/リスペクトしている

というメンバーです。今回はBさんがドライバーをしました。

今回の流れ

実際にどんな感じでやったのか?ですが、基本は

  1. つっつき会
  2. メシ
  3. 修正会

という3つのフェーズで行いました。
会議室で大画面を使用してみんなで唸ります。

つっつき会

今回のメインはモブプロをやることではないので、まずはリファクタリング対象(ディレクトリ/ファイル単位)を決め、
コードを読んでみてよくなさそうなところをつっついて、方針決めをします。大体4時間ぐらい。

このつっつきはTODO: つっつき serviceとして切り出す というようなTODOコメントとして残し、コミットします。

今回は、以下のような観点でつっついていきました。

  • そのファイルの構造、ネームスペースが正しいか(分割されただけのファイルがconcernsとして存在しているため)
  • そのクラスの責務を逸脱したメソッドがあるか(↑と大体一緒)
  • 冗長(わかりづらい)な書き方をしていないか
  • これらを大幅な変更せず綺麗にできるか

この時点でも、3人分の目と知識があるので、誰かが「この書き方はなんだ?」「これどうなってるんだ」となっても、知ってる/わかってる人が説明するということもあり、一人でやるときよりもスムーズです。

また、

これらを大幅な変更せず綺麗にできるか

という判断も1人だと消極的な理由でやらないことを選択してしまいそうな部分も、誰かの「実は簡単に修正できるのでは」という知識の後押しにより、積極的な判断をすることもできます。もちろん、やらないという判断もあります。

特に、やるか/やらないか という部分以外にも判断材料を出せる人が増えるというのは多人数の有利な部分です。

メシ

ramen.jpg

修正会

実際につっつき会でつっついた部分をリファクタリングしていきます。大体2時間ぐらい
基本的につっつき会で方針が出ているのでその方針に沿ってリファクタリングするだけです。
とはいえ、この段階でも「あ、もっといい方法あるじゃん」というのはあるので、随時取り入れていきます。

KPT

Keep

  • 誰かの「これどうなるんだろう?」に対して「じゃあ、試してみるか」というラフな形でやってみることができる
  • コードではわからないお互いの思考フローが知れる
  • ドメインエキスパートが居ないことによる「名前から直感的に判断できるか」という部分に焦点をあてれる

Problem

  • 仲が良いからこそ、脱線すると脱線し続けそうになる
  • ドメインエキスパートが居ないので仕様がわからないのでパスというのがあった
  • 複数人いるのでガッツリ集中して書くというのは難しい

Try

  • ドメインエキスパートも呼んでみる
    • 仕様についてをサクッと聞けるので時間短縮になりそう
    • keepの部分とコンフリクトしそう
  • 人数を増やしてみる
    • 人が増えれば集合知というのは単純な加算になるのか

終わりに

ペアプロ、モブプロは人と人の知識をリアルタイムでつなぎ合わせる というのがコアだと思うので、
「ドライバーは10分」「ナビゲーターの指示に従う」といった感じで厳密にやる必要は別になく、人が集まってみんなでコードに向かうというのは大事だと感じました。
特にkeepでも書いた、

コードではわからないお互いの思考フローが知れる

というのはお互いに勉強になります。
また、エンジニア間のコミュニケーションも取れるのでお互いに刺激になり、メインの業務にもハリが出ると思います。

更に、人間誰しも見落としや勘違いもあるので、単純に人数が増えることにより、見落としや勘違いなどをし続けることがなくなりやすいです。 (ダブルチェックと一緒で単純に増えればいいわけではないですが)

と、いう感じで、定期/不定期に限らずこういうことをやるのはいい感じだと思います。

おわり。

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

初心者!current_userの使い方 〜ログイン中のユーザ名を表示〜

current_userでログイン中のユーザ名とメールアドレスの表示の仕方を紹介します。

書き始めるときはcurrent_userの前に=を忘れないでくださいね!

ユーザー名を表示 〜current_user〜

.header__left-box__list
  = current_user.user

スクリーンショット 2020-02-06 0.57.26.png

メールアドレスを表示させたい↓

.header__left-box__list
  = current_user.email   #nameからemailに変更

スクリーンショット 2020-02-06 0.58.08.png

なぜ、このように
= current_user.nameや= current_user.email
のようになるかというと、、、テーブルに注目。

   ↓

○usersテーブル

id name email
1 mirai mirai@mirai.com
2 : :
3 : :

nameカラムやemailカラムから引っ張ってきているのです!
=current_userの後ろには.を忘れず書く事!!

また、each doも同じです。

ではまた(^_^ )


おまけ

ユーザー名を表示 〜each do〜

.header__left-box__list
  - @group.users.each do |hhgg|
    = hhgg.name

スクリーンショット 2020-02-06 0.57.26.png

メールアドレスを表示させたい↓

.header__left-box__list
  - @group.users.each do |hhgg|
    = hhgg.email   #nameからemailに変更

スクリーンショット 2020-02-06 0.58.08.png

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

【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.rb
def 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に書くプルリクエストが来たら修正してもらいましょう

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