Rails: Removing unnecessary database queries - ruby-on-rails

Say I have a post and category model, with each post belonging to a category. On pretty much every page, I'm getting the list of categories:
#categories = Category.all
This produces an array of Category objects. Now, say that each category has id and name attributes. When viewing a post, I want to display the category name.
I was originally getting the category name by doing something like this:
#post = Post.find(params[:id], :include => :category)
However, I realize that I already have the #categories array. It seems unnecessary to :include when I already have a list of categories. So, instead, I'm now performing a find on the array:
category = #categories.find { |category| #post.category_id == category.id }.name
This works and reduces the number of queries. My question is if this is the best way to tackle reducing the number of queries (without caching)?

That's perfectly sensible.
The only risk (and it's a miniscule one) is that you are possibly subject to a race condition. For example, someone could change the post's category after you fetched the categories list, or they could update the list of categories, and what you're showing would no longer be correct. Subsequently, if they, say, clicked on a category name to get a list of all posts with that category, they'd probably get an error unless you were handling that sort of thing.
IMO, that's an awfully small price to pay, though.

The only issue here would be when you decide you don't need the categories list anymore and try removing it.
Otherwise you got a good solution.

And what if you have a large number of categories? Are you still going to be fetching them all? Doing an :include is much better as SQL will always win over Ruby.

Related

How can I filter products by multiple categories with ActiveRecord?

I'd like to add the ability to filter products by multiple categories to a Rails ecommerce application. I currently allow filtering products by category, but now I'd like to provide the ability to filter further.
For example, I'd like to allow a user to select "Men's" and "Outerwear" to display only products in both of those categories.
Knowing that supplying an array of category IDs in my Product query will find products in any of the specified categories, and hoping for a nice ActiveRecord-y type solution, I first tried adding multiple categories.id conditions in the query, but this didn't work out.
Product.joins(:categories).where(:'categories.id' => 123, :'categories.id' => 456)
The result here was that the first category ID was overwritten by the second.
And, of course, this will find products in either of the categories, rather than only products in both:
Product.joins(:categories).where(:'categories.id' => [123, 456])
Additionally, the solution I need should work with an arbitrary number of categories. It could be two, it could be three, or more.
After doing some research, I don't think there's a nice Rails-y way to do this, but I'm stuck on finding the actual correct solution.
My application is running Rails 5.2 with MariaDB.
Based on #muistooshort's comment above, I found this SO post with the solution I needed:
Selecting posts with multiple tags
Updating my query like so gave the products I wanted, those in ALL of the specified categories (lines wrapped for readability):
Product.joins(:categories).where(:categories => { :id => category_ids })
.having('count(categories.name) = ?', category_ids.size)
.group('products.id')

HATBM association, how can I get pairs of associated models?

Given following models and association:
(source: rubyonrails.org)
How can I get an array of pairs (physician_name, patient_name) that are appointed for certain day (appointment_date)? You can assume that one patient will never go to the same physician twice. Never.
I already tried things like:
#appointments = Appointment.where(appointment_date: params[:date])
but I have no idea what to do further. Should I iterate through this array and get every pair like this below?
#appointments.each do |appointment|
#physician = Physicians.where(id: :appointment.physician_id)
#patient = Patients.where(id: :appointment_patient_id)
I believe there's much easier way.
I'm using Rails 4.2.5.1.
I think what you want is approximately this:
Appointment.includes([:physician, :patient]).where(:date => appointment_date).map{|a| [a.physician.name, a.patient.name]}
Since only the Physician and Patient models have the names, they'll need to be loaded in the query (ok, you could avoid it by doing some fancy SQL trickery, but this is database-agnostic, which is convenient). Hence includes, which eager-loads associated models.
Then use .where to return only the appointments on the day you want (may be more complex if you're actually setting times in those DateTime values).
And finally, iterate over the list and return an Array of Arrays (Ruby not having Tuples) containing the names.

Rails - get objects of objects WITH duplicates

I received some really good help in solving an issue in which I needed to get objects from objects in one query. That worked like a charm.
This is a follow up to that question. I chose to create a new question to this since the last one was answered according to my previous specification. Please do refer to that previous question for details.
Basically, I wanted to get all the objects of multiple objects in one single query. E.g. if a Product has several Categories which in turn has several Products, I want to get all the Products in that relation, easier (and erronously) put:
all_products = #my_product.categories.products
This was solved with this query. It is this query I would (preferably) like to alter:
Product.includes(:categories).where(categories: { id: #my_product.categories.pluck(:id) } )
Now, I realized something I missed using this solution was that I only get a list of unique Products (which one would expect as well). I would however like to get a list with possible duplicates as well.
Basically, if a "Blue, Electric Car" is included in categories ("Blue", "Electric" and "Car") I would like to get three instances of that object returned, instead of one unique.
I guess this does not make Rails-sense but is there a way to alter the query above so that it does not serve me a list of unique objects in the returned list but rather the "complete" list?
The includes method of AREL will choose between two strategies to make the query, one of which simply does two distinct query and the other one does an INNER JOIN.
In both cases the products will be distinct.
You have to do manually a right outer join:
Product.joins('RIGHT JOIN categories ON categories.product_id = products.id').where(categories: { id: #my_product.categories.pluck(:id) } )
adds also .preload(:categories) if you want to keep the eager loading of the categories.
Since you want duplicates, just change includes to joins, (I tested this just now). joins will essentially combine (inner-join) the two tables giving you a list of records that are all unique (per Product and Category). includes does eager loading which just loads the associated tables already but does an outer-join, and therefore, the retrieved records are also unique (but only per Product).
Product.joins(:categories).where(categories: { id: #my_product.categories.pluck(:id) } )

How to remove some items from a relation?

I am loading data from two models, and once the data are loaded in the variables, then I need to remove those items from the first relation, that are not in the second one.
A sample:
users = User.all
articles = Articles.order('created_at DESC').limit(100)
I have these two variables filled with relational data. Now I would need to remove from articles all items, where user_id value is not included in the users object. So in the articles would stay only items with user_id, that is in the variable users.
I tried it with a loop, but it was very slow. How do I do it effectively?
EDIT:
I know there's a way to avoid doing this by building a better query, but in my case, I cannot do that (although I agree that in the example above it's possible to do that). That thing is that I have in 2 variables loaded data from database and I would need to process them with Ruby. Is there a command for doing that?
Thank you
Assuming you have a belongs_to relation on the Article model:
articles.where.not(users: users)
This would give you at most 100, but probably less. If you want to return 100 with the condition (I haven't tested, but the idea is the same, put the conditions for users in the where statement):
Articles.includes(:users).where.not(users: true).order('created_at DESC').limit(100)
The best way to do this would probably be with a SQL join. Would this work?
Articles.joins(:user).order('created_at DESC').limit(100)

How do you normally sort items in Rails?

I have a little example Rails app called tickets, which views and edits fictional tickets sold to various customers. In tickets_controller.rb, inside def index, I have this standard line, generated by scaffolding:
#tickets = Ticket.find(:all)
To sort the tickets by name, I have found two possible approaches. You can do it this way:
#tickets = Ticket.find(:all, :order => 'name')
... or this way:
#tickets = Ticket.find(:all).sort!{|t1,t2|t1.name <=> t2.name}
(Tip: Ruby documentation explains that sort! will modify the array that it is sorting, as opposed to sort alone, which returns the sorted array but leaves the original unchanged).
What strategy do you normally use? When might you use .sort! versus the :order => 'criteria' syntax?
Use :order => 'criteria' for anything simple that can be done by the database (ie. basic alphabetical or chronological order). Chances are it's a lot faster than letting your Ruby code do it, assuming you have the right indexes in place.
The only time I could think you should use the sort method is if you have a complex attribute that's calculated at run-time and not stored in the database, like a 'trustworthiness value' based off number of good/bad responses or something. In that case it's better to use the sort method, but be aware that this will screw things up if you have pagination in place (each page will have ITS results in order, but the set of pages as a whole will be out of order).
I specify an order in the ActiveRecord finder or in the model association because sorting using SQL is faster. You should take advantage of the features offered by the RDBMS when you're able to do so.

Resources