DEV Community

Cover image for Avoid Ransack's N+1 Pitfall!
Robert Hustead
Robert Hustead

Posted on

Avoid Ransack's N+1 Pitfall!

I've written about Ransack before, because it's a handy, little gem for searching quickly through both models and their relationships. But you may be surprised to know it's deceptively easy to create N+1 queries using Ransack! The good news is, it's just as easy to fix.

To review basic Ransack use, let's say you have a model Service, which has properties for name and description. If you wanted to search both of those properties, you'd set up your view with something like this:



<%= search_form_for @q do |f| %>
  <%= f.label :name_cont %>
  <%= f.search_field :name_description_cont %>
<% end %>


Enter fullscreen mode Exit fullscreen mode

And your controller would be:



def index
  @q = Service.ransack(params[:q])
  @services = @q.result(distinct: true)
end


Enter fullscreen mode Exit fullscreen mode

Pretty basic stuff, right? But what if you have another model associated with Services? For example, let's say you have a Provider model with name and address properties, and each Provider has_many Services.

Now, you want to search the name and description properties for Service, but you also want to include the Providers name property as well. How do you do that?

Ransack provides an easy approach. You can change your view to:



<%= search_form_for @q do |f| %>
  <%= f.label :name_cont %>
  <%= f.search_field :name_description_provider_name_cont %>
<% end %>


Enter fullscreen mode Exit fullscreen mode

Things are working smoothly. But hold on, let's look at the SQL query that runs when we try to search for something:
lots of sql queries

Yikes! That's not what we want at all!

Those familiar with rails will know the dreaded N+1 Problem, but will also likely know we can usually solve it with eager loading and using includes(:model_name) . But where do we put it? Easy, in the Controller of course!



def index
  @q = Service.includes(:provider).ransack(params[:q])
  @services = @q.result(distinct: true)
end


Enter fullscreen mode Exit fullscreen mode

Running our search again:

Much better!

(Please note that in my examples, I also have Sub Categories loading for another part of my view)

This isn't a difficult problem to solve, but many may not be aware that using Ransack with associated models can end up creating this N+1 problems.

Good luck, and happy coding!

Top comments (6)

Collapse
 
ben profile image
Ben Halpern

Super helpful little post

Collapse
 
almokhtar profile image
almokhtar bekkour

Hi @husteadrobert, small question what if you search with multiple models ? each model has diff relationships

Collapse
 
defman profile image
Sergey Kislyakov

Any reasons not to use relations and joins?

Collapse
 
husteadrobert profile image
Robert Hustead

Sorry for the late reply!

I'm not sure I understand your question. In the example, Service and Provider are related, and I suppose you could do something like,

@q = Service.all.joins(:provider).ransack(params[:q])

perhaps? But I have not tested it. Moreover, this would bypass any resource authorization libraries (like Cancan)

Collapse
 
storrence88 profile image
Steven Torrence

Awesome reminder! Thanks!

Collapse
 
bkspurgeon profile image
Ben Koshy

doesn't using includes eagerload or preload the association: wouldn't it be more efficient to simply use a joins method if in fact we are only seeking Service records?