Renaming Foreign keys in Rails
Tags: ruby, webdev, beginners, tutorial
Published: 2023-02-16 01:49:40

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`
```Ruby
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
```
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:  
```Ruby
>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>

```
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.
```Ruby
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

```

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: 

```Ruby
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

```
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.` 

```Ruby
>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

```


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`.

```Ruby
>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 >

```
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.`

```Ruby
>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 >
```
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.

```Ruby
#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

```

```Ruby

>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">                                       
  

```



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: 


```Ruby
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


```

```Ruby
#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
            }
        ]
    }
```
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:
```Ruby
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"
            }
        }
    }

```
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/