views:

115

answers:

3

Is this a relationship that can be described in Ruby on Rails' ActiveRecord model relationships?

   Customer                          Address
   ===================               =========
   Billing_Address_Id  >------}
                              }---|- AddressId
   Shipping_Address_Id >------}

So that I could have data that looks like this:

Address:

   Id | Addr           | City     | State | Zip   |
   ================================================
   1  | 123 Main       | New York | NY    | 99999 |
   2  | 200 2nd Street | New York | NY    | 99999 |
   3  | 300 3rd Street | Albany   | NY    | 99998 |
   4  | PO Box 4       | Albany   | NY    | 99998 |

Customer:

   Id | Name | Billing_Address_Id | Shipping_Address_Id  |
   =======================================================
   1  | Bob  | 1                  | 1                    |
   2  | Al   | 2                  | 1                    |
   3  | Joe  | 3                  | 4                    |

I want to store addresses in their own table because the data may be shared across customers (shipping address especially). But there would only even be two addresses for any given customer.

I'd like to avoid a many-to-many relationship unless there is no other way.

+2  A: 

Given table definitions like this:

create_table :addresses do |t|
  t.string :street
  t.string :city
  t.string :state
  t.string :zip
  t.timestamps
end

create_table :customers do |t|
  t.string     :name
  t.references :shipping_address
  t.references :billing_address
  t.timestamps
end

You can associate a billing and shipping address with your customer like this:

class Customer < ActiveRecord::Base
  belongs_to :shipping_address, :class_name => "Address"
  belongs_to :billing_address,  :class_name => "Address"
end
Ryan McGeary
The table design will help. I was not aware of the .references "data type".
y0mbo
+5  A: 

Yes, it's perfectly possible to do that. Given a customers table with the two foreign keys shipping_address_id and billing_address_id to the addresses table, your Customer model could look like this:

class Customer < ActiveRecord::Base
  belongs_to :billing_address, :class_name => 'Address'
  belongs_to :shipping_address, :class_name => 'Address'
end

This will let a customer reference the same address row for shipping and billing addresses, and will also let several customers share addresses.

Update: When sharing references to addresses like this you'll probably want to carefully consider how to handle address updates. In your example, Bob and Al share the same shipping address. Now, if Bob updates his shipping address you probably want to create a new Address record for Bob's new address rather than update the existing record, to avoid changing Al's address too. Sometimes, you actually might want to update both customers' addresses in this situation, but in most cases you probably don't.

Pär Wieslander
I agree with your design assessment. My example is a little simplified from what I'll actually be doing; the customers would actually be part of the same account in the same household. They may share the same address, or may want shipping to a different location.
y0mbo
A: 

The documentation for ActiveRecord associations has a section on has_one vs belongs_to. In addition, the section on has_one mentions that this should be used only if the other class has the foreign key. So, to model what you want, you would use.

class Address < ActiveRecord::Base
  has_one :shipto_customer, :class_name => "Customer", :foreign_key => "shipping_address_id"
  has_one :billto_customer, :class_name => "Customer", :foreign_key => "billing_address_id"
end

class Customer < ActiveRecord::Base
  belongs_to :shipping_address, :class_name => "Address"
  belongs_to :billing_address,  :class_name => "Address"
end

Example usage:

>> customer = Customer.new(:name => "John Smith",
?>     :shipping_address => Address.new(:address => "123 M St",
?>       :city => "Phoenix", :state => "AZ", :zip => "85015"),
?>     :billing_address => Address.new(:address => "555 W Main Dr",
?>       :city => "Phoenix", :state => "AZ", :zip => "85015")
>>   )
=> #<Customer id: nil, name: "John Smith", billing_address_id: nil, shipping_address_id: nil, created_at: nil, updated_at: nil>
>> customer.save
  Address Create (0.8ms)   INSERT INTO "addresses" ("address", "city", "zip", "created_at", "updated_at", "state") VALUES('555 W Main Dr', 'Phoenix', '85015', '2009-11-14 17:03:28', '2009-11-14 17:03:28', 'AZ')
  Address Create (0.2ms)   INSERT INTO "addresses" ("address", "city", "zip", "created_at", "updated_at", "state") VALUES('123 M St', 'Phoenix', '85015', '2009-11-14 17:03:28', '2009-11-14 17:03:28', 'AZ')
  Customer Create (0.2ms)   INSERT INTO "customers" ("name", "billing_address_id", "shipping_address_id", "created_at", "updated_at") VALUES('John Smith', 1, 2, '2009-11-14 17:03:28', '2009-11-14 17:03:28')
=> true
>> customer.shipping_address
=> #<Address id: 2, address: "123 M St", city: "Phoenix", state: "AZ", zip: "85015", created_at: "2009-11-14 17:03:28", updated_at: "2009-11-14 17:03:28">
>> customer.billing_address
=> #<Address id: 1, address: "555 W Main Dr", city: "Phoenix", state: "AZ", zip: "85015", created_at: "2009-11-14 17:03:28", updated_at: "2009-11-14 17:03:28">
>>
Matt Haley
The OP wanted to be able to share addresses between customers, so the relationships in the `Address` model should be `has_many` rather than `has_one` relationships.
Pär Wieslander
It doesn't seem quite clear from the OP's post, in the data example he's showing addresses being shared, ie: has_many, in the code example he's suggesting has_one. Then he states that he'd rather not have many-to-many. In my opinion, shared addresses would be a bad idea. What happens when customer A updates his current address to add a zip+4, or an Apt number?
Matt Haley
Yes, shared addresses are definitely troublesome. Keeping addresses and customers in separate tables is probably a good idea for clarity, but sharing references to address rows will most likely cause problems when deciding how to handle address updates.
Pär Wieslander
I tried to simplify my example for my question. The customers in question would literally be part of the same household, so changes to one address should update for all.
y0mbo