Rails Association Issue - ruby-on-rails

I don't seem to be understanding this "association" business in rails fully...hoping someone can help.
Model One
class Vendor < ActiveRecord::Base
has_many :products
end
Model Two
class Product < ActiveRecord::Base
belongs_to :vendor
end
When I go to find all "products" that are currently supported and then associate there vendor names...I'm not getting the correct results:
Product.where("is_supported = true").joins(:vendor)
The resulting query is:
SELECT `products`.* FROM `products` INNER JOIN `vendors` ON `vendors`.`id` = `products`.`vendor_id` WHERE (is_supported = true)
The issue is SELECT products.* instead of SELECT *
What am I missing to get the vendor table to join its own fields with the products table?

That is because you are making a JOIN and now you are working with 2 tables at same time. Then , rails makes a specific query.

An initial comment: When using Ruby on Rails (or any other SQL hiding framework) one should not worry (too much) about the underlying SQL queries. They are generated as needed (and might be 'sub-optimal')
To answer your question: 'How can I get the vendor information from a product object' (or that is at least how I read it)
The belongs_to, has_many and other associations add an attribute to the implementing class (in your case the Product and Vendor classes). You can use this attribute to get the information of the associated object.
p = getSomeProduct(...)
v = p.vendor
vendorName = p.vendor.name # or use v.name of course
This code will probably (can't check to be sure sure ATM) generate and execute multiple SQL queries. This is the price to pay to use a framework that hides SQL from the developer.
Please read the guide to Active Record Associations for more information about associations in Ruby on Rails.

Related

How to select from a model all elements that have no relation to another model

Sorry for the vague title.
I have 3 tables: User, Place and PlaceOwner.
I want to write a scope in the "PlaceOwner" model to get all the "Places" that don't have an owner.
class User < ApplicationRecord
has_one :place_owner
end
class PlaceOwner < ApplicationRecord
belongs_to :user
belongs_to :place
#scope :places_without_owner, -> {}
class Place < ApplicationRecord
has_many :place_owners
end
I tried checking for association in the rails console for each element and it worked. But I don't know how to implement this at scope. I've seen people solve similar problems by writing SQL but I don't have the knowledge to write it that way. I understand that I need to check all IDs from Place to see if they are in the PlaceOwner table or not. But I can't implement it.
For example:
There are 3 records in the "Place" table: House13, House14, House15.
There are 2 records in the "PlaceOwner" table: House13 - User1, House 14 - User2
I want to get House15
I hope I explained clearly what I'm trying to do. Please help or at least tell me where to go. Thanks in advance!
I would use the ActiveRecord::QueryMethods::WhereChain#missing method which was introduced in Ruby on Rails 6.1:
Place.where.missing(:place_owners)
Quote from the docs:
missing(*associations)
Returns a new relation with left outer joins and where clause to identify missing relations.
For example, posts that are missing a related author:
Post.where.missing(:author)
# SELECT "posts".* FROM "posts"
# LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# WHERE "authors"."id" IS NULL
In older versions:
Place.includes(:place_owners).where(place_owners: { id: nil })
Place.left_joins(:place_owners).where(place_owners: { id: nil })
Another interesting option is using EXISTS. Very often such queries have better performance, but unfortunately rails haven't such feature yet (see discussions here and here)
Place.where_not_exists(:place_owners)
Place.where_assoc_not_exists(:place_owners)
To use these methods you need to use where_exists or activerecord_where_assoc gem

Finding nil has_one associations in where query

This may be a simple question, but I seem to be pulling my hair out to find an elegant solution here. I have two ActiveRecord model classes, with a has_one and belongs_to association between them:
class Item < ActiveRecord::Base
has_one :purchase
end
class Purchase < ActiveRecord::Base
belongs_to :item
end
I'm looking for an elegant way to find all Item objects, that have no purchase object associated with them, ideally without resorting to having a boolean is_purchased or similar attribute on the Item.
Right now I have:
purchases = Purchase.all
Item.where('id not in (?)', purchases.map(&:item_id))
Which works, but seems inefficient to me, as it's performing two queries (and purchases could be a massive record set).
Running Rails 3.1.0
It's quite common task, SQL OUTER JOIN usually works fine for it. Take a look here, for example.
In you case try to use something like
not_purchased_items = Item.joins("LEFT OUTER JOIN purchases ON purchases.item_id = items.id").where("purchases.id IS null")
Found two other railsey ways of doing this:
Item.includes(:purchase).references(:purchase).where("purchases.id IS NULL")
Item.includes(:purchase).where(purchases: { id: nil })
Technically the first example works without the 'references' clause but Rails 4 spits deprecation warnings without it.
A more concise version of #dimuch solution is to use the left_outer_joins method introduced in Rails 5:
Item.left_outer_joins(:purchase).where(purchases: {id: nil})
Note that in the left_outer_joins call :purchase is singular (it is the name of the method created by the has_one declaration), and in the where clause :purchases is plural (here it is the name of the table that the id field belongs to.)
Rails 6.1 has added a query method called missing in the ActiveRecord::QueryMethods::WhereChain class.
It returns a new relation with a left outer join and where clause between the parent and child models to identify missing relations.
Example:
Item.where.missing(:purchase)

Rails way to COUNT the nr of records in my case

I am using Rails v2.3.2.
I have a model called UsersCar:
class UsersCar < ActiveRecord::Base
belongs_to :car
belongs_to :user
end
This model mapped to a database table users_cars, which only contains two columns : user_id, car_id.
I would like to use Rails way to count the number of car_id where user_id=3. I konw in plain SQL query I can achieve this by:
SELECT COUNT(*) FROM users_cars WHERE user_id=3;
Now, I would like to get it by Rails way, I know I can do:
UsersCar.count()
but how can I put the ...where user_id=3 clause in Rails way?
According to the Ruby on Rails Guides, you can pass conditions to the count() method. For example:
UsersCar.count(:conditions => ["user_id = ?", 3])
will generates:
SELECT count(*) AS count_all FROM users_cars WHERE (user_id = 3)
If you have the User object, you could do
user.cars.size
or
user.cars.count
Another way would be to do:
UserCar.find(:user_id => 3).size
And the last way that I can think of is the one mentioned above, i.e. 'UserCar.count(conditions)'.
With the belogngs to association, you get several "magic" methods on the parent item to reference its children.
In your case:
users_car = UsersCar.find(1) #=>one record of users_car with id = 1.
users_car.users #=>a list of associated users.
users_car.users.count #=>the amount of associated users.
However, I think you are understanding the associations wrong, based on the fact that your UsersCar is named awkwardly.
It seems you want
User has_and_belongs_to_many :cars
Car has_and_belongs_to_manu :users
Please read abovementioned guide on associations if you want to know more about many-to-many associations in Rails.
I managed to find the way to count with condition:
UsersCar.count(:condition=>"user_id=3")

How can I do a LEFT OUTER JOIN using Rails ActiveRecord?

I don't have any ideas. Could you give me any clues (like reference sites). Any help will be appreciated.
Model1: GROUP(id, name)
Model2: USER_GROUP_CMB(id, user_id, group_id)
Expected SQL statement:
SELECT *
FROM groups AS g LEFT OUTER JOIN user_group_cmbs AS cmb
ON g.id = cmb.group_id
WHERE cmb.user_id = 1
I tried to set up associations below but I dont know what to do after this.
class Group < ActiveRecord::Base
has_many :user_group_cmb
end
class UserGroupCmb < ActiveRecord::Base
has_many :group
end
Rails Version: 3.1.1
I believe an includes will use a LEFT OUTER JOIN query if you do a condition on the table that the includes association uses:
Group.includes(:user_group_cmb).where(user_group_cmbs: { user_id: 1 })
Rails 5+ allows you to do the following:
Group.left_outer_joins(:user_group_cmb)
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-left_joins
You can do this in rails 3.x regardless if you are referencing the table or not in a where clause:
Group.eager_load(:user_group_cmb)
...and it will perform a left outter join
It might be quite important to mention that using includes has possibly unwanted side-effects.
if the filtered association is subsequently scoped, all of the
original filtering disappears
As it turns out, by scoping on the filtered association we’ve lost any
filtering-as-side-effect that we attained from includes. And it’s not
because of how we searched, either.
Make sure to read the complete article and alternatively there's a gem for that.
The problem with the accepted answer is that it will actually do a LEFT INNER JOIN technically because it won't display entries where the user_group_cmbs. user_id is null (which would be the reason to do a LEFT OUTER JOIN).
A working solution is to use #references:
Group.includes(:user_group_cmb).references(:user_group_cmb)
Or the even more convenient #eager_load:
Group.eager_load(:user_group_cmb)
Read the more detailed explanation here.
Use a custom joins statement:
Group.joins("left outer join user_group_cmbs as cmb on groups.id = cmb.group_id")
.where("cmb.user_id = ?", 1)
Use has_and_belongs_to_many if you just need to link users to groups. Use has_many :through if you need to store additional membership information.
Example:
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
end
class Group < ActiveRecord::Base
has_and_belongs_to_many :users
end
Couldn't comment to an earlier answer because well I don't have 50 reputation.
But here's how it worked for me when I ran into below error
includes-referenced relation raises a StatementInvalid exception, complaining it misses a FROM-clause entry. I need to add an explicit call to join, which defaults to being inner.
I had to use .references and mention the table name or for it get added to the FROM-clause

Ruby on rails activerecord joins - select fields from multiple tables

models:
#StatusMessage model
class StatusMessage < ActiveRecord::Base
belongs_to :users
default_scope :order => "created_at DESC"
end
#User Model
class User < ActiveRecord::Base
has_many :status_messages
end
In controller I want to join these two tables and get fields from both table. for example I want email field from User and status field from StatusMessage. When I use :
#status = User.joins(:status_messages)
Or
#status = User.includes(:status_messages)
It gives me only the user table data.
How can I implement this requirement?
You need to use includes here. It preloads data so you won't have another SQL query when you do #user.status_messages.
And yes you can't really see the effect: you need to check your logs.
First of all, I don't think it is possible (and reasonable) what you want to do. The reason for that is that the relation between User and StatusMessage is 1:n, that means that each user could have 0 to n status messages. How should these multitudes of attributes be included in your user model?
I think that the method joints in class ActiceRecord has a different meaning, it is part of the query interface. See the question LEFT OUTER joins in Rails 3
There are similar questions on the net, here is what I have found that matches most:
Ruby on Rails: How to join two tables: Includes (translated for your example) in the user a primary_status_message, which is then materialized in the query for the user. But it is held in one attribute, and to access the attributes of the status_message, you have to do something like that: #user.primary_status_message.status
When you use #status = User.includes(:status_messages) then rails eagerley loads the data of all the tables.
My point is when you use this User.includes(:status_messages) it will loads the data of status_messages also but shows only users table data then if you want first user status_messages then you have to #user.first.status_messages

Resources