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.
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, theRails docs are a good starting point to understanding this in that specific context.
Let’s say you want to build a content site where differentcreators can createposts, you’d have a relational database with two tablesCreator andPost that may look something like this:
Your creator table has anID andName column for every item that gets created and your post table has anID,Title,Type andBody column for every item. In each column, the primary key will be theID column since that’s going to be the unique identifier for each record. Theprimary key is important when creating associations because it acts as theforeign 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 acreator can have manyposts since onecreator can create manyposts in which case, the association in Rails would look like:
classCreator<ApplicationRecordhas_many:postsendclassPost<ApplicationRecordbelongs_to:creatorend
And our database would look like:
Thecreator_id
would be pulled from theid
column, which is the primary key in thecreator table and act as a foreign key in thepost table. So if you wanted to find all the posts created by Tiffany Pollard, you’d do a search on thepost table where thecreator_id
matches Tiffany Pollard’sid
in thecreator table.
has_one
Ahas_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 acreator can only ever create onepost in which case, our Rails code would look like:
classCreator<ApplicationRecordhas_one:postendclassPost<ApplicationRecordbelongs_to:creatorend
Acreatorhas_one
post and a postbelongs_to
acreator. The database looks the same as before:
And just like withhas_many
thecreator_id
is the foreign key for the creator’sid
in the post table.
Okay, so now onto the problem.
...
The Problem
In Rails when you create ahas_one
association, understandably the assumption is that acreator can only ever have onepost. So what happens when acreator tries to create anotherpost? 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:
This is a common error withhas_one
and is summed up nicely in thisissue:
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 unclearwhy 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
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:
classCreator<ApplicationRecordhas_one:post,dependent: :destroyendclassPost<ApplicationRecordbelongs_to:creatorend
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
before you create any post. Andy Croll from Coverage Book recommendsrolling things up in a transaction and there are more suggestions in theGitHub issue too.
Further Reading
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse