Building has_one from has_many Associations
Tags: rails
Published: 2022-06-22 14:22:41

The extent of "options" one could pass to a Rails association macro are somewhat vast and in some cases, at least for me, make one wonder when such "options" would ever be used.

Today, I found myself having to dig into the documentation and google-fu'ing on the fly to try to put into code what a coworker was saying verbally about an approach to building out some model relationships.

To start, let's say that the application we are working on has a model called `Account`. Let's also say that for billing and shipping purposes, we also want to add an `Address` model and relate the two models such that:
```ruby
class Account < ApplicationRecord
  has_many :addresses
end
```

*Disclaimer* For the purpose of experimenting and learning I'm choosing not to debate about pros and cons of the implementation as it relates to the scenario at hand.

Now, what we would really like is for our `Account` model to have this set up but without having to make separate models that will most likely have the exact same attributes:
```ruby
class Account < ApplicationRecord
  has_many :addresses
  has_one :billing_address
  has_one :shipping_address
end
```

Let's say we decided test our hand (and mind), and proceed. Our first step is to put the parent model relationship in place along with the finer detailed relationships that we desire. In this case I'm referring to `:addresses` as the parent model and `:billing_address` and `:shipping_address` as the "finer detailed" or (perhaps) child relationships.

Now that we (hopefully) are on level ground with what we want, proceeding, we now have an `Account` model that looks like so:
```ruby
class Account < ApplicationRecord
  has_many :addresses
  has_one :billing_address
  has_one :shipping_address
end
```
The question now is... can we and how do we... make this work?

The short answer is yes, we can make this work. And as we explore how, my hope is that a bit more understanding and curiosity is unlocked for us all.

Let's start making this a reality. 

Generate a new rails application however you would normally do so. Then run:
```ruby
rails g model Account
rails g model Address account:references address_type:integer
```

We are adding the `address_type:integer` piece because we are going to make this an enum column with the two options of `:billing` and `:shipping`.

Let's migrate `rails db:migrate`

Now, let's add our enum setup in the `Address` model like so:
```ruby
class Address < ApplicationRecord
  belongs_to :account
 
  enum address_type: [:billing, :shipping]
end
```

And then setup a `has_many` relationship in our `Account` model:
```ruby
class Account < ApplicationRecord
  has_many :addresses
end
```

Next let's hop into the rails console `bin/rails console` and then run:
```ruby
account = Account.create!
address = Address.create! address_type: "billing", account: account
address = Address.create! address_type: "shipping", account: account
```

Great, now let's use the account we created to see all the addresses associated to it:

```ruby
account.addresses
=>                                                                   
[#<Address:0x00007fa1ab6d86f8                                             
  id: 1,                                                                  
  account_id: 1,                                                          
  address_type: "billing",                                                
  created_at: Fri, 01 Apr 2022 02:11:01.334935000 UTC +00:00,             
  updated_at: Fri, 01 Apr 2022 02:11:01.334935000 UTC +00:00>,            
 #<Address:0x00007fa1ab6d3b08                                             
  id: 2,                                                                  
  account_id: 1,                                                          
  address_type: "shipping",                                               
  created_at: Fri, 01 Apr 2022 02:11:15.381033000 UTC +00:00,             
  updated_at: Fri, 01 Apr 2022 02:11:15.381033000 UTC +00:00>]
```

Ok, this looks promising! Let's try to use our `has_one :billing_address` association:

```ruby
account.billing_address
=>
'rescue in compute_class': Rails couldn't find a valid model for BillingAddress association. Please provide the :class_name option on the association declaration. If :class_name is already provided, make sure it's an ActiveRecord::Base subclass. (NameError) 
uninitialized constant Account::BillingAddress (NameError)
```

Whoops! Rails doesn't seem to be able to find a valid model named `BillingAddress` which makes sense because we don't have a `BillingAddress` model. We really would like it if Rails could search on the `Addresses` table and use information from there to find the billing address record. 

As it turns out we can do this by adding a couple of options to the call to `has_one :billing_address` and `has_one :shipping_address`.

The first change we will make is to pass in the `class_name` option which we will then point to the `Address` class so that Rails will use that model to perform queries on versus trying to use our non-existent `BillingAddress` model. Update both `has_one` calls like so:

```ruby
class Account < ApplicationRecord
  has_many :addresses
  has_one :billing_address, class_name: "Address"
  has_one :shipping_address, class_name: "Address"
end
```

With this in place let's go back into our Rails console and see what this change will do:

```ruby
account.billing_address
=>
#<Address:0x00007fa02d699888                                                 
 id: 1,                                                                      
 account_id: 1,                                                              
 address_type: "billing",                                                    
 created_at: Mon, 25 Apr 2022 15:24:48.167699000 UTC +00:00,                 
 updated_at: Mon, 25 Apr 2022 15:24:48.167699000 UTC +00:00>
```

Look at that! No error this time and we got an address of type "billing" back. Neat! Let's now try our `shipping_address`:

```ruby
account.shipping_address
=>
 id: 1,                                                                   
 account_id: 1,                                                           
 address_type: "billing",                                                 
 created_at: Mon, 25 Apr 2022 15:24:48.167699000 UTC +00:00,              
 updated_at: Mon, 25 Apr 2022 15:24:48.167699000 UTC +00:00>
```

Not as exciting this time around. We get back the same record as when we called `account.billing_address` with in both cases seem to just be returning the first record found.

It would stand to reason that we need some why to tell Rails how to differentiate between the two. Thankfully, by leveraging the optional second argument to our [has_one](https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one) call we can pass in a scope and tell Rails how to found the records we want.

Let's update our `Account` model as such:
```ruby
class Account < ApplicationRecord
  has_many :addresses
  has_one :billing_address, -> { where(address_type: "billing") }, class_name: "Address"
  has_one :shipping_address, -> { where(address_type: "shipping") }, class_name: "Address"
end
```

Now, let's go back into our Rails console one more time and see if this gives us the result we are looking for:

```ruby
account.billing_address
=>
#<Address:0x00007fb309b8e548                                              
 id: 1,                                                                   
 account_id: 1,                                                           
 address_type: "billing",                                                 
 created_at: Mon, 25 Apr 2022 15:24:48.167699000 UTC +00:00,              
 updated_at: Mon, 25 Apr 2022 15:24:48.167699000 UTC +00:00> 

account.shipping_address
=>
#<Address:0x00007fb3088d35c0                                              
 id: 2,                                                                   
 account_id: 1,                                                           
 address_type: "shipping",                                                
 created_at: Mon, 25 Apr 2022 15:29:22.137482000 UTC +00:00,              
 updated_at: Mon, 25 Apr 2022 15:29:22.137482000 UTC +00:00> 
```

We did it! We should take a moment to celebrate this win. After a brief celebration, lets make this a little cleaner and a bit safer.

Safety first so let's start there. We want to help enforce the fact that we only want an account to have one billing or shipping address.

Let's add a validation on the `Address` model that says that we want to validate the uniqueness of the `address_type` attribute and scope it to being unique to an `Account` record. Here's the line addition:

```ruby
class Address < ApplicationRecord
  # belongs_to :account

  # enum address_type: [:billing, :shipping]

  validates :address_type, uniqueness: { scope: :account_id }
end
```

As an additional note, it would probably be a good idea to also add a unique constraint at the database level with something like:

```ruby
add_index :addresses, [:address_type, :account_id], unique: true
```

Now for the clean up. We can drop the `where` call in both of the `scope:` arguments to the `has_one` macro. We can do this because since we are using enums, Rails gives us some new methods that we can leverage to remove the `where` calls. Based on the allowed values of the enum field the two scopes that we gain are `Address.billing` and `Address.shipping`.

With this new knowledge we can update our `has_one` scope arguments from:

```ruby
has_one :billing_address, -> { where(address_type: "billing") }, class_name: "Address"
has_one :shipping_address, -> { where(address_type: "shipping") }, class_name: "Address"
```

to:

```ruby
has_one :billing_address, -> { billing }, class_name: "Address"
has_one :shipping_address, -> { shipping }, class_name: "Address"
```

Pretty slick!

What a really special thing we did here and I hope you found it fun to dig into a few of the options you can pass to your association calls to customize the behavior of them.

Thanks for reading and happy Rubying!