Rails: accessing a has_many relation from ActiveRecord_AssociationRelation - ruby-on-rails

class Article < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :article
end
Is there a way to access all books of a set of authors ?
For example: Article.where(...).books that will return the associated books of selected articles?

You can do this using a subquery:
Book.where(article: Article.where(...))
Note that Activerecord still turns this into a single SQL query:
SELECT `books`.* FROM `books` WHERE `books`.`article_id` IN (SELECT `articles`.`id` FROM `articles` WHERE (...))

Related

Join for has_many_through in rails

I have 3 models as following.
class Order
belongs_to :item
belongs_to :category
end
class Item
has_many :orders
belongs_to :category
end
class Category
has_many :items
has_many :orders, through: :items
end
I want to join the tables like Order.joins(:item).joins(:category), but it's not working.
Desired SQL is
SELECT * FROM `orders`
INNER JOIN `items` ON `items`.`id` = `orders`.`item_id`
INNER JOIN `categories` ON `items`.`category_id` = `categories`.`id`
I hope your helps.
I'm a little confused because Order and Item both belongs_to Category and Category already has_many Orders with that setup, the :through option is unnecesary.
For your desired output I guess you want to do a nested join (order > item > category) instead of multiple joins (order > item+category)
https://guides.rubyonrails.org/active_record_querying.html#joining-multiple-associations
12.1.3.1 Joining Nested Associations (Single Level)
Article.joins(comments: :guest)
This produces:
SELECT articles.* FROM articles
INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
So, you should do something like Order.joins(item: :category)
The syntax you're looking for is
Order.joins(item: :category)
Check here for more information.
The proper way to setup these associations is:
class Order < ApplicationRecord
belongs_to :item
# This references items.category_id
has_one :category, through: :item
end
class Item < ApplicationRecord
has_many :orders
belongs_to :category
end
class Category < ApplicationRecord
has_many :item, through: :orders
has_many :orders
end
You want to remove the orders.category_id column (if it exists) and use an indirect association through the items table to avoid duplication. The semantics of belongs_to and has_one can be confusing but belongs_to assumes that the foreign key is on this model (orders), while has_one places it on the other models table (items).
This will let you join/include/eager_load the association with:
irb(main):002:0> Order.joins(:category)
Order Load (1.4ms) SELECT "orders".* FROM "orders" INNER JOIN "items" ON "items"."id" = "orders"."item_id" INNER JOIN "categories" ON "categories"."id" = "items"."category_id" LIMIT $1 [["LIMIT", 11]]
And as you can see Rails will handle joining the join table (items) automatically.
If you want both associations to be loaded you can use a hash or just list both:
Order.eager_load(item: :category)
Order.eager_load(:item, :category)

Rails complex query with multi level associations

I have a deeply nested layout as follows:
Contract -> has many Packages -> has many Services
Payment -> belongs_to Invoice -> belongs_to Contract
class Contract < ActiveRecord::Base
has_many :invoices
has_many :contract_packages
has_many :packages, through: :contract_packages
end
class Package < ActiveRecord::Base
has_many :services
has_many :contract_packages
has_many :contracts, through: :contract_packages
end
class ContractPackage < ActiveRecord::Base
belongs_to :contract
belongs_to :package
end
class Service < ActiveRecord::Base
belongs_to :package
end
class Invoice < ActiveRecord::Base
belongs_to :contract
end
class Payment < ActiveRecord::Base
belongs_to :invoice
end
I want to find what Services, and how many times were invoiced in a certain period of time, based on payment date. Invoice date may not be the same as payment date.
I know hot to do it by pure SQL, and it works, but I am stuck if I want to do it the rails way.
Any ideas?
Edit:
The pure sql query:
select s.[name], count(*), s.[price] from payments p
left join invoices i on p.invoice_id=i.id
left join contracts c on i.[contract_id]=c.id
left join contract_packages cp on cp.contract_id=c.id
left join packages pk on cp.[package_id]=pk.id
left join services s on s.package_id=pk.id
where ... conditions
group by s.id
order by s.id asc
In my original question I left out, for brevity, a join table, because a package may belong to many contracts. The sql here includes the join table. I updated the models also.
Doing joins in in activerecord is quite straight forward as long as you have defined the relationships in your models. You just pass a hash to joins and it figures out what keys to use. Adding the where conditions can be done in similar fashion.
I noticed that there was no has_many :payments in your invoice is this by design? In that case why?
The select clause I have written will give all Service objects created with this query an extra method count where you will find your value.
Service.select('*, count(*) as count')
.joins({
package: {
contract: {
invoices: :payment
}
}
})
.where(conditions_hash)
.group('services.id asc')
.order(:id)

Finding an inverse relationship with ActiveRecord

I'm trying to find all Captains that do not have a boat with the classification sailboat. I can find all captains with a sailboat, but can't figure out how to do the inverse using ActiveRecord.
class Captain < ActiveRecord::Base
has_many :boats
end
class Boat < ActiveRecord::Base
belongs_to :captain
has_many :boat_classifications
has_many :classifications, through: :boat_classifications
end
class BoatClassification < ActiveRecord::Base
belongs_to :boat
belongs_to :classification
end
If you don't mind a little sql, you can left join the captain to their boats and classifications and select the ones that don't have a sailboat.
Captain.joins("
LEFT JOIN boats ON captains.id = boats.captain_id
LEFT JOIN boat_classifications ON boats.id = boat_classifications.boat_id
LEFT JOIN classifications ON boat_classifications.classification_id = classifications.id AND classifications.name = 'Sailboat'
").where("classifications.id IS NULL")
Or use your existing ones and use sql to select the captains not in that group
Captain.where("id NOT IN (?)", Captain.sailors.map(&:id))

Rails sorting result from group(..).sum(..)

I am using rails and graphing some data. I use the following:
<%= column_chart User.includes(:levels).group(:email).sum(:score) %>
How do i make this group command sort the returned array by score from highest to lowest?
My models are arranged as follows
class User < ActiveRecord::Base
has_many :games
contains id, email
end
class Game < ActiveRecord::Base
has_many :levels
belongs_to :user
#contains id, user_id, name
accepts_nested_attributes_for :levels
end
class Level < ActiveRecord::Base
belongs_to :game
#contains id, score and game_id
end
Is your score in Level or in User ?
OK, they're in a deeper nested relation.
You can make your life easier, if your User model declares that:
class User < ActiveRecord::Base
has_many :games
has_many :levels, through: :games
end
Then you have to join the levels.
Looking at the SQL generated by ActiveRecord, you can see that
User.joins(:levels).group(:email).sum(:score)
generates
SELECT sum(score) AS sum_score, email FROM users INNER JOIN games ON games.user_id = users.id INNER JOIN levels ON levels.games_id=games.id GROUP BY email
As sum doesn't return a Relation, but an ActiveSupport::OrderedHash, you cannot append .order() to it.
What you can do, is inject the order before the sum:
User.joins(:levels).group(:email).order('sum_score DESC').sum(:score)
generates
SELECT sum(score) AS sum_score, email FROM users
INNER JOIN games ON games.user_id = users.id
INNER JOIN levels ON levels.games_id=games.id
GROUP BY email
ORDER BY sum_score DESC
which is, what you are looking for.

has_one association with user and customer and rails view form

I've got the an has_one association:
has_one association user -> customer models
will the user have the customer_id or customer will have the user_id attribute?
other question: into my _form i'd like to have a select/option with all the users that hasn't associated with a customer which is the best way to do that?
thanks a lot.
The _id field is always in the model with the belongs_to, and refers to the other table name.
class Customer < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :customer
end
In this case, the customers table will have a user_id field.
For the second question, missing values are found in SQL using outer joins.
The SQL you want would be
select
from users
left outer join customers on users.id = customers.user_id
where customers.id is null
In ActiveRecord, add a scope to your User class.
In Rails 3.x:
class User < ActiveRecord::Base
has_one :customer
scope :missing_customer,
includes(:customer).where("customers.id is null")
end
In Rails 2.3.x:
class User < ActiveRecord::Base
named_scope :missing_customer,
{ :joins => "left outer join customers on users.id = customers.user_id",
:conditions => "customers.id is null" }
end

Resources