So in my application I have the models People and Outfits. In my show controller for people, I get the list like this:
#people = Person.where("description LIKE ?", "%#{params[:description]}%")
And in my view I show the outfits of each person like this:
<% #people.each do |person| %>
<p> Name: <%= person.name %> </p>
<% #outfits = person.outfits %>
<% #outfits.each do |outfit|
<p> Name: <%= outfit.name %> </p>
<p> Description: <%= outfit.description %> </p>
<% end %>
<% end %>
But loading the outfits for each person, as I load many people on the page, takes too long. Is there some way I can inherit the outfits of each person so I don't have to wait so long for the page to load? Or is the only way to speed this up to make an index between outfits and people? Thanks for any help
Use a join to load the associated records:
#people = Person.eager_load(:outfits)
.where("description LIKE ?", "%#{params[:description]}%")
.limit(20) # optional
Otherwise you have what is called a N+1 query issue where each iteration through #people will cause a separate database query to fetch outfits.
And yes the outfits.person_id or whatever column that creates the association should have a foreign key index. Using the belongs_to or references macro in the migration will do this by default:
create_table :outfits do |t|
t.belongs_to :person, foreign_key: true
end
Active Record Query Interface - Eager Loading Associations
Making sense of ActiveRecord joins, includes, preload, and eager_load
you should set a limit like this:
#people = Person.where("description LIKE ?", "%#{params[:description]}%").limit(20)
change the number according to your preference.
You can use .joins or .includes
If you have a table full of Person and you use a :joins => outfits to pull in all the outfit information for sorting purposes, etc it will work fine and take less time than :include, but say you want to display the Person along with the outfit name, description, etc. To get the information using :joins, it will have to make separate SQL queries for each user it fetches, whereas if you used :include this information is ready for use.
Solution
Person.includes(:outfits).where("description LIKE ?", "%#{params[:description]}%")
Related
I have written some code to search via a couple of attributes in all my Recipe records. The code works, but I would like some input on if it's ok, or how to make it better/faster.
I have a Recipe model with various attributes including name:string and ingredients:[array of integers] (postgres database). The ingredients are the ID's of a separate model Ingredient. This is a learning experience, I don't want to use any gems.
My form
index.html.erb
<%= form_tag recipes_path, :method => 'get' do %>
<p>
<%= text_field_tag :search, params[:search] %>
<%= collection_select( :ingredients, :ingredient_ids, Ingredient.all, :id, :name, {:include_blank => false, include_hidden: false}, { :multiple => true } ) -%>
<%= submit_tag "Search" %>
</p>
<% end %>
recipes_controller.rb
def index
#recipes = Recipe.search(params[:search], params[:ingredients])
end
recipe.rb
def self.search(search, ids)
array = []
if search && !search.empty?
meals = where('name ILIKE ?', "%#{search}%")
meals.each do |meal|
array.push(meal)
end
if ids && !ids.empty?
ingredients(array, ids)
else
return array
end
elsif ids && !ids.empty?
ingredients(all, ids)
else
all
end
end
def self.ingredients(meals, ids)
newarray = []
if ids
meals.each do |me|
a = me.ingredients
b = ids[:ingredient_ids].map(&:to_i)
if (b - a).empty?
newarray.push(me)
end
end
return newarray
else
return meals
end
end
This works fine at the moment as I don't have many records, but I don't trust that it'll be very fast if I had hundreds or thousands of records. Any advice on improving things?
If you know you're going to be searching on one or more columns frequently, try adding a database-level index for those columns. Without an index, any search will be O(n) time where n is the number of records. However, if you use an index, a search will be O(log(n)) time, because with the data ordered by your search column, you can binary search through it.
Take a look at http://weblog.rubyonrails.org/2011/12/6/what-s-new-in-edge-rails-explain/ for more information at how to check if your query performance can be improved.
Two other things to consider performance-wise:
1) For your all condition, you might be returning waaaay more records than you want. You may want to consider using pagination (I know you mentioned no other gems, but there are some great gems for pagination out there).
2) If you do actually want to return a ton of records at once, consider using ActiveRecord's batching (I usually use #find_each) to ensure you don't load everything into memory at once and end up OOM-ing.
I have a model course which has_many subcategories. I want to build a page that shows courses grouped by their subcategory. So far, I have
#courses = Course.personal_enrichment.order('subcategory_id').page params[:page]
#courses_facet = #courses.group_by(&:subcategory_id)
which works fine, but I need to show the actual subcategory name in the view, not the number. I've seen some other answers about this type of thing, but most of them assume the attribute you're grouping by is already human readable. Maybe I'm missing something?
When rendering the view you can just access the referenced models' attributes. Since group_by returns a hash, you could do something like this:
<% #courses_facet.each do |subcategory_id, courses| %>
<% subcategory_name = courses.first.subcategory.name rescue nil %>
<label><%= subcategory_name %></label>
<% end%>
Unless relevant subcategory models are cached this will generate N+1 queries to fetch the subcategory names. One way to avoid that is to include subcategory records to the initial resultset.
#courses.includes(:subcategories).group_by(&:subcategory_id)
I'm currently displaying on my homepage all Current Bets (current is an option of status attribute) from all members. Here is my code:
<% #bets.where(status: "Current").each do |bet| %>
<%= bet.match_name %>
<%= bet.bet_choice %>
<% end %>
(where #bets = Bet.all in my Page controller)
What I would like to do is to display on my homepage all Current bets from members who are in the "team" (team is a boolean in User).
How can I do that?
Assuming that, you have proper associations defined in your User and Bet models.
Try something like this:
User.where(team: true).joins(:bets).where('bets.status' => 'Current')
Update_1:
I see you have a column id_user in your bets table, which should be user_id instead, assuming your associations are like: User has_many bets and Bet belongs_to User.
Update_2:
If you want all such bets and then loop through the bets collection, then you have to modify the above query little bit like this:
Bet.where(status: "Current").joins(:user).where('users.team' => 'true')
Here is the right code :
<% Bet.where(status: "Current").joins(:user).where('users.team' => 'true').each do |bet| %>
Thanks for the "joins" tip KM. It helped me well.
Using Rails 4.2
I have two models, suppliers and clients. Both models contain a name (string) and email (string). They do not have any relationship between them.
I would like to generate a list of all the names and emails from both suppliers and clients. In this list I would also like to know if the partner is a supplier or client.
Controller
#suppliers = Supplier.all
#clients = Client.all
#all_partners = (#suppliers + #clients).sort { |x, y| x.name <=> y.name }
View
<% #all_partners.each do |partner| %>
<%= partner.name %>, <%= partner.email %>, <%= partner.type %>
<!-- I need some way to know if the partner type is a supplier or client -->
<% end %>
How can I put in which type of partner it is? Is there a way to do this with one single AR call or query? This is basically how to use an SQL Union statement in Rails.
You could get the class name of the object I believe <%= partner.class.model_name.human %>
Thanks for the help all.
I ended up using the same controller as in the question, with some additional information in the view.
View
<% #all_partners.each do |partner| %>
<%= partner.name %>, <%= partner.email %>, <%= partner.try(:client_type) %>, <%= partner.class.model_name.human %>
<% end %>
Union in ActiveRecord works only within a single model. You could use union for two different tables using raw SQL, something like this:
Supplier.connection.execute("(SELECT id, ..., 'suppliers' as table FROM suppliers WHERE...) UNION (SELECT id,... 'clients' as table FROM clientsWHERE...)")
but the result would be of type PG::Result.
So the best way, unfortunately, is to use two ActiveRecord queries.
OR if clients and suppliers have similar fields, you could put them in one table
class Partner < ActiveRecord::Base
default_scope where(is_supplier: true)
scope :clients, -> { where(is_supplier: false) }
end
so Partner.all will output only suppliers, Partner.unscoped - all partners
To start, I'm a pretty new to Rails
I've created some methods and put them into my model, but it looks messy and just wondered if the code belongs in the model or the controller? What makes my code unique (not one model per controller anyhow) is that I have only one model "Products" but have 3 controllers that interact with it, "Merchants, Categories, Brands". Maybe there is an easier way I have completely overlooked?? I don't really want to split the data up into 3 tables / models with links between.
p.s. This is the first time I have slipped away from the comfort of a Rails book so please go easy on me! Any other general suggestions to my code will be much appreciated.
Product model
class Product < ActiveRecord::Base
validates :brand, :presence => true
def product_name
name.capitalize.html_safe
end
def product_description
description.html_safe
end
#Replace '-' with ' ' for nice names
def brand_name
brand.capitalize.gsub('-',' ')
end
def category_name
category.capitalize.gsub('-',' ')
end
def merchant_name
merchant.capitalize.gsub('-',' ')
end
#Replace ' ' with '-' for urls
def brand_url
brand.downcase.gsub(' ','-')
end
def category_url
category.downcase.gsub(' ','-')
end
def merchant_url
merchant.downcase.gsub(' ','-')
end
end
Merchants Controller
class MerchantsController < ApplicationController
def index
#merchants = Product.find(:all, :select => 'DISTINCT merchant')
end
def show
#products = Product.find(:all, :conditions => ['merchant = ?', params[:merchant]])
#merchant = params[:merchant].capitalize.gsub('-',' ')
end
end
Merchant view (index)
<h1>Merchant list</h1>
<%= #merchants.count%> merchants found
<% #merchants.each do |merchant| %>
<p><%= link_to merchant.merchant_name, merchant.merchant_url %></p>
<% end %>
Merchant view (show)
<h1>Products from merchant: <%= #merchant %></h1>
<%= #products.count%> products found
<% #products.each do |product| %>
<h3><%= product.product_name %></h3>
<p>
<img src="<%= product.image %>" align="right" alt="<%= product.product_name %>" />
<%= product.product_description %>
</p>
<p><%= product.price %></p>
<p>Brand: <%= product.brand_name %></p>
<p>Category: <%= product.category_name %></p>
<p>Sub category: <%= product.sub_category %></p>
<p>Merchant: <%= product.merchant_name %></p>
<p>More information</p>
<hr />
<% end %>
So your data model does seem to be getting to the point where you might at least want to split merchants out. You can tell this from the select 'DISTINCT merchant' query. If your merchants are user-based input and are saved inside your products table it seems like a good time to move them into their own model so that they are easily searchable and manageable. As you get more merchants and more products it will get harder and harder to perform this query. Once you want to add additional merchant information you'll be in a worse position as well. Just keep in mind Rails was made for easy refactoring. Making this change shouldn't be daunting, it should just be another regular task in your agile development process.
What the above change would also allow you to do is change these lines:
#products = Product.find(:all, :conditions => ['merchant = ?', params[:merchant]])
#merchant = params[:merchant].capitalize.gsub('-',' ')
into:
#merchant = Merchant.find_by_name(params[:name])
#products = #merchant.products
You could then have a capitalize and gsub name with a model function:
#merchant.display_name
The next step would be to DRY up your model code a little bit, for example:
class Product
def brand_name
make_name brand
end
def category_name
make_name category
end
def merchant_name
make_name merchant
end
private
def make_name name
name.capitalize.gsub('-', ' ')
end
end
You could do something similar to the _url functions as well. If you wanted to venture further you could clean this up using meta-programming as well.
Final Thoughts: make sure you actually want to be calling html_safe on your strings. If they are user based input it's best to let them go through the h function in your views. Do you want users to be able to enter HTML strings as brands, merchants and categories? If so, then leave the html_safe string there, otherwise let the strings be made html_safe in your views.
In general you are on the right path: Skinny Controllers and Views and Fat Models is the way to go. This means put your logic and your heavy-lifting into your Models and let your Controllers and Views be small and simple.
You should probably normalise your database. You need 3 tables instead of one: Products, Merchants and Brands. Your product table will then have references to merchant and brand tables. You can then have separate models (with belongs_to/has_many relationships between them) and separate controllers.
You will still be able to write things like product.merchant.name but some of your code will be simpler.
Conventions are just that, conventional. There is no right or wrong no matter who in Atlanta tells you that there are. F#$k him.
Anyways, if you are going with a Skinny Controller Fat model, then yeah, you're on the right track.
As they say, do all the heavy lifting in your model.
I'd look to refactor those methods personally in the model. All those places where you are calling *.downcase.gsub...
Also look into to_param, a method you can overwrite to get purdy urls.