Rails complex query with multi level associations - ruby-on-rails

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)

Related

Constructing Materialized View or use ActiveRecord for polymorphic Rails models

I'm working on an existing app which has the structure (simplified and) described below. One of our new queries is to find all activity in a company which is rather complicated and non-performant to build. It seems hard to write the query in ActiveRecord, so, I'm trying to use Scenic and build a Materialized View since this query is going to be mostly read-only.
So, we have Person, Group, Project, Report, Update and ActivityReceipt along with some join models. A person belongs to various groups and various projects.
My goal is to get show a feed of group activity of all of groups that a member is part of, so I'm looking to performantly fetch Activity by group, along with the author information, sorted by time and it seems like a materialized view with the following columns would make that happen:
activity_receipt_id, group_id, person_id
class Person < ApplicationRecord
has_many :groups
has_many :projects
end
class Membership < ApplicationRecord
# id, group_id, person_id
belongs_to :group
belongs_to :person
end
class Company < ApplicationRecord
has_many :projects
has_many :people
end
class GroupDonation < ApplicationRecord
# id, group_id, project_id
belongs_to :group
belongs_to :project_id
end
class Project < ApplicationRecord
has_many :group_donations
has_many :people
end
class ActivityReceipt < ApplicationRecord
# polymorphic belongs_to relation to 'postable' which are reports, & updates
# so, it has id, postable_id, and postable_type as columns and then other specific metadata
belongs_to :postable
end
class Report < ApplicationRecord
# project_id, membership_id and other specific metadata
belongs_to :project
belongs_to :membership #to track the author
has_one :activity_receipt
end
class Update < ApplicationRecord
# project_id, membership_id and other specific metadata
belongs_to :project
belongs_to :membership #to track the author
has_one :activity_receipt
end
Conceptually, the query is for all the given group_ids, fetch the associated projects and then their associated reports & updates, and then their activity receipts which is eventually needed. I'm not very familiar with writing SQL to generate a view so I have been struggling on how to make such a materialized view with polymorphic relations, and if it is even possible / recommended
Here is what I have so far:
SELECT groups.id AS group_id, people.id AS people_id, activity_receipts.id as activity_receipt_id
FROM groups
JOIN group_donations ON group_donations.group_id = groups.id
JOIN projects ON projects.id = group_donations.group_id
JOIN reports ON reports.project_id = project.id
JOIN updates ON updates.project_id = project.id
JOIN activity_receipts ON
// Stuck here

Rails: Two different many to many relationships between two models

I have two tables named books (id, name, author_name) and users (id, name, location). A book can either be viewed by a user or edited by a user. So, for these two relationships I have two join tables viz. book_editor_users (book_id, user_id) and book_viewer_users (book_id, user_id).
How do I model this in Rails such that I can retrieve editor users and viewer users like this:
Book.find(1).book_editor_users
Book.find(1).book_viewer_users
My attempt for the book and user model are:
class Book < ActiveRecord::Bas
has_many :book_editor_users
has_many :users, through: :book_editor_users
has_many :book_viewer_users
has_many :users, through: :book_viewer_users # I am confused on how to setup this line
end
class User < ActiveRecord::Base
has_many :books, through: :book_editor_users
has_many :books, through: :book_viewer_users # I am confused here too
end
Join models I have written are:
class BookEditorUser < ActiveRecord::Base
belongs_to :book
belongs_to :user
end
class BookViewerUser < ActiveRecord::Base
belongs_to :book
belongs_to :user
end
There is another work around that I thought of, but I am not sure whether it is the Rails way. That work around is to have a single join table book_users (book_id, user_id, type) where the type column can capture whether it is an editor relationship or a viewer relationship.
Single joining table(books_users) is the best way for doing this with permission column in it. Lets say integer column with 1 for view 2 for edit(3 for both if it could be a possibilty). And to get editor or viewer users you should write scope in their joining model(BooksUsers)
scope :viewers, -> { where(permission: 1) }
scope :editors, -> { where(permission: 2) }
now you can find books particular user from these scope
Book.find(1).books_users.viewers
Book.find(1).books_users.editors

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.

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