DEV Community

Lola for Samsung Internet

Posted on

The Trouble with `has_one`

Recently I was working on my dissertation and came across an interesting problem with using the has_one association in Rails. But before we get into that, let’s take a step back to understand a little about how associations work in Rails.

A woman on big brother looking confused

Rails Associations

I’m not going to assume everyone reading this knows or understands in depth about databases and object relational mappers so I’ll try to keep this brief and simple. If you want a more in depth understanding, the Rails docs are a good starting point to understanding this in that specific context.

Let’s say you want to build a content site where different creators can create posts, you’d have a relational database with two tables Creator and Post that may look something like this:

A spreadsheet with two tables, Creator with the rows ID and Name and Post with the rows ID, Title, Type and Body

Your creator table has an ID and Name column for every item that gets created and your post table has an ID, Title, Type and Body column for every item. In each column, the primary key will be the ID column since that’s going to be the unique identifier for each record. The primary key is important when creating associations because it acts as the foreign key in the associated table.

Now we need to decide what kind of relationship the two tables will have with each other since we’d likely want to know which creators created which posts. There are a few different kinds of associations you can create but I’ll only touch on two.

has_many

A has_many association, says one item in a table can be associated with many items in another table. For example, if our system is a standard content creation system then a creator can have many posts since one creator can create many posts in which case, the association in Rails would look like:

class Creator < ApplicationRecord
  has_many :posts
end
class Post < ApplicationRecord
  belongs_to :creator
end
Enter fullscreen mode Exit fullscreen mode

And our database would look like:
A spreadsheet with two tables, Creator with the rows ID and Name and Post with the rows ID, Title, Type, Body and Creator_ID

The creator_id would be pulled from the id column, which is the primary key in the creator table and act as a foreign key in the post table. So if you wanted to find all the posts created by Tiffany Pollard, you’d do a search on the post table where the creator_id matches Tiffany Pollard’s id in the creator table.

has_one

A has_one relationship says one item in a table can only be associated with one item in another table, no more than one. We may want to create a system where a creator can only ever create one post in which case, our Rails code would look like:

class Creator < ApplicationRecord
  has_one :post
end
class Post < ApplicationRecord
  belongs_to :creator
end
Enter fullscreen mode Exit fullscreen mode

A creator has_one post and a post belongs_to a creator. The database looks the same as before:
A spreadsheet with two tables, Creator with the rows ID and Name and Post with the rows ID, Title, Type, Body and Creator_ID

And just like with has_many the creator_id is the foreign key for the creator’s id in the post table.

Okay, so now onto the problem.

...

The Problem

In Rails when you create a has_one association, understandably the assumption is that a creator can only ever have one post. So what happens when a creator tries to create another post? You’d hope that it’d check to see if a post exists and ask the creator if they want to replace the post or just replace the post with the assumption that that’s the intention of the creator. But that’s not what happens, instead, you get this error:

A rails error message that reads "Failed to remove the existing associated post. The record failed to save after its foreign key was set to nil.

This is a common error with has_one and is summed up nicely in this issue:

This is because prior to deleting, the foreign key of the target association is set to nil and a save operation is performed on the target.

It’s unclear why this happens, if you go through the issue you’ll notice it was created in 2014 and the discussion is still ongoing. It seems this started out as a bug but may have evolved to become the desired functionality.

The Fix

A small dog wearing glasses and a shirt using a laptop

There are a number of ways to fix this and it all depends on your system. Essentially, you want to make sure the old association is deleted before you try to create a new one. So, you can do this in your model:

class Creator < ApplicationRecord
  has_one :post, dependent: :destroy
end
class Post < ApplicationRecord
  belongs_to :creator
end
Enter fullscreen mode Exit fullscreen mode

Or in the method where you create your resource (either in your controller or specific lib file) you can run something like:

creator = Creator.find_by_id(id)
creator.post&.delete
Enter fullscreen mode Exit fullscreen mode

before you create any post. Andy Croll from Coverage Book recommends rolling things up in a transaction and there are more suggestions in the GitHub issue too.

Further Reading

Discussion (0)