I'm wondering how you would simulate a :has_many :through using an AR call.
Ultimately, I'm trying to find the subcategories that belong to a site, not the top level categories, which is what the query currently gives me.
Models:
class Categories < AR
:has_many :subcategories, :through => :cat_subcat_links
:has_many :cat_subcat_links
end
Linking Model:
class CatSubcatLinks < AR
:category_id
:subcategory_id
:site_id
end
At the moment, if I want to get which categories belong to a particular site I perform:
Category.joins(:cat_subcat_links).where(:cat_subcat_links => {:site_id => 1})
The query that returns:
SELECT `categories`.* FROM `categories` INNER JOIN `cat_sub_links` ON `cat_sub_links`.`category_id` = `categories`.`id` WHERE `cat_sub_links`.`site_id` = 1
The problem is
`cat_sub_links`.`category_id` = `categories`.`id`
I need it to say
`cat_sub_links`.`subcategory_id` = `categories`.`id`
That will cause the query to return me the subcategories. Thoughts?
Thanks in advanced,
Justin
Assuming you have in your site.rb
class Site < AR
has_many :cat_subcat_links
has_many :subcategories, :through => :cat_subcat_links
has_many :categories, :through => :cat_subcat_links
end
Couldn't you just do:
Site.find([your_site_id]).categories
Also:
Site.find([your_site_id]).subcategories
Also just a thought; are you sure it wouldn't be better to use acts_as_tree or awesome_nested_set instead?
Ultimately, I had to write the join instead since I can't specify a :source like I normally would if I created the relationship.
Category.joins("JOIN cat_sub_links ON cat_sub_links.subcategory_id=categories.id").where(: cat_sub_links => {:site_id => #site_ids})
I'll leave the answer out for a few days if anyone has a more elegant solution.
Related
I have the following models:
class Product < ActiveRecord::Base
has_many :product_recommendation_sets, :dependent => :destroy
has_many :recommendation_sets, :through => :product_recommendation_sets
end
class RecommendationSet < ActiveRecord::Base
has_many :product_recommendation_sets, :dependent => :destroy
has_many :products, :through => :product_recommendation_sets
has_many :recommendations
end
class Recommendation < ActiveRecord::Base
belongs_to :recommendation_set
end
And am adding recommendations recommendations_set like so:
p = Product.find_by_wmt_id(product) || Product.create( ItemData.get_product_data(product) )
recommendation = find_by_rec_id(rec_id) || create( ItemData.get_product_data(rec_id) )
rec_set = RecommendationSet.find_or_create_by_rating_set_id_and_model_version_and_product_id(rating_set.id, model_version, p.id)
sec_set.update_attributes(
:rating_set_id => rating_set.id,
:product_id => p.id,
:model_version => model_version,
:notes => note
)
sec_set.recommendations << recommendation
sec_set.save
prs = ProductRecommendationSet.find_or_create_by_recommendation_set_id_and_rating_set_id_and_product_id(rec_set .id, rating_set.id, p.id,)
prs.update_attributes(
:recommendation_set_id => rec_set.id,
:rating_set_id => rating_set.id,
:product_id => p.id
)
This works as expected, however my problem is that I have multiple recommendation_sets which belong to multiple products, and each of the recommendation_sets may have the same recommendation. By saving each recommendation to a recommendation_set as I am currently doing, if two recommendation_sets have the same recommendation, only one of the sets will add that recommendation. Is there anyway of saving each recommendation to multiple recommendation_sets using a secondary id, such as save by recommendation_id_and_product_id, or would I need to change this releationship to a has_many :through?
Based on your clarification, I think you basically have a many-to-many relationship between RecommendationSet and Recommendation. Presently, you have a one-to-many.
There are a couple of options:
Use the has_and_belongs_to_many method in both models to describe the relationship;
Manually create a "join" model and then give both RecommendationSet and Recommendation a has_many to this join model (with two corresponding belongs_to lines in the join model pointing to the other two models);
A has_many ... :through style, like you mentioned
Note that the first two options require you to have a join table.
If you require additional information on the join table/model, I tend to go with the 2nd option. Otherwise, either the first or third are perfectly valid.
Ryan Bates of RailsCasts made an episode about this here: http://railscasts.com/episodes/47-two-many-to-many
And some more information from the Rails documentation: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Many-to-many
In short, if you don't need extra info on the join, I think your idea of the has_many ... :through is perfectly fine.
Let me know whether that helps
I have three tables: Devices, Actions, and ActionTypes. ActionTypes is a lookup table - nothing but a name and id. Here are the relationships:
Device
has_many :actions
has_many :action_types, :through => :actions
has_many :tasks
Action
belongs_to :device
has_one :action_type
ActionType
has_many :actions
has_many :devices, :through => :actions
I'm very new to Rails, so I have the feeling these relationships are not correct - my goal is to be able to use something like #device.action_types, which this is ALMOST doing, but this is the SQL it generates in that case:
SELECT "action_types".* FROM "action_types" INNER JOIN "actions" ON "action_types"."action_id" = "actions"."id" WHERE "actions"."device_id" = 1
This would be perfect, except it should join on action_types.id = actions.action_type_id instead. There is no actiontypes.action_id (and there shouldn't be), and I understand that AR is looking for it because of my relationships... I'm just not sure which part is wrong!
If you want the table actions to have an action_type_id, then your model Action need:
belongs_to :action_type
A useful resource to read is the rails guide about associations: here
I'm wondering what is the best way to display unique records from a has_many, through relationship in Rails3.
I have three models:
class User < ActiveRecord::Base
has_many :orders
has_many :products, :through => :orders
end
class Products < ActiveRecord::Base
has_many :orders
has_many :users, :through => :orders
end
class Order < ActiveRecord::Base
belongs_to :user, :counter_cache => true
belongs_to :product, :counter_cache => true
end
Lets say I want to list all the products a customer has ordered on their show page.
They may have ordered some products multiple times, so I'm using counter_cache to display in descending rank order, based on the number of orders.
But, if they have ordered a product multiple times, I need to ensure that each product is only listed once.
#products = #user.products.ranked(:limit => 10).uniq!
works when there are multiple order records for a product, but generates an error if a product has only been ordered once. (ranked is custom sort function defined elsewhere)
Another alternative is:
#products = #user.products.ranked(:limit => 10, :select => "DISTINCT(ID)")
I'm not confident that I'm on the right approach here.
Has anyone else tackled this? What issues did you come up against? Where can I find out more about the difference between .unique! and DISTINCT()?
What is the best way to generate a list of unique records through a has_many, through relationship?
Thanks
Have you tried to specify the :uniq option on the has_many association:
has_many :products, :through => :orders, :uniq => true
From the Rails documentation:
:uniq
If true, duplicates will be omitted from the collection. Useful in conjunction with :through.
UPDATE FOR RAILS 4:
In Rails 4, has_many :products, :through => :orders, :uniq => true is deprecated. Instead, you should now write has_many :products, -> { distinct }, through: :orders. See the distinct section for has_many: :through relationships on the ActiveRecord Associations documentation for more information. Thanks to Kurt Mueller for pointing this out in his comment.
Note that uniq: true has been removed from the valid options for has_many as of Rails 4.
In Rails 4 you have to supply a scope to configure this kind of behavior. Scopes can be supplied through lambdas, like so:
has_many :products, -> { uniq }, :through => :orders
The rails guide covers this and other ways you can use scopes to filter your relation's queries, scroll down to section 4.3.3:
http://guides.rubyonrails.org/association_basics.html#has-many-association-reference
On Rails 6 I got this to work perfectly:
has_many :regions, -> { order(:name).distinct }, through: :sites
I couldn't get any of the other answers to work.
in rails6 use -> { distinct } in scope it will work
class Person
has_many :readings
has_many :articles, -> { distinct }, through: :readings
end
person = Person.create(name: 'Honda')
article = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 7, name: "a1">]
Reading.all.inspect # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
You could use group_by. For example, I have a photo gallery shopping cart for which I want order items to be sorted by which photo (each photo can be ordered multiple times and in different size prints). This then returns a hash with the product (photo) as the key and each time it was ordered can be listed in context of the photo (or not). Using this technique, you could actually output an order history for each given product. Not sure if that's helpful to you in this context, but I found it quite useful. Here's the code
OrdersController#show
#order = Order.find(params[:id])
#order_items_by_photo = #order.order_items.group_by(&:photo)
#order_items_by_photo then looks something like this:
=> {#<Photo id: 128>=>[#<OrderItem id: 2, photo_id: 128>, #<OrderItem id: 19, photo_id: 128>]
So you could do something like:
#orders_by_product = #user.orders.group_by(&:product)
Then when you get this in your view, just loop through something like this:
- for product, orders in #user.orders_by_product
- "#{product.name}: #{orders.size}"
- for order in orders
- output_order_details
This way you avoid the issue seen when returning only one product, since you always know that it will return a hash with a product as the key and an array of your orders.
It might be overkill for what you're trying to do, but it does give you some nice options (i.e. dates ordered, etc.) to work with in addition to the quantity.
I have this working to a degree, but I am looking for some input on how to query for the siblings in a one to many relationship to see if there is a more elegant way of accomplishing this.
Consider the following classes
class Post < ActiveRecord::Base
has_many :post_categories
has_many :categories, :through => :post_categories
end
class Category < ActiveRecord::Base
has_many :post_categories
has_many :posts, :through => :post_categories
end
A post by definition can have multiple categories, what I would need this for is to show a "related posts" area on the site. Like I mentioned previously I do have a working version which is to simply do the following:
Post.find(id, :include => {:categories => :posts})
Looking at the logs the application then has to do five queries to get the end data that I am looking for.
Any thoughts are appreciated!
The only problem I see you having with what you have already is that you probably don't want to return all posts which share a category with a post.
#post = Post.find params[:id]
#related_posts = Posts.find(:all, :joins => :post_categories,
:select => "posts.*, count(post_categories) post_category_count",
:conditions => {:post_categories => {:category => #post.categories}},
:group => "posts.id", :order => "post_category_count desc")
This will return the most relevant posts first, ie. those which have the most shared categories, and you can either add a limit or paginate in order to limit results returned.
If you need support for large object trees, you might want to look at awesome nested set thought it may be overkill for this problem.
So Rails doesn't have support for :through associations through a habtm relationship. There are plugins out there that will add this in for Rails 2.x, but I'm using Rails 3 / Edge, and only need the association for one particular model. So I thought I'd stump it out myself with the beauty that is Arel.
First, the models:
class CardSet < ActiveRecord::Base
has_and_belongs_to_many :cards, :uniq => true
end
class Card < ActiveRecord::Base
has_and_belongs_to_many :card_sets, :uniq => true
has_many :learnings, :dependent => :destroy
end
class Learning < ActiveRecord::Base
belongs_to :card
end
I want to get all the learnings that belong to a particular card set, :through => cards.
This is what I have so far in my CardSet model:
def learnings
c = Table(Card.table_name)
l = Table(Learning.table_name)
j = Table(self.class.send(:join_table_name, Card.table_name, CardSet.table_name))
learning_sql = l.where(l[:card_id].eq(c[:id])).where(c[:id].eq(j[:card_id])).join(j).on(j[:card_set_id].eq(self.id)).to_sql
Learning.find_by_sql(learning_sql)
end
which gives me (damn, Arel is beautiful!):
SELECT `learnings`.`id`, `learnings`.`card_id`, `learnings`.`user_id`, `learnings`.`ef`, `learnings`.`times_seen`, `learnings`.`next_to_be_seen`, `learnings`.`interval`, `learnings`.`reps`, `learnings`.`created_at`, `learnings`.`updated_at`, `card_sets_cards`.`card_set_id`, `card_sets_cards`.`card_id` FROM `learnings` INNER JOIN `card_sets_cards` ON `card_sets_cards`.`card_set_id` = 1 WHERE `learnings`.`card_id` = `cards`.`id` AND `cards`.`id` = `card_sets_cards`.`card_id`
which is oh so close to what I'm aiming for - just needs to add in the cards table in the FROM part of the statement.
And there's my question: I've looked through the Arel source, and for the life of me I can't figure out how to add in another table.
As a sidenote, is there a better way to run the result from Arel (which usually returns an Arel::Relation, which I don't want) through ActiveRecord, other than rendering to sql and running the query with find_by_sql as I'm doing?
I'm not familiar with Arel, but won't it handle:
l.includes(:card)
This is easier than what I wrote above, after I learned how Rails encapsulates Arel so you don't need to build the query directly.
If you're trying to stub nested habtm in the CardSet model, a.k.a.:
has_many :learnings, :through => :cards
then you can use this to simulate the behaviour:
class CardSet < ActiveRecord::Base
has_and_belongs_to_many :cards, :uniq => true
def learnings
Learning.joins(:card).where(:cards => { :id => self.card_ids })
end
end
then a query like this will work:
CardSet.first.learnings