Rails multiple joins into one method - ruby-on-rails

I have a user in my application that can have multiple assessments, plans, and materials. There is already a relationship between these in my database. I would like to show all these in a single tab without querying the database too many times.
I tried to do a method that joins them all in a single table but was unsuccessful. The return was the following error: undefined method 'joins' for #<User:0x007fcec9e91368>
def library
self.joins(:materials, :assessments, :plans)
end
My end goal is to just itterate over all objects returned from the join so they can be displayed rather than having three different variables that need to be queried slowing down my load times. Any idea how this is possible?
class User < ApplicationRecord
has_many :materials, dependent: :destroy
has_many :plans, dependent: :destroy
has_many :assessments, dependent: :destroy
end
class Material < ApplicationRecord
belongs_to :user
end
class Assessment < ApplicationRecord
belongs_to :user
end
class Plan < ApplicationRecord
belongs_to :user
end

If all you want to do is preload associations, use includes:
class User < ApplicationRecord
# ...
scope :with_library, -> { includes(:materials, :assessments, :plans) }
end
Use it like this:
User.with_library.find(1)
User.where(:name => "Trenton").with_library
User.all.with_library
# etc.
Once the associations are preloaded, you could use this for your library method to populate a single array with all the materials, assessments and plans of a particular user:
class User < ApplicationRecord
# ...
def library
[materials, assessments, plans].map(&:to_a).flatten(1)
end
end
Example use case:
users = User.all.with_library
users.first.library
# => [ ... ]
More info: https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations

Prefer includes over joins unless you have a specific reason to do otherwise. includes will eliminate N+1 queries, while still constructing usable records in the associations: you can then loop through everything just as you would otherwise.
However, in this case, it sounds like you're working from a single User instance: in that case, includes (or joins) can't really help -- there are no N+1 queries to eliminate.
While it's important to avoid running queries per row you're displaying (N+1), the difference between one query and three is negligible. (It'd cost more in overhead to try to squish everything together.) For this usage, it's just unnecessary.

Related

How do I modify a record in a different model

I'm a rails begginer and I was coding a simple app to train the language and other stuff.
In my app, I have three different scaffolds generated, one for People, one for House Activities and one last to link them together called Assignments. It's a many to many dependency situation.
So I was trying to calculate the total time a person would have to spend doing all the house activities assigned to them and store it inside the Person in an attribute called "time_allocated". So if I have two activities assigned to someone, it would return the sum of the duration of those activities.
After searching I discovered that creating an attribute with three dependencies is no good, but I don't know how to do it other way.
These are the models and the things that I tried to do:
Person Model
class Person < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :house_activities, through: :assignments
extend FriendlyId
friendly_id :name, use: :slugged
end
House Activity Model
class HouseActivity < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :people, through: :assignments
extend FriendlyId
friendly_id :name, use: :slugged
end
Assignment Model
class Assignment < ActiveRecord::Base
belongs_to :person
belongs_to :house_activity
def self.time_allocation #fulltime
Assignment.all.each do |assignment|
if (assignment.person.time_allocation.present?)
assignment.person.time_allocation += assignment.house_activity.duration
else
assignment.person.time_allocation = assignment.house_activity.duration
end
end
end
end
If I understand correctly, you're trying to get the sum of the durations of all of a Person's house_activities. You can get this directly from the database using Rails' ActiveRecord::Calculations#sum method:
person = Person.find(123)
puts person.house_activities.sum(:duration)
# => 500
Of course, you could create a helper method for this as well:
class Person < ActiveRecord::Base
# ...
def total_activities_duration
house_activities.sum(:duration)
end
end
person = Person.find(123)
puts person.total_activities_duration
# => 500
I would advise against storing this sum in the database, because then you have to ensure its consistency (e.g. every time an Assignment is created, edited, or deleted, you have to ensure that the associated Person is updated with the new sum). You might think that calculating the sum anew every time will slow down your app, and it may at some time in the future when you have thousands of records, but there's no need to optimize this unless and until an actual performance problem arises.

Is there a way to get a single record back through a has_many relationship in Rails?

In my Rails (3.2) app, an Order has many LineItems. A LineItem has many LineItemPayments. A LineItemPayment has one Payment. (LineItems can potentially be payed for multiple times (subscriptions), which is why I have the join table there.)
I need to be able to query for order information from a payment record. I can get an array of orders via relationships, but I know they will always be the same order. Is there a way in Rails to set up the association to reflect this? If not, would it better to set up a method for retrieving the array and then picking the order out of that, or rather just storing the order_id with the payment and set up a direct relationship that sidesteps all this?
You'll need to work with the orders collection and narrow it down accordingly per your own logic. Although you certainly 'can' add the order_id to the payment directly, that will denormalize your data (as a cache) which is only recommended when you start hitting performance bottlenecks in your queries - otherwise it's asking for trouble in the area of data integrity:
class Payment < ActiveRecord::Base
has_many :line_item_payments
has_many :line_items, :through => :line_item_payments
has_many :orders, :through => :line_items
# use this to get the order quickly
def order
orders.first
end
# use this to narrow the scope on the query interface for additional modifications
def single_order
orders.limit(1)
end
end
class LineItemPayment < ActiveRecord::Base
belongs_to :line_item
belongs_to :payment
end
class LineItem < ActiveRecord::Base
belongs_to :order
has_many :line_item_payments
end

ActiveRecord: Has_many inside another has_many - with rails3

So I am setting up a system of pages and apps, and app content. Where a page has_many :app_instances, and app_instances has_many :app_contents.
I have models:
page, app, app_instance, app_content
In my models:
class Page < ActiveRecord::Base
has_many :app_instances
end
class App < ActiveRecord::Base
belongs_to :page
end
class AppInstance < ActiveRecord::Base
belongs_to :page
belongs_to :app
has_many :app_contents
end
class AppContent < ActiveRecord::Base
belongs_to :app_instance
belongs_to :app
end
I'd like to have one object, where all the contents are under the individual contents live under the app instance... but is there a way to do this?
I setup this in one of my controllers:
#page = Page.includes(:app_instances).find(params[:id])
I can successfully debug #page.app_instances, but soon as I say #page.app_instances.app_contents i get an error. I am guessing this is just because i am missing the include within the include, but not sure how i write that, or even if that is "good practice"
I know i could probably do:
has_many :app_contents, :through => :app_instances
but id rather have the object organized with the instance holding the contents (that makes sense, right?)... so i could loop through all the instances, and print out the contents of the instance
Does my DB architecture make sense? or am I going at this the wrong way
Joel
You can include nested using:
Page.includes(:app_instances=>[:app_contents])
But main reason you are getting an error is because, in #page.app_instances.app_contents
page.app_instances doesn't have any app_contents. You would be looking for an union of app_contents of all app_instances of #page
So it can be done via:
#page.app_instances.app_contents.each{|ai| (list || =[]) << ai.app_contents}
list.flatten!
OR
define page has many app_contents through app_instances relationship in your model.
The first case would run query for each app_instances , however with using includes as I mentioned earlier that would result in single query. The second case would result in a single join query joining app_contents, app_instances table

Ruby On Rails ActiveRecord 3 Way Join

I have 3 models:
class ProductLine < ActiveRecord::Base
has_many :specifications
has_many :specification_categories, :through => :specifications,
end
class Specification < ActiveRecord::Base
belongs_to :product_line
belongs_to :specification_category
end
class SpecificationCategory < ActiveRecord::Base
has_many :specifications
has_many :product_lines, :through => :specifications
end
Basically, we are showing the specifications as a subset of data on the product line page and we would like to do something like (example only, yes I'm aware of N+1):
Controller:
#product_line = ProductLine.find(params[:id])
#specification_categories = #product_line.specification_categories)
View:
#specification_categories.each do |specification_category|
...
specification_category.specifications.each do |specification|
...
end
end
The issue here is getting rails to filter the specifications by ProductLine. I've tried constructing queries to do this but it always generates a separate NEW query when the final association is called. Even though we aren't using the code above now (not a good idea here, since we could potentially run into an N+1 problem), I'd like to know if it's even possible to do a 3 way join with association filtering. Has anyone run across this scenario? Can you please provide an example of how I would accomplish this here?
Prevent the N+1 by altering your line to:
#specification_categories = #product_line.specification_categories).include(:specifications)
OR
Construct your own query using .joins(:association), and do the grouping yourself.

Rails 3: Querying from associated tables

Im very new to Ruby on Rails 3 and ActiveRecord and seem to have been thrown in at the deep end at work. Im struggling to get to grips with querying data from multiple tables using joins.
A lot of the examples Ive seen either seem to be based on much simpler queries or use < rails 3 syntax.
Given that I know the business_unit_group_id and have the following associations how would I query a list of all related Items and ItemSellingPrices?
class BusinessUnitGroup < ActiveRecord::Base
has_many :business_unit_group_items
end
class BusinessUnitGroupItem < ActiveRecord::Base
belongs_to :business_unit_group
belongs_to :item
belongs_to :item_selling_price
end
class Item < ActiveRecord::Base
has_many :business_unit_group_items
end
class ItemSellingPrice < ActiveRecord::Base
has_many :business_unit_group_items
end
I'm confused as to whether I need to explicity specify any joins in the query since the associations are in place.
Basically, you do not need to specify the joins:
# This gives you all the BusinessUnitGroupItems for that BusinessUnitGroup
BusinessUnitGroup.find(id).business_unit_group_items
# BusinessUnitGroupItem seems to be a rich join table so you might
# be iterested in the items directly:
class BusinessUnitGroup < ActiveRecord::Base
has_many :items through => :business_unit_group_items
# and/or
has_many :item_selling_prices, through => :business_unit_group_items
...
end
# Then this gives you the items and prices for that BusinessUnitGroup:
BusinessUnitGroup.find(id).items
BusinessUnitGroup.find(id).item_selling_prices
# If you want to iterate over all items and their prices within one
# BusinessUnitGroup, try this:
group = BusinessUnitGroup.include(
:business_unit_group_item => [:items, :item_selling_prices]
).find(id)
# which preloads the items and item prices so while iterating,
# no more database queries occur

Resources