食べられません

プログラミングとか漫画とか生活とか

AASMとcounter_cultureを同時に使うとno_direct_assignment出来ない

約1年半ぶりの記事となった

微妙なエッジケース(?)に当たりAASMのコードから頑張ってステップ実行を繰り返しcounter_cultureに行き着いたというお話。

結論

AASMを利用しているクラスでcounter_cultureを利用(counter_culture :xxxxを記述する)とAASMでno_direct_assignmentは設定出来ない。
ただしskip_validationも併用すれば利用は可能。

以下説明

github.com github.com

class Article < ApplicationRecord
  belongs_to :area
  counter_culture :area
  aasm do
    state :draft, initial: true
    state :published

    event :publish do
      transitions from: :draft, to: :published, after: -> { self.published_at = Time.current }
    end
  end
end

AASMはクラスの状態管理gemで、no_direct_assignmentは状態の直接設定をエラーとし、イベントによる状態遷移を強制するための設定である。(という認識)
また、counter_cultureはRails標準のcounter_cacheの高機能版gem。

例示のイメージとしては、下書きと公開済みのステータスをもち、何らかのエリアに紐づく記事クラスである。
エリアで記事数のカウントをキャッシュするためにcounter_cultureを設定している。
また、publishイベントにより公開された際に記事の公開日時を保存するようにしているとする。

さて、このコードの段階ではまだ何も問題はない。
ここで、状態を直接設定される(直接公開状態にされる)と公開日時が設定されないので
イベント発火による公開しか出来ないようno_direct_assignmentを設定しておこう、となったとする(なった)。以下が修正後。

no_direct_assignment設定で行われる内容はこんな感じ
わかりやすく、aasmのカラム名に対して直接代入しようとする部分を書き直してエラー処理を追加している。

class Article < ApplicationRecord
  belongs_to :area
  counter_culture :area
  aasm no_direct_assignment: true do
    state :draft, initial: true
    state :published

    event :publish do
      transitions from: :draft, to: :published, after: -> { self.published_at = Time.current }
    end
  end
end

この設定を追加したことにより、このクラスはイベント発火後のデータ保存のタイミングで必ず死ぬようになる。
article.publish!の処理の流れを(ちょっと端折りつつ)順に追っていこう。

ここで言う所のselfarticle自身。article.saveである。
おや?エラー起きないじゃないかと思いきやココからcounter_cultureへと話が進む。そうコールバックだ。

  • counter_culture :areaによりココに話が飛ぶ
  • そして問題のコレ

コメントが書かれているのがなにやら不安にさせてくれる。
どうやらカウンタキャッシュに変更が無いか確認するらしい。

そう、データ変更のあったカラムに変更前の値を代入している。わかりやすいコードだ。ん?
https://github.com/aasm/aasm/blob/master/lib/aasm/base.rb#L57
ほげぇぇぇぇぇぇぇぇ!

というわけで、counter_cultureによってno_direct_assignment設定が使えなくなる事を確認する旅はここで終わる。

回避方法としてはココでvalidationを無視してupdate_columnに処理を通す事。
もしくはAASMまたはcounter_cultureのどちらかの利用をやめることである。

ridgepoleをmigrateっぽく使えるようにする

rake db:migrate すると ridgepole でSchemafile読み込んでApplyしてSchemafileに書き出す感じのタスクを作った。 ridgepoleの運用的に

  • rake db:migrate
  • rake db:schema:dump
  • rake db:schema:load

がアレばまぁ良いだろ、的な割りきった作り。

Rake.application.instance_variable_get('@tasks').delete('db:version')
Rake.application.instance_variable_get('@tasks').delete('db:rollback')
Rake.application.instance_variable_get('@tasks').delete('db:migrate')
Rake.application.instance_variable_get('@tasks').delete('db:migrate:status')
Rake.application.instance_variable_get('@tasks').delete('db:schema:dump')
Rake.application.instance_variable_get('@tasks').delete('db:schema:load')
Rake.application.instance_variable_get('@tasks').delete('db:schema:cache:dump')
Rake.application.instance_variable_get('@tasks').delete('db:schema:cache:clear')
Rake.application.instance_variable_get('@tasks').delete('db:structure:dump')
Rake.application.instance_variable_get('@tasks').delete('db:structure:load')

namespace :db do
  desc 'Apply database schema (options: DRYRUN=false, VERBOSE=false)'
  task migrate: :environment do
    Rake::Task['ridgepole:apply'].invoke
    Rake::Task['ridgepole:export'].invoke unless ENV['DRYRUN']
  end
  namespace :schema do
    desc 'Creates a db/Schemafile'
    task dump: :environment do
      Rake::Task['ridgepole:export'].invoke
    end

    desc 'Loads a Schemafile into the database'
    task load: :environment do
      Rake::Task['ridgepole:apply'].invoke
    end
  end
end

namespace :ridgepole do
  desc 'Apply database schema (options: DRYRUN=false, VERBOSE=false)'
  task apply: :environment do
    options = ['--apply']
    options << '--dry-run' if ENV['DRYRUN']
    options << '--verbose' if ENV['VERBOSE']
    ridgepole(*options, "--file #{schema_file}")
  end

  desc 'Export database schema'
  task export: :environment do
    options = ['--export']
    ridgepole(*options, "--output #{schema_file}")
  end

  private

  def schema_file
    Rails.root.join('db/Schemafile')
  end

  def config_file
    Rails.root.join('config/database.yml')
  end

  def ridgepole(*options)
    command = ['bundle exec ridgepole', "--config #{config_file} --env #{Rails.env}"]
    system [command + options].join(' ')
  end
end

EnumerizeカラムをJSON化したときに数値で出力したい

掲題の通り

目的

controllerで@model.to_jsonとかrender :json, @modelとか雑にJSONを返しており enumerizeを使い始めたことにより数値だったカラムが文字列で返るようになってしまったので controllerやviewを変えることなく数値で返るようにしたい

方法

# 対象モデル
class Model
  def read_attribute_for_serialization(key)
    if [:column1, :column2].include?(key.to_sym)
      __send__(key).value
    else
      __send__(key)
    end
  end
end

としてやればto_jsonなりas_jsonなりしたときに数値出力することが出来る。

結論

雑にJSON化しないで、jbuilderとか使って必要なものを必要な形で整形して出力するようにした方がいい。

wercker Classic(Andorian)からDocker enable(Ewok)に移行する

背景

werckerでは4月あたまぐらいからDockerベースになったv2(Ewok)が始まり、
記事を書いてる6月時点でdefaultとなっている。
その割にEwokでの記事があまりないので移行にあわせてメモしておく。

前提

移行の話なので既にAndorianで色々やっている前提です。 環境は

簡単に変更内容

  • DockerベースとなったためAndorianで使っていたwerckerのものではなくDockerHubなどのイメージを使う。
  • DeployTargetはCustomしか選択出来ない。
  • オプションや環境変数がちょっと変わった

変更

# 最新のrubyとnode.jsが使える公式のイメージがこれだった。
box: rails:4.2.1

# postgres公式イメージ。POSTGRES_PASSWORDは必須
services:
  - id: postgres
    env:
      POSTGRES_PASSWORD: $DATABASE_PASSWORD

build:
  steps:
    - bundle-install:
        # jobsオプションが効かなくなってしまったのはまだ原因不明...
        jobs: 4

    - rails-database-yml:
        # ここをpostgresqlからpostgresql-dockerに変更
        service: postgresql-docker

    # phantomjsを使う場合
    - aussiegeek/install-phantomjs

    # script部分は特に変更なし
    - script:
        name: echo ruby information
        code: |
            echo "ruby version $(ruby --version) running"
            echo "from location $(which ruby)"
            echo -p "gem list: $(gem list)"

    - script:
        name: db:schema:load
        code: RAILS_ENV=test bin/rake db:schema:load

    - script:
        name: rspec
        code: bin/rspec --color -f d

deploy:
  steps:
    ## HerokuへのDeployで必要な環境変数
    # HEROKU_APP_NAME
    # HEROKU_KEY(API_KEY)
    # HEROKU_USER(HerokuID)
    - heroku-deploy:
        install-toolbelt: true
        key-name: HEROKU_DEPLOY_KEY
    - script:
        name: rake db:migrate
        code: heroku run rake db:migrate --app $HEROKU_APP_NAME

Migrationの気付き

仕事ではMySQLしか使ってないので気づいてなかったけど、
Postgresqlではカラムを位置指定して追加することは出来ないようだ。。。

あと、調べてもあんまり出てこなかったので書いておくけど
Rails4.2時点ではオプションもちゃんと書いてやることでremove_columnもchange内に書ける
(いつから出来たのかは調べてない)

def change
  remove_column :users, :nickname, :string
end

これでrollback時にはちゃんとadd_columnされる。

Rails4.2.0のadd_foreign_keyについて

Rails 4.2使い始めてschema.rbをgit管理している場合、
add_foreign_keyadd_indexな感覚で使うと差分が発生してしまいそうなのでメモしておく。

add_index

add_index :suppliers, :nameとしてやるとIndex名はデフォルトでindex_suppliers_on_nameとなる。
それは https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb#L588 にある通り。
対象のカラムが特別多くなければ:nameオプションを使うこともないだろう。

add_foreign_key

add_foreign_key :articles, :authorsとしてやると外部キー名は意外な事に
fk_rails_fa2b6bcbc5(ランダム)となってしまう。
実装は https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb#L983 の通り。

というわけで、外部キーを作成するときは意識して:nameオプションを使っておかないと
毎回別名で作成されてschema.rbが犯されちゃうよー、というお話。

devise group

Railsでよく使う認証のgem。知らない人いないのでは。

今回ちょっと面倒な感じのログイン仕様でdeviseの中ずっと調べてたんだけど 3.3.0で追加されたらしい機能使ったのでメモ。

複数のモデル(Roleではなく)でログイン出来る作りのとき [:user, :staff, :admin]の3モデルあったとしたら

before_action :authenticate_user!
before_action :authenticate_staff!
before_action :authenticate_admin!

みたいな感じでそれぞれ書いてたと思うんだけど、それを

devise_group :blogger, contains: [:user, :staff, :admin]
before_action :authenticate_blogger!

って書けちゃう。楽ちん。 ログインしてるデータを取得するときも、いつものdeviseの感じで current_bloggerってやれば取得出来る。

こんな仕様にならない様に作ったほうが良いと思うんだけどねぇ…