How to Query Multiple Associated Tables in Rails - ruby-on-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 ] ]

Related

How do I write a Rails finder method through a chain of belongs_to associations?

I'm using Rails 5.1. How do I write a finder method when there is a chain of "belongs_to" associations? I have the following models ...
class Plan < ApplicationRecord
...
has_many :plan_items, :dependent => :destroy
class PlanItem < ApplicationRecord
...
belongs_to :offer, :optional => false
class Offer < ApplicationRecord
belongs_to :package, :optional => false
class Package < ApplicationRecord
has_and_belongs_to_many :items
I want to write a finder that gets all Plans with an Item with id = "blah". But the below is failing ...
[19] pry(main)> Plan.joins(plan_items: :offer).joins(packages: :item).where(:item => {:id => "abac"}).count
ActiveRecord::ConfigurationError: Can't join 'Plan' to association named 'packages'; perhaps you misspelled it?
from /Users/davea/.rvm/gems/ruby-2.5.1/gems/activerecord-5.2.2.1/lib/active_record/associations/join_dependency.rb:188:in `find_reflection'
How do I write a finder when there is a chain of belongs_to associations?
First, maybe your table name is wrong. Second, to pass method between belong_to association, you can use delegate
I'm assuming PlanItem is a join table between Plan and Item (that would be inline with the Rails naming convention). This could be done neatly with through associations and scopes. I would do it like this...
class Plan < ApplicationRecord
has_many :plan_items, dependent: :destroy
has_many :items, through: :plan_items
scope :blah_items { items.id_of_blah }
class PlanItem < ApplicationRecord
belongs_to :offer, optional: false
belongs_to :item
class Item < ApplicationRecord
scope :id_of_blah { where(id: 'blah') }
Then you can call it like so... Plan.with_blah_items or if you had an active record collection of plans you could use the scope to narrow it down plans.with_blah_items.
Since ActiveRecord associations will return ActiveRecord relations, you can chain them with any other active record methods (e.g. Plan.first.items.where(item: { id: 'blah' }) Scopes just make it nice and neat. : )
If PlanItem is not a join table between Plan and Item, first thing you should do is rename it. This is not just a best practice, rails spends a lot of time assuming what things are named, and it could cause a bug. After you rename it you should create a join table between Plan and Item called PlanItem. If a join between these tables doesn't make sense with your application architecture, you could always string through associations together, but that would be a code smell.
If you didn't want to mess with scopes, you could always just do a query like this plan.items.where(items: { id: 'blah' }).
Hope that helps!

Saving a rails has_many association using multiple keys

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

Rails 3 merging scopes with joins

Setup
For this question, I'll use the following three classes:
class SolarSystem < ActiveRecord::Base
has_many :planets
scope :has_earthlike_planet, joins(:planets).merge(Planet.like_earth)
end
class Planet < ActiveRecord::Base
belongs_to :solar_system
belongs_to :planet_type
scope :like_earth, joins(:planet_type).where(:planet_types => {:life => true, :gravity => 9.8})
end
class PlanetType < ActiveRecord::Base
has_many :planets
attr_accessible :gravity, :life
end
Problem
The scope has_earthlike_planet does not work. It gives me the following error:
ActiveRecord::ConfigurationError: Association named 'planet_type' was
not found; perhaps you misspelled it?
Question
I have found out that this is because it is equivalent to the following:
joins(:planets, :planet_type)...
and SolarSystem does not have a planet_type association. I'd like to use the like_earth scope on Planet, the has_earthlike_planet on SolarSystem, and would like to avoid duplicating code and conditions. Is there a way to merge these scopes like I'm attempting to do but am missing a piece? If not, what other techniques can I use to accomplish these goals?
Apparently, at this time you can only merge simple constructs that don't involve joins. Here is a possible workaround if you modify your models to look like this:
class SolarSystem < ActiveRecord::Base
has_many :planets
has_many :planet_types, :through => :planets
scope :has_earthlike_planet, joins(:planet_types).merge(PlanetType.like_earth)
end
class Planet < ActiveRecord::Base
belongs_to :solar_system
belongs_to :planet_type
scope :like_earth, joins(:planet_type).merge(PlanetType.like_earth)
end
class PlanetType < ActiveRecord::Base
has_many :planets
attr_accessible :gravity, :life
scope :like_earth, where(:life => true, :gravity => 9.8)
end
** UPDATE **
For the record, a bug was filed about this behavior - hopefully will be fixed soon...
You are reusing the conditions from the scope Planet.like_earth, which joins planet_type. When these conditions are merged, the planet_type association is being called on SolarSystem, which doesn't exist.
A SolarSystem has many planet_types through planets, but this is still not the right association name, since it is pluralized. You can add the following to the SolarSystem class to setup the planet_type association, which is just an alias for planet_types. You can't use the Ruby alias however since AREL reflects on the association macros, and doesn't query on whether the model responds to a method by that name:
class SolarSystem < ActiveRecord::Base
has_many :planets
has_many :planet_types, :through => :planets
has_many :planet_type, :through => :planets, :class_name => 'PlanetType'
scope :has_earthlike_planet, joins(:planets).merge(Planet.like_earth)
end
SolarSystem.has_earthlike_planet.to_sql # => SELECT "solar_systems".* FROM "solar_systems" INNER JOIN "planets" ON "planets"."solar_system_id" = "solar_systems"."id" INNER JOIN "planets" "planet_types_solar_systems_join" ON "solar_systems"."id" = "planet_types_solar_systems_join"."solar_system_id" INNER JOIN "planet_types" ON "planet_types"."id" = "planet_types_solar_systems_join"."planet_type_id" WHERE "planet_types"."life" = 't' AND "planet_types"."gravity" = 9.8
An easy solution that I found is that you can change your joins in your Planet class to
joins(Planet.joins(:planet_type).join_sql)
This will create an SQL string for the joins which will always include the correct table names and therefore should always be working no matter if you call the scope directly or use it in a merge. It's not that nice looking and may be a bit of a hack, but it's only a little more code and there's no need to change your associations.

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

Ruby on Rails Associations clarification

I'm starting out with the whole (wonderful) idea of database associations in Rails but I'm having problems because I'm working with an existing database that does not conform to Rails standards and cannot figure out how to name the associations. There are a couple of similar posts, but I can't wrap my head around the naming for my particular situation which is as follows:
table book with book.formatId looks up values in book_format.id
So foreign key book.formatId
My models are named: Book and BookFormat (I read that you use camelCase when your tables are separated by underscore).
Under the Book model I have this:
has_one :bookFormat, :foreign_key => 'book_format.id' # not sure if this format table.field is correct or I have to use something else here. Also not sure about the bookFormat, should it be BookFormat or bookformat?
The BookFormat model has this:
belongs_to :book
But when I try to do
book = Book.first
book.bookFormat.empty?
I get an error of method not found for bookFormat. So obviously something's wrong, but I can't figure out where.
A second part of the question is the use of many to many relationships. Example:
Tables
book, book_subjects, book_subjects2title
book_subjects.id => book_subjects2title.pId
book.id => book_subjects2title.bookId
I'm reading the Beginning Rails 3 book from Apress (which is a great book) but it's not very clear on all this or I'm just not getting it.
Thanks.
Since the book stores the formatId on it, you should use belongs_to, and change the foreign key as such:
belongs_to :book_format, :class_name => 'BookFormat', :foreign_key => 'formatId'
For the table name, i did some quick searching, and found the following method: set_table_name
So you should be able to add it at the top of your model, like so:
class Book < ActiveRecord::Base
set_table_name 'book'
# the rest of book model code
end
class BookFormat < ActiveRecord::Base
set_table_name 'book_format'
# rest of book_format model code
end
Normally rails uses plural table names, so hence why you need to specify it there.
agmcleod put me on the right track, so here's the full answer in the hopes that this helps other people with similar problems:
I created the model with a different name for easier reading. So model Books will have:
class Books < ActiveRecord::Base
set_table_name 'bookpedia'
belongs_to :format, :foreign_key => 'formatId' # we need to specify the foreign key because it is not the rails default naming
has_many :author2title, :foreign_key => 'bookId'
has_many :author, :through => :author2title
end
model Format:
class Format < ActiveRecord::Base
set_table_name 'book_format'
has_many :books
end
Then an instance of the class will have the format method:
#book = Books.first
#book.format # Hardcover
For many to many relationships I'll paste the syntax here as well since that took me a while to figure out:
class Author < ActiveRecord::Base
set_table_name 'book_author'
has_many :author2title, :foreign_key => 'pId' # junction table
has_many :books, :through => :author2title # establishing the relationship through the junction table
end
This is the actual junction table:
class Author2title < ActiveRecord::Base
set_table_name 'book_author2title'
belongs_to :author, :foreign_key => 'pId' # foreign key needs to be here as well
belongs_to :books, :foreign_key => 'bookId'
end
The book model above has the necessary entries for this many to many relationship already in it.
If anyone needs clarification on this I'd be happy to oblige since I struggled for more than a day to figure this out so would be glad to be of help.
Cheers.

Resources