How to fetch data through "has_many" associations? - ruby-on-rails

I have the following structure of models:
class User < ActiveRecord::Base
has_many :favorites
end
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :company
end
class Color < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :colors
has_many :favorities
end
It means that a company has many colors. Every user can favorite a company (and I can print out then every color that the respective company offers).
But I am trying to display all colors that companies I've favorited offers.
I've tried it something like this:
favorited_colors = current_user.favorites.colors
undefined method `colors' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Favorite:0x007fe037da01f0>
and
favorited_colors = current_user.favorites.companies.colors
undefined method `companies' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Favorite:0x007fe038ce8db0>
Is there any other way how to get a list of all colors from favorited companies than to iterated via each loops through all favorited companies and save all colors into an array?
Thank you

This should work from rails 3.1 on:
class User
has_many :favorites
has_many :companies, :through => :favorites
has_many :favorit_colors, :through => :companies
end
And then
favorited_colors = current_user.favorit_colors
This is not tested, though.

Collections
The problem you have is that you're calling the companies method on a collection object
The issue here is that you can only call methods on single instances of objects. This is demonstrated quite well with the error you're seeing:
undefined method `companies' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Favorite:0x007fe038ce8db0>
The way you should attempt to get this to work is to call the following:
current_user.favorites.each do |favourite|
= favourite.colors
end
I understand what you're looking to achieve, so let me give you some ideas:
Association Extensions
There's a functionality of Rails called ActiveRecord Association Extensions, which could give you the ability to provide what you're seeking:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :favorites do
def colors
# Business logic for colours here
end
end
end
Now this is totally untested. However, I have experience with this deep part of Rails, and I therefore know that you'll have the following data-sets to work with:
proxy_association.owner returns the object that the association is a part of.
proxy_association.reflection returns the reflection object that describes the association.
proxy_association.target returns the associated object for belongs_to or has_one, or the collection of associated objects for
has_many or has_and_belongs_to_many.
From this, you'll be able to construct an array / object based off the colors you wish to show:
has_many :favorites do
def colors #totally untested
colors = {}
favorites = proxy_association.target
favorites.each do |favorite|
favorite.company.colors.each do |color|
colors << color
end
end
return colors
end
end

Related

Ruby on Rails: Is it possible to link a column with a column on another table?

I have models with deep associations in my Ruby on Rails API, sometimes 4 associations deep. For example:
Group has_many Subgroups has_many: Posts has_many: Comments
If I want to return Group.title with my comments, I need to say:
#comment.post.subgroup.group.title
Since this is way too many queries per Comment, I have added a column to the Comment table called group_title. This property is assigned when the Comment is created. Then every time the associated Group.title is updated, I have an after_update method on the Group model to update all associated Comment group_titles.
This seems like a lot of code to me and I find myself doing this often in this large scale app. Is there a way to link these 2 properties together to automatically update Comment.group_title every time its associated Group.title is updated?
I also had a similar relation hierarchy, and solved it (maybe there are better solutions) with joins.
Quarter belongs_to Detour belongs_to Forestry belongs_to Region
For a given detour, I find region name with one query.
Quarter.select("regions.name AS region_name, forestries.name as forestry_name, \
detours.name AS detour_name, quarters.*")
.joins(detour: [forestry: :region])
Sure, you can encapsulate it in a scope.
class Quarter
...
scope :with_all_parents, -> {
select("regions.name AS region_name, forestries.name as forestry_name, \
detours.name AS detour_name, quarters.*")
.joins(detour: [forestry: :region])
}
end
You can also use same approach.
class Comment
...
scope :with_group_titles, -> {
select("groups.title AS group_title, comments.*").joins(post: [subgroup: :group])
}
end
You can build hierarchies by using indirect associations:
class Group
has_many :subgroups
has_many :posts, through: :subgroups
has_many :comments, through: :posts
end
class Subgroup
belongs_to :group
has_many :posts
has_many :comments, through: :posts
end
class Post
belongs_to :subgroup
has_one :group, through: :subgroup
has_many :comments
end
class Comment
belongs_to :post
has_one :subgroup, through: :post
has_one :group, through: :post
end
The has_many :through Association
The has_one :through Association
This allows you to go from any end and rails will handle joining for you.
For example you can do:
#comment.group.title
Or do eager loading without passing a nested hash:
#comment = Comment.eager_load(:group).find(params[:id])
This however does not completely solve the performance issues related to joining deep nested hierarchies. This will still produce a monster of a join across four tables.
If you want to cache the title on the comments table you can use an ActiveRecord callback or you can define a database trigger procedure.
class Group
after_save :update_comments!
def update_comments!
self.comments.update_all(group_title: self.title)
end
end
You can do this by updating all the Comments from one side.
class Group
after_update do
Comment.joins(post: [subgroup: :group]).where("groups.title=?", self.title).update_all(group_title: self.title)
end
end

Deleting first occurrence from many to many collection entry at rails?

In many to many fields delete method is deleting all the occurrence of collection. Say I have:
class user < ActiveRecord::Base
has_and_belongs_to_many :cars
end
class car < ActiveRecord::Base
has_and_belongs_to_many :users
end
users and cars are many to many relationship, I have defined my users_cars table. Now user can have repetitive car entry as relation. For example:
Car: A,B,C
User: U1,U2,U3
U1=[A,B,C,A,A,A,B]
Which can be implemented using many to many relationship, the way I have implemented. BUT, at the time when I want to delete one of the car entries of user the problem occurs.
User.cars.delete(car) #deletes all occurrence of car
User.cars.delete_at(User.cars.find_index(video_card)) #delete_at does not exist
Now how to resolve this?
First of all, you can't call User.cars unless you have defined a class level method cars in your User model, but in this way, you would return all cars, and that - in no way - would make sense.
Second, delete_at is a method that works on Array objects, and expects an integer to be passed in. So as a little hack, you can turn your ActiveRecord::Associations object into an array, and then call delete_at method.
user = User.first
user.cars.to_a.delete_at(Car.last.id) # assuming that the last car belongs
# to the first user, something you would never do in actual
# production code.
Edit:
You can also try the following to achieve the same functionality:
user = User.first
user.cars.where("cars.id = ?", Car.first.id).first.delete
Edit 2:
For what you asked in comment, you can have a model for the table cars_users.
rails g model CarUser
class Car < ActiveRecord::Base
has_many :cars_users
has_many :users, through: car_users
end
class User < ActiveRecord::Base
has_many :cars_users
has_many :cars, through: car_users
end
class CarUser < ActiveRecord::Base
belongs_to :car
belongs_to :user
end
And now, you can do:
CarUser.where("car_id = ? AND user_id = ?", Car.first.id, User.first.id).first.delete

Rails: Query to get collection associated with collection

So I have these four classes:
class User < ActiveRecord::Base
has_many :water_rights
end
class WaterRight < ActiveRecord::Base
belongs_to :user
has_many :place_of_use_area_water_rights
has_many :place_of_use_areas, through: :place_of_use_area_water_rights
end
class PlaceOfUseAreaWaterRight < ActiveRecord::Base
belongs_to :place_of_use_area
belongs_to :water_right
end
class PlaceOfUseArea < ActiveRecord::Base
has_many :place_of_use_area_water_rights
has_many :water_rights, through: :place_of_use_area_water_rights
end
and I call User.first.water_rights and get a collection of WaterRights. My question is how do I get a collection of PlaceOfUseAreas associated with those WaterRights without doing something like this:
areas = []
water_rights.each do |wr|
areas << wr.place_of_use_areas
end
areas.flatten.uniq{ |a| a.id }
This works but it makes a new query for every single WaterRight. I'm looking for a way to make one query to get the collection of associated PlaceOfUseAreas.
You just want to get all associated PlaceOfUseAreas objects in single query, right?
If so, Rails have pretty single line solution for it:
PlaceOfUseArea.joins(:water_wights).uniq
Read more about joins method if you want more information.

Rails - insert many random items on create with has_many_through relation

I want to create a random pack of 15 cards which should be invoked in the cardpacks_controller on create. I have the following models:
Card:
class Card < ActiveRecord::Base
# relations
has_many :cardpacks, through: :cardpackcards
belongs_to :cardset
end
Cardpack:
class Cardpack < ActiveRecord::Base
#relations
has_many :cards, through: :cardpackcards
belongs_to :cardset
# accept attributes
accepts_nested_attributes_for :cards
end
Cardpackcards:
class Cardpackcard < ActiveRecord::Base
#relations
belongs_to :card
belongs_to :cardpack
end
Cardsets:
class Cardset < ActiveRecord::Base
#relations
has_many :cards
has_many :cardsets
end
How can I create 15 Cardpackcards records with random card_id values and with the same cardpack_id (so they belong to the same pack)
I have watched the complex form series tutorial but it gives me no comprehension as how to tackle this problem.
I hope anyone can help me solve this problem and give me more insight in the rails language.
Thanks,
Erik
Depending on the database system you might be able to use an order random clause to find 15 random records. For example, in Postgres:
Model.order("RANDOM()").limit(15)
Given the random models, you can add a before_create method that will setup the associations.
If the Cardpackcard model doesn't do anything but provide a matching between cards and cardpacks, you could use a has_and_belongs_to_many association instead, which would simplify things a bit.
Without it, the controller code might look something like this:
cardset = Cardset.find(params[:cardset_id])
cardpack = Cardpack.create(:cardset => cardset)
15.times do
cardpack.cardpackcards.create(:card => Card.create(:cardset => cardset))
end

Making record available only to certain models in Rails 3

I have a weird design question. I have a model called Article, which has a bunch of attributes. I also have an article search which does something like this:
Article.project_active.pending.search(params)
where search builds a query based on certain params. I'd like to be able to limit results based on a user, that is, to have some articles have only a subset of users which can see them.
For instance, I have an article A that I assign to writers 1,2,3,4. I want them to be able to see A, but if User 5 searches, I don't want that user to see. Also, I'd like to be able to assign some articles to ALL users.
Not sure if that was clear, but I'm looking for the best way to do this. Should I just store a serialized array with a list of user_id's and have -1 in there if it's available to All?
Thanks!
I would create a join table between Users and Articles called view_permissions to indicate that a user has permission to view a specific article.
class ViewPermission
belongs_to :article
belongs_to :user
end
class User
has_many :view_permissions
end
class Article
has_many :view_permissions
end
For example, if you wanted User 1 to be able to view Article 3 you would do the following:
ViewPermission.create(:user_id => 1, :article_id => 3)
You could then scope your articles based on the view permissions and a user:
class Article
scope :viewable_by, lambda{ |user| joins(:view_permissions).where('view_permissions.user_id = ?', user.id) }
end
To search for articles viewable by a specific user, say with id 1, you could do this:
Article.viewable_by(User.find(1)).project_active.pending.search(params)
Finally, if you want to assign an article to all users, you should add an viewable_by_all boolean attribute to articles table that when set to true allows an article to be viewable by all users. Then modify your scope to take that into account:
class Article
scope :viewable_by, lambda{ |user|
joins('LEFT JOIN view_permissions on view_permissions.article_id = articles.id')
.where('articles.viewable_by_all = true OR view_permissions.user_id = ?', user.id)
.group('articles.id')
}
end
If an Article can be assigned to multiple Writers and a Writer can be assigned to multiple Articles, I would create an Assignment model:
class Assignment < AR::Base
belongs_to :writer
belongs_to :article
end
Then you can use has_many :through:
class Article < AR::Base
has_many :assignments
has_many :writers, :through => :assignments
end
class Writer < AR::Base
has_many :assignments
has_many :articles, :through => :assignments
end

Resources