DEV Community

Brandon C
Brandon C

Posted on

Renaming Foreign keys in Rails

Ruby on Rails allows for model and table associations on a database such as sqlite3 through a "convention over configuration" framework design. Lets say I have three tables: users and another table named products and the last is orders , with the relationships between these tables is that a user has many products and many orders, a product belongs to a user and has many orders, and an order belongs to a user and product. The model files and migration files for creating the tables would look like this:
users -< products
user-< orders >- products

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :username
      t.timestamps
    end
  end
end
...
class CreateProducts < ActiveRecord::Migration[7.0]
  def change
    create_table :products do |t|
      t.string :name
      t.belongs_to :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end
...
class CreateOrders < ActiveRecord::Migration[7.0]
  def change
    create_table :orders do |t|
      t.integer :quantity

      t.timestamps
    end
  end
end

#separate model files
class User < ApplicationRecord
   has_many :products
   has_many :orders
end
...
class Product < ApplicationRecord
   belongs_to :user
   has_many :orders
end
..
class Order < ApplicationRecord
   belongs_to :product
   belongs_to :user
end
Enter fullscreen mode Exit fullscreen mode

Running the migration creates three tables where the foreign key user_id is implicitly given to any product row entry in the products table, an order entry holds foreign keys of user_id and product_id . The reason why the foreign key is user_id in products is because Active Record attaches "_id" to the name of the model being referenced. belongs_to can be interchanged with references in the products migration file. Thanks to Active Record providing us with class methods and instance methods for defined associations, an example of accessing created entries through the console, model or controller could look like this:

>user = User.first
>user.products
=>#gives list of products instances associated to user
>user.products.first
#or
>User.first.products.first
=>#<Product id: 1, name: "Potted Plant", user_id: 1>
>buyer = User.last
>buyer.orders.first
=>#<Order id: 1, quantity: 2,  product_id: 1, user_id: 2>

Enter fullscreen mode Exit fullscreen mode

What we're trying to convey here is that users can put products up for sale and other users can buy them through orders. Users can be sellers of products and also buyers through the orders join table. What if we wanted to change the name of the association and foreign key to make the association more descriptive? We want the user to be a "seller" of a product and also be "buyers" of other products through making orders. We can change the name of the user foreign key by using additional options for migrations, models and serializers. First, we can go back to the migrations and start over from the column descriptions either by rolling back, deleting the database, or altering the table columns.

class CreateProducts < ActiveRecord::Migration[7.0]
  def change
    create_table :products do |t|
      t.string :name
      t.belongs_to :seller,  null: false, foreign_key: {to_table: :users}

      t.timestamps
    end
  end
end
...
class CreateOrders < ActiveRecord::Migration[7.0]
  def change
    create_table :orders do |t|
      t.integer :quantity
      t.belongs_to :product,  null: false, foreign_key: true
      t.belongs_to :buyer,  null: false, foreign_key: {to_table: :users}
      t.timestamps
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

In these migrations, we are changing the foreign key table reference from "user" to "buyer" in the orders table and "seller" in the products table. Using the references buyer and seller will generate buyer_id and seller_id as the foreign keys. We also have to use the to_table option to specify what the original table association reference is since putting seller and buyer as references will cause Active Record to look for the tables sellers and buyers which don't exist. In our case the original name of the table being used in both migration cases is users, so the foreign keys will be buyer_id and seller_id which are made by deriving the primary id key from the users table. The next is the models:

class User < ApplicationRecord
    has_many :products, foreign_key: :seller_id
    has_many :orders, foreign_key: :buyer_id
    has_many :bought_products, through: :orders, source: :product
end
...
class Product < ApplicationRecord
    belongs_to :seller, class_name: 'User'
#or belongs_to :user, foreign_key: :seller_id
    has_many :orders
    has_many :buyers, through: :orders
#or has_many :users, through: :orders
end
...
class Order < ApplicationRecord
    belongs_to :product
    belongs_to :buyer, class_name: 'User'
#or belongs_to :user, foreign_key: :buyer_id
end

Enter fullscreen mode Exit fullscreen mode

In User, we specify the name of the foreign key provided to the associated models, so the id of User is given to Product as seller_id and given to Order as buyer_id. We also have to specify a has_many "through" relationship to describe products bought by orders so we can distinguish the associated class with a name like "bought_products" while we use source: to point to the original name of the associated class, which is also products. When we use the instance method user.products, it will show us the products sold by the user and when we use user.bought_products, it will show us the products bought by the user through order.

>user = User.last
=>#<User: id: 2, username: "Richard">
>user.orders.first
=>#<Order: id: 1,  quantity: 2, product_id: 1, buyer_id: 2>  
>user.orders.first.product
=>#<Product: id: 1, name: "Potted Plant", seller_id: 1>
>user.bought_products.first
=>#<Product: id: 1, name: "Potted Plant", seller_id: 1>
#product bought by user with ".bought_products"
>user.products.first
=>#<Product:id: 2, name: "Tasty Bannana", seller_id: 2>
#product sold by user with ".products", note the matching User id and seller_id

Enter fullscreen mode Exit fullscreen mode

For Order and Product, we can use two ways to describe the belongs_to macro association.

The first macro is using belongs_to :seller/:buyer which causes Active Record to assume the implicit foreign key as seller_id and buyer_id. sellers and buyers have no associated model so we have to use the class_name option to point out what model is supposed to be used for seller and buyer, which is 'User'. Doing this means that Active Record also provides the association instance methods as .seller and .buyer instead of .user for instances of product and order.

>Product.first
=>#<Product: id: 1,name: "Potted Plant", seller_id: 1> 
>Product.first.seller
=>#<User: id: 1, username: "Alan">
>Order.first
=>#<Order: id: 1, quantity: 2, product_id: 1, buyer_id: 2>   
>Order.first.buyer
=>#<User: id: 2, username: "Richard >

Enter fullscreen mode Exit fullscreen mode

The second macro is using belongs_to :user which causes Active Record to recognize the class model User but requires using foreign_key: :buyer_id/:seller_id to specify what foreign key name is being derived from the User model. This will retain the association instance methods provided as .user for product and order.

>Product.first
=>#<Product: id: 1,name: "Potted Plant", seller_id: 1> 
>Product.first.user
=>#<User: id: 1, username: "Alan">
>Order.first
=>#<Order: id: 1, quantity: 2, product_id: 1, buyer_id: 2>   
>Order.first.user
=>#<User: id: 2, username: "Richard >
Enter fullscreen mode Exit fullscreen mode

We could also rewrite the migrations and model associations to maintain the user_id foreign key in order and product while keeping the buyer and seller instance method associations. This can be done by keeping the migration files unchanged from what we originally had.

#migrations
class CreateProducts < ActiveRecord::Migration[7.0]
  def change
    create_table :products do |t|
      t.string :name
      t.belongs_to :user,  null: false, foreign_key: true

      t.timestamps
    end
  end
end
class CreateOrders < ActiveRecord::Migration[7.0]
  def change
    create_table :orders do |t|
      t.integer :quantity
      t.belongs_to :product,  null: false, foreign_key: true
      t.belongs_to :user,  null: false, foreign_key: true
      t.timestamps
    end
  end
end
...
#models
class User < ApplicationRecord
    has_many :products
    has_many :orders
    has_many :bought_products, through: :orders, source: :product
end
class Product < ApplicationRecord
    belongs_to :seller, class_name: 'User', foreign_key: :user_id
    has_many :orders
    has_many :buyers, through: :orders
end
class Order < ApplicationRecord
    belongs_to :product
    belongs_to :buyer, class_name: 'User', foreign_key: :user_id
end

Enter fullscreen mode Exit fullscreen mode

>user = User.last
=>#<User: id: 2, username: "Richard">
>user.orders.first
=>#<Order: id: 1,  quantity: 2, product_id: 1, user_id: 2>  
>user.orders.first.product
=>#<Product: id: 1, name: "Potted Plant", user_id: 1>
>user.bought_products.first
=>#<Product: id: 1, name: "Potted Plant", user_id: 1>
>user.products.first
=>#<Product:id: 2, name: "Tasty Bannana", user_id: 2>
>product = Product.first
=>#<Product: id: 1, name: "Potted Plant", user_id: 1>
>product.seller
=>#<User: id: 1, username: "Alan">
>product.buyers.first
=>#<User: id: 2, username: "Richard">                                       


Enter fullscreen mode Exit fullscreen mode

If we use a serializer, we also have to set the association there depending on the class association we specified in the model. How we choose to define the model association will have an effect on the default json provided:

class ProductSerializer < ActiveModel::Serializer
  attributes :id, :name, :seller_id
  belongs_to :seller
#or belongs_to :user
end
class OrderSerializer < ActiveModel::Serializer
  attributes :id, :quantity, :buyer_id
  belongs_to :buyer
#or belongs_to :user
end
class UserSerializer < ActiveModel::Serializer
  attributes :id, :username
  has_many :products
  has_many :orders
  has_many :bought_products, through: :orders, source: :products
end


Enter fullscreen mode Exit fullscreen mode
#using belongs_to :buyer/:seller in serializer and model
#get request for product
{
        "id": 1,
        "name": "Potted Plant",
        "seller_id": 1,
        "seller": {
            "id": 1,
            "username": "Alan"
        }
    }
#get request for order
{
        "id": 1,
        "quantity": 2,
        "buyer_id": 2,
        "buyer": {
            "id": 2,
            "username": "Richard"
        }
    }
#using belongs_to :user in serializer and model
#get request for product
 {
        "id": 1,
        "name": "Potted Plant",
        "seller_id": 1,
        "user": {
            "id": 1,
            "username": "Alan"
        }
    }
#get request for order
 {
        "id": 1,
        "quantity": 2,
        "buyer_id": 2,
        "user": {
            "id": 2,
            "username": "Richard"
        }
    }
#get request for user index with include: ['orders', 'orders.product', 'products', 'bought_products']
{
        "id": 1,
        "username": "Alan",
        "products": [
            {
                "id": 1,
                "name": "Potted Plant",
                "seller_id": 1
            }
        ],
        "orders": [],
        "bought_products": []
    },
    {
        "id": 2,
        "username": "Richard",
        "products": [
            {
                "id": 2,
                "name": "Tasty Bannana",
                "seller_id": 2
            }
        ],
        "orders": [
            {
                "id": 1,
                "quantity": 2,
                "buyer_id": 2,
                "product": {
                    "id": 1,
                    "name": "Potted Plant",
                    "seller_id": 1
                }
            }
        ],
        "bought_products": [
            {
                "id": 1,
                "name": "Potted Plant",
                "seller_id": 1
            }
        ]
    }
Enter fullscreen mode Exit fullscreen mode

If we set up a custom serializer, we can show the associations involved with a get request and cut down the unneeded attributes for the json return:

class OrderSerializer < ActiveModel::Serializer
  attributes :id, :quantity
  belongs_to :buyer
  belongs_to :product, serializer: OrderProductSerializer
end
class OrderProductSerializer < ActiveModel::Serializer
  attributes :id, :name
  belongs_to :seller
end
#get request for an order with include: ['buyer', 'product', 'product.seller']
{
        "id": 1,
        "quantity": 2,
        "buyer": {
            "id": 2,
            "username": "Richard"
        },
        "product": {
            "id": 1,
            "name": "Potted Plant",
            "seller": {
                "id": 1,
                "username": "Alan"
            }
        }
    }

Enter fullscreen mode Exit fullscreen mode

Even though Ruby on Rails is a convention over configuration framework, this is just one way to deviate from its implicit based design by using the options that it provides us.

Done with Ruby 2.7.4

References

-https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference
-https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to
-https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
-https://flatironschool.com/

Top comments (1)

Collapse
 
khanhdevos profile image
Khanh

Great!