Avoid creating static references to record instances
When using an ORM, be careful when using static (class-level) references to record instances.
What is a “static reference to a record instance”? 🔗
Here’s an example of a static reference to a record instance:
class Category < ApplicationRecord
def self.agent_category
find_by(name: "Real Estate Agent")
end
end
It’s whenever you use a static method to refer to something that is actually an instance of an ORM record.
Why is this dangerous? 🔗
This example assumes that there will only ever be one record with this name, which can be easily overlooked:
Category.create(name: "Real Estate Agent")
Category.create(name: "Real Estate Agent") # Category.agent_category is now non-deterministic
Great care must be made so that the record’s name never changes:
cat_a = Category.create(name: "Real Estate Agent")
cat_b = Category.create(name: "Real Estate Brokerage")
cat_b.update(name: "Real Estate Agent") # Category.agent_category is now non-deterministic
Rails assumes that your app and tests are thread safe, but static methods are not thread safe.
class Category < ApplicationRecord
def self.agent_category
find_or_create_by(name: "Real Estate Agent")
end
end
Thread.new { Category.agent_category }
Thread.new { Category.agent_category } # Category.agent_category is now non-deterministic. There may be duplicate records.
What is a safer alternative? 🔗
If your application assumes that something must exist, then it should be represented as code rather than as a record in the database. There are several techniques that are a safer alternative:
ActiveRecord’s enum 🔗
If the category is only a label, consider not creating a separate model and just use an enum.
class Company < ApplicationRecord
enum category: { agent: 1, ... }
end
ActiveHash 🔗
ActiveHash acts like an ActiveRecord object, but it is defined in memory and is thread safe. It’s also read-only, which reduces the number of validation checks that will need to be written and maintained.
class Category < ActiveHash::Base
field :name
add slug: "real-estate-agent", name: "Real Estate Agent"
end
Validations and Database Constraints 🔗
If you absolutely need a record, make sure that you add a database constraint and validation to prevent duplicates.
class AddUniquenessConstraintToCategory < ActiveRecord::Migration
def change
add_index :categories, :name, unique: true
end
end
class Category < ApplicationRecord
def self.agent_category
find_or_create_by(name: "Real Estate Agent")
end
validates :name, uniqueness: true
end