A Conservative Case for Rails Concerns
There are many, many articles which condemn the use of concerns in a Rails project, and they all have valid points. I mostly agree with the arguments, such as them causing Bi-directional dependencies and arbitrarily splitting up code into multiple files. That said, I think a lot of these articles go too far by claiming you should never use concerns.
To be clear, if I saw a model that started with 20 concerns being included, I would be… Well, concerned. That’s why I’ve titles this a conservative case for concerns. I think they should be used very sparingly, but not avoided like the Coronavirus.
Namely, I think when a concern does not depend on any specific methods or attributes of a model existing, doesn’t contain code that is likely to evolve with business requirements, and could be used in a completely different project, then it’s likely a valid use for a concern.
Recently, I was working on a project where we needed to import related CSV files. In these files the associations were done based on the name of the record. For example if we have a CSV file for articles, and each article belongs to an author, the CSV would have an author column which could have the name “Keeyan Nejad”. Then in the authors CSV we would have a name column which would include the names (assume for this example that all authors have a unique name).
When importing the CSV, the Author model would get a name from the name column. Then I would import the articles, but when it would come time to associate an Article with an Author all I would have is the authors name, not the ID. To solve this I would have to write something like this:
Article.create(
title: csv_row['title'],
content: csv_row['content'],
author: Author.find_by(name: csv_row['author'])
)
This isn’t too bad, but then I it turns out that the Authors belong to a country, and instead of having a country ID we have the name of the country.
So to import the Authors I would have to do this:
Author.create(
name: csv_row['name'],
country: Country.find_by(name: csv_row['country'])
)
I wanted to clean this up a bit and this is where I found a valid use for a Rails concern. I wanted a way to associate a record by the name rather than by the ID column.
After a bit of playing around I came up with this code:
module AssociableByName
extend ActiveSupport::Concern included do
def self.associate_by_name(model)
define_setter_for_model(model)
end def self.define_setter_for_model(model)
define_method("#{model}_name=") do |reference|
association_class = self.class.reflect_on_association(model).klass
association = association_class.find_by(name: reference)
raise "Could not find #{model} by name #{reference}" if association.nil? send("#{model}=", association)
end
end
end
end
Then in the Article model I just add these lines:
include AssociableByName
associate_by_name :author
What this code will do is create a new setter in the model called author_name=
which will simply get the author ID from their name and create the association.
With that change, I can then update the importers to work more consistently:
Article.create(
title: csv_row['title'],
content: csv_row['content'],
author_name: csv_row['author']
)
And that’s it! Now the article will automatically associate with the Author by the name, rather than having to do the lookup, I also get the added benefit that it will throw an error if the association doesn’t exist (which is consistent with author_id
if the ID didn't exist)
What do you think? I’m happy to be proven wrong and learn why this code isn’t a good Concern, so let me know if you have any thoughts!