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

Free Training By Email:
Building a Rails Engine

We will send udpates but we care about your data.
No spams.

Back to homepage