Madogiwa Blog

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

Ruby: FactoryBot.lintでFactoryの壊れないようにチェックする

FactoryBotを利用してテストデータを管理していて、開発が進んでいくとデフォルトで作成したときにエラーになるようなFactoryが作られてしまったりします。

github.com

そんなときにFactoryBot.lintを使うとFactoryが壊れたときに気づけて便利だったのでメモしておきます📝

やり方は簡単で以下のようなテストを追加するだけです。

require 'rails_helper'

RSpec.describe FactoryBot, type: :model do
  describe '.lint' do
    it 'すべてのFactoryが正常に機能すること' do
      FactoryBot.lint traits: true
    end
  end
end

上記設定だとdefault及びtraitを付与した状態でFactoryをcreateしたときにエラーが発生しないかチェックしてくれます。 ※実態はpublic_sendでstrategy(create or build)を実行してエラー発生有無を監視しているようですね👀

https://github.com/thoughtbot/factory_bot/blob/92114db048b70096183af622173506535b150e65/lib/factory_bot/linter.rb#L73

エラーが発生した場合には以下のような形でエラーになります。

  1) FactoryBot.lint すべてのFactoryが正常に機能すること
     Failure/Error: FactoryBot.lint traits: true
     
     FactoryBot::InvalidFactoryError:
       The following factories are invalid:

以下のFacotyBotのREADMEに詳しい使い方が書いてあるので気になる人は見てみてください📕

github.com

Ruby on Rails: Rails 6で追加された`insert_all`の実行時にデフォルトでTimestampを付与するMEMO

Rails 6でbulk insertの機能が実現されました🎉
※概要は以前紹介してるので興味のある方は参照してください。

madogiwa0124.hatenablog.com

非常に便利なのですが、記事でも記載した以下の通り、timestampが補完されずにcreate_at、updated_atにNOT NULL制約を付与していると、エラーになってしまうのが、activerecord-importからの乗り換え目的等で検討すると、やはりtimestampをデフォルトで補完したいケースもあるのかなと、、、

created_atとupdated_atを補完してくれないので自分で設定しないといけない点に注意です。実装をシンプルにしてパフォーマンスを担保するのとupdate_allの仕様に合わせているのが理由のようです🤔 https://github.com/rails/rails/issues/35493#issuecomment-470100313

github.com

なので、ちょっと対応方法を考えたのでMEMOしておきます📝

DBのDEFAULT制約を利用する

tableを作るときに以下のような形でDAFAULT制約を用いてCURRENT_TIMESTAMPを実行するようにすることでtimestampのカラムに現在時刻を自動的に設定します。

class CreateFoo < ActiveRecord::Migration[6.0]
  def change
    create_table :foo do |t|
      t.timestamps default: -> { 'CURRENT_TIMESTAMP' }
    end
  end
end

これは下記のissueでコメントされている方法です。

github.com

SQLを直接実行するのでDBMSによって多少方言が違う可能性があるのがネックですが、CURRENT_TIMESTAMPMySQLにもPostgreSQLにもSQLite実装されている関数のようなので基本的には大丈夫そうです※挙動は微妙に違うかもですが。。。

ActiveRecordにパッチを当てる

あとはActiveRecordにパッチを当ててTime.currentで取得した値を差し込むのもありかなとも思いました。

class ApplicationRecord < ActiveRecord::Base
  # NOTE: insert_all実行時にデフォルトでtimestampを付与するパッチ
  def insert_all(attributes, returning: nil, unique_by: nil, now: Time.current)
    attributes.map! { |attr| attr.merge(created_at: now, updated_at: now) }
    super
  end

  def self.insert_all!(attributes, returning: nil, now: Time.current))
    attributes.map! { |attr| attr.merge(created_at: now, updated_at: now) }
    super
  end
end

上記のコードではinsert_allでは、引数で渡されたattributesをもとにbulk insertを実行するので、created_atupdated_atTime.currentで取得した現在時刻を設定したHashをattributesにmergeしています。

https://github.com/rails/rails/blob/f855139f3d2bb9b032613279d0adfbd6a77a2d07/activerecord/lib/active_record/persistence.rb#L131-L133

以下のような形でtimestampeを指定しなくても実行出来るようになります。

Foo.insert_all([{ title: 'foo1', body: 'bar1' }, { title: 'foo2', body: 'bar3' }])

具体的には、以下のようなコードを実行しているのとほぼ同じ挙動になります。

now = Time.current
Foo.insert_all([
  { title: 'foo1', body: 'bar1', created_at: now, updated_at: now },
  { title: 'foo2', body: 'bar3', created_at: now, updated_at: now },
])

おわりに

やりすぎは注意ですが、Rubyはライブラリのコードでも柔軟に拡張出来るのが良いですね✨

Heroku:Free DynoでRailsアプリケーションとSidekiqを動かすMEMO📝

HerokuでRailsアプリケーションを運用しているのですが、Sidekiqを導入するときにredisを構築したり、sidekiqのプロセスを立ち上げたり、色々と調べることがあったので、そのあたりの内容をメモしておきます📝

pumaからhookしてsidekiqのプロセスをwebのdynoのインスタンス上で立ち上げる方法もあるようですが、今回はsidekiq用のプロセスタイプを別に定義して構築する方法を記述しています。

bilalbudhani.com

freeプランでもプロセスタイプ数は2つまで無料のようなので・・・!

jp.heroku.com

Heroku Redisの導入

Sidekiqを利用するためにHeroku Redisを使ってRedisを構築します。

devcenter.heroku.com

Heroku RedisはHobby Devであれば無料で構築できます。

導入は簡単で以下のコマンドを実行するかHerokuのダッシュボードから手動で構築することができます。

$ heroku addons:create heroku-redis:hobby-dev -a your-app-name --version your-redis-version

※デフォルトだと6.0系がインストールされるようです👀(2021/03/28時点)

構築は数分で終わって完了すると環境変数REDIS_URLに構築したHeroku Redisへの接続情報が自動的に設定されます。(便利✨)

As of Redis version 6, Hobby plans support both TLS and unencrypted connections, while production plans require TLS connections.

https://devcenter.heroku.com/articles/heroku-redis#create-a-new-instance

redis 6以降のredisを構築すると、REDIS_TLS_URLも同時に環境変数に定義され、こちらを利用するとTLSを活用した暗号化もサポートされているようです👀

sidekiqの導入

SidekiqのWiki等を参考にgemをインストールします。

gam 'sidekiq'

私は以下のような設定ファイルをconfig/sidekiq.ymlに置きましたが、このへんは好みなので設置せずに実行時に指定するなり、諸々対応します。

---
:concurrency: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
:queues:
  - high
  - default
  - low
development:
  :logfile: log/sidekiq.log
  :pidfile: tmp/pids/sidekiq.pid

あとはroutesにSidekiqの管理画面をmountして、initializerの中でベーシック認証を追加しておきます。

Rails.application.routes.draw do
  require 'sidekiq/web'
  mount Sidekiq::Web, at: '/sidekiq'
end
require 'sidekiq/web'

# NOTE: Sidekiqのwebuiにベーシック認証をかける
Sidekiq::Web.use(Rack::Auth::Basic) do |user, password|
  [user, password] == [ENV['BASIC_AUTH_USER'], ENV['BASIC_AUTH_PASSWORD']]
end

※ SidekiqはデフォルトでREDIS_URLを参照してくれるので接続情報等の設定はなくても大丈夫です。

You may also use the generic REDIS_URL which may be set to your own private Redis server. Using Redis · mperham/sidekiq Wiki · GitHub

Sidekiq用のプロセスタイプを定義

プロジェクトのルートに以下を記載したProcfileを用意してSidekiq用のworkerのプロセスタイプを定義し、herokuにpushします。

web: bin/rails server -p ${PORT:-5000} -e $RAILS_ENV
worker: bundle exec sidekiq

HerokuはこのProcfileに記載されたプロセスタイプごとにインスタンスを生成するようです。

devcenter.heroku.com

sidekiq用のインスタンスを構築する。

以下のコマンドを実行し、HerokuにRailsアプリケーションが動作するwebとSidekiqが動作するworkerインスタンスを1つずつ立ち上げます。

$ heroku ps:scale web=1 worker=1

※ 前述の「 Sidekiq用のプロセスタイプを定義」を実施しないとコマンド実行時に以下のエラーが発生します。前述の通り。Herokuはプロセスタイプに従ってインスタンス化するようなので、事前にProcfileにプロセスタイプを定義する必要があるようです👀

$ heroku ps:scale web=1 worker=1
Scaling dynos... !
 ▸    Couldn't find that process type (worker).

これでHerokuでRailsアプリケーションとSidekiqを起動することができました🙌

以下のようなコマンドを実行してJobをエンキューして無事に処理されることを確認できれば問題なさそうです。

$ heroku run bin/rails c -e production
$ MySampleJob.perform_later

おわりに

Herokuの無料プランでSidekiqまで動かせると思っていなかったのですが、ここまで出来るとは・・・!

参考

yutojp.com

uesaiso.netlify.app

sunday-morning.app

Ruby: RubyCriticでコードを静的解析して品質を計測するMEMO📝

個人で運用しているRuby on Rails製のWebサービスRubyCriticを使ってCIで品質レポートを取得するようにしたので、そのあたりのやり方等をメモしておきます📝

rubycriticとは?

RubyCritic is a gem that wraps around static analysis gems such as Reek, Flay and Flog to provide a quality report of your Ruby code.

上記の通り複数の静的解析ツールを活用していい感じのレポートを作成してくれるgemです💎

github.com

実行すると以下のようなレポートを作成してくれます。

f:id:madogiwa0124:20210320154053p:plain

※htmlだけではなくjson等も出力可能のようです

RubyCriticを導入する

以下をGemfilleに追記してbundle installすればRubyCriticが使用出来るようになります。

gem "rubycritic", require: false 

しかしデフォルトだとすべてのファイルが対象になってしまいライブラリのコードも検査してしまうので、実行時に--pathで任意のディレクトリを指定するか、プロジェクトのルートに.rubycritic.ymlを作成すると良さそうです。

paths:
  - 'app/'
  - 'lib/'

RubyCriticで特定のルールを無効化する

RubyCriticで特定のルールを無効化する場合には、RubyCriticが利用するツールに沿って無効化してあげる必要があります。

例えばreekが検知するIrresponsibleModuleを無効化したい場合には、プロジェクトのルートディレクトリに以下のような.reek.ymlを追加してあげると無効化することができます。

detectors:
  IrresponsibleModule:
    enabled: false

github.com

RubyCriticをCIで実行する

CircleCIの場合は以下のようなcommandを作ってあげると良さそうです、RubyCriticを実行して結果をartifactで永続化しています。

commands:
  store_atifacts:
    parameters:
      path:
        type: string
    steps:
      - store_artifacts:
          path: << parameters.path >>
  run_and_store_rubycritic:
    steps:
      - run:
          name: run rubycritic
          command: bundle exec rubycritic 
      - store_atifacts:
          path: tmp/rubycritic

おわりに

個人での開発だとレビューとかあまり無いので、こういうツールを積極的に活用していきたいですね!!

参考

github.com

Ruby: Deviseで現在有効になっているOmniAuthのproviderのリストを取得する

Deviseで現在の環境で有効になっているproviderを確認して有効なものだけのログインボタンを出す(例:開発環境でだけdeveloperストラテジを有効化しているので開発環境でだけ導線を出したい) 等の場合に現在有効なproviderを取得したいときのやり方をMEMOしておきます✍

deviseのバージョンは4.7.1です。

やりかた

以下でDeviseで現在有効になっているOmniAuthのproviderのリスト取得することが出来ます。

Devise.omniauth_providers
#=> [:developer, :twitter]

該当ソースは以下

https://github.com/heartcombo/devise/blob/45b831c4ea5a35914037bd27fe88b76d7b3683a4/lib/devise.rb#L329-L331

callback先とかも含めた全体的な情報は以下で取得出来るようです👀

Devise.omniauth_configs

https://github.com/heartcombo/devise/blob/45b831c4ea5a35914037bd27fe88b76d7b3683a4/lib/devise.rb#L271-L272

便利✨

Ruby: FactoryBotでglobalに使えるtraitを定義するMEMO

以下の記事を追記するときにFactoryBotでfactory跨いで使いたいtraitを定義する方法調べていて見つけたのでMEMOしておきます📝

madogiwa0124.hatenablog.com

検証したバージョンはfactory_bot 6.1.0です。

やりたいこと

以下のようなstrict loadingを使用したN+1を検知する設定 + strict laodingを無効化するためのメソッドを用意した実装を行っているとします。

class ApplicationRecord < ActiveRecord::Base
  self.strict_loading_by_default = true

  def strict_loading!(value = true)
    @strict_loading = value
  end
end

上記を行ったけどテストコードではstrict loadingを一時的に無効化したい場合にtraitを使ってシンプルにかけるようにしたいというのがやりたいことになります。

describe '#foo' do
  let(:foo) { create(:foo, :strict_loaded) }

  before do 
    create(:bar, foo: foo)
  end

  it { expect(foo.bars.length).to eq 1 }
end

各factoryで個別にtraitを書くのは手間なのでglobalに以下のようなtraitを定義したいものです。

  trait :strict_loaded do
    after(:build) { |record| record.strict_loading!(false) }
  end

globalに使えるtraitを定義する

globalに使えるtraitを定義する方法は簡単で以下のようにfactoryに紐付けずFactoryBot.define直下にtraitを置くことで定義できます。

FactoryBot.define do
  trait :strict_loaded do
    after(:build) { |record| record.strict_loading!(false) }
  end
end

全然知らなかったのですが、以下の通りテストコードでも検証されているので公式でサポートされている利用方法のようです👀✨

github.com

おわりに

FactoryBot便利ですね🤖✨

参考

stackoverflow.com

Ruby on Rails: BulletからRails標準のstrict loadingに乗り換えるMEMO📝

今までN+1の検知にBulletを使っていたのですが🔫

github.com

Rails 6.1から導入された新機能strict loadingでも同じようなことが実現できそうだったので、個人のWebサービスをBulletからstrict loadingに載せ替える手順をメモしておきます📝

strict loadingの概要は以前まとめているので、興味がある人はこちらからどうぞ

madogiwa0124.hatenablog.com

strict_loadingをグローバルに導入する

今回はBulletの代用としてアプリケーション全体でN+1の検知を有効にしたかったので、ApplicationRecoredのstrict_loading_by_defaultをtrueに設定し、ApplicationRecordを継承したModelではstrict loadingによるN+1の検知を有効にしました。

class ApplicationRecord < ActiveRecord::Base
  self.strict_loading_by_default = true
end

これでeager_loadingをせずに関連モデルを取得しようとした際にActiveRecord::StrictLoadingViolationErrorが発生するようになりました🔫👋

strict loadingを無効化する

ActiveRecordインスタンスにstrict_loadingを無効化するメソッドを追加

先程の手順でstrict loadingによるN+1の検知が可能になったのですが、以下のような作成したインスタンスをそのまま参照して関連するmodelのインスタンスを取得したいケース等でもstrict loadingによるN+1の検知によりActiveRecord::StrictLoadingViolationErrorが発生してしまいます。

  describe '#foo' do
    let(:foo) { create(:foo) }

    before { create(:bar, foo: foo) }

    # eager_loadingせずに参照するためActiveRecord::StrictLoadingViolationErrorが発生する。
    it { expect(foo.bars.length).to eq 1 }
  end

しかし、現在(2021/02/27)のstableの最新Rails 6.1.3ではActiveRecordインスタンスに対してstrict loadingを有効にするstrict_loading!メソッドは実装されていますが、無効にすることはできません😢

Foo.preload(:bars).find(foo)のようにeagar_loadingして取得し直すような対応も考えられますが、そもそも1回アクセスするだけだったら再取得する分、かえってSQLの発行回数が増えてしまう恐れがあるのと、既存のテストコードに与える影響が大きいので、N+1を許容したいケースもあるのかなと思います。

今回は以下のようなメソッドをApplicationRecordに追加してstrict loadingを無効化出来るようにしました。

class ApplicationRecord < ActiveRecord::Base
  self.strict_loading_by_default = true

  def strict_loading!(value = true)
    @strict_loading = value
  end
end

上記実装はrailsのmasterにマージされている実装をもってきたものです🚃

github.com

FactoryBotで作成したインスタンスはstrict_loadingを無効化する

先程紹介したメソッドを以下のように使ってあげるとActiveRecord::StrictLoadingViolationErrorを回避することができます。

describe '#foo' do
  let(:foo) { create(:foo) }

  before do 
    foo.strict_loading!(false)
    create(:bar, foo: foo)
  end

  it { expect(foo.bars.length).to eq 1 }
end

FactoryBotのglobalなtraitを定義して以下のようにするもの良さそうですね🤖

FactoryBot.define do
  trait :strict_loaded do
    after(:build) { |record| record.strict_loading!(false) }
  end
end

describe '#foo' do
  let(:foo) { create(:foo, :strict_loaded) }

  before do 
    create(:bar, foo: foo)
  end

  it { expect(foo.bars.length).to eq 1 }
end

または以下のような形でfactory内でtraitをデフォルトで呼び出すこともできそうです👀

FactoryBot.define do
  trait :strict_loaded do
    after(:build) { |record| record.strict_loading!(false) }
  end

  factory :foo do
    strict_loaded
  end
end

describe '#foo' do
  let(:foo) { create(:foo) }

  before do 
    create(:bar, foo: foo)
  end

  it { expect(foo.bars.length).to eq 1 }
end

console時にstrict loadingを無効化する

console時にも毎回strict_loading!(false)を実行するのは面倒なので、application.rb内で以下のように記述するとconsole時のみstrict loadingを無効化することができます。

# config/application.rb
module MyApp
  class Application < Rails::Application
    # rails consoleではstrict loadingを無効化
    console { ApplicationRecord.strict_loading_by_default = false }
  end
end

config.consoleの説明は以下を参照してください。

config.console: これを用いて、コンソールでrails consoleを実行する時に使われるクラスをカスタマイズできます。このメソッドはconsoleブロックで使うのが最適です。

Rails アプリケーションを設定する - Railsガイド

おわりに

単純にN+1を検知するだけだったらRails標準機能の範囲でお手軽に実現出来るのは便利ですね✨

追記:2021/04/05

N+1が発生したときだけraiseするn_plus_one_onlyオプションが追加される(まだリリース予定は未定っぽい)ようなのでこちらを利用すると、より実用的な感じになりそうですね✨

github.com

参考

github.com

stackoverflow.com