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