ActiveRecord で id 以外のカラムをプライマリキーにして、しかも型が int じゃないときの話
http://www.hsbt.org/diary/20120524.html#p01 見てそういえばハマった話思い出したので、次みたときなんでこうしたんだっけ、とならないように書いておく。
既に存在するDBをそのまま使う必要があったりする時で、しかもプライマリキーが int(11) とかじゃないときは、少し残念な気持ちになりながら以下のようなマイグレーションファイルを用意することになる。
class CreatePrefMaster < ActiveRecord::Migration def change create_table :pref_master, id: false do |t| t.column :code, :"char(2)", null: false t.column :name, :"varchar(64)" end execute "ALTER TABLE pref_master ADD PRIMARY KEY (code)" end end
テーブル名が複数形でアンダースコア繋ぎという Rails の標準的な命名規則ではない場合もあるので、モデルのほうで table_name を指定する。
class PrefMaster < ActiveRecord::Base self.table_name = "pref_master" attr_protected :title, :body end
さらに、config/application.rb で、db/schema.rb じゃなく db/structure.sql を吐くよう指定する。
module Aaa class Application < Rails::Application # Use SQL instead of Active Record's schema dumper when creating the database. # This is necessary if your schema can't be completely dumped by the schema dumper, # like if you have constraints or database-specific column types config.active_record.schema_format = :sql # ↑ここのコメントアウトはずす。 # ... end end
なぜかと言うと、上記の手順でマイグレーションして schema.rb を吐いたりすると以下のようなファイルができる。create_table オプションに直接 :primary_key オプションを指定しているため、Rails 的な primary_key の解釈で int な型のカラムで create table してしまう。
ActiveRecord::Schema.define(:version => 20120531162643) do create_table "pref_master", :primary_key => "code", :force => true do |t| t.string "name", :limit => 64 end end
テストが通らずなんでかなーと思って見たら何か意図した型でない状態になっていて気付く。
structure.sql なら安心。
CREATE TABLE `pref_master` ( `code` char(2) COLLATE utf8_unicode_ci NOT NULL, `name` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL, PRIMARY KEY (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Rails の標準からはずれてくるといろいろつらいおもいするのでこういう事態を避けられるなら、そっちのほうがいいと思う。
Rails のシンプルな証跡管理ライブラリ auditable
https://github.com/harleyttd/auditable
- ちいさくてシンプル(かわいい)
- メソッド(属性)であれば、変更を記録できる(汎用的)
- ポリモーフィック関連とシリアライズしたものをカラムに入れるみたいなつくりで、トリッキーなことをあまりしていない(よめる)
あたらしめで、ちょいちょい更新されてるかんじ。
証跡管理は要件でブレがおおきいのかなー?という感じがするので、オシゴトでこれをそのまま使えるのかは知らない。仲間内でつかうようなものになら、気軽に利用してみてもいい気がする。
https://github.com/harleyttd/auditable/commit/d3c78b047998c83419cab6207d94f003caccc47c#README.md の時点の README の雰囲気訳。
Ruby toolbox のバージョニングカテゴリと証跡カテゴリにはたくさんの gem があるが、それぞれに様々な問題がある(これを書いている今現在も)。
- ほとんどが古く、Rails 3.2.2 では動かない。人気のライブラリのいくつか、たとえば、papertrail と vestal_versions はたくさんの issue と pull request があげられているが、どれも処理されたりマージされたりしていない。こういう gem ライブラリのくさるほどある fork を見ると、みんなはそれぞれで gem を改善したもので良いと思ってるようだ。いい感じのものもあるのだが、自分の見たものだと、やたらとハックが多用されてたりした(最近の Rails の変更に対応するため)。そのなかで人気のあるものを試したけど、かんたんな、自分のやりたいこともちゃんとできない。
- この手の gem は、ときとともに進化して、だんだん(すごく)扱いにくくなる
- たいてい、データベースのカラムしかサポートしていなくて、たとえば、メソッドとか関連で動かない(か、微妙に動かない)。papertrail は has_one には対応しているが、作者はそれ以上に対応するのは簡単じゃないと言っている
- シンプルで軽いのが欲しかった
上記のカテゴリの gem の多くはよくできてる。自分は、かんたんにモデルのアトリビュートとかメソッドの変更を扱えるような、もう完全にシンプルな gem を目標にした。そう、メソッドもですよ、アトリビュートだけじゃなくて。これが自分のライブラリのアプローチの特徴的なところです。
もし、込み入った情報、たとえば関連するレコードとかの変更を記録したいなら、関連するレコードを表現するような値を返すメソッド定義だけして、あとは、auditable にその値の変更を記録させておけばいい。
基本原理としては、
- デフォルトでは、どのカラムについても、記録してほしくない。指定したアトリビュートとかメソッドだけでいい
- ややこしい関連は扱いたくない。かわりにメソッドをつかう
- データベースのカラムだけでなく、アトリビュートやメソッドの値を追いかけることに興味がある
- シンプルで、ActiveRecord::Dirty#changes に似たものが欲しかったが、永続的に保存したい。下記の Auditable::Auditing#audited_changes の使い方を見てほしい。
導入
アプリケーションの Gemfile に以下の行を追加する。
gem 'auditable'
で、以下を実行。
$ bundle
あるいは gem コマンドから自分でインストールする。
$ gem install auditable
使い方
まず、audits テーブルをデータベースに追加する。
rails generate auditable:migration rake db:migrate
そして、モデルで証跡管理したいメソッドのリストを audit メソッドに渡す。
class Survey has_many :questions attr_accessor :current_page audit :title, :current_page, :question_ids end
デモ
テストコードにあるテストモデルでデモしたいと思います。監査対象のモデルを、たぶん、コンソールで試したいかと思うので。
詳細については、spec ディレクトリに含まれているテストケースを見てもらったほうがいいと思う。
$ bundle console >> require(File.expand_path "../spec/spec_helper", __FILE__) => true >> s = Survey.create :title => "demo" => #<Survey id: 1, title: "demo"> >> Survey.audited_attributes => [:title, :current_page] >> s.audited_changes => {"title"=>[nil, "demo"]} >> s.update_attributes(:title => "new title", :current_page => 2) => true >> s.audited_changes => {"title"=>["demo", "new title"], "current_page"=>[nil, 2]} >> s.update_attributes(:current_page => 3, :action => "modified", :changed_by => User.create(:name => "someone")) => true >> s.audited_changes => {"current_page"=>[2, 3]} >> s.audits.last => #<Auditable::Audit id: 3, auditable_id: 1, auditable_type: "Survey", user_id: 1, user_type: "User", modifications: {"title"=>"new title", "current_page"=>3}, action: "modified", created_at: ...> >> s.audit_tag_with("something memorable") # 最新の audit にタグをつけた。それでは、s を変更してみる # ... # もし、s に何か変更を加えたとしても >> s.audited_changes(:tag => "something memorable") # 上でタグつけたバージョンに対する変更が返ってくる(タグをつけたバージョンとの差分が見られる) # ※ s.audited_changes は、1つ前の audit との変更が返ってくる。 # ほかのフィルターもあって、たとえば、s.audited_changes(:changed_by => some_user, :audit_action => "modified") # (これで以前の some_user の audit との変更が見られる) # ※ これは引数で指定したもの(:tag とか :changed_by)にマッチした audit の一番直近のものを使う。
仕組み
Audit モデル
上で見た、Audit モデルのマイグレーションファイルをこんな感じで用意する感じになる:
class CreateAudits < ActiveRecord::Migration def change create_table :audits do |t| t.belongs_to :auditable, :polymorphic => true t.belongs_to :user, :polymorphic => true t.text :modifications t.string :action t.timestamps end end end
推察できるとおもうけど、audits.modifications
は、監査(証跡)対象の属性のキーと値の表現をシリアライズしたものになる。
誰が、どのアクションで変更したのか?
誰がそのレコードを変更したのかとっておきたいならば、こんなかんじで changed_by 属性にそれを入れてあげればいい:
# `attr_accessor: changed_by` は、gem で Survey クラスで定義される >> @survey.update_attributes(:changed_by => current_user, # あと他に attributes があればそれも) # すると @surveys.audits.last.user は current_user がはいってる # もちろん、changed_by に値を入れて save でも動く。
action
は create
または update
だけ、レコードの操作に対応して入るが、それを仮想的な属性で上書きでき、change_action
でよべる。(change_action
は audit_action
に変更されてる気がする)。
>> @survey.changed_action = "add page" >> @survey.update_attribute :page_count, 2
以上が、自分のできる README ドリブンなアプローチでした。またきます。
TODO
- API を改善する(まだ扱いづらい) -- 良い書き方があったらください
- 提案とフィードバックをもらう
- README の更新
たとえば、今、変更はシリアライズして audits.modifications
カラムに入れているが、複数の監査(証跡)対象をそれぞれ保存したいときはどうしたものか。こういう感じの書き方でサポートしようと思ってる:
# audits.trivial_changes カラム(かんたんに追加できる) にスナップショットを入れておく。 audit :modifications => [:method_1, :method_2], :trivial_changes => [:method_3, :method_4, :method_5]
Contributing
提案を歓迎します。Github で issue をあげてください。
あと、pull require も歓迎です!
- fork する
- feature ブランチをつくる (
git checkout -b my-new-feature
) - 変更をコミットする(
git commit -am 'Added some feature'
) - ブランチに push(
git push origin my-new-feature
) - Pull Request する
rspec でふつうにテストを書く方法しりたい
先日の railsdevcon 以後、ちゃんとテスト書くようにしはじめたものの、何をお手本にしたらいいものかわからず、手探りでテスト書いてるので、ふつうのテストの書きかたを知りたい。
だんだんテストコードじゃないのが増えてくる
テストのヘルパー的メソッドが増えてきたら、/spec/support に別のモジュールでわけるようにしてるけど、こういう使いかたでいいのかなー?
module HogeModelTestHelper FIXTURES = [:hoges] def self.included(closure) closure.extend Helper closure.send :include, Helper closure.send(:fixtures, *FIXTURES) if closure.respond_to?(:fixtures) end module Helper # なんかいろいろ書く end end
spec のほうで呼んでつかう。
describe Hoge do include HogeModelTestHelper context 'ほげふがなとき' do subject { create_hoge } it { should be_valid } # とかなんとか end end
実際にテストしてるのじゃないコードは、わけちゃってる。
携帯からのアクセスとかをテストする方法がわからない
スタブをつかうべきなのかもしれないと思いつつも、jpmobile がいろいろいいかんじにいろいろやってるので、スタブだとちょっと大変かも、と思ってるところ。仕方ないので、インチキしてる。
class ActionController::TestRequest def set_env(key, value) @env[key] = value end end
とか書いて /spec/support においておいて、コントローラのテストでインチキをする。携帯の UID をつかった機能のテストとかで使う。
@request.set_env('HTTP_X_DCMGUID', uid)
あと、UA を偽装した風のリクエストをなげたりするときは、以下のようなインチキをしたりしてる。
module MobileAccessTestHelper private [:get, :put, :post, :delete].each do |http_method| define_method("#{http_method}_by_mobile") do |*args| set_mobile_ua send http_method, *args end end def set_mobile_ua(ua = "DoCoMo/2.0 SH902i(c100;TB;W24H12)") @request.user_agent = ua end end
ナイステスト情報しりたい。
railsdevcon2010
http://railsdevcon.jp/ いってきた。
感想
- おもしろかった!
- 非常に参考になりました(真面目)
- インターネットでは、Rails ってけっこう使われてるのかなーという雰囲気は感じるけど、実際どうなんだろうか、と思ってた。今日行って、実際けっこう使われてる雰囲気を感じて何故かびびった。
- テスト駆動とかわりと普通にやってる、みたいな雰囲気が会場にあって、びびった(テストあんま書けてないですお)
- 自分のまわりではドリコムの onk さんのセッションナイス!みたいな感じだった(内容すごく濃かった)
- rails のカンファレンスとか勉強会含め行くのはじめてだったしテンションあがった
- お金はらってでもいきたいと思った
- オラクルさん太っ腹
- 運営のみなさんおつかれさまでした
以下メモ
ほぼスライドかきうつしてる状態なので、あんま意味ないこれ。
渡米して感じたこと (@masuidrive)
- 渡米の理由→英語ができないから
- 2006 railsconf 行って1週間ほどいたら言ってることわかるようになってきた
- むこうでくらしたら英語できるようになるんじゃね?
- ワイズノット民事再生
- Big Canvas
- seattle.rb
- いろいろあった
- 2年半居たけどしゃべれるようにならなかった
- 身振り手振り、ホワイドボードのスキル伸びる
- 仕様の交渉 (交渉技術、語学で負ける)
- アメリカの ruby/rails 事情
- 英語できてないから、またいけるならいきたい
- 住環境いいし、車社会だけどいい
- 働く環境自由(フレックス、リモート、犬ok... 交渉次第)
- 家で夕食後に働いてるひとおおい(家で)
- いつクビになるかわからない、でも就職先は沢山ある(らしい)
- 一度海外でると日本に縛られない(日本じゃなくてもいいじゃんおもう)
- rails 世界で通用する
- Appcelerator(Titanium Mobile)
- rails 検定の問題つくってる
rails 情報源の歩きかた (@knsmr)
- @IT 編集部
- 英語だと情報いっぱいある
- 公式サイト、screencast公式ブログ、edgeRails、公式レポジトリ
- プラグイン、ウォッチ数200くらいこえてるといいかんじ
- プラグインナイスなのは? → Asakusa.rb, rubygems.orb, ruby tools ...
- railscasts (asciicasts)
- Gregg の動画ナイス(クオリティたかい)
- RailsTutorial.org (html, pdf, 動画)
- rails wiki
- Confreaks (カンファレンス情報)
- カンファレンス参加するのがいい
- yehuda katz → jquery やりたいらしい
- StackOverflow よくぐぐるとでてくる(けっこうピンポイント)
- rails hub (12/22)
とあるソーシャルアプリの開発運用 (@onk)
課題
- API 利用
- 人口爆発
- 負荷
- アクセス数
- 巨大SNSだからスモールスタートさせてくれない
- 週10万ユーザ、月で30万ユーザとか
- データ量
- 1 click で1レコード増えるアクション(あしあととか)
- db なんとかする
- 更新系(マスタ)、参照(slave)
- マゾヒズムの gem 利用(マスタ/スレーブという関係…)
- クエリキャッシュの罠とかある(マスタ更新したのにスレーブでデータ古いとか)
- db縦分割
- リレーションで使えない(使わない)
- 横分割
- sharding octopus(3対応してる)
- MongoDB で解決?
- 負荷テスト
- 手抜きしたところはたいていボトルネックになる
- count → カウンタキャッシュ, like やらないなど
- 正しく web アプリをつくる
- エンジニアの甘えゆるさない
- アクセス数
- 負荷
- 短納期
Railsプロジェクトを成功させるために現場ができること (@ukstudio)
- タイトルの成功 → なに → お客さんが満足
- 要望とそれをコードなどで実現するぶぶん をどれだけはやく、高品質にできるか
- 技術的負債はいつか返さないといけない
- バグ対くりかえすプロジェクトは負債の利子返すだけでいっぱいいっぱい(自転車操業)
- 開発をいそいでしまう原因(仕様かわる、納期はやまる、あとなんか1つ)
- 技術的負債をなくす!
- テスト書こう
- テストないと変更時こまる → 負債
- テスト駆動(設計含む意味)
- モジュール間の依存をなるべくへらす
- リファクタリングしよう
- 返せる負債はすぐ返す
- 後ではかえせない(後になったら他のタスクにおわれてる)
- 未解決の負債の解決に週の20%(とかいくらか)をあてるなどするって決める
- バージョン管理
- git でブランチきってリファクタリング
- テストの資産価値
- 上位のテストで動作を保証
- 下位のテストの変更を自由に
- cuke とかで要求仕様にそってればユニットテストすてられる
- テストコード、どやってのこしてくか
- 書いてるひとの想定外のテストは書けない(設計能力に依存)
- レビュー、設計の勉強とかはやはり必要
- スキニーコントローラ、ファットモデル
- 負債とスケジュール、トレードオフ
- 営業、機能ふやしがちで使われない機能できたりして負債(パッケージとかだととくに)
- お客の要求をちゃんと理解してないと使わない機能できたりして負債が
現実の世界で "はじめる!Cucumber" (@moro)
- Cucumber について
- ドキュメント(お客読める)をテストとして実行できる ナイス
- 現実
- 日本語の情報 → あります
- 達人出版の cuke 本
- 今のプロジェクトに適用する作戦が思いつかない
- 簡単なところから少しずつ
- GET / (トップページからやってみる、テストの練習としても)
- そこからリンクたどってみる
- POST してみる
- 今やってるタスクのテスト書いてみる(関心あることのほうが書ける)
- 簡単なところから少しずつ
- 日本語のシナリオの使いみち
- よみましょう(よみやすい日本語だし)
- 実行できなくてもよみやすい振舞(読んだひとがイメージできる)を書く(だいじ)
- 対話のきっかけに(イメージしりたいお客、タスク見積たいメンバ、実装の手掛かりわかりたい自分)
- そとからテスト書いて実装してテストパスしてくのが正しい一歩?
- それっぽい cuke, spec, 実装, テストパス…みたいなとこからはじめる
- 最終的になにしたいのか、という振舞書けば、何すればいいのか、自分の理解の助けになる(だいじ)
- 日本語の情報 → あります
- うごくようにするには
- 最低限のエラーとって、表記揺れ正して、ステップ定義追加して、ちょこちょこ実装、テストの修正
- cucumber のリズム
- プロダクトオーナーと会話、やることおおまかにきめる
- cuke かきながらふるまい考える(かきながらかんがえる)
- rspec かく
- 実装する
- cuke とおす
- テストのバリエーションふやしたり、つめたり
- お客に見せるか? → 見せる
Rails Add-onsで楽々開発 - youRoomを題材に - (@mat_aki)
- skip のひと(SonicGarden アジャイルなところ)
- rails addons → サービスやプラグイン
- youroom
- youroom つかってるものなど紹介
- cuke
- グリーンならリリースできる
- 低コストな品質保証
- 肝心なところは徹底的して書く(ユーザ登録みたいなの)、バリデーションひっかかる系とかも
- ajax つかってる部分のテストがむずかしい
- OmniAuth
- SearchLogic
- plugin
- 検索機能をシンプル・DRY に
- activerecord 拡張してるので rails バージョンアップで不具合おこりうる(おきた)
- google docs (gdata api)
- アプリケーション以外のデータ(ユーザ数のスナップショット)
- アプリケーションにはアプリケーションのデータだけ
- github
- 有料なプライベートリポジトリのフォークは無料でプライベートに(ウラワザ)
- amazon
- hoptoad
- アプリケーションエラー通知/管理サービス
- web app のエラーだけじゃなくて、cron や delayed_job のエラーも収集するように工夫した
- batch logger, notifier 的なかんじかなー?
- pivotal tracker
- アジャイル開発向け its
- うえから順に優先度
- yourooom
- グループの情報共有、どこからでもアクセスできてスッピードアップ
- API もあるので、ここで一括して情報をあつかえる
- ドッグフードを食う的ないみもあるのかなー?
- cuke
- プラグイン、サービスを利用して、低コスト、高速、高品質な開発
初めてがRuby (パネルディカッション)
- 紹介など
- 教育関係
- http://www.rubyworld-conf.org/ja/archives/2009/program/abstract/a-2/
- liverevolution (amatsuda さん, yuumi3 による教育)
- grooves R&D (amatsuda さんによる教育。ペアプロとかアジャイルためしたりとか、ラボの時間あったりとか、読書会あったりとか)
- 未経験から教育(うらやましい)。
- 松田さん → 新人だったひとたちに質問
OS X でなんか Ruby の openssl のエラーでて困ったとき
ねぎ式: rubyでSSLを使った接続を行うとcertificate verify failedと言われてしまう。 のとおり、そのまま、Debianの/etc/ssl/certsからごっそりファイルを持ってきて/opt/local/etc/openssl/certs/にコピーした
らエラーでなくなった。正解よくわからない。
Ruby で del.icio.us からはてなブックマークへデータを移行する感じのスクリプト
100に満たない数のブックマークでは一応動いてる感じだったけど、ちゃんと動くかは保証できない。
参考にしたところ
- del.icio.us からはてなブックマークへデータを移行できたのではてなブックマークに移転したいと思います - NaN days - subtech
- はてなフォトライフのAtomAPIをRubyでほげほげしてみた - Post-itみたいな
CPAN の使いかたがわからなかった…。なので Ruby。
my $cache = file('delicious.xml');
の部分がよくわからなかったので、いったんファイルに書きだしてみた。
要 rubygems, atomutil なので、sudo gem install atomutil
とかやって atomutil を導入しておく必要があります。atomutil については Atompub を。
あとよくわからないけど、Bad Content Type: application/x.atom+xml; charset=utf-8
とか言われるけど、スルーした。
#!/usr/bin/env ruby require 'open-uri' require 'rexml/document' require 'rubygems' require 'atomutil' hatena_user = '<hatena username>' hatena_pass = '<hatena password>' del_user = '<del.icio.us username>' del_pass = '<del.icio.us password>' del_api = 'https://api.del.icio.us/v1/posts/all' hatena_api = 'http://b.hatena.ne.jp/atom/post' file = 'delicious.xml' unless File.exist?(file) open(del_api, :http_basic_authentication => [del_user, del_pass]) do |io| open(file, 'w'){|f| f.write(io.read) } end end content = open(file){|f| f.read } doc = REXML::Document.new(content) posts = doc.elements.to_a('posts/post') auth = Atompub::Auth::Wsse.new(:username => hatena_user, :password => hatena_pass) hatena = Atompub::Client.new(:auth => auth) posts.reverse.each do |post| href = post.attributes['href'] tag = "[" + post.attributes['tag'].split(' ').join('][') + ']' unless post.attributes['tag'].empty? extended = post.attributes['extended'] summary = tag.to_s + extended link = Atom::Link.new( :rel => 'related', :type => 'text/html', :href => href ) entry = Atom::Entry.new( :title => 'dummy', :link => link, :summary => summary ) hatena.create_entry(hatena_api, entry) print "posted: #{href}\n" end
もしも、なんかブックマークの登録のときミスって再実行しようとしたとき、また delicious にとりにいかないように、delicious.xml というファイル作ってるんだけど、つまりこのファイルがあると最新の delicious のデータじゃなくてローカルにあるデータを使う。なので最新のデータとってきて使いたい場合は delicious.xml を消す必要がある。あとインポート終わったら delicious.xml はいらないので消して。