Alpha60
Alpha60

Reputation: 431

Ruby on Rails API: computed attribute pattern best practice

I would like to know the best practice when I need a computed attribute that require a call to the database.

If I have a Parent that has many Child, how would I render a children_count attribute in ParentController#index as I don't want to render the children, just the count? what's the best way to do it?

Thank you!

Model:

class Parent < ApplicationRecord
  has_many :children

  def children_count
    children.count # Wouldn't it ask the database when I call this method?
  end
end

Controller:

class ParentsController < ApplicationController
  def index
    parents = Parent.all

    render json: parents, only: %i[attr1, attr2] # How do I pass children_count?
  end
end

Upvotes: 3

Views: 177

Answers (2)

spickermann
spickermann

Reputation: 106922

The Rails way to avoid additional database queries in a case like this would be to implement a counter cache.

To do so change

belongs_to :parent

in child.rb to

belongs_to :parent, counter_cache: true

And add an integer column named children_count to your parents database table. When there are already records in your database then you should run something like

Parent.ids.each { |id| Parent.reset_counters(id) }

to fill the children_count with the correct number of existing records (for example in the migration in which you add the new column).

Once these preparations are done, Rails will take care of incrementing and decrementing the count automatically when you add or remove children.

Because the children_count database column is handled like all other attributes you must remove your custom children_count method from your Parent class and can still simple call

<%= parent.children_count %> 

in your views. Or you can add it to the list of attributes you want to return as JSON:

render json: parents, only: %i[attr1 attr2 children_count]

Upvotes: 5

Tony Arra
Tony Arra

Reputation: 11109

children.count will call the database, yes; however, it will do it as a SQL count:

SELECT COUNT(*) FROM "children" WHERE "children"."parent_id" = $1

It doesn't actually load all of the child records. A more efficient method is to use a Rails counter_cache for this specific case: https://guides.rubyonrails.org/association_basics.html#options-for-belongs-to-counter-cache

Upvotes: 4

Related Questions