ActiveRecord: Has_many inside another has_many - with rails3 - ruby-on-rails

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

Related

Rails multiple joins into one method

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.

Modeling a Subscription in Rails

So, after thinking on this for a while, I have no idea what the proper way to model this is.
I have a website focused on sharing images. In order to make the users happy, I want them to be able to subscribe to many different collections of images.
So far, there's two types of collections. One is a "creator" relationship, which defines people who worked on a specific image. That looks like this:
class Image < ActiveRecord::Base
has_many :creations
has_and_belongs_to_many :locations
has_many :creators, through: :creations
end
class Creator < ActiveRecord::Base
has_many :images, ->{uniq}, through: :creations
has_many :creations
belongs_to :user
end
class Creation < ActiveRecord::Base
belongs_to :image
belongs_to :creator
end
Users may also tag an image with a subjective tag, which is something not objectively present in the image. Typical subjective tags would include "funny" or "sad," that kind of stuff. That's implemented like this:
class SubjectiveTag < ActiveRecord::Base
# Has a "name" field. The reason why the names of tags are a first-class DB model
# is so we can display how many times a given image has been tagged with a specific tag
end
class SubjectiveCollection < ActiveRecord::Base
# Basically, "User X tagged image Y with tag Z"
belongs_to :subjective_tag
belongs_to :user
has_many :images, through: :subjective_collection_member
end
class SubjectiveCollectionMember < ActiveRecord::Base
belongs_to :subjective_collection
belongs_to :image
end
I want users to be able to subscribe to both Creators and SubjectiveTags, and to display all images in those collections, sequentially, on the home page when they log in.
What is the best way to do this? Should I have a bunch of different subscription types - for example, one called SubjectiveTagSubscription and one called CreatorSubscription? If I do go this route, what is the most efficient way to retrieve all images in each collection?
What you want to use is a Polymorphic Association.
In your case, it would look like this:
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :subscribeable, polymorphic: true
end
The subscriptions table would need to include the following fields:
user_id (integer)
subscribeable_id (integer)
subscribeable_type (string)
This setup will allow a subscription to refer to an instance of any other model, as ActiveRecord will use the subscribeable_type field to record the class name of the thing being subscribed to.
To produce a list of images for the currently logged in user, you could do this:
Subscription.where(user_id: current_user.id).map do |subscription|
subscription.subscribeable.images.all
end.flatten
If the performance implications of the above approach are intolerable (one query per subscription), you could collapse your two types of subscribeables into a single table via STI (which doesn't seem like a good idea here, as the two tables aren't very similar) or you could go back to your initial suggestion of having two different types of subscription models/tables, querying each one separately for subscriptions.

Rails 4 - Selecting associated models via collection (similar to .NET Linq's .SelectMany())

Forgive me if this has already been asked (as I believe it has), but I couldn't find this exact issue (and it's very likely I'm not searching properly).
I have the following models:
class Company < ActiveRecord::Base
has_many :jobs
end
class Job < ActiveRecord::Base
belongs_to :company
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :jobs
end
What I'm trying to accomplish is a list of tags by company, but I'm failing to figure out how (I should note that I'm pretty new to Ruby and Rails). I come from a .NET background, and with Linq I'd use something like Company.Jobs.SelectMany(j => j.Tags).
I tried to do Company.first.jobs.tags, which fails with NoMethodError: undefined method 'tags' for #<Job::ActiveRecord_Associations_CollectionProxy:0x892dca0>, but strangely enough, if I run Company.first.jobs.instance_methods on a rails console, there is a :tags method. And this is what I get when I use the console's autocomplete:
Any suggestions?
Thanks.
What you are looking for is called 'has many through'.
class Company < ActiveRecord::Base
has_many :jobs
has_many :tags, through: :jobs
end
This is how you can go through an association (in this case jobs) to get to that association's association.
This way you can run company.tags on an instance of a company.
The problem with your existing approach is that jobs is a collection, but you need to hit the method on individual instances. This would work with no adjustment:
Company.first.jobs.collect { |job| job.tags }

Referencing all of a collection of dependent objects objects

I have three activerecord classes: Klass, Reservation and Certificate
A Klass can have many reservations, and each reservation may have one Certificate
The definitions are as follows...
class Klass < ActiveRecord::Base
has_many :reservations, dependent: :destroy, :autosave => true
has_many :certificates, through: :reservations
attr_accessible :name
def kill_certs
begin
p "In Kill_certs"
self.certificates.destroy_all
p "After Destroy"
rescue Exception => e
p "In RESCUE!"
p e.message
end
end
end
class Reservation < ActiveRecord::Base
belongs_to :klass
has_one :certificate, dependent: :destroy, autosave: true
attr_accessible :klass_id, :name
end
class Certificate < ActiveRecord::Base
belongs_to :reservation
attr_accessible :name
end
I would like to be able to delete/destroy all the certificates for a particular klass within the klass controller with a call to Klass#kill_certs (above)
However, I get an exception with the message:
"In RESCUE!"
"Cannot modify association 'Klass#certificates' because the source
reflection class 'Certificate' is associated to 'Reservation' via :has_one."
I('ve also tried changing the reservation class to "has_many :certificates", and then the error is...
"In RESCUE!"
"Cannot modify association 'Klass#certificates' because the source reflection
class 'Certificate' is associated to 'Reservation' via :has_many."
It's strange that I can do Klass.first.certificates from the console and the certs from the first class are retrieved, but I can't do Klass.first.certificates.delete_all with out creating an error. Am I missing something?
Is the only way to do this..
Klass.first.reservations.each do |res|
res.certificate.destroy
end
Thanks for any help.
RoR docs have clear explanation for this (read bold only for TLDR):
Deleting from associations
What gets deleted?
There is a potential pitfall here: has_and_belongs_to_many and
has_many :through associations have records in join tables, as well as
the associated records. So when we call one of these deletion methods,
what exactly should be deleted?
The answer is that it is assumed that deletion on an association is
about removing the link between the owner and the associated
object(s), rather than necessarily the associated objects themselves.
So with has_and_belongs_to_many and has_many :through, the join
records will be deleted, but the associated records won’t.
This makes sense if you think about it: if you were to call
post.tags.delete(Tag.find_by(name: 'food')) you would want the ‘food’
tag to be unlinked from the post, rather than for the tag itself to be
removed from the database.
However, there are examples where this strategy doesn’t make sense.
For example, suppose a person has many projects, and each project has
many tasks. If we deleted one of a person’s tasks, we would probably
not want the project to be deleted. In this scenario, the delete
method won’t actually work: it can only be used if the association on
the join model is a belongs_to. In other situations you are expected
to perform operations directly on either the associated records or the
:through association.
With a regular has_many there is no distinction between the
“associated records” and the “link”, so there is only one choice for
what gets deleted.
With has_and_belongs_to_many and has_many :through, if you want to
delete the associated records themselves, you can always do something
along the lines of person.tasks.each(&:destroy).
So you can do this:
self.certificates.each(&:destroy)

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.

Resources