マジックナンバーをSettingsLogicを使ってまとめて定数化する
技術系YouTuberの勝又さんのサロンに私も入っているのですが、 そこで初学者向けに「マジックナンバーは定数化した方がいいよ!」という話が出ていたので、 調べてみて、実装してみました。
マジックナンバーとは
「マジックナンバー」とはまずなんなのかと思い、ググったところ、 以下の記事が勉強になりました。
まとめると
「実装した本人以外がソースをみたときに、 その数値がなんの為に使われているのかが分からない数字」
がマジックナンバーとなるようですね。
enumとかでやっていることが、マジックナンバーの定数化というところでしょうか。
SettingLogic
configというgemもあるようですが、環境ごとにファイルを作成するみたいで、 今回の実装では、マジックナンバーをただ定数化したいだけで (なら直書きで定数を定義した方がいい気もしますが、) そんなに多くもないので、 SettingsLoigicを使って管理したいと思います。
# Gemfile gem 'settingslogic'
bundle install
公式に設定ファイルはapp/models/settings.rb
に書くべしとあったので、
# app/models/settings.rb class Settings < Settingslogic config_source = Rails.root / 'config' / 'application.yml' source config_source.to_path namespace Rails.env end
あとはconfig/application.yml
に定数を定義していきます。
# config/application.yml defaults: &defaults page: 10 post: long_name: 8 long_description: 12 ogp: base_image_path: './app/assets/images/base.png' gravity: 'center' text_position: '0,0' font: './app/assets/fonts/ipag.ttf' font_size: 30 idention_count: 8 row_limit: 8 development: <<: *defaults test: <<: *defaults production: <<: *defaults
マジックナンバー以外にも定数として使用していたものをまとめて定義しておきました。
以上でマジックナンバー等をまとめて定数化することができました。
Active StorageのN+1問題に対処する
まず最初に最近データベースとかのあたりを勉強していると見かけた、 N+1問題というのを知って調べてみた。
N+1問題
繰り返し処理の中で関連があるときに普通にeachなどを使うと、 その関連を探しにデータベースにその関連がある回数だけアクセスしてしまう。 ⇨(SQLが実行されまくる) というような理解でいいのかな?
対策
関連のデータベースをいちいち参照しに行くのではなく、 includesを使ってデータベースをくっつけてしまうことで、 最小の回数データベースをみに行くようにする ってな感じでしょうか。
ここまではこの記事をみて理解しました。
Active Storageの対策
というわけでここまでで自分のプロジェクトを確認すると、 私の場合では、プロフィール画像のところでN+1問題がおきていました。
失敗
# app/models/user.rb has_one_attached :avatar # app/views/users/index.html.erb <% @users.each do |user| %> --------------------- <%= image_tag user.user_icon, class: "icon_users" %> # user_iconはuser.avatarを使ってユーザーのプロフィール画像を返す自作メソッド ---------------------- # app/controllers/users_controller.rb def index @users = User.all end
というようなことをしていたため、
def index @users = User.all.includes(:avatar) end
としたんですが、user.avatar
はカラムを参照しているわけではなく、
単なる便利メソッドだということをにエラーが出て気づきました。
成功パターン
というわけで、ActiveStorageを使用する時にどうやってincludesしたらいいか調べてみると
with_attached_attachment_name
を使用するといいということが下の記事からわかりました。
よってこれを使って
def index @users = User.with_attached_avatar end
として解決しました。
関連の関連がActiveStorageを使用していた時
問題発生
ここまではよかったんですが、 他のところでもN+1問題おきてないかなと探していると、 もう一つ見つけました。
# app/models/post.rb belongs_to :user
postがuserを繰り返しの中で使っていて、 そのuserがまたしても プロフィール画像を使用していたため、 そこでもN+1問題がおきていました。
「あ、また、with_attached_avatar
使えばいいのか」
と思っていましたが、
コントローラーをみてみると
# app/controllers/posts_controller.rb def index @posts = Post.page(params[:page]).per(9) # kaminariのページネーションを使用 end
となっていて、どう書けばいいかわからなくなってしまいました。
問題解決
with_attached_avatar
をなんとか書ければできるなと考えて、
何をしているかソースを調べてみると
# activestorage/lib/active_storage/attached/macro.rb scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
というのがありました…!!!
あとはこれにしたがってwith_attached_avatar
を includes({avatar_attachment: :blob)
に書き換えて
下の記事にあるように実装してしまうだけです。
# app/controllers/posts_controller.rb def index @posts = Post.page(params[:page]).includes(user: { avatar_attachment: :blob }).per(9) end
これでActiveStorageのN+1問題を解決することができました。
CircleCIのビルドにrubocopを導入する
静的コード解析ツールであるrubocopを 導入するだけなら簡単にできたので記録に残しておきたいと思います。
.rubocop.ymlの準備
# Gemfile gem 'rubocop'
でbundle install
プロジェクトのルートに.rubocop.ymlを配置
# .rubocop.yml AllCops: Exclude: - "tmp/**/*" - "config/initializers/*" - "vendor/**/*" - "db/schema.rb" - "node_modules/**/*" - "db/migrate/*.rb" - "bin/*" DisplayCopNames: true TargetRubyVersion: 2.6.3 Rails: Enabled: true Style/AndOr: EnforcedStyle: conditionals Style/AsciiComments: Enabled: false Style/Documentation: Enabled: false Style/NumericLiterals: Enabled: false Style/ClassAndModuleChildren: Enabled: false Bundler/OrderedGems: Enabled: false Lint/ShadowedException: Enabled: false LineLength: Enabled: false Metrics/BlockLength: Exclude: - 'spec/**/*' Max: 40 Metrics/AbcSize: Enabled: true Max: 20 Metrics/MethodLength: Enabled: true Max: 15
ほぼほぼ、以下のサイトのかたの設定で作ってしまいました。(ありがとうございます。)
【Rails】GithubとCircleCIを連携してcommit時にrspecとrubocopを動かす
specファイルはブロックが長くなりがちなので、 Metrics/BlockLengthでひっかりまくってしまいます。 そのため、
Exclude: - 'spec/**/*'
でspecファイル諸々を対象から除外してしまっています。
CircleCI
あとは普通に実行するには
bundle exec rubocop
とすれば、
先ほど作成した.rubocop.ymlの設定に基づいて、
ファイルを自動でチェックしてくれます。
(bundle exec rubocop -a
でできる範囲で自動整形してくれるのが嬉しい…)
CircleCIも普通に実行するだけです。 私は、テストとrubocopのチェックを同じタイミングでするようにしてしまいました。
version: 2 jobs: build: machine: image: circleci/classic:edge steps: - checkout - run: name: Decode dotenv file command: echo ${ENV} | base64 --decode > /home/circleci/project/.env - restore_cache: keys: - gem-cache-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }} - gem-cache-{{ arch }}-{{ .Branch }} - gem-cache - run: name: docker-compose build command: docker-compose build - run: name: bundle install command: docker-compose run web bundle install --jobs=4 --retry=3 - save_cache: key: gem-cache-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }} paths: - ~/.bundle - run: name: docker-compose up command: docker-compose up -d - run: name: install dockerize command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz environment: DOCKERIZE_VERSION: v0.3.0 - run: name: Wait for DB command: dockerize -wait tcp://localhost:5432 -timeout 1m - run: name: before_test: setup db command: docker-compose run web bin/rails db:create db:migrate - run: name: rubocop command: docker-compose run web bundle exec rubocop - run: name: rspec command: docker-compose run web bin/rspec - run: name: docker-compose down command: docker-compose down
今日は以上です。 (にしてもいつもこれ、キャッシュ効いてる気がしない)
OmniAuthでTwitterのプロフ画像を取得する
OmniAuthでTwitterのログインを実装する記事なんかはたくさんあるのですが、 Twitterのアバター画像をOmniAuth経由で取得する記事はあまりなかったので書いていきたいと思います。
前提
前提として、OmniAuthを通じてログインができる状態を作っておきます。 私は以下の記事を参考に実装しました。
- 【Rails】omniauthでGoogle/Twitterログインを実装する手順【キャプチャ付き解説】
- 【Rails4.2.x】omniauth(twitter/facebook/github)実装まとめ
画像アップロードにはActive Storageを使用しています
実装
ログイン機能ができたら、早速画像の取得も実装していきます。
# app/controllers/users/omniauth_callbacks_controller.rb def twitter callback_from :twitter end def callback_from(provider) provider = provider.to_s @user = User.find_for_oauth(request.env['omniauth.auth']) if @user.persisted? flash[:notice] = I18n.t 'devise.omniauth_callbacks.success', kind: provider.capitalize sign_in_and_redirect @user, event: :authentication session[:user_id] = @user.id else session["devise.#{provider}_data"] = request.env['omniauth.auth'] redirect_to new_user_registration_url end end # app/models/user.rb class << self def find_for_oauth(auth) provider = auth[:provider] uid = auth[:uid] user_name = auth[:info][:name] image_url = auth[:info][:image] uri = URI.parse(image_url) # パースする必要がある image = uri.open email = User.dummy_email(auth) password = Devise.friendly_token[0, 20] find_or_create_by(provider: provider, uid: uid) do |user| user.name = user_name user.email = email user.password = password user.avatar.attach(io: image, filename: "#{user.name}_profile.png") end end end # app/config/initializers/devise.rb Devise.setup do |config| config.omniauth :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET'], :image_size => 'original'
- TwitterのAPIからの情報を取得(ここで画像のURLも取得)
- URLをパースしてimageに挿入
- 一意の値になるproviderとuidの組み合わせでユーザーがいるか確認し、いなければユーザーを作る(この中で画像をアバターとしてアタッチする)
というような流れになっています。
ポイント
ポイントは
- 画像URL等を直で叩く場合は、parseしなければならない
- Twitterのデフォルトの画像サイズではかなり小さいため、画像サイズを設定しなければならない
の2点です。
画像のパース
image_url = auth[:info][:image] uri = URI.parse(image_url) # パースする必要がある image = uri.open
の部分を
image_url = auth[:info][:image] image = open(image_url)
としてしまうと
下記のように、コマンドラインで直接コマンドを叩かれてしまう可能性があります。
open('|/bin/sleep 20;').read #20秒停止するコマンド
Twitterの画像のサイズの設定
image_urlで取得できるurlにある画像は_normalというサフィックス(?)がついていてかなり小さいです。 (000000000000000000_normal.jpgのような感じ)
そのため、オリジナル(設定された時の大きさ)で取得するには app/config/initializers/devise.rbのなかで、
Devise.setup do |config| config.omniauth :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET'], :image_size => 'original'
とする必要があります。
これで、Twitterでログイン時にアイコンが取得できるようになりました。
参考
OmniAuthでのログインをテストする
OmniAuthとdeviseを使ってTwitter、Googleのログイン機能を実装しましたが、 ログインのテストができずにいたので、今回system specの実装をしてみました。
準備
テストモードを有効にする
まずOmniAuthのテストモードを有効にする為に、
OmniAuth.config.test_mode = true
rails_helper.rbに記載しました。
こうすることで、テスト時にOmniAuthと実際に通信することが無くなります。
モックを作成する
OmniAuth.config.mock_authを使うことで、テスト時にOmniAuthでログインしようとすると ここで作られたモックを返すことができます。
# spec/support/omniauth_helpers.rb module OmniAuthHelpers def set_omniauth(service = :twitter) OmniAuth.config.test_mode = true OmniAuth.config.mock_auth[service] = OmniAuth::AuthHash.new({ provider: service.to_s, uid: '1234', info: { name: 'mockuser', image: "https://test.com/test.png" } }) end end
モックを実装したヘルパーを呼び出す
このモジュールをrails_helper
でinclude
することで、
先ほどのモックがスペックの中で実際に使えるようになります。
# spec/rails_helper.rb OmniAuth.config.test_mode = true config.include OmniAuthHelpers
テストの実装
あとは実際のテストの実装になります。
# spec/system/omniauth_users_spec.rb require 'rails_helper' RSpec.describe "Users through OmniAuth", type: :system do describe "OmniAuthのログイン" do context "twitterでのログイン" do before do OmniAuth.config.mock_auth[:twitter] = nil Rails.application.env_config['omniauth.auth'] = set_omniauth visit root_path end it "ログインをするとユーザー数が増える", js: true do expect { click_link 'Twitterでログイン' }.to change(User, :count).by(1) expect(page).to have_content 'プロフィール設定' expect(page).to have_content 'Twitter アカウントによる認証に成功しました' end end context "googleでのログイン" do before do OmniAuth.config.mock_auth[:google] = nil Rails.application.env_config['omniauth.auth'] = set_omniauth :google visit root_path end it "ログインをするとユーザー数が増える", js: true do expect { click_link 'Googleでログイン' }.to change(User, :count).by(1) expect(page).to have_content 'プロフィール設定' expect(page).to have_content 'Google アカウントによる認証に成功しました' end end end end
今回は以上です。
参考
Active Storageの簡単なバリデーションの実装とテスト
今日はActive Storageにバリデーションを設定したので、 その内容をまとめておきます。
背景
Active Storageにはデフォルトのバリデーションというのがないようです。 (そのせいで、みんなCarrierWaveで実装しているのかな?)
Rails5.2から入る新機能ActiveStorageを使うべきか?
うーん、なんか移行した方が良さそうなのかな。。。
とはいえ、せっかくS3、clodfrontの設定までしたばかりなので、 ひとまずは簡単なバリデーションを実装しておこうと思います。
実装
今回はこちらの記事を参考に(ほぼまるパクリですみません)で実装しました。
validate :validate_avatar def validate_avatar return unless avatar.attached? # ファイルがアタッチされていない場合は何もしない if avatar.blob.byte_size > 10.megabytes avatar.purge # アタッチされたファイルの削除 errors.add(:avatar, 'ファイルのサイズが大きすぎます') elsif !image? avatar.purge # アタッチされたファイルの削除 errors.add(:avatar, 'ファイルが対応している画像データではありません') end end private def image? %w[image/jpg image/jpeg image/gif image/png].include?(avatar.blob.content_type) end
purgeというのがActiveStorageのAPIでアタッチされたファイルの削除を行います。
これとは別にdetachという実体ファイルは残したまま、関連を削除するといったものもあるようです。 (attachmentで関連を、blobで実体を管理しているというようなイメージかな?)
これで無事実装できました。 ただこれだけだとただの丸パクリなので、テストも実装しておきます。
テスト
単体テストを実装していきます。
始める前にファクトリで添付ファイルを扱えるように設定します。
# spec/rails_helper.rb FactoryBot::SyntaxRunner.class_eval do include ActionDispatch::TestProcess end # spec/factories/users.rb FactoryBot.define do ... trait :with_avatar do avatar { fixture_file_upload(Rails.root.join('spec', 'support', 'assets', 'test.png')) } end ... end
これでファクトリでActiveStorageの画像をテストすることができるようになりました。
この他にも
を用意しました。
あとはテストを書いていきます。
# spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do let(:user) { FactoryBot.create(:user) } let(:user_attached_image) { FactoryBot.create(:user, :with_avatar) } let(:user_too_large_avatar) { FactoryBot.build(:user, :with_too_large_avatar) } let(:user_attached_not_image) { FactoryBot.build(:user, :with_not_image) } describe "avatarのバリデーション", :focus do context "ファイルサイズが10MBを越える時" do it "ファイルサイズが大きすぎますというエラーメッセージが返る" do user_too_large_avatar.valid? expect(user_too_large_avatar.errors[:avatar]).to include 'ファイルのサイズが大きすぎます' end end context "ファイルの種類がjpeg, jpg, png, gif以外の時" do it "ファイルが対応している画像データではありませんというエラーメッセージが返る" do user_attached_not_image.valid? expect(user_attached_not_image.errors[:avatar]).to include 'ファイルが対応している画像データではありません' end end end end
これで(否定にする検証も込みで)テストがグリーンになったので、実装完了とします。
参考
CircleCIで.envファイルを使用する
CircleCIを始める時に
「環境変数全部一つずつ登録するのめんどくさ…」 ってなったので、調べてみた。
手順
コマンドラインから自分の.envファイルと同じディレクトリで
base64 .env
とすれば、.envファイルをbase64でエンコードしたものがechoされます。
「base64って?」となり、調べてみると、
Base64は、データを64種類の印字可能な英数字のみを用いて、 それ以外の文字を扱うことの出来ない通信環境にてマルチバイト文字やバイナリデータを扱うためのエンコード方式である。 (Wikipedia参照)
なるほど。これで改行なんかが入った.envファイルを一行にまとめて、CircleCIの環境変数に入れればいいわけですね。
- CircleCIの画面から環境変数を登録する
これは公式サイトを見ればできます。
config.yml上で環境変数を直接書き込むような方法もあるようですが、私の場合APIのKEY等が入っていたため、素直にGUIから登録しました。
- config.yml内でbase64で書かれた元の.envをデコードして、CircleCIのコンテナ内の.envに書き込む
これはconfig.ymlで
run: name: Decode dotenv file command: echo ${ENV} | base64 --decode > /your/root/directory/.env
といった感じにします。
(/your/root/directory/
のところは自分の環境に合わせてください。)
これで、docker-composeやアプリケーションから.envを参照できるようになりました。