プログラマとプロマネのあいだ

プログラマもやるし、プロマネもやるし、たまに似非アーキとか営業っぽいこともやる

「RailsによるアジャイルWebアプリケーション開発」18章読み中

Active Recordその2
今度はテーブル間のリレーションシップに関する話題です。
長いので一旦書く。

1対1のリレーションシップ

ここでいきなり、これまでに出てきていないInvoiceモデルが登場します。
この程度はわざわざちゃんと説明しなくても理解しろよってことか?


ここでは真面目にやってみる。
とりあえずInvoiceモデルを作ってみる。

>ruby script/generate model invoice
ruby script/generate model invoice
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/invoice.rb
      create  test/unit/invoice_test.rb
      create  test/fixtures/invoices.yml
      exists  db/migrate
      create  db/migrate/010_create_invoices.rb

>

で、db/migrate/010_create_invoices.rbを編集

class CreateInvoices < ActiveRecord::Migration
  def self.up
    create_table :invoices do |t|
      t.column :order_id,  :integer
      t.column :name, 	   :string

      t.timestamps
    end
  end

  def self.down
    drop_table :invoices
  end
end

2カラム追加しました。
order_idは、Orderモデルへの外部キー、nameは請求先の名前です。


で、早速マイグレート。

>rake db:migrate
rake db:migrate
(in c:/InsRails/rails_apps/depot)
== 9 Irreversible: migrating ==================================================
== 9 Irreversible: migrated (0.0000s) =========================================

== 10 CreateInvoices: migrating ===============================================
-- create_table(:invoices)
   -> 0.3120s
== 10 CreateInvoices: migrated (0.3120s) ======================================


>

009のマイグレートは前回仕込んだIrreversibleのワナがあったので、それを除去した上でやりました。無事行ったようです。


で、ここからが本題。
まず、Orderモデルにhas_oneを追加します。

class Order < ActiveRecord::Base
  has_one :invoice
end

次にInvoiceモデルにbelongs_toを追加します。

class Invoice < ActiveRecord::Base
  belongs_to :order
end

これでつながったはず。
ちゃんと動くかどうかテストコードを書いてみる。

require 'rubygems'
require 'activerecord'

ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => "../db/development.sqlite3")

class Order < ActiveRecord::Base
  has_one :invoice
end

class Invoice < ActiveRecord::Base
  belongs_to :order
end

order1 = Order.create(
	:name => "taka_2",
	:address => "Kanagawa prefecture",
	:email => "taka_2@test.com",
	:payment_type_id => 1)

Invoice.create(
	:order_id => order1.id,
	:name => "taka_22")

p order1

p order1.invoice

# 後始末
Invoice.delete(order1.invoice.id)

Order.delete(order1.id)

結果はこう。

#<Order id: 22, name: "taka_2", address: "Kanagawa prefecture", email: "taka_2@test.com", created_at: "2008-06-15 10:38:39", updated_at: "2008-06-15 10:38:39", payment_type_id: 1>
#<Invoice id: 2, order_id: 22, name: "taka_22", created_at: "2008-06-15 10:38:39", updated_at: "2008-06-15 10:38:39">

無事できました。
createメソッドには、idは指定できないのですね。。
こんなエラーが出てハマりました。

C:/InsRails/ruby/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:2313:in `remove_attributes_protected_from_mass_assignment': undefined method `debug' for nil:NilClass (NoMethodError)
	from C:/InsRails/ruby/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:2114:in `attributes='
	from C:/InsRails/ruby/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:1926:in `initialize'
	from C:/InsRails/ruby/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:571:in `new'
	from C:/InsRails/ruby/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:571:in `create'
	from ar5.rb:14

nil:NilClassにdebugメソッドが無いというエラーのようですが、これでバグ探せって言われても無理があります。。

1対多のリレーションシップ

これは10章でやっているので飛ばし。

多対多のリレーションシップ

ここでもまた新しいモデルが登場します。
ので、早速作ってみる。

>ruby script/generate model category
ruby script/generate model category
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/category.rb
      create  test/unit/category_test.rb
      create  test/fixtures/categories.yml
      exists  db/migrate
      create  db/migrate/011_create_categories.rb

>ruby script/generate migration categories_products
ruby script/generate migration categories_products
      exists  db/migrate
      create  db/migrate/012_categories_products.rb

>

まずは、011_create_categories.rbの編集。

class CreateCategories < ActiveRecord::Migration
  def self.up
    create_table :categories do |t|
      t.column :name, :string

      t.timestamps
    end
  end

  def self.down
    drop_table :categories
  end
end

name列を追加しました。


次に、012_categories_products.rbの編集。

class CategoriesProducts < ActiveRecord::Migration
  def self.up
    create_table :categories_products, :id => false do |t|
      t.column :category_id, :integer, :null => false
      t.column :product_id, :integer, :null => false
    end
  end

  def self.down
    drop_table :categories_products
  end
end

結合テーブルなので、id列を作らないようにしているのがミソですな。


でもってマイグレート

>rake db:migrate
rake db:migrate
(in c:/InsRails/rails_apps/depot)
== 11 CreateCategories: migrating =============================================
-- create_table(:categories)
   -> 0.1400s
== 11 CreateCategories: migrated (0.1400s) ====================================

== 12 CategoriesProducts: migrating ===========================================
-- create_table(:categories_products, {:id=>false})
   -> 0.1870s
== 12 CategoriesProducts: migrated (0.1870s) ==================================


>

試しにsqlite3コマンドでcategories_productsテーブルのレイアウトを見てみる。

.schema categories_products
CREATE TABLE categories_products ("category_id" integer NOT NULL, "product_id" integer NOT NULL);

ちゃんとid列無しで作られていますね。


で、ここからが本題。
まず、Categoryモデルにhas_and_belongs_to_manyを追加します。

class Category < ActiveRecord::Base
  has_and_belongs_to_many :products
end

次にProductモデルに同じくhas_and_belongss_to_manyを追加します。

class Product < ActiveRecord::Base
  has_and_belongs_to_many: categories
end

これでつながったはず。
ちゃんと動くかどうかテストコードを書いてみる。

require 'rubygems'
require 'activerecord'

ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => "../db/development.sqlite3")

class Category < ActiveRecord::Base
  has_and_belongs_to_many :products
end

class Product < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

p1 = Product.create(
   :title => "Google Hacks",
   :description => "Google Hacks second edition",
   :image_url => "gh.png",
   :price => 3200
)

c1 = Category.create(
   :name => "Web Technique"
)

c2 = Category.create(
   :name => "O'reilly"
)

p1.categories << c1
p1.categories << c2

p1.categories.each { |c|
  puts(c.name)
  puts(c.products[0].title)
}

# 後始末
# 関連テーブルも行削除するためには、この行が必要。
p1.categories.clear()
Product.delete(p1)

Category.delete(c1)
Category.delete(c2)

結果はこう。

Web Technique
Google Hacks
O'reilly
Google Hacks

というわけで、無事相互参照できてそう。

最初関連テーブルのレコードが削除されなかったのですが、
配列をクリアしてからdeleteすることで削除されました。