Posts Tagged: Rails


21
Aug 10

rvmふたたび

ruby 1.9.2が出たのでrvmを入れ直して整理した。

$ gem install --user-install rvm
$ PATH=$HOME/.gem/bin:$PATH
$ rvm-install
$ vi ~/.zshrc # ~/.gem/binをパスに追加、rvmの初期化コードを追加
$ exit

1.9.2と1.8.7をインストールする。 ただしMacPortsで入れたreadlineを使うために今回はオプションを付ける。

$ rvm install 1.8.7 -C "--enable-shared=true,--with-opt-dir=/opt/local"
$ rvm install 1.9.2 -C "--enable-shared=true,--with-opt-dir=/opt/local"

とりあえず1.8.7にRails2.3のgemsetを作り、1.9.2にRails3 RCのgemsetを作る。 そして前者をデフォルト(シェルを立ち上げたときに最初に読み込む)のgemsetとする。

$ rvm use 1.8.7@rails23 --create --default
$ gem install rails -v 2.3.8
$ rvm use 1.9.2@rails3rc --create
$ gem install rails --pre

rails2.3で作ったプロジェクトでは1.8.7@rails23を使いたいので、RAILS_ROOTで次のように.rvmrcを作る。rails3rcのほうも同様。

$ echo "rvm use 1.8.7@rails23" > .rvmrc

17
Aug 10

Rails 2.2.xでsession_idをもとにセッションにデータを突っ込む

例えば、外部にある認証系がこちらのAPIを叩く場合など、セッションが継続してないからsessionからデータを入れられないんだけど、session_idはわかってるからどうにかしたいな、というときに。

今更ながらRails 2.2.2で試してみた。2.3.x以降ではCGIモジュールは破棄されてRackに移行したので、全く変わってる気がする。

class SomeController < ActionController::Base
  session :off
  def index
    session_id = (get session_id ...)
    
    # CgiRequestとRackRequestに対応
    options = request.class.get_const("DEFAULT_SESSION_OPTIONS").merge(:session_id => session_id)
    my_session = CGI::Session.new(request.cgi, options.stringify_keys)
    my_session[:foo] = :bar
    my_session.close
  end
end

7
Jul 10

モジュールでnamed_scopeを追加

モデルの機能ごとにモジュールにまとめてあとからまとめてincludeしたいというときに、どうせならその機能のために用意したちょっと複雑なnamed_scopeも同じように外に出してしまいたい。

そんな場合にはincludedとclass_evalを使う。

class Message < ActiveRecord::Base
  include Extensions::Search
end

module Extensions
  module Search
    def self.included(mod)
      mod.class_eval do
        named_scope :name_like, lambda { |arg| {
          :conditions => ["name LIKE ?", "%%arg%%"]
        }}
      end
    end
  end
end
> Message.name_like("ob").first.name #=> "Bob"

named_scopeも結局はクラスメソッド。この方法はnamed_scope以外にも利用できる。


20
May 10

名前空間付き定数対応のObject.cost_set

Object.const_getをハックした話 を参考に、Object.const_setもモジュールによる名前空間付き定数に対応させてみた。車輪の再発明をした気がしなくはないけど、そこは気にしない。


class Object
  def self.nested_const_get(name)
    stack = (name.is_a?(Array))  ? name : name.split("::")
    klass = Object
    while const = stack.shift
      klass = klass.const_get(const)
    end
    return klass
  end

  def self.nested_const_set(args = {})
    args.each do |name, value|
      stack = (name.is_a?(Array)) ? name : name.to_s.split("::")
      last_const = stack.pop

      klass = nested_const_get(stack)
      klass.const_set(last_const, value)
    end
  end
end

使い方は、

Object.nested_const_set("Foo::Bar" => "foooo")

RSpec: Stubbing RAILS_ENV and other constantsのように、一時的に定数を換えたいという場合に使えると思う。

ただしまだテストはしていない :)


28
Mar 10

非同期処理にはWorkling

Ajaxで非同期に処理をキックして、一定間隔でその進捗を確認して…というのを書く必要があったのでBackgrounDRbを試してみたら、どうも使いにくかった。理由としては、

  • MetaWorkerのサブクラスでワーカを実装したのに、MiddleMan.workerで呼び出したときにはRailsWorkerProxyにすり替わっている
  • キューイングはしないのに、キューイングのためのテーブルを作らないとエラーが出る
  • 名前が好きじゃない
  • 引数のハッシュに:workerが妙に出て来て冗長
  • RSpecでテストしづらい(できなくはない)
  • APIがよくわからない

後半はなんだか個人的な理由だけど、とにかく調べるのに疲れたわりに、あまり実り多い感じではなかった。

で、その代わりに見つけたのがWorkling。これは非同期に処理を呼び出すためのプラグイン。サンプルは、githubのREADMEの通りなので特に書かない。

キューイングサーバが必要ならばStarlingと組み合わせる。これはTwitterでの利用実績もあるらしい

ただ注意する必要があるのは、進捗を保存しておくサンプルでWorkling::Return::Store::MemoryReturnStoreが利用されてるけど、これはそのプロセス内でのみ有効という点(ただのハッシュに値を保存してるだけ)。なので、バックグランドで進行中のところをAjaxで見に行く場合などにはその値にはアクセスできない。別途memcacheなどに値を保存しておく必要がある。

もしかしたらWorkling::Return::Store::StarlingReturnStoreを使えばいいのかもしれないけど、Starlingはキューイングのサーバだしなぁと詳しく把握しきれていなかったので、とりあえずmemcacheに直でアクセスするものを自分で追加した

追記1

調べてみたところ、StarlingReturnStoreはキューと関係なくただ単にmemcacheをラップしてるだけ(より正確には、StarlingReturnStoreはWorkling::Clients::MemcacheQueueClientをラップしていて、こいつはMemCacheを使ってconfig/workling.ymlで設定されたmemcacheサーバと接続してデータのやりとりをしてる)。

なのでStarlingReturnStoreを使うのが手軽で、接続先memcacheサーバの設定をworkling.ymlに外出しできるので絶対的に便利。


15
Mar 10

rvm

基本は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

20
Feb 10

Machinist+DatasetでFixture代替

概要

Machinistは定義された条件下でテストデータを生成するプラグイン・gem。 DatasetはRubyのコードで記述したテストデータをDBに読み込むプラグイン・gem。

この2つを組み合わせることで(比較的)メンテナンスのしやすいfixtureの代替を構築することが可能になる。 流れとしては、Dataset上でMachinistを呼び出して複数のデータを生成して、それをDBに流し込むという感じ。

メリットは、

  • 条件下でランダムなデータを生成してくれる(「特定のデータをfixture上に作って読み込む」というようなことも可能)
  • リレーション先のデータも適宜生成してくれる(素敵!)。

デメリットは、

  • データがランダムなので、全条件を必ずカバーするのは苦手&毎回同じデータが入ってくるとは限らない(回避できるかなぁ)
  • fixtureに依存した書き方をしていると大幅な書き換えが必要かもしれない(レコード数によるテストなど。集計系では難しい…)

最終結果のコードをGithubにあげたので、そちらも参考に。

下準備

MachinistDataset、そして適当な文字列のデータを作ってくれるFakerをインストール・設定する。なおテストフレームワークにはRSpec on Railsを用いる。

YAMLに頼らないテストデータの作成を参考にさせていただきました。

Machinist

$ 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) }

Dataset

$ 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

Faker

$ sudo gem install faker

Fakerについては取り扱われた記事が複数あり、RDocだけでも十分な気がするので、今回は特に説明を行わない。

Machinistでデータのひな形定義・作成

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 }部分では次の二つを実行するように指定している:

  • Personのテストデータが生成されるときに、belongs_to先のGroupをGroup.blueprintで定義された通りに自動的に生成・保存してね
  • ついでにそのGroup#idをさっきPerson#group_idに設定してね。

このおかげで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

specファイルから呼び出す

下準備の段階で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

場合によっては、通常の属性値とは異なるパターンの属性値を持ったテストデータを作りたくなることがある。 その場合には、次のように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でRubyでテストデータを突っ込む

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でパースして…とやるよりは効率は良いと思われる。

specファイルからdatasetを読み込む

使い方は単純で、fixturesメソッドの代わりにdatasetメソッドを利用すればいい:

describe Person
 dataset :people
#  fixtures :people
   :
   :

あとはfixtureとほとんど同じで、people(:person1)というようにデータ名を指定して呼び出すこともできる(はず)。

MachinistとDatasetを組み合わせる

MachinistとDatasetを組み合わせて使うことで、リレーションが構築されたテストデータを(fixtureほど)意識せずに利用できる。

dataset用のblueprintを定義する

「下準備」で書いたように、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を利用することになる。

datasetファイルを作る

例えば、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を組み合わせることで、リレーションが自動的に構築されたテストデータを生成することが可能になる。

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が利用できるので、 テストデータが必要な場合にはそれらを呼べば良く、毎回適当な値を考える必要は無い。

参考資料


11
Feb 10

acts_as_paranoidの挙動

acts_as_paranoidを使うことでレコードの論理削除が容易になる。さらにリレーションが設定されているときには関連する論理削除も行われるように設定できる。

けど、どのメソッドがどのような挙動を示すのかがいまいち不鮮明だったので、ざっくりと調べてみた。

モデル定義

GroupとPersonが1対多のリレーションを持っているとする(多対多の場合も同様だと思う)。

app/models/group.rb


class Group < ActiveRecord::Base
  acts_as_paranoid
  has_many :people, :dependent => :destroy
end

:dependent => :destroyは、レコードの削除時にリレーション先のレコードをどう処理するかを指定するオプションで、:destroyを指定するとリレーション先のレコードに対してdestroyメソッドを実行する。詳細はActiveRecord::Associations::ClassMethodsを参照。

app/models/person.rb


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!

9
Feb 10

jrubyでrails

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

1
Feb 10

Gitで無視するファイルを設定する

gitの操作(addやstatus)で特定のファイルを無視するには、リポジトリのルートディレクトリに.gitignoreファイルを設定すればよい。

ただし以下の点に注意。

  • .gitignore自体をadd/commitしないと効果は現れない
  • 一度commitしたファイルはあとから無視設定をしても効果がない(一旦リポジトリから削除する必要がある)(gitignoreでハマる

以下、自分の設定内容をメモっておく。

Xcodeで作成したプロジェクト

*.DS_Store
profile
build/*
*.pbxuser
*.mode1v3

buildディレクトリ以下の更新内容は無視して構わない(と以前どこかで読んだ)。あとはnib/xib以下にできるバックアップファイルとか、Finderの設定ファイルとか。

Stack Overflowにもいろいろな例があがっている(そこからgistを作った人もいる)。

Rails

Railsアプリをgitで管理するときのやり方を参考にするとよさそう。