How to Use a Different Foreign Key in Rails
Learn how to override the default ActiveRecord behavior and use a different foreign key in a Ruby on Rails application.
Guide Tasks
  • Read Tutorial

Typically I like to keep the default options that ship with rails when it comes to the database configuration. However there are times when overriding the default behavior makes for a more elegant implementation. Let's look at a base case. Usually when you have a has_many and belongs_to setup it will look something like this:

# Model:
User

# Attributes:
name
email

# Model:
Post

# Attributes:
title
body
user_id

The user_id functions like the foreign key so that Rails knows the relationship between users and posts. This type of setup allows you to make database queries such as post.last.user and have it bring up the post owner. And each post would have a user_id that would be the id for the user.

However that wouldn't be a natural fit for our app. We have clients and notifications. We don't want to have to have outside applications pass in the ActiveRecord id in order to verify the relationship.

Overriding the Foreign Key

Instead we're going to tell rails that it should use the source_app attribute as the foreign key that connects both models.

Creating Tests for Separate Foreign Key

Since this is a vital behavior element in our application, we want to ensure that it's fully tested, so let's write some specs that will create an expectation for what needs to happen. There's a pretty straightforward way to test relationships in Rails, let's add the following describe block to the Notification model spec:

# spec/models/notification_spec.rb

  describe 'relationship' do
    it 'has a connection to a client based on the source_app attribute' do
      client = Client.create(source_app: "myapp", api_key: "RbZHfHtD1h9XZvs4fGPJUgtt")
      notification = client.notifications.create!(phone: '9999999999', body: 'message content')
      expect(notification.source_app).to eq('myapp')
    end
  end

Let's take a second to see what's going on here. I'm choosing not to use a factory because I want to be very explicit with what we're doing. I'm using the line:

client = Client.create(source_app: "myapp", api_key: "RbZHfHtD1h9XZvs4fGPJUgtt")

To create a new client and add them to the test database.

notification = client.notifications.create!(phone: '9999999999', body: 'message content')

Next, instead of creating a new Notification from scratch, I'm creating one directly from the client variable. This type of implementation would break and throw an error if the relationship isn't set properly.

expect(notification.source_app).to eq('myapp')

Lastly, I'm checking to make sure that the new notification has the same source_app value as the client.

If I run the tests now they will complain that notifications is an undefined method.

large

Let's fix this with the implementation.

Custom Foreign Key Implementation

Let's update the Notification model file as shown here:

# app/models/notification.rb

belongs_to :client, foreign_key: 'source_app', primary_key: 'source_app'

Next, we need to update the Client class:

# app/models/client.rb

has_many :notifications, foreign_key: 'source_app', primary_key: 'source_app'

This is s slightly different implementation than you may be used to when connecting models. The reason is because we're overriding the default behavior of both the foreign key and the primary key in both model files.

If you run the specs now you'll see that they're all passing.

large

Testing Out a Different Foreign Key in the Rails Console

This is a slightly different rails convention, so let's try this out in the console as well. Opening up a rails c session, let's create a new Client:

client = Client.create!(source_app: "anotherapp", api_key: "RbZHfHtD1h9XZvs4fGPJUgtt")

Now let's be very explicit with our foreign key and manually create a notification:

notification = Notification.create!(phone: '8888888888', body: 'message body', source_app: 'anotherapp')

Notice how I manually entered in the source_app value, the same source_app as from the client. That worked, now let's run a query:

notification.client

Will return the client object. Now let's test out the other side of the relationship:

client.notifications

Will successfully query the database and bring back the correct set of notifications.

Summary

This was pretty cool! We were able, in only a few lines of code, to override the default foreign_key pattern in rails to build an implementation that better fits our needs. Also, notice how we didn't have to change the database schema to get this working. Usually we would have had to add a client_id column in the notifications database, however with this setup we can keep the table clean.

What's Next

With our connection between the model complete, let's start working on the Client functionality, starting with validations.

Resources