Question

Multiple foreign keys referencing the same table in RoR

I want a Customer to reference two Address models, one for the billing address and one for the shipping address. As I understand it, the foreign key is determined by its name, as _id. Obviously I can't name two rows address_id (to reference the Address table). How would I do this?

create_table :customers do |t|
  t.integer :address_id
  t.integer :address_id_1 # how do i make this reference addresses table?
  # other attributes not shown
end
 45  30174  45
1 Jan 1970

Solution

 75

This can be kind of confusing to people new to Rails (as I was recently), because some parts of the answer take place in your Migrations and some in your Models. Also, you actually want to model two separate things:

  1. An address belongs to a single customer and each customer has many addresses. In your case this would be either 1 or 2 addresses, but I would encourage you to consider the possibility that a customer can have more than one shipping address. As an example, I have 3 separate shipping addresses with Amazon.com.

  2. Separately, we want to model the fact that each customer has a billing address and a shipping address, which might instead be the default shipping address if you allow more than one shipping address.

Here's how you would do that:

Migrations

class CreateCustomers < ActiveRecord::Migration
  create_table :customers do |t|
    def up
      t.references :billing_address
      t.references :shipping_address
    end
  end
end

Here you are specifying that there are two columns in this table that will be referred to as :billing_address and :shipping_address and which hold references to another table. Rails will actually create columns called 'billing_address_id' and 'shipping_address_id' for you. In our case they will each reference rows in the Addresses table, but we specify that in the models, not in the migrations.

class CreateAddresses < ActiveRecord::Migration
  create_table :addresses do |t|
    def up
      t.references :customer
    end
  end
end

Here you are also creating a column that references another table, but you are omitting the "_id" at the end. Rails will take care of that for you because it sees that you have a table, 'customers', that matches the column name (it knows about plurality).

The reason we added "_id" to the Customers migration is because we don't have a "billing_addresses" or "shipping_addresses" table, so we need to manually specify the entire column name.

Models

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

Here you are creating a property on the Customer model named :billing_address, then specifying that this property is related to the Address class. Rails, seeing the 'belongs_to', will look for a column in the customers table called 'billing_address_id', which we defined above, and use that column to store the foreign key. Then you're doing the exact same thing for the shipping address.

This will allow you to access your Billing Address and Shipping Address, both instances of the Address model, through an instance of the Customer model, like this:

@customer.billing_address # Returns an instance of the Address model
@customer.shipping_address.street1 # Returns a string, as you would expect

As a side note: the 'belongs_to' nomenclature is kind of confusing in this case, since the Addresses belong to the Customers, not the other way around. Ignore your intuition though; the 'belongs_to' is used on whichever thing contains the foreign key which, in our case, as you will see, is both models. Hah! how's that for confusing?

Finally, we are specifying that a Customer has many addresses. In this case, we don't need to specify the class name this property is related to because Rails is smart enough to see that we have a model with a matching name: 'Address', which we'll get to in a second. This allows us to get a list of all of Customer's addresses by doing the following:

@customer.addresses

This will return an array of instances of the Address model, regardless of whether they are billing or shipping addresses. Speaking of the Address model, here's what that looks like:

class Address < ActiveRecord::Base
  belongs_to :customer
end

Here you're accomplishing the exact same thing as with the 'belongs_to' lines in the Customer model, except that Rails does some magic for you; looking at the property name ('customer'), it sees the 'belongs_to' and assumes that this property references the model with the same name ('Customer') and that there is a matching column on the addresses table ('customer_id').

This allows us to access the Customer that an Address belongs to like this:

@address.customer # Returns an instance of the Customer model
@address.customer.first_name # Returns a string, as you would expect
2012-04-17

Solution

 29

This sounds like a has_many relationship to me - put the customer_id in the Address table instead.

Customer
  has_many :addresses

Address
  belongs_to :customer

You can also provide a foreign key and class in the assoc declaration

Customer
   has_one :address
   has_one :other_address, foreign_key => "address_id_2", class_name => "Address"
2010-01-30