Saving a rails has_many association using multiple keys - ruby-on-rails

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

Related

How to Query Multiple Associated Tables in Rails

I have two associated table in my application and I cannot figure it how to join them together in rails, below is my Model:
class Lead < ApplicationRecord
has_many :employee_leads
has_many :employees, :through => :employee_leads
end
class EmployeeLead < ApplicationRecord
belongs_to :employee
belongs_to :lead
end
class Employee < ApplicationRecord
has_many :employee_leads
has_many :leads, :through => employee_leads
has_many :emp_stores
has_many :stores, :through => emp_stores
end
class EmpStore < ApplicationRecord
belongs_to :store
belongs_to :employee
end
class Store < ApplicationRecord
has_many :emp_stores
has_many :employees, :through => :emp_stores
end
My application required me to find out which store each lead belongs to. I know how to join the lead to the employee, which is:
Lead.joins(employee_leads: :employee)
and I also know how to join the employee to the store
Employee.joins(emp_stores: :store)
Those are working for me without issue. When I try to get the lead join to store, I used:
Lead.joins(employee_leads: :employee { emp_stores: :store })
This gave me a syntax error, I refer to the link of Active Record regarding Joining Nested Associations (Multiple Level) and I still can't figure it out. I'm very new to this, please someone take some time to explain and help me out. Thank you.
Try this:
Lead.joins(employee_leeds: [employee: [amp_stores: :store] ])
I believe these variants should also work:
Lead.joins(employee_leeds: [{employee: [{amp_stores: :store}] }])
Lead.joins(employee_leeds: {employee: {amp_stores: :store} })
Although, depending on how you're using this you might want to consider using includes rather than joins. Joins are lazy-loaded whereas includes are eager-loaded.
As an explanation to why your syntax was wrong ... :something is a symbol. And in the old (pre ruby 1.9) world, hash key/value pairs were written like so: :key => :value. However, from Ruby 1.9 the newer syntax of key: :value was provided.
In your attempt to get this working you had a key/value pair followed (without a separator) by another hash. In order to fix it, you need to provide just one value to the first key:
# Old syntax
:employee_leads => [ :employee => [ :emp_stores => :store ] ]
Which when you consider the new syntax for hashes, hopefully makes this more logical to understand:
# New syntax
employee_leads: [ employee: [ emp_stores: :store ] ]

Has_many: through in Rails. Can I have two foreign keys?

I am a rails newbie and I am trying to create a database schema that looks like the following:
There are many matches. Each match has 2 teams.
A team has many matches.
The team model and match model are joined together through a competition table.
I have that competition model with a match_id and a team1_id and a team2_id.
But I don't know how to make this work or if it's even the best way to go about it. I don't know how to make certain teams team1 and others team2.... two foreign keys? Is that possible?
The match table also needs to hold additional data like team1_points and team2_points, winner and loser, etc.
You can have as many foreign keys as you want in a table. I wrote an application that involved scheduling teams playing in games.
The way that I handled this in the Game class with the following:
class Game < ActiveRecord::Base
belongs_to :home_team, :class_name => 'Team', :foreign_key => 'team1_id'
belongs_to :visitor_team, :class_name => 'Team', :foreign_key => 'team2_id'
You can add appropriate fields for team1_points, team2_points, etc. You'll need to set up your Team model with something like:
class Team < ActiveRecord::Base
has_many :home_games, :class_name => 'Game', :foreign_key => 'team1_id'
has_many :visitor_games, :class_name => 'Game', :foreign_key => 'team2_id'
def games
home_games + visitor_games
end
#important other logic missing
end
Note that some of my naming conventions were the result of having to work with a legacy database.
I faced a similar problem, and extending the previous answer, what I did was:
class Game < ActiveRecord::Base
def self.played_by(team)
where('team1_id = ? OR team2_id = ?', team.id, team.id)
end
end
class Team < ActiveRecord::Base
def games
#games ||= Game.played_by(self)
end
end
This way, Team#games returns an ActiveRecord::Relation instead of an Array, so you can keep chaining other scopes.

Simulating has_and_belongs_to_many nested through behavior in Rails 3

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

Rails + simple role system through associative table

So I have the Ninja model which has many Hovercrafts through ninja_hovercrafts (which stores the ninja_id and the hovercraft_id).
It is of my understanding that this kind of arrangement should be set in a way that the associative table stores only enough information to bind two different classes.
But I'd like to use the associative table to work as a very streamlined authorization hub on my application. So i'd also like this table to inform my system if this binding makes the ninja the pilot or co-pilot of a given hovercraft, through a "role" field in the table.
My questions are:
Is this ugly?
Is this normal?
Are there methods built into rails that would help me to automagically create Ninjas and Hovercrafts associations WITH the role? For exemple, could I have a nested form to create both ninjas and hcs in a way that the role field in ninjas_hovercrafts would be also filled?
If managing my application roles this way isn't a good idea, whats the non-resource heavy alternative (my app is being designed trying to avoid scalability problems such as excessive joins, includes, etc)
thank you
This might not answer you question completely, but if you are only going to have two roles for hovercrafts I would instead set up the associations like this
class Hovercraft < ActiveRecord::Base
belongs_to :pilot, :class_name => 'Ninja', :foreign_key => 'pilot_id'
belongs_to :copilot, :class_name => 'Ninja', :foreign_key => 'copilot_id'
end
class Ninja < ActiveRecord::Base
has_many :pilotings, :class_name => 'Hovercraft', :foreign_key => 'pilot_id'
has_many :copilotings, :class_name => 'Hovercraft', :foreign_key => 'copilot_id'
end
Now if you have more roles than that, or if you need more flexibility you can use a third model to link them together.
class Hovercraft < ActiveRecord::Base
has_many :hovercraft_roles
has_many :ninjas, :through => :hovercraft_roles
end
class HovercraftRole < ActiveRecord::Base
belongs_to :hovercraft
belongs_to :ninja
end
class Ninja < ActiveRecord::Base
has_many :hovercraft_roles
has_many :hovercrafts, :through => :hovercraft_roles
end
With a role attribute in HovercraftRole model to indicated if it is a 'pilot' or 'copilot'.

Rails polymorphic many to many association

I'm trying setup a generic sort of web of related objects. Let say I have 4 models.
Book
Movie
Tag
Category
I would like to able to do:
book = Book.find(1)
book.relations << Tag.find(2)
book.relations << Category.find(3)
book.relations #=> [Tag#2, Category#3]
movie = Movie.find(4)
movie.relations << book
movie.relations << Tag.find(5)
movie.relations #=> [Book#1, Tag#5]
Basically I want to be able to take any 2 objects of any model class (or model class that I allow) and declare that they are related.
Obviously I don't want to create a huge mess of join tables. This seems like it's not quite a has many through association, and not quite a polymorphic association.
Is this something that Rails can support via it's association declarations or should I be rolling my own logic here?
Support for polymorphism has improved dramatically since the early days. You should be able to achieve this in Rails 2.3 by using a single join table for all your models -- a Relation model.
class Relation
belongs_to :owner, :polymorphic => true
belongs_to :child_item, :polymorphic => true
end
class Book
has_many :pwned_relations, :as => :owner, :class_name => 'Relation'
has_many :pwning_relations, :as => :child_item, :class_name => 'Relation'
# and so on for each type of relation
has_many :pwned_movies, :through => :pwned_relations,
:source => :child_item, :source_type => 'Movie'
has_many :pwning_movies, :through => :pwning_relations,
:source => :owner, :source_type => 'Movie'
end
A drawback of this kind of data structure is that you are forced to create two different roles for what may be an equal pairing. If I want to see all the related movies for my Book, I have to add the sets together:
( pwned_movies + pwning_movies ).uniq
A common example of this problem is the "friend" relationship in social networking apps.
One solution used by Insoshi, among others, is to register an after_create callback on the join model ( Relation, in this case ), which creates the inverse relationship. An after_destroy callback would be similarly necessary, but in this way at the cost of some additional DB storage you can be confident that you will get all your related movies in a single DB query.
class Relation
after_create do
unless Relation.first :conditions =>
[ 'owner_id = ? and owner_type = ? and child_item_id = ? and child_item_type = ?', child_item_id, child_item_type, owner_id, owner_type ]
Relation.create :owner => child_item, :child_item => owner
end
end
end
I have come up with a bit of solution. I'm not sure it's the best however. It seems you cannot have a polymorphic has_many through.
So, I fake it a bit. But it means giving up the association proxy magic that I love so much, and that makes me sad. In a basic state, here is how it works.
book = Book.find(1)
book.add_related(Tag.find(2))
book.add_related(Category.find(3))
book.related #=> [Tag#2, Category#3]
book.related(:tags) #=> [Tag#2]
I wrapped it up in a reusable module, that can be added to any model class with a single has_relations class method.
http://gist.github.com/123966
I really hope I don;t have to completely re-implement the association proxy to work with this though.
I think the only way to do it exactly as you described is the join tables. It's not so bad though, just 6, and you can pretty much set-and-forget them.
depending on how closesly related your movies/books db tables are
what if you declared
class Items < ActiveRecord::Base
has_many :tags
has_many :categories
has_and_belongs_to_many :related_items,
:class => "Items",
:join_table => :related_items,
:foreign_key => "item_id",
:associated_foreign_key => "related_item_id"
end
class Books < Items
class Movies < Items
make sure you put type in your items table

Resources