Rails table display and database design? - ruby-on-rails

I have two tables, Beacon belongs to Category and Category has many beacons, in beacon table, I add a foreign key: category_id, but when i want display in table, i need the category_name in the category table, now I use a stupid way as following:
...
<% #beacons.each do |beacon| %>
<tr>
<td><%= beacon.beacon_uuid %></td>
<% #category_name = Category.find(beacon.category_id).category_name %>
<td><%= #category_name %></td>
...
but when beacon data get bigger, request get bigger, how to change my code to defence this stupid question?can someone give me some suggestion or reference? Thanks a lot.

It sounds like you're referring to an N+1 query, wherein the number of queries executed is equal to the number of beacons + 1. If so, your queries look similar those below.
select beacons.* from beacons;
select categories.* from categories where categories.id = 1;
select categories.* from categories where categories.id = 2;
select categories.* from categories where categories.id = 3;
Assuming your beacon model has the belongs_to :category relation declared, this can be avoided by using includes to eager load the association in the controller
def index
#beacons = Beacon.all.includes(:category)
end
The view can then be changed to take advantage of this, instead of pulling a fresh copy of the category from the database for each beacon
<%= beacon.category.category_name %>

There is a helper of the category and its attributes in the beacon itself.
In Rails, you can go both ways:
Category.beacons will return an array of all the beacons the category has and their attributes
Beacon.category will return the attributes of its category.
Following the same logic, you just need this:
<%= beacon.category.category_name %>

Related

Rails 5 has_many_through include pivot table in result

I have a Rails 5 app with restaurants and products. One product has_many restaurant and one restaurant has many products. I created a pivot table and created and has_many_through relation since I want to work with the pivot table.
Product:
has_many :restaurant_products, dependent: :destroy
has_many :restaurants, through: :restaurant_products
Restaurant:
has_many :restaurant_products, dependent: :destroy
has_many :products, through: :restaurant_products
Since every restaurant can modify the price for each product I have added a custom_price column to my restaurant_products table.
Now I can get all products for a certain restaurant:
#restaurant.products
But this way, when I list the products, I don't have access to the custom price. How can I somehow include or access the correct pivot record to get the custom price?
You can use the following:
#restaurant.restaurant_products.joins(:products).select('restaurant_products.custom_price, products.*')
That will give you access to instances including all of a product's columns, as well as the join table's custom price.
The SQL generated will look similar to:
SELECT restaurant_products.custom_price, products.*
FROM `restaurant_products`
INNER JOIN `products` ON `products`.`id` = `restaurant_products`.`product_id`
WHERE `restaurant_products`.`restaurant_id` = 1
Quick tip: what's returned might look a little strange, something like:
[#<RestaurantProduct id: 1, custom_price: 10>]
...though if you call attributes, or a product attribute on an instance here, you'll have access to everything you want.
The above gives quick and efficient access to the records' data, though if you need to access methods on the product itself, you might prefer the following:
#product_details = #restaurant.restaurant_products.includes(:products)
And then to loop through the data:
#product_details.each do |product_detail|
product_detail.custom_price
product_detail.product.column_data
product_detail.product.a_method
end
Hope these help - let me know how you get on or if you have any questions.
Just provide a custom select that includes the row from the join table:
#products = #restaurant.products
.select('products.*, restaurant_products.custom_price')
This returns restaurant_products.custom_price as if it where a column on products. You can us AS to provide a alias if you want to call it something else than custom_price.
So you can do:
<table>
# ...
<% #products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= product.custom_price %></td>
</tr>
<% end %>
</table>
In such cases, it always better to query on pivot table as bellow
#restaurant_products = RestaurantProduct.includes(:product)
.where(restaurant_id: #restaurant.id)
You can display it in the view simply as follows.
<table>
<% #restaurant_products.each do |resta_product| %>
<tr>
<td><%= resta_product.product.name %></td>
<td><%= resta_product.custom_price %></td>
</tr>
<% end %>
</table>
You can even delegate all product methods to work directly on restaurant_product.

Rails has_and_belongs_to_many on 2 level association

This is related to this previously asked question
I have a has_and_belongs_to_many in place between Product and Supplier.
In my view I use:
<td><%= product.suppliers.map {|supplier| supplier.NAME }.join(', ') %></td>
To show list of suppliers comma separated on each row for each product in my table.
I now need to show the same list on invoices index view. Invoices table has a column PRODUCT. I have already set belongs_to :product on Invoice model.
I tried in my invoices index view:
<td><%= invoice.product.suppliers.map {|supplier| product.supplier.NAME }.join(', ') %></td>
but it returns
error undefined local variable or method `product'
Why isn't that working? How can I fix it? Thanks in advance.
you build wrong .map, try
invoice.product.suppliers.pluck(:NAME).join(', ')
BTW
it's bad practice use logic in view, you should move your logic to models, and in view use something like:
<%= invoice.suppliers_names %>
what should return # => 'Name_1, Name_2, etc'

Doing group_by on class with belongs_to in Rails

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)

Select multiple values from associated model rails

Assuming I have this association
User have_many posts
Post belongs_to user
User Post
----------------
id id
name title
user_id
How to list only post title and username with includes/joins ?
(list of posts [title - username])
#posts = Post.includes(:user).select('........')
don't offer this
#posts = Post.all.each {|p| p.user.username}
__________________UP_____________________
It worked for joining 2 tables.
What if I want to use it for more complex example?
check out my prev question optimize sql query rails
#Humza's answer partly worked.
it might be something like this
#posts = Post.joins(:user, :category).paginate(:page => params[:page]).order("created_at DESC")
but It doesn't display posts that don't have category
I also need to display gravatar but I think I can just use user.email as usr_email and use gravatar_for (post.usr_email) but I'll have to customize gravatar helper for this.
posts_controller.rb
def index
#posts = Post.includes(:user).includes(:comments).paginate(:page => params[:page]).order("created_at DESC")
end
index.html.erb
<%= render #posts %>
_post.html.erb
<%= gravatar_for post.user, size:20 %>
<%= link_to "#{post.title}", post_path(post) %>
<%= time_ago_in_words(post.created_at) %>
<%= post.comments.count %>
<%= post.category.name if post.category %>
Take a look at pluck.
Post.joins(:user).pluck(:title, :name)
Note that it works in this case because there's no ambiguity regarding the name column, you might want to specify the table explicitly (pluck(:title, "users.name")).
includes is used in case of eager-loading. You need joins in this case.
posts = Post.joins(:user).select("posts.title AS title, users.name AS username")
You can access the values then in the following way:
post = posts.first
post.title # will give the title of the post
post.username # will give the name of the user that this post belongs to
If you can pluck multiple columns, then the following might be more useful to you:
posts = Post.joins(:user).pluck("posts.title", "users.name")
The result will be a 2D array, with each element being an array of the form [post_title, post_username]
Post.joins(:user, :category)
but It doesn't display posts that don't have category
That's because joins uses INNER JOIN to join the tables together. If you want to everything from Post even though the particular record doesn't have its counterpart in the other table, you need to use LEFT JOIN. Unfortunately ActiveRecord doesn't have a nice way of generating it and you will need to do that manually:
Post.joins("LEFT OUTER JOIN categories ON categories.post_id = posts.id")...
See A Visual Explanation of SQL Joins for more information.
You can call array methods on a scope so:
Post.includes(:user).map { |p| [p.title, p.user.name] }
will get the posts with included user and map each post to a tuple of the post title and the user name.
That may not entirely answer your question as I think you might want to restrict the results of the query to just the required fields in which case, I think you can add a .select('title', 'users.name') to the query. (Not in a position to test at the moment)

List data from two models

I have two models, Items and Calibrations. Items has many calibrations, meaning that every year the instruments have to be calibrated. Fields, date_calibration and date_expired, are located in the "calibrations" table. (Items: has_many :calibrations, calibration: belongs_to item)
I need list/show all the items that are expiring. I can list all the items without problem of course but, I don't know how to add date_expired to the list.
In the Items controller:
#items = Item.all.order("created_at DESC")
In the Index:
<% #items.each do |item| %>
<tr>
<td><%= item.cod %></td>
<td><%= item.number %></td>
<td><%= item.den_cont %></td>
<td><%= item.branch %></td>
<td><%= item.model %></td>
</tr>
<% end %>
I'm using Aptana and PostgreSQL version 9.1, Ruby 2.1 and Rails 4.1.
Can anyone of you suggest any solution or point me to the right direction?
UPDATE
What should I change to show the item using the sentence below..
Item_controller
Item.includes(:calibrations).where('calibrations.date_expired <= ?' , 2014/07/12)
Index
<% #items.each do |item| %>
Return undefined method each.
ALSO
Any idea on how to show a traffic light depending on how many days left to calibration_date ?? Tks again!
As long you have your relations properly defined in your models I do believe something similar to the following should do the trick!
Item.joins(:calibrations).where(date_expired <whatever condition>)
The equivalent SQL being:
SELECT Items.*
FROM Items
LEFT OUTER JOIN Calibrations
ON Calibrations.item_id = Items.item_id
WHERE date_expired <whatever condition>
With the equivalent SQL of the above statement being (using the includes method):
SELECT *
FROM Items
LEFT OUTER JOIN Calibrations
ON Calibrations.item_id = Items.item_id
WHERE date_expired <whatever condition>
Hope this helps!
Also, if you're not wanting to return any data related to the calibrations table (which it looks like this is the case), I would go with the joins method, however if you are, the includes method would be the way to go (http://tomdallimore.com/blog/includes-vs-joins-in-rails-when-and-where/)
Also, the following may be of interest: http://guides.rubyonrails.org/active_record_querying.html (particularly section 12)!
#items = Item.includes(:calibrations)
.where('calibrations.date_expired <= ?', some_date)
include will join the two tables and allow you to specify conditions on items based on columns from the calibrations table. I think that's what you wanted isn't it?

Resources