Multiple foreign keys referencing the same table in RoR
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:
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.
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
I figured out how to do it thanks to Toby:
class Address < ActiveRecord::Base
has_many :customers
end
class Customer < ActiveRecord::Base
belongs_to :billing_address, :class_name => 'Address', :foreign_key => 'billing_address_id'
belongs_to :shipping_address, :class_name => 'Address', :foreign_key => 'shipping_address_id'
end
The customers table includes shipping_address_id and billing_address_id columns.
This is essentially a has_two relationship. I found this thread helpful as well.
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"
In Rails 5.1 or greater you can do it like this:
Migration
create_table(:customers) do |t|
t.references :address, foreign_key: true
t.references :address1, foreign_key: { to_table: 'addresses' }
end
This will create the fields address_id
, and address1_id
and make the database level references to the addresses
table
Models
class Customer < ActiveRecord::Base
belongs_to :address
belongs_to :address1, class_name: "Address"
end
class Address < ActiveRecord::Base
has_many :customers,
has_many :other_customers, class_name: "Customer", foreign_key: "address1_id"
end
FactoryBot
If you uses FactoryBot then your factory might look something like this:
FactoryBot.define do
factory :customer do
address
association :address1, factory: :address
end
end