rails:コンソールから生SQLを実行する

以下のように、rails consoleから生SQL実行できる。
戻り値がActiveRecord::Resultなので、hash化すると見やすい。
(hash化しなくても見れる)

sql = 'SELECT staff_type, count(*) FROM staffs where is_valid = true group by staff_type order by staff_type'
ActiveRecord::Base.connection.select_all(sql).to_hash

結果0件の場合は、to_hashだとエラーになるので注意

以下でも実行できるが、クエリーの結果をのぞくには扱いにくい。

ActiveRecord::Base.connection.execute(sql)

rails:更新系処理の悲観的ロックのサンプル

掲題を実現する場合のサンプル。

処理で更新されるテーブルのモデルに、以下のようなメソッドを実装。

class BatchExecUpdateDate < ApplicationRecord
  # batch_exec_update_dates
  #
  # id        :bigint(8)  not null, primary key
  # proc_type :integer    not null
  # upated_at :datetime   not null

  enum proc_type : { daily: 0, monthly: 1}

  class << self
    def transaction_with_lock(proc_type, &block)
      target_rec = find_or_create_by(proc_type: proc_type)

      ActiveRecord::Base transacrion do
        # 行ロック取得
        lock_rec = BatchExecUpdateDate.lock('FOR UPDATE NOWAIT').find(target_rec.id)
        # 引数で渡されたブロックを処理する
        block.call
        lock_rec.update(updated_at: Time.zone.now)
      end
      true
    rescue ActiveRecord::StatementInvalid => e
      return false if e.cause&.kind_of?(PG::LockNotAvailable)

      raise e
    end
  end
end

modelでupdate_atを必須valideteかけている場合は、
明示的にupdated_atをセットしないとエラーになる
validate指定を外すと、自動でセットしてcreateしてくれる

validates :updated_at, presence: true

target_rec = find_or_create_by(proc_type: proc_type) {|beud| beud.updated_at = Time.zone.now}

以下のような処理があったとして

# 処理クラス
class AaaBatch
  class << self
    def update_process
      # 更新処理
    end
  end
end

以下のように呼び出すことで、実現できる。

# 起動するとき
BatchExecUpdateDate.transaction_with_lock(proc_type) do
  AaaBatch.update_process(0)
  BbbBatch.update_process(0)
  :
end

RSpec:よく使うmatcherメモ

付け足し付け足ししていく予定です。

インスタンスの内容のテスト

expect(section).to have_attributes(name: 'default')
expect(section.is_valid).to be_truthy
expect(section.retired_at).to be nil

件数に変更がない(unchange推奨)

expect{subject}.to change {Company.count}.by(0).and change {Section.count}.by(0)
expect{subject}.to unchange {Company.count}.and unchange {Section.count}

参考:毎度多謝)使えるRSpec入門
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita
使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita
使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita

rails:コールバック処理

あらかじめ定義しておき、必要なタイミングで自動で動く処理。
以下だと、Companyがcreateされると、直後にSectionもcreateされる。
(Section.name を仮設定しているのもコールバック)

# app/model/company.rb
class Company < ApplicationRecord
  has_many :sections

  # callback
  after_create :create_default_section

  def create_default_section
    section.create!(
      code: Section::DEFAULT_SECTION_NAME,
      company: self 
      )
  end
end

# app/model/section.rb
class Section < ApplicationRecord
  belong_to :company

  # callback
  before_create :set_name

  DEFAULT_SECTION_NAME = 'DEFAULT'.freeze

  def set_name
    self.name ||= '(temporary)' # 未設定の場合は仮名をセット
  end 
end

おまけ)create と create! の違い
例外(赤い画面)になるかならないか、が異なる。
create! はバリデーションNGになるとActiveRecord::RecordInvalid例外が発生する。
!がない場合は、保存されないが例外(赤い画面)も出ない。

ruby:ハッシュ操作の応用1

以下のようなハッシュを画面に返すとする。

  # 画面に引き渡すインスタンス変数(Hash)
  # @conpany_inf カンパニー情報(ヘッダ情報)
  #   {
  #     company_name [string] カンパニー名
  #     company_started_at [datetime] 創設日
  #     staff_count [integer] スタッフ人数
  #   }
  # @staff_inf スタッフ一覧情報
  #    [{
  #       staff_no: [string] スタッフNO
  #       staff_name: [string] スタッフ名
  #       staff_class: [string] 一般職/管理職
  #       license_info: 資格情報
  #         [{
  #           license_name [string] 資格名
  #           license_get_date [datetime] 取得日
  #         }]
  #   }]

各モデルは以下のようなイメージ。

# app/model/company.rb
class Company < ApplicationRecord
  has_many :staffs
end

# app/model/staff.rb
class Staff < ApplicationRecord
  belong_to :company
  has_many :licenses

  scope :alive, -> { where(is_retire: false) }
  scope :manage_alive, -> { alive.where(is_manage: true) }
end

# app/model/license.rb
class License < ApplicationRecord
end

コントローラは以下のようなロジックで実装。

# controller/staff_controller.rb

class StaffController  < ApplicationController

  # 画面に引き渡すインスタンス変数(Hash)
  # @conpany_inf カンパニー情報(ヘッダ情報)
  #   {
  #     company_name [string] カンパニー名
  #     company_started_at [datetime] 創設日
  #     staff_count [integer] スタッフ人数
  #   }
  # @staff_inf スタッフ一覧情報
  #    [{
  #       staff_no: [string] スタッフNO
  #       staff_name: [string] スタッフ名
  #       staff_class: [string] 一般職/管理職
  #       license_info: 資格情報
  #         [{
  #           license_name [string] 資格名
  #           license_get_date [datetime] 取得日
  #         }]
  #   }]


  def index
    @conpany_inf = conpany_inf_rec(company)

    @staff_inf = []
    staffs = company.staffs.order(:staff_no).alive
    staffs.each do |s|
      @staff_inf << staff_inf_rec(s)
    end
  end

  private

  def company
    @company ||= Company.find(params[:id])
  end

  def conpany_inf_rec(company)
    {
      company_name: company&.name
      company_started_at: company.&.started_at
      staff_count: company.staffs.count
     }
  end

  def staff_inf_rec(staff)
    {
      staff_no: staff&.staff_no
      staff_name: staff&.name
      staff_class: staff&.is_manage_lank & 'manage' : 'normal'
      license_info: license_info_rec(staff)
    }
  end

  def license_info_rec(staff)
    staff.licenses.order(:get_date).map do |li|
      {
        license_name: li.name
        license_get_date: li.get_date
      }
    end
  end
end

rails:コンソールからFactoryBotでデータ作成する

Rspec実行時以外、railsコンソールからもFactoryBotを使ってデータ作成できる。
以下は例。

> c = Company.First
> s = FactoryBot.create(:staff, name: 'kenji', company: c)

FactoryBot実装済みが前提。
戻り値はインスタンス

rails:判定ロジックサンプル備忘1

以下のようなケースの判定ロジックを考える。
モデルで以下のように定数定義。
app/models/worker.rb

class Worker < ApplicationRecord
  WORKER_TYPES = { TEACHER: 1, PILOT: 2 }.each_value(&:freeze).freeze
end

デコレータは以下のように継承して定義。
app/decorators/

class WorkerDecorator < ApplicationDecorator
end

class TeacherDecorator < WorkerDecorator
end

class PilotDecorator < WorkerDecorator
end

ロジックでは以下のように判定して利用する。

decorate_worker = 
  if params[:worker_type] == Worker::WORKER_TYPES[:TEACHER].to_s
    TeacherDecorator.decorate(worker)
  else
    PilotDecorator.decorate(worker)
  end
puts decorate_worker.abillity

上記を、メソッド化して、以下のように整理。
メソッドからは、クラスを返却している。

decorate_worker = worker_decorator(params[:worker_type]).decorate(worker)
puts decorate_worker.abillity

def teacher?(worker_type)
  worker_type == Worker::WORKER_TYPES[:TEACHER].to_s
end

def pilot?(worker_type)
  !teacher?(worker_type)
end

def worker_decorator(worker_type)
  # classを返却
  teacher?(worker_type) ? TeacherDecorator : PilotDecorator
end

メソッド化しないのであれば、省略記述できるが、可読性は下がる。
以下、記述例。

# ex1
decorator = params[:worker_type] == Worker::WORKER_TYPES[:TEACHER].to_s ? TeacherDecorator : PilotDecorator
decorate_worker = decorator.decorate(worker)

# ex2
decorator = params[:worker_type] == Worker::WORKER_TYPES[:TEACHER].to_s ? TeacherDecorator : PilotDecorator
decorate_worker = decorator.decorate(worker)

# ex3
decorate_worker = decorator(params[:worker_type]).decorate(worker)

def decorator(worker_type)
  worker_type == Worker::WORKER_TYPES[:TEACHER].to_s ? TeacherDecorator : PilotDecorator
end