Best Practices: Refactoring spaghetti sql in rails.
Quick Review: The Initial Misstep
Consider the initial approach in app/controllers/pets_controller.rb
:
# app/controllers/pets_controller.rb
def show
@pet = Pet.find(params[:id])
@toys = Toy.where(pet_id: @pet.id, cute: true)
end
This code demonstrates typical mishandling of Active Record capabilities.
By not utilizing association, it forces additional SQL management directly in the controller, increasing potential for errors and redundancy.
This is typical when a more senior developer starts using Rails and does not know how powerful the ORM is.
In my experience, juniors do the opposite. They rely on the ORM because they do not yet understand why they should learn SQL.
Step-by-Step Refactoring
Move Logic to the Model
This change isolates the data fetching mechanics to Toy
, but does not yet exploit the associations.
# app/controllers/pets_controller.rb
class PetsController < ApplicationController
def show
@pet = Pet.find(params[:id])
@toys = Toy.find_cute_for_pet(@pet)
end
end
# app/models/pet.rb
class Pet < ActiveRecord::Base
def self.find_cute_for_pet(pet)
where(pet_id: pet.id, cute: true)
end
end
It seems good but it’s not.
Leverage Active Record Associations
The best practice is to create a relationship between Pet
and Toy
:
# app/models/pet.rb
class Pet < ActiveRecord::Base
has_many :toys
def find_cute_toys
toys.where(cute: true)
end
end
With has_many :toys
, Pet
directly accesses its toys, improving code clarity.
We are almost there, but the issue is that we don’t need to create the method find_cute_toys
Extract Association Logic
Shift repetitive query logic into an association scope inside the Pet
model:
# app/models/pet.rb
class Pet < ActiveRecord::Base
has_many :toys do
def cute
where(cute: true)
end
end
end
Now it’s much better, we can access our data like: @pet.toys.cute
DRY with Association Extensions
To avoid repeating the .cute
method across different models:
# app/models/concerns/toy_association_methods.rb
module ToyAssociationMethods
def cute
where(cute: true)
end
end
# app/models/pet.rb
class Pet < ActiveRecord::Base
has_many :toys, extend: ToyAssociationMethods
end
Using a module maintains clean and reusable code across models.
Useful in the context of feature flags or tagging systems where the same scope can be defined in multiple models for tackling complexity using denormalization.
THE best practice, use: scope
Define a scope directly in the Toy
model that can be reused efficiently:
# app/models/toy.rb
class Toy < ActiveRecord::Base
scope :cute, -> { where(cute: true) }
end
In my opinion, this is the way to go. The reason is that the scope is centralized in the toy model and will be accessible in any definition of has_many
.
Special Magic with proxy_association
(If you want to FLEX to your teammates)
Utilizing proxy_association
within association blocks can address more complex queries:
# app/models/toy.rb
class Toy < ActiveRecord::Base
# has column :minimum_age
end
# app/models/pet.rb
class Pet < ActiveRecord::Base
# has column :age
has_many :toys do
def appropriate
where("minimum_age < ?", proxy_association.owner.age)
end
end
end
pet.toys.appropriate
returns toys suitable for the pet’s age, utilizing the proxy_association.owner.age
to fetch Pet
’s current age.
I bet you did not know this last trick!
Conclusion
The database is the heart of your system.
Rails’ ORM is at the heart of cleaner, more maintainable, and efficient codebases.
Related posts
Building a Rails Engine