Performance difference when calling class methods in model vs controller in rails? - ruby-on-rails

In my rails app I am fetching a batch of data from the DB with around a million records. I am simply calling the following query combined with some pagination logic, and right now it is working very well. The code is defined in my model, like so:
def find_records(current_page, max_records, start_value, end_value)
where(value_range: start_value..end_value)
.offset((current_page - 1) * max_records).limit(max_records)
end
However, in my previous attempt, I had the following code defined in my model:
def find_records(max_records, start_value, end_value)
where(value_range: start_value..end_value)
end
And I called .offset and .limit inside the controller like so:
def index
current_page = params[:page]
max_records = 3
start_value = 4
end_value = 8
Model.find_records(start_value, end_value).offset((current_page - 1) * max_records).limit(max_records)
end
When I did this, my memory completely gave up on the 3rd or 4th page and my app just crashed. I don't know why calling .limit and .offset in the model solved the issue.
So my question is, how does calling class methods in your model rather than the controller improve code execution performance? I mean this query is obviously data-related so it makes sense to call it inside the model anyways, but I would still like to know the wonders behind the magic.
Thank you!

how does calling class methods in your model rather than the controller improve code execution performance?
It should not. Both your queries return a ActiveRecord::Relation. Both offset and limit are used to build the query, so in both scenarios you should see the same query in your logs. Please check your development.log when in doubt.
Having the code query code in your model makes sense. The controller shouldn't know all those details.
About the pagination, there are a few solutions in the rails world - Kaminari, will_paginate

Related

Independent ActiveRecord query inside ActiveRecord::Relation context

There is some ruby on rails code
class User < ActiveRecord::Base
def self.all_users_count
User.all
end
end
User.all_users_count
returns, for example, 100
User.limit(5).all_users_count
Now it return 5 because of ActiveRecord::Relation context, in despite of i wroute name of class User.all instead simple all
(.to_sql show that query always contains limit or where id or other things in other cases)
So, how can i make context-independent AR queries inside model methods? like User.all and others?
Thank you!
Ps. Or maybe my code has an error or something like this, and in fact User.all inside any methods and context always must returns correct rows count of this model table
This is very weird and unexpected (unfortunately I can't confirm that, because my computer crashed, and have no rails projects at hand).
I would expect
User.all
to create a new scope (or as you call it - context)
Try working around this with
User.unscoped.all
Edit:
I tried it out on my project and on clean rails repo, and the results are consistent.
And after thinking a bit - this is maybe not even an issue - I think your approach could be faulty.
In what scenario would you chain User.limit(2).all_users_count ?? I can't think of any. Because either you need all users count, and you call User.all_usert_count (or just User.count)
... or you need something else and you call User.limit(2).where(...) - there's no point in calling all_users_count in that chain, is it?
And, when you think of it, it makes sense. Imagine you had some different method like count_retired, what would you expect from such call:
User.limit(2).count_retired ?
The number of retired users not bigger than 2, or the number of all retired users in the system? I would expect the former.
So I think one of two possibilities here:
either you implemented it wrong and should do it in a different way (as described above in the edit section)
or you have some more complex issue, but you boiled your examples down to a point where they don't make much sense anymore (please follow up with another question if you please, and please, ping me in the comment with a link if you do, because it sounds interesting)

Rails custom model method in where query

In my rails app I have defined in the Kid model a calculation based on the fields from the Kids DB. the method is as follows:
def flip_date
self.dob.advance(months: 10)
end
I want to use this in my controller as I have a method where I am defining something as follows:
new_kids = Kid.where(discharge_date: nil).where('flip_date > ?', Date.current.advance(year: 1).beginning_of_year)
However I keep getting the following error:
SQLite3::SQLException: no such column: flip_date: SELECT "kids".* FROM "kids" WHERE "kids"."discharge_date" IS NULL AND (flip_date < '2017-01-01')
Any ideas on how can I make this work? All help is appreciated!
If you really want to use model methods take a look at http://apidock.com/rails/v4.0.2/ActiveRecord/QueryMethods/select
For your case:
new_kids = Kid.where(discharge_date: nil).select{|k| k.flip_date > Date.current.advance(year: 1).beginning_of_year}
But select method takes every object in memory before returning final result. Hence I will advise to use normal where clause and instead of flip_date take dob (which is a column in database) in consideration.
Like this
new_kids = Kid.where(discharge_date: nil).where('dob > ?', <date criteria>)
The select method (http://apidock.com/rails/v4.0.2/ActiveRecord/QueryMethods/select) works great if you are okay with the return being an Array.
I am still looking for a way to do this with an ActiveRecord_Relation return.
If others know how to do this, it would be much appreciated if you can share.
This example doesn't respond to your specific code, but to the extent it helps someone else with a similar question, here's a very simple example of how .select can be really handy:
#expired_memberships = User.select{|u| u.membership_expired_yesterday?}
In that example you've looped through all your Users and filtered them based on a custom method you defined on the User model (membership_expired_yesterday?). Now you can easily do stuff with that collection like this example in a mailer:
#expirations.each do |user|
MembershipExpirationMailer.with(user: user).first_reminder.deliver_now
end

Rails common method for updating a database field

I am new to rails and I have a task to write a common method that will update a specific database field with a given value. And I should be able to invoke the method from anywhere in the app.(I understand about the security flaw and so on.. But I was asked to do it anyway) In my application controller I tried
def update_my_model_status(model,id,field, value)
#model = model.find(id)
#model.update(field: value)
end
Of course this doesn't work.. How to achieve this? What is the right way to do this? And if it is possible how to pass a model as an argument to a method?
If you're using Rails, why not use Rails?
Compare update_all:
MyModel.where(id: 1).update_all(banned: true)
or maybe update_attribute:
my_model.update_attribute(:banned, true)
to:
update_my_model_status(MyModel, 1, :banned, true)
Notice how, despite being shorter, the first two approaches are significantly more expressive than the last - it is much more obvious what is happening. Not only that, but they are immediately more familiar to any Rails developer off the street, while the custom one has a learning curve. This, combined with the added code from the unnecessary method adds to the maintenance cost of the application. Additionally, the Rails methods are well tested and documented - are you planning to write that, too? Finally, the Rails methods are better thought out - for example, your prototype naively uses attribute validations, but does not check them (which could result in unexpected behavior) and makes more SQL queries than it needs to. It's fine to write custom methods, but let's not write arbitrary wrappers around perfectly fine Rails methods...
Try this:
def update_my_model_status(model,id,field, value)
#model_var = model.capitalize.constantize.find(id)
#model_var.update_attributes(field: value)
end
Instead of just using update you should use update_attributes:
def update_my_model_status(model,id,field, value)
#model_var = model.find(id)
#model.update_attributes(field: value)
end
http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update

Controllers in Rails do not "care" how requests are executed

I was reading an article about Rails controllers, can you help me understand please what is meant by the following phrase:
"The best controller is Dilbert-esque: It gives orders without knowing (or caring) how it gets done."
Is it true, in your opinion?
If, for example, I am accessing the index page associated with the subjects controllers, I would define the index method in the subjects_controller.rb rigorously, so I am confused as to what they mean in the article, as I would have thought the opposite.
Any pointers, please?
Thank you and sorry if this is too interpretable. This is the original article: http://betterexplained.com/articles/intermediate-rails-understanding-models-views-and-controllers/
This article is talking about MVC architecture. What's important to take away from an article like this is the fact that Rails is best written with Fat Models and Thin Controllers. This means that you want to have the bulk of your methods/functions in your Model and want to have calls to the functions from your controller. Index is a bad example since typically you're not going to have a lot going on in there.
Your controller for index will typically look something like this
def index
#subjects = Subject.all
end
If you want to scope order for displaying your subjects though, you would do that in your model with a block as follows:
default_scope { order("id DESC") }
A less contrived example might look something like this: Say for example you have an app that accepts input, takes that input and tallies several counters based on what the user entered. Your controller might be named subject_tally and look like this:
def subject_tally
#subject = Subject.find(params[:id])
#subject.winnings += 1
#subject.total_matches += 1
#subject.win_percentage = #subject.winnings.to_f/#subject.total_matches
redirect_to subjects_path
end
THIS IS WRONG. This is a very fat controller and easily moved to the Model where it should be.
If written properly it would look something like this:
subjects_controller.rb: (The Controller)
def subject_tally
#subject = Subject.find(params[:id])
#subject.subject_tally
redirect_to subjects_path
end
subject.rb: (The Model)
def subject_tally
self.winnings += 1
self.total_matches += 1
self.win_percentage =winnings.to_f/total_matches
end
So as you can see, you make only one call from the controller and it "doesn't care" what is actually going on in the backend. It's literally there to pass a value (in this case, the ID of the subject in question) and direct you to another page, in this case, the index.
Furthermore, if you'll notice, you don't need to add that pesky #subject everywhere in your model's subject_tally function... you can reference the attributes of the object just by using self.winnings where you're assigning to an attribute. Ruby is smart enough to know the current subject the method applies to (since you called that function ON a subject from the controller) and in fact you don't even need the self. if you're just retrieving the attributes instead of assigning them... which is why we didn't need self before winnings.to_f or the last line's total_matches.
Very convenient, less code, less time, yay.
The best controller is Dilbert-esque: It gives orders without knowing
(or caring) how it gets done.
means that you should put less logic as you can in the controller,
the controller should only know what to call to get what it needs, and should not know how to carry out a certain action.
In the "Sandy Metz rules" for rails developers (http://robots.thoughtbot.com/sandi-metz-rules-for-developers), she says:
Controllers can instantiate only one object. Therefore, views can only
know about one instance variable and views should only send messages
to that object
only one object could seem a bit extreme, but makes the idea about how much business logic (no logic) you should put in the controller.

Refactor and Improve Query in Rails

Outlet.rb:
def latest_reports
weekly_reports.limit(10)
end
Outlet_controller.rb:
#all_outlets = Outlet.includes(:weekly_reports)
#search = #all_outlets.search(params[:q]) # load all matching records
#outlets = #search.result.order("created_at DESC").page(params[:page])
outlet/index.slim:
- #outlets.each do |outlet|
tr
td= link_to outlet.name, outlet_path(outlet)
th
ul.reports
li class="#{'done' if outlet.monitored_today}"
th
ul.reports
- for report in outlet.latest_reports
li class="#{'done' if report.quota_met}"= report.times_monitored
I'm not sure why, but this loads it up as several different queries. I'm pretty sure it's because the include in my controller isn't correct (because I'm using a method in the model).
If anyone could help me improve this, I would be extremely grateful :).
Note: I'm developing on PostgreSQL
Update:: Posted the full controller action.
In rails 3 at least, if you use
Model1.includes :model2
then the result is one query for each model. You can access instances of the associated model from the result and no extra queries will be made.
If you really want it all in one query, you can do this:
Model1.joins(:model2).includes(model2)
This will produce a nice long JOIN query that loads all the data for both models in one go. Rails will populate the result with instances of both models already loaded.
So, you should be able to replace
#all_outlets = Outlet.includes(:weekly_reports)
with
#all_outlets = Outlet.includes(:weekly_reports).joins(:weekly_reports)
and it should combine everything into one query.

Resources