基本はasakusa.rbによる記事を見ればok。
デフォルトのバージョンを指定する
useサブコマンドに–defaultオプションを付ける。
rvm use 1.8.7 --default
gemsetを使う
rvm use 1.8.7 rvm gemset create rails222 rvm use 1.8.7%rails222 gem install rails -v 2.2.2 gem list
help
rvm help
基本はasakusa.rbによる記事を見ればok。
useサブコマンドに–defaultオプションを付ける。
rvm use 1.8.7 --default
rvm use 1.8.7 rvm gemset create rails222 rvm use 1.8.7%rails222 gem install rails -v 2.2.2 gem list
rvm help
Machinistは定義された条件下でテストデータを生成するプラグイン・gem。 DatasetはRubyのコードで記述したテストデータをDBに読み込むプラグイン・gem。
この2つを組み合わせることで(比較的)メンテナンスのしやすいfixtureの代替を構築することが可能になる。 流れとしては、Dataset上でMachinistを呼び出して複数のデータを生成して、それをDBに流し込むという感じ。
メリットは、
デメリットは、
最終結果のコードをGithubにあげたので、そちらも参考に。
Machinist、Dataset、そして適当な文字列のデータを作ってくれるFakerをインストール・設定する。なおテストフレームワークにはRSpec on Railsを用いる。
YAMLに頼らないテストデータの作成を参考にさせていただきました。
$ cd RAILS_ROOT $ ruby script/plugin install git://github.com/notahat/machinist.git $ vi spec/blueprints.rb # 新規作成/下記参照 $ vi spec/spec_helper.rb # 下記参照
RAILS_ROOT/spec/blueprints.rbに追記:
require 'machinist/active_record' require 'sham'
RAILS_ROOT/spec/spec_helper.rbに追記:
# 冒頭のほうに記述
require File.expand_path(File.dirname(__FILE__) + "/blueprints")
# Spec::Runner.configureブロック内に以下を記述
# -> Shamで1度生成したデータはリスト形式でキャッシュされ、
# 以降はbeforeが呼ばれるたびにその先頭から順に取り出すようになる
config.before(:all) { Sham.reset(:before_all) }
config.before(:each) { Sham.reset(:before_each) }
$ cd RAILS_ROOT $ ruby script/plugin install git://github.com/aiwilliams/dataset.git $ mkdir spec/datasets $ vi spec/spec_helper.rb # 下記参照
RAILS_ROOT/spec/spec_helper.rbに追記
require 'dataset'
class Test::Unit::TestCase
include Dataset
datasets_directory "#{RAILS_ROOT}/spec/datasets"
end
$ sudo gem install faker
Fakerについては取り扱われた記事が複数あり、RDocだけでも十分な気がするので、今回は特に説明を行わない。
Machinistではモデルのテストデータのひな形(blueprint)を定義し、そこからテストデータをランダムで生成する。デザインパターン的にはファクトリパターンを活用していることになる。
以下の例に用いるモデルとしてPersonとGroupを考え、Group has_many Personという関係があるとする。
Groupは次のような属性値を持つ(timestampは省略):
| カラム名 | 型 | |
|---|---|---|
| PK | id | integer |
| name | string | |
| hidden | boolean |
Personは次のような属性値を持つ(timestampは省略):
| カラム名 | 型 | |
|---|---|---|
| PK | id | integer |
| name | string | |
| FK | group_id | integer |
| hidden | boolean |
MachinistではRAILS_ROOT/spec/blueprint.rbでモデルごとにテストデータのひな形を定義する。
まず、属性値に使われるダミーデータの条件をMachinistに同梱されたShamを使って定義する。その際のフォーマットは次のようになる:
Sham.データ名 { データ生成メソッド }
このデータ名はモデルの属性名と一致する必要は無い(使い回しが可能)。 次に例を示す。
Sham.person_name { Faker::Name.name } # Faker::XXX.xxxは自動的にランダム生成してくれる
Sham.group_name { Faker::Company.catch_phrase }
Sham.age { rand(100) } # 自分でデータを用意するときは、適当にrand関数を使う
# Shamはダミーデータを生成するときに、デフォルトではユニークになるように調整する
# ユニークな必要が無い・それだとデータ数が足りない場合には:uniqueオプションでfalseを設定して重複を許可する
Sham.boolean(:unique => false) { [true, false].rand }
上記は次のようにまとめて定義することもできる。
Sham.define do
person_name { Faker::Name.name } # Group#nameとPerson#nameは似ているけど別物なので
group_name { Faker::Company.catch_phrase } # 別個に定義しておく
age { rand(100) }
boolean(:unique => false) { [true, false].rand } # boolean型は使い回しが効くので、一般的な名前をつけておく
end
続いて、モデルのテストデータのひな形は次のように定義する。
Group.blueprint do
name { Sham.group_name } #=> データ生成時にはSham.group_nameで定義したFaker::Company.catch_phraseが呼び出される
hidden { Sham.boolean }
end
Person.blueprint do
name { Sham.person_name }
age #=> Sham中に同じデータ名があればそれが利用される(ここではSham.ageが呼ばれる)
hidden { Sham.boolean }
group { Group.make } #=> ここがキモ!(後述)
end
Machinistではモデルにmakeというメソッドが追加されており、このメソッドでblueprintの定義に従ってテストデータを生成・DBに保存する。
そして、Person.blueprint中のgroup { Group.make }部分では次の二つを実行するように指定している:
このおかげでbelongs_toが自動的に生成されるため、ちょっとリレーションの設定が楽になる。 ただしhas_one/has_many関係は別途生成する必要がある(きっとあとで書く)。
最終的に次のようなRAILS_ROOT/spec/blueprint.rbになる。
require 'machinist/active_record'
require 'sham'
require 'faker'
Sham.define do
person_name { Faker::Name.name }
group_name { Faker::Company.catch_phrase }
age { rand(100) }
boolean(:unique => false) { [true, false].rand }
end
Group.blueprint do
name { Sham.group_name }
hidden { Sham.boolean }
end
Person.blueprint do
name { Sham.person_name }
age
hidden { Sham.boolean }
group { Group.make }
end
下準備の段階でMachinistに関するファイルは読み込まれるようになっているので、特にspecファイル中でrequireを行う必要は無い。
以下の3つのメソッドを利用してテストデータを生成して利用する:
| メソッド名 | 概要 |
|---|---|
Person.make |
Personをblueprintに従って生成し、DBに保存したのちにそのインスタンスを返す(Person.createに相当) |
Person.make_unsaved |
Personをblueprintに従って生成し、DBに保存せずにそのインスタンスを返す(Person.newに相当) |
Person.plan |
Personの属性値をblueprintに従って生成し、それをHashで返す(:idは含まない) |
blueprintの定義を一部上書きしてデータを生成したい場合には、次のように各メソッドの引数にHashで指定する:
grand_father = Person.make(:age => 200)
場合によっては、通常の属性値とは異なるパターンの属性値を持ったテストデータを作りたくなることがある。 その場合には、次のようにblueprintの先頭に引数でパターンの名前を付けて別個に定義する。
次の例ではPersonの別blueprintとして:wizardというものを定義しており、:ageプロパティが1000より大きくなるように定義している。
Person.blueprint(:wizard) do
age { Sham.age + 1000 }
end
名前の省略されているblueprintの名前は:masterとなり、 省略された属性値(ここではname, hidden, group)は:masterのものと同じ条件で値を生成される。 つまり、名前付きblueprintは:masterのblueprintを継承し、属性値の定義を上書きしたものである。
Datasetはfixtureの代わりに、Rubyのコードで定義したデータをDBに流し込むというものである。
以下の例に用いるモデルとしてPersonとGroupを考え、Group has_many Personという関係があるとする。
Groupは次のような属性値を持つ(timestampは省略):
| カラム名 | 型 | |
|---|---|---|
| PK | id | integer |
| name | string | |
| hidden | boolean |
Personは次のような属性値を持つ(timestampは省略):
| カラム名 | 型 | |
|---|---|---|
| PK | id | integer |
| name | string | |
| FK | group_id | integer |
| hidden | boolean |
モデルごとに”RAILS_ROOT/spec/datasets/テーブル名_dataset.rb”というファイルを作り、そこに次のような記述をする。
class テーブル名Dataset < Dataset::Base def load create_record モデル名, データ名, 属性値のハッシュ end end
モデル名、データ名はStringでもSymbolでも構わない。 属性値のハッシュには:idを指定することもできる。
例えばPersonのテストデータRAILS_ROOT/spec/datasets/people_dataset.rbは次のように作る:
class PeopleDataset < Dataset::Base def load create_record :person, :person1, :name => "Bob", :age => 18, :hidden => false, :group_id => 1 create_record :person, :person2, :name => "Anthony", :age => 17, :hidden => false, :group_id => nil end end
これは、RAILS_ROOT/spec/fixtures/people.ymlに次のような記述をしたものとほとんど等しい:
person1: name: "Bob" age: 18 hidden: false group_id: 1 person2: name: "Anthony" age: 17 hidden: false group_id: null
このままではDatasetを使うメリットがない(逆に言えばfixtureと同じ使い方も可能であるということ)。 しかし、datasetファイルはRubyで記述するのでループを使えば大量のデータを少ない行数で生成できる:
require 'faker'
class PeopleDataset < Dataset::Base
def load
group_ids = Group.all.map { |group| group.id }
10.times do |i|
create_record :person, "person#{i}", :name => Faker::Name.name, :age => rand(100), :hidden => [true, false].rand, :group_id => group_ids.rand
end
end
end
実際この程度のことならfixtureをerbで記述することでも可能だけど、 erbをパースしてそれをさらにYAMLでパースして…とやるよりは効率は良いと思われる。
使い方は単純で、fixturesメソッドの代わりにdatasetメソッドを利用すればいい:
describe Person dataset :people # fixtures :people : :
あとはfixtureとほとんど同じで、people(:person1)というようにデータ名を指定して呼び出すこともできる(はず)。
MachinistとDatasetを組み合わせて使うことで、リレーションが構築されたテストデータを(fixtureほど)意識せずに利用できる。
「下準備」で書いたように、Shamは生成データをリストでキャッシュし、以降はbeforeが呼ばれるたびにそのリストの先頭から順に値を呼び出す。 このため、specファイルからmakeやplanを呼び出すときにはdatasetでDBに保存されているものと同じデータをキャッシュから読み出してしまい、 ユニーク制約を付けている属性値などを取り扱う際にエラーが発生する。 これを回避するために、datasetを読み込む際に生成するテストデータに「datasetで読み込んだデータである」という しるし を、名前付きblueprintを使ってつける。
今回の例ではGroup#nameとPerson#nameにユニーク制約がかかっているとして、datasetから読み込むときには:datasetという名前付きblueprintを使って、 それぞれの値の末尾に"(dataset)"というしるしをつけるとする:
Group.blueprint(:dataset) do
name { Sham.group_name + "(dataset)" }
end
Person.blueprint(:dataset) do
name { Sham.person_name + "(dataset)" }
group { Group.make(:dataset) } # ここ大事!
end
datasetから生成したデータのnameには必ず(dataset)という文字列をつけなければならないので、 PersonからGroupをmakeしたときにも:datasetのblueprintを使用するように設定する。
こうすることで、datasetから生成する場合には:datasetと名付けられたblueprintが利用され、 specファイルからは今まで通り引数無しで:masterのblueprintを利用することになる。
例えば、Personのテストデータを作りたいときには、RAILS_ROOT/spec/datasets/people_dataset.rbに次のように記述する:
class PeopleDataset < Dataset::Base
def load
10.times do |i|
create_record :person, "person#{i}", Person.plan(:dataset) # さきほど定義したdataset用のblueprintを指定
end
end
end
Person.plan時にリレーション先のGroupも生成・保存されるため、これだけで10個のPersonとそれぞれに対応したGroupのデータを作ることができる。
逆にGroupのテストデータを作りたいときには、RAILS_ROOT/spec/datasets/groups_dataset.rbに次のように記述する:
class GroupsDataset < Dataset::Base
def load
3.times do |i|
group_id = create_record :group, "group#{i}", Group.plan(:dataset) # さきほど定義したdataset用のblueprintを指定
# Group.plan時にはリレーションが張ってあるPersonは生成されない(blueprintを参照)ため、
# 別個に生成を行う必要がある
5.times do |j|
# さきほど定義したdataset用のblueprintを指定
create_record :person, "person#{j}_group{i}", Person.plan(:dataset, :group_id => group_id)
end
end
end
end
ここでcreate_recordではなく、Machinistで用意されるmake等を使っても問題は無いと思われる。
このようにMachinistとDatasetを組み合わせることで、リレーションが自動的に構築されたテストデータを生成することが可能になる。
specファイルからdatasetファイルを利用するには、次のように記述すればよい:
describe Person dataset :people # fixtures :people, :groups : :
fixturesでは:peopleと:groupsの両方を指定していたが、datasetでは:peopleのみを指定している。 これはpeople_dataset.rbを読み込めば、PersonだけででなくGroupのテストデータも読み込まれるためである。 ただし、groups_dataset.rbの値も読み込みたい場合には、続けて:gorupsを指定すればよい(なにかしらバッティングが生じるかもしれない)。
また、specファイルからはMachinistのmake、make_unsaved、planが利用できるので、 テストデータが必要な場合にはそれらを呼べば良く、毎回適当な値を考える必要は無い。
acts_as_paranoidを使うことでレコードの論理削除が容易になる。さらにリレーションが設定されているときには関連する論理削除も行われるように設定できる。
けど、どのメソッドがどのような挙動を示すのかがいまいち不鮮明だったので、ざっくりと調べてみた。
GroupとPersonが1対多のリレーションを持っているとする(多対多の場合も同様だと思う)。
class Group < ActiveRecord::Base
acts_as_paranoid
has_many :people, :dependent => :destroy
end
:dependent => :destroyは、レコードの削除時にリレーション先のレコードをどう処理するかを指定するオプションで、:destroyを指定するとリレーション先のレコードに対してdestroyメソッドを実行する。詳細はActiveRecord::Associations::ClassMethodsを参照。
class Person < ActiveRecord::Base
acts_as_paranoid
belongs_to :group
end
Groupに対して削除系メソッドを実行したときに、それと関連するPersonがどうなるかを調べた。
なお、"!"がついているメソッドは破壊的メソッドなので、物理削除(DELETE文)が実行される。
| メソッド名 | Group | 関連するPerson |
|---|---|---|
| Group.delete(id) | 論理削除 | 変化無し |
| Group.destroy(id) | 論理削除 | 論理削除 |
| Group.delete_all | 論理削除 | 変化無し |
| Group.delete_all! | 物理削除 | 変化無し |
| Group.destroy_all | 論理削除 | 論理削除 |
クラスメソッドはnamed_scopeなどとともにチェインすることが可能。
| メソッド名 | Group | 関連するPerson |
|---|---|---|
| Group#delete | 論理削除 | 変化無し |
| Group#destroy | 論理削除 | 論理削除 |
| Group#destroy! | 物理削除 | 論理削除 |
Group#destroy!の挙動はちょっと中途半端。Personも含めて完全に物理削除したい場合は次のようにすれば良い。
group = Group.first
group.people.delete_all!
group.destroy!
jrubyでrailsを動かす際の、導入手順のメモ。DBはMySQL。
sudo port install jruby vi ~/.zshrc > export JRUBY_HOME=/opt/local/share/java/jruby source ~/.zshrc sudo jgem install rails # JDBC用のドライバをインストール # 詳しくは http://jruby-extras.rubyforge.org/activerecord-jdbc-adapter/ sudo jgem install activerecord-jdbc-adapter # 下のmysql用アダプタだけで大丈夫かも sudo jgem install activerecord-jdbcmysql-adapter sudo ln -s $JRUBY_HOME/bin/rails /opt/local/bin/jrails # 既存のものがあるかも jrails -d mysql app_name cd app_name jruby script/generate jdbc vi config/database.yml > development: > adapter: jdbcmysql # mysqlから編集 > username: app_name > password: > hostname: localhost > database: app_name_development jruby script/generate scaffold memo title:string body:text jrake db:create jrake db:migrate jruby script/server
gitの操作(addやstatus)で特定のファイルを無視するには、リポジトリのルートディレクトリに.gitignoreファイルを設定すればよい。
ただし以下の点に注意。
以下、自分の設定内容をメモっておく。
*.DS_Store profile build/* *.pbxuser *.mode1v3
buildディレクトリ以下の更新内容は無視して構わない(と以前どこかで読んだ)。あとはnib/xib以下にできるバックアップファイルとか、Finderの設定ファイルとか。
Stack Overflowにもいろいろな例があがっている(そこからgistを作った人もいる)。
Railsアプリをgitで管理するときのやり方を参考にするとよさそう。
~/.irbrcに、irbを起動したときに読み込むスクリプトを記述できる。さらにこの設定はRailsのscript/consoleでも有効になる。詳しくはirbとscript/consoleの超便利なTipsなどを参照。
というわけで、自分の今の設定をメモしておく。
require 'rubygems'
require 'wirble'
require 'pp'
Wirble.init
Wirble.colorize
# Log to STDOUT if in Rails
if ENV.include?('RAILS_ENV') && !Object.const_defined?('RAILS_DEFAULT_LOGGER')
require 'logger'
RAILS_DEFAULT_LOGGER = Logger.new(STDOUT)
end
以前の記事が恐ろしく分かりにくいので、軽くまとめ直す。
module A
def instance_method_of_a
puts "A#instance_method_of_a was called."
end
module ClassMethods
# A::ClassMethodsで定義されたインスタンスメソッドは、
# Aをincludeしたクラスのクラスメソッドとして利用できる
def class_method_of_a
puts "A::ClassMethods.class_method_of_a was called. Not A.a_class_method!"
end
end
def self.included(mod)
# ModuleのインスタンスmodがAをincludeした際に呼び出され、
# A::ClassMethodsのインスタンスメソッドをmodに特異メソッドとして追加する。
mod.extend ClassMethods
end
end
class B
include A
end
使ってみると、
B.class_method_of_a # A::ClassMethods.class_method_of_a was called. Not A.class_method_of_a! b = B.new b.instance_method_of_a # A#instance_method_of_a was called.
これを上手く使って機能ごとにモジュールで名前空間を設定し、かつファイルも分割できれば開発しやすいだろうなぁと思う。Railsがまさにそんな感じ。その代わり管理が大変だけど。
次のようなFooモジュールの特異メソッドbarをBazモジュールに特異メソッドとして使えるようにしたい場合、BazでFooをincludeするだけでは不十分。これは、includeによって追加されるのが、Fooのインスタンスメソッドのみだから。
module Foo
def bar
puts "bar"
end
end
module Baz
include Foo
end
なので、次のように細工をしてやる。
module Foo
# Module#extendのためにインスタンスメソッドにする
def bar
puts "bar"
end
# self(この場合はFoo)が他のモジュール(仮引数mod == Baz)にincludeされた際に呼ばれる
def self.included(mod)
# modにself(この場合はFoo)のインスタンスメソッドを特異メソッドとして追加する。
# mod == Bazだから、つまりBazにクラスメソッドを追加することになる
mod.extend self
end
end
module Baz
include Foo
end
このように、Module.includedというフックメソッドを用いて、includeされた際にModule.extendでインスタンスメソッドを特異メソッドとして追加してやればよい。
このようなやり方は、ActiveRecord::Validations::ClassMethodsをActiveRecord::Baseから利用できるようにする際などに利用されている。
上記の方法だと、Baz#bar(Bazのインスタンスメソッド)とBaz.bar(クラスメソッド)の両方が定義される。Bazをモジュールではなくクラスとして作成した場合には注意が必要となる。
一応、以下のようにすることでBaz#barを未定義にできる。
module Baz
include Foo
# Fooのメソッドを直接指定して未定義とする
# 直接指定なあたりがちょっとかっこわるい
undef_method :bar
# 当然ながら(?)このあとにbarを再定義すれば、そちらが利用される
# まぁ再定義するぐらいならundef_methodの必要がない気もするけど。
def bar
puts "hello"
end
undef_methodだとスーパークラスでの定義さえもみなくなってしまうので、できればremove_methodがいいんだけど、それだとBaz#barが定義されていないというエラーが出た。
先のBaz#barとBaz.barについて、includeしたせいで両方定義されてしまうというのは、extendだけ行えば済むことかとやっと気づいた。つまり、
module Foo
def bar
puts "bar"
end
end
module Baz
extend Foo
end
ActiveRecord::ValidationsだけでなくActiveRecord::Validations::ClassMethodがあるのは、includeとextendで別個に分けるためだったのか…。
恐ろしく分かりにくい記事なので、書き直した。
named_scopeに引数を渡した場合、その型によって設定するSQLを動的に変更したいことがある。 こういうときにはlambdaに渡しているのブロック中で条件分岐すれば良い。
named_scope :member, lambda { |m|
case m
when Member
{ :conditions => { :member_id => m.id } }
when Integer
{ :conditions => { :member_id => m } }
else
{}
end
}
要するに、lambdaに渡したブロックの処理の結果、findに渡すHashパラメータが返ればそれでok、という感じなんじゃないかと。
Ruby 1.8.7で。
hash.map { |k, v| { :name => k, :count => v } }.sort { |a, b| b[:count] <=> a[:count] }