Finding an inverse relationship with ActiveRecord - ruby-on-rails

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))

Related

has_many and belongs_to with join table

I have Users and Trucks. I want the ability to say #truck.drivers << #users and #user.truck = #truck.
The solution is simple until I want the relationship to be stored in a join table.
# tables
:users
:id
:truck_drivers
:user_id
:truck_id
:truck
:id
I've gotten it to where I can say #truck.drivers << #user and #user.trucks << #truck, but I would like to limit a user to occupy one truck at a time, for my sanity.
Is it possible? A has_many/belongs_to with a join table? Or should I try a different approach? I'm not using a third model for the join table. It's just a table. Here's what I have so far.
class User < ApplicationRecord
has_and_belongs_to_many :trucks,
join_table: :truck_drivers, # points to the table
class_name: :Truck # looks for the truck model in the table
end
class Truck < ApplicationRecord
belongs_to :company
has_and_belongs_to_many :drivers,
join_table: :truck_drivers,
class_name: :User
end
The reason I need a join table in the first place is because each User can have many Roles: Admin, Manager, Driver, Customer Service, etc. I thought it didn't make sense to add a truck_id to all the users if all the users are not going to be using trucks.
It seems like you ought to be able to do something like:
#user.trucks << #truck unless #user.trucks.any?
Yes this is a standard strategy with rails using the :through keyword.
rails documentation: http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
Make a model called TruckUser with truck_id and user_id
then edit your classes:
class User < ApplicationRecord
has_many :truck_users
has_many :trucks, through: :truck_users
end
class Truck < ApplicationRecord
belongs_to :company
has_many :truck_users
has_many :drivers, through: :truck_users
end
class TruckUser < ApplicationRecord
belongs_to :truck
belongs_to :user
end

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)

ActiveRecord select through multiple HABTM

I have three models:
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
end
class Group < ActiveRecord::Base
has_and_belongs_to_many :channels
has_and_belongs_to_many :users
end
class Channel < ActiveRecord::Base
has_and_belongs_to_many :groups
end
What's the most efficient way to get all the channels (with no duplicates) for a particular user?
So effectively:
SELECT DISTINCT name FROM users
JOIN groups_users ON users.id=groups_users.user_id
JOIN channels_groups ON groups_users.group_id=channels_groups.group_id
JOIN channels ON channels_groups.channel_id=channels.id;
This should do it:
user = User.first
channels = user.groups.flat_map(&:channels).uniq

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.

Querying many to many relation in Ruby on Rails

Let's say I have an app where users could rate books. Tables are users(id), books(id) and rating(user_id, book_id, value). I've made these models
class Rating < ActiveRecord::Base
belongs_to :user
belongs_to :book
end
class User < ActiveRecord::Base
has_many :ratings
end
class Book < ActiveRecord::Base
has_many :ratings
end
I want to get a list of all (both rated and unrated) books with their ratings made by current user. It's easy in SQL with outer join but I can't figure out a way to do it in Rails 3.
According to LEFT OUTER joins in Rails 3 you'll have to specify the outer join in SQL...
it's quite simple in rails too. You probably should add a relationship in user with books as well.
class User < ActiveRecord::Base
has_many :ratings
has_many :users, :through => :ratings
end
current_user.books.includes(:ratings).all
should work.

Resources