SQL:条件付きCOUNT

今まで知りませんでした。
条件付きのcountがこんなに簡単にとれるとは。

select
   count(*) # 全件
  ,count(staffs.type = 1 OR NULL) as byte_count
  ,count(staffs.type = 2 OR NULL) as part_count
from
  staffs;
where
  staffs.invalid = true

参考:多謝)
【SQL】COUNT関数で条件に合致する件数を取得する - Qiita
SQLでSELECT句のCOUNTをきわめる【複数の条件指定も可能】 | ジユーズ

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実装済みが前提。
戻り値はインスタンス