食べられません

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

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とか使って必要なものを必要な形で整形して出力するようにした方がいい。

ruby1.8から2.0へのupgrade

1.9すっ飛ばして2.0に更新。
凄い今更感の強い更新備忘録だけど、ちょっと嵌りかけた所があったので。

大抵どこでも1.8からの更新時には「文字」周りで嵌るとは注意してるので、
そこは特に問題はなかった(問題ではあったが)
それよりもpublic_instance_methodsとかinstance_variablesとかを使った
黒魔術めいた処理内で文字列とinclude?で比較してたりすると全滅する。

これらのメソッドstringではなくsymbolを返すようになったため。
注意。

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

AWSの無料枠についてまとめた

登録から12ヶ月間無料で使える枠と、その後も引き続き無料で使える枠がゴッチャになるのでまとめた

新規利用から12ヶ月間(毎月)無料の枠

  • EC2
    • t2.micro Linux 750時間
    • ELB 750時間 + 15GB分のデータ処理
    • EBS 30GB + 1GB分のスナップショットストレージ
  • S3
    • 5GBの標準ストレージ
    • 20,000 Getリクエスト
    • 2,000 Putリクエスト
  • RDS
    • micro DBインスタンス Linux 750時間(Single-AZ)
    • 20GBのストレージ
    • 1000万のI/O
    • 自動バックアップと任意スナップショット用 20GBストレージ
  • CloudFront
    • 50GBのデータ転送(アウト)
    • 2,000,000件のHTTP/HTTPSリクエスト
  • Cognite
    • 10GBのクラウド同期ストレージ
    • 1,000,000回の同期操作
  • AppStream
    • 20時間分
  • データ転送
    • 全てのサービスを総合して帯域幅「送信(アウト)」15GB
  • DataPipeline
    • 低頻度の前提条件 3つ
    • 低頻度のアクティビティ 5つ
  • ElastiCache
    • マイクロキャッシュノード 750時間

12ヶ月後も引き続き(毎月)無料で利用できる枠

  • DynamoDB
    • 25GBのストレージ
    • 25ユニット読み込み容量
    • 25ユニット書き込み容量
  • Cognite
    • 無制限のユーザー認証とID生成
  • SWF
    • 開始する1,000件のワークフロー実行
    • 合計10,000件のアクティビティタスク、シグナル、タイマー、マーカー
    • 使用する30,000ワークフロー日
  • SQS
    • 1,000,000件のリクエスト
  • SNS
    • 1,000,000件のリクエスト
    • 100,000件のHTTP通知
    • 1,000件のEメール通知
  • ElasticTranscoder
    • SDトランスコード20分 または HDトランスコード10分
  • CloudWatch
    • 10メトリックス
    • 10アラーム
    • 1,000,000APIリクエスト
  • MobileAnalytics
    • 100,000,000の無料イベント
  • KeyManagementService
    • 20,000件の無料リクエスト
  • Lambda
    • 1,000,000件の無料リクエスト
    • 3,200,000秒のコンピューティング時間