[](https://hackmd.io/@hayat01sh1da/bothered-by-polymorphic-relations)
<img src="https://hackmd.io/_uploads/r1p5_FHOA.png" alt="Ruby on Rails" />
## 1. Environment
* Ubuntu 20.04.5 LTS
* ruby 3.1.2
* Rails 7.0.4
## 2. Requirements
Add username to the search form item in an auto-sent mail history page.
## 3. Tables
\* Partially quoted
### 3-1. mail_events
|id |email |
|:--|:------------------|
|1 |user@example.com |
|2 |office@example.com |
### 3-2. mail_event_sent_to
|id |mail_event_id |type |holder_id |office_id |
|:--|:--------------|:--------|:---------|:---------|
|1 |1 |User |1 |1 |
|2 |2 |Office |1 |1 |
### 3-3. offices
|id |name |email |
|:--|:--------|:--------------------|
|1 |Office01 |office01@example.com |
### 3-4. users
|id |office_id |name |
|:--|:---------|:------|
|1 |1 |User01 |
## 4. Relations
\* Class name is non-existent.
### 4-1. MailEvent
```ruby
class MailEvent < ApplicationRecord
has_many :mail_event_sent_to, foreign_key: :mail_event_id, inverse_of: :mail_event, dependent: :destroy
end
```
### 4-2. MailEventSentTo
```ruby
class MailEventSentTo < ApplicationRecord
belongs_to :mail_event
belongs_to :sent_to, polymorphic: true
belongs_to :office
end
```
### 4-3. Office
```ruby
class Office < ApplicationRecord
has_many :users
end
```
### 4-4. User
```ruby
class User < ApplicationRecord
has_many :entrance_and_exits
end
```
## 5. Troubles
1. Normally, I should define a scope in a class `MailEventSentTo` as the related one. However, `sent_to` attribute is related to `polymorphic: true`, so it does not exist as the `SentTo` class.
2. `type` and `holder_id` defines a receiver(e.g. `User` + `1` is a user in `users` table where id is equal to `1`. `Office` + `1` is a user in `offices` table where id is equal to `1`). So the keyword input in the search form must run SQL which searches both `users` table and `offices` table.
## 6. Source Code
\* Class name is non-existent.
### 6-1. Views
Add `sent_to` to form_with
```html
<%= search_form_field(f, :sent_to, 'name') %>
```
### 6-2. Controllers
Add `sent_to` to the white list of Strong Parameters.
```ruby
class MailEventsController < ApplicationController
def index
@offices = Office.all
@search_form = MailEvent::SearchForm.new(search_params)
@mail_events = @search_form.search...
end
private
def search_params
params.fetch(:mail_event_search_form, {}).permit(..., :sent_to)
end
end
```
***
Add `sent_to` as String data in the class for search.
`search` method calls a scope `MailEvent#sent_to`.
```ruby
class MailEvent::SearchForm < SearchForm::Base
...
attribute :sent_to, String
def search
MailEvent
...
.sent_to(@name)
end
end
```
### 6-3. Models
Call `MailEventSentTo#sent_to` via `MailEvent#sent_to` when search is done with a name.
\* I would originally like to make an inner join with `joins(:mail_event_sent_to)` in `MailEvent::SearchForm#search` methods and directly implement a search logic. But some `MailEvent` does not have `sent_to`. That is why the logic looks complicated and a little strange.
```ruby
class MailEvent < ApplicationRecord
...
scope :sent_to, ->(name) {
return if name.blank?
joins(:mail_event_sent_to).merge(MailEventSentTo.sent_to(name)).distinct
}
end
...
```
***
Call `MailEventSentTo#sent_to`.
```ruby
class MailEventSentTo < ApplicationRecord
...
scope :sent_to, ->(name) {
return if name.blank?
office_ids = Office.like_office_name(name).pluck(:id)
user_ids = User.like_user_name(name).pluck(:id)
search_office_ids = where(type: 'Office').where(holder_id: office_ids)
search_user_ids = where(type: 'User').where(holder_id: user_ids)
sql = search_office_ids.or(search_user_ids).select(:id).to_sql
where("#{table_name}.id IN (#{sql})")
}
...
end
```
What `MailEventSentTo#sent_to` does are:
Select the records from `offices` table which include the keyword input in the search form and assign projection of `id` column to `office_ids` variable.
```ruby
office_ids = Office.like_office_name(name).pluck(:id)
```
Select the records from `users` table which include the keyword input in the search form and assign projection of `id` column to `user_ids` variable.
```ruby
user_ids = User.like_user_name(name).pluck(:id)
```
Select all records where `type` is `Office` in `mail_event_sent_to` table and where `holder_id` is office_ids.
Then assign them to the `search_office_ids` variable.
```ruby
search_office_ids = where(type: 'Office').where(holder_id: office_ids)
```
Select all records where `type` is `User` in `mail_event_sent_to` table and where `holder_id` is user_ids.
Then assign them to `search_user_ids` variable.
```ruby
search_user_ids = where(mail_address_holder_type: 'User').where(mail_address_holder_id: user_ids)
```
Take the logical disjunction of `search_office_ids` and `search_user_ids`, extract foreign_key, then convert it to SQL.
Then assign them to the `sql` variable.
```ruby
sql = search_office_ids.or(search_user_ids).select(:id).to_sql
```
Select records which are equal to the sql from `mail_events`.
```ruby
where("#{table_name}.id IN (#{sql})")
```
***
`Office#like_office_name` and `User#like_user_name` are:
1. Call [arel_table](https://apidock.com/rails/v6.0.0/ActiveRecord/Core/ClassMethods/arel_table) with a receiver, `Office` or `User`, and load `name` column as an instance of `Arel::Table`.
2. Sanitise the keyword input in the search form and convert it into SQL, which is assigned to the LIKE phrase.
3. Issue SQL which selects all records where the keyword is included.
## 7. Conclusion
Do you remember the requirements of this task?
It was just to add a `name` section to a search form in an index page.
I thought that I could make it done soon because I viewed it as a simple task, but it was actually not.
The complicated table and SendGrid makes this task a profound one.
I asked for a review so many times after a lot of tries and errors, and finally made it.
I thought of writing raw SQL, but the code managed to ride on Rails Way.
This task made me understand:
* What is [Polymorphic Associations](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations)?
* How to project the same named columns in plural tables with only a transaction
* How deprecated [arel_table](https://apidock.com/rails/v6.0.0/ActiveRecord/Core/ClassMethods/arel_table) and [sanitize_sql_like](https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql_like) works
* Crazy table design can make production codes dirtier to supplement faults.