AASMとcounter_cultureを同時に使うとno_direct_assignment出来ない
約1年半ぶりの記事となった
なんかAASMでno_direct_assignment設定したら同じ操作してもエラー発生する時としない時と起き始めて混乱してる。
— 天然ほっけ (@NaturalHokke) 2017年12月26日
原因がさっぱりわからんぞい・・・
AASM、no_direct_assignment設定した時にvalidation通さなければ確実に通るのは確認出来たけど、validation通した時になぜエラー発生してしまうのかがまだ謎
— 天然ほっけ (@NaturalHokke) 2017年12月29日
んあー、わかった・・・・
— 天然ほっけ (@NaturalHokke) 2017年12月29日
AASMとcounter_cultureの相性が悪いんだコレ・・・
えぇ・・・これcounter_cultureがなんで変更前のモデル再現する必要があるのかまで追わないと行けないのか・・・?
— 天然ほっけ (@NaturalHokke) 2017年12月29日
つらすぎる・・・
aasm/no_direct_assignment/counter_culture
— 天然ほっけ (@NaturalHokke) 2017年12月29日
で検索して何も出ないって事は踏んだ奴おらんのか
微妙なエッジケース(?)に当たりAASMのコードから頑張ってステップ実行を繰り返しcounter_cultureに行き着いたというお話。
結論
AASMを利用しているクラスでcounter_cultureを利用(counter_culture :xxxx
を記述する)とAASMでno_direct_assignment
は設定出来ない。
ただしskip_validation
も併用すれば利用は可能。
以下説明
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!
の処理の流れを(ちょっと端折りつつ)順に追っていこう。
- まずはイベント発火のココ
- トランザクションを開始し内部処理へsuper
- イベント処理の実態のココ
- 素直に処理を進め状態遷移処理へ
- 状態遷移処理の中の実際に代入処理が行われるのがココ。そしてココ
- ココでついに状態が設定される
- そしてそのまま保存処理が始まるhttps://github.com/aasm/aasm/blob/v4.12.3/lib/aasm/persistence/orm.rb#L24 https://github.com/aasm/aasm/blob/v4.12.3/lib/aasm/persistence/active_record_persistence.rb#L68
ここで言う所のself
はarticle
自身。article.save
である。
おや?エラー起きないじゃないかと思いきやココからcounter_cultureへと話が進む。そうコールバックだ。
コメントが書かれているのがなにやら不安にさせてくれる。
どうやらカウンタキャッシュに変更が無いか確認するらしい。
- 変更前のデータを作ろうとして https://github.com/magnusvk/counter_culture/blob/master/lib/counter_culture/counter.rb#L261
- これである https://github.com/magnusvk/counter_culture/blob/master/lib/counter_culture/counter.rb#L267
そう、データ変更のあったカラムに変更前の値を代入している。わかりやすいコードだ。ん?
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
なりしたときに数値出力することが出来る。
結論
wercker Classic(Andorian)からDocker enable(Ewok)に移行する
背景
werckerでは4月あたまぐらいからDockerベースになったv2(Ewok)が始まり、
記事を書いてる6月時点でdefaultとなっている。
その割にEwokでの記事があまりないので移行にあわせてメモしておく。
前提
移行の話なので既にAndorianで色々やっている前提です。 環境は
- Rails 4.2.1
- postgresql
- phantomjs
- Heroku
簡単に変更内容
- 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_key
をadd_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
ってやれば取得出来る。
こんな仕様にならない様に作ったほうが良いと思うんだけどねぇ…