Ruby on Rails lambda with left join and condition - ruby-on-rails

Room has_many bookings
Booking belongs_to room
I have this code and it works fine:
available_rooms = Room.select {|room| room.bookings.where("day = ?", date).count < 3 || room.bookings.empty?}
but I wonder if it is possible to rewrite it like a lambda with left join
scope :available, lambda {|date| joins('LEFT OUTER JOIN bookings on bookings.room_id = rooms.id').......
I tried this but got back only Rooms WITH at least one Booking, so totally empty rooms WITHOUT Bookings at all were excluded:
def self.available(date)
# You can use `Arel.star` instead of `:id` on postgres.
b = Booking.arel_table[:id]
group(:id)
.left_joins(:bookings)
.where(bookings: { day: date })
.having(b.count.lt(3)) # COUNT(bookings.id) < 3
end

UPDATE
To satisfy the requirement you are actually going to need a complex join please try the following instead
class Room < ApplicationRecord
def self.available(date)
b = Booking.arel_table
join_statement = Arel::Nodes::OuterJoin.new(b,
Arel::Nodes::On.new(
arel_table[:id].eq(b[:room_id])
.and(b[:day].eq(Arel::Nodes.build_quoted(date)))
))
group(:id)
.joins(join_statement)
.having(b[:id].count.lt(3))
end
end
Here we are LEFT JOINing bookings based on the relation to room and the date requested. Now if a room is not booked for that date or it has less than 3 bookings for that date it should show up as requested.

Related

Ruby on Rails Active Record query joining two tables and query based on condition

I have two tables: Transactions and Properties.
I have one condition to satisfy that doesn't require joining tables.
On my Transactions query:
rows where sales_date is in a certain month
rows where sold_or_leased is "leased"
My next condition requires joining properties to transactions so that I can:
rows where transactions.sales_date is in a certain month
rows where transactions.sold_or_leased is null AND
rows where properties.for_sale is false AND properties.for_lease is true
Basically, a new column was added to transactions called sold_or_leased and a lot of them are null. I need to an extra query to cover the null columns.
#test variables for month
date = "2019-11-01"
month = Date.parse date
# below satisfies my first part
#testobj = Transaction.where(sold_or_leased: "leased")
.where("sales_date >= ? AND sales_date < ?", month.beginning_of_month, month.end_of_month).count
But now I need to extend this query to include properties and test a property column
I'm not sure where to go from here:
#testobj = Transaction.joins(:property)
.where(sold_or_leased: "leased")
.where("sales_date >= ? AND sales_date < ?", month.beginning_of_month, month.end_of_month)
.or(
Transaction.where(sold_or_lease: nil)
).count
Also, when I add a join and then an or clause, i get an error Relation passed to #or must be structurally compatible. Incompatible values: [:joins]
I will share relevant model info:
Transaction Model:
class Transaction < ApplicationRecord
belongs_to :user
belongs_to :property
end
Property Model:
class Property < ApplicationRecord
has_one :property_transaction, class_name: 'Transaction', dependent: :destroy
end
With the help of Sebastian, I have the following (which still produces the structural error message):
Transaction.joins(:property)
.where(sales_date: month.all_month,
sold_or_leased: nil,
properties: { for_sale: false, for_lease: true })
.or(
Transaction.joins(:property)
.where(sold_or_leased: "leased")
.where("sales_date >= ? AND sales_date < ?", month.beginning_of_month, month.end_of_month)
)
In theory, you should be able to access the properties table columns after a join.
Looking to your current code and what you need to get you could try with:
Transaction
.joins(:property)
.where(sales_date: month.all_month)
.where(
"(sold_or_leased IS NULL AND properties.for_sale = false AND properties.for_lease = true) OR
(sold_or_leased = 'leased')"
)
If you're unable to use the ActiveRecord::QueryMethods#or, you can always use the SQL OR operator within a string argument to where.
Notice month.all_month produces the whole range of dates for a corresponding month, which when used with where is converted to the first and last day of the month:
SELECT ... WHERE "transactions"."sales_date" BETWEEN $1 AND $2 AND ... [["sales_date", "2019-11-01"], ["sales_date", "2019-11-30"]]
Shorter than the month.beginning_of_month and month.end_of_month variation.

How to order cumulated payments in ActiveRecord?

In my Rails app I have the following models:
class Person < ApplicationRecord
has_many :payments
end
class Payment < ApplicationRecord
belongs_to :person
end
How can I get the payments for each person and order them by sum?
This is my controller:
class SalesController < ApplicationController
def index
#people = current_account.people.includes(:payments).where(:payments => { :date => #range }).order("payments.amount DESC")
end
end
It gives me the correct numbers but the order is wrong. I want it to start with the person having the highest sum of payments within a range.
This is the current Payments table:
How can this be done?
This should work for you:
payments = Payment.arel_table
sum_payments = Arel::Table.new('sum_payments')
payments_total = payments.join(
payments.project(
payments[:person_id],
payments[:amount].sum.as('total')
)
.where(payments[:date].between(#range))
.group( payments[:person_id])
.as('sum_payments'))
.on(sum_payments[:person_id].eq(Person.arel_table[:id]))
This will create broken SQL (selects nothing from payments which is syntactically incorrect and joins to people which does not even exist in this query) but we really only need the join e.g.
payments_total.join_sources.first.to_sql
#=> INNER JOIN (SELECT payments.person_id,
# SUM(payments.amount) AS total
# FROM payments
# WHERE
# payments.date BETWEEN ... AND ...
# GROUP BY payments.person_id) sum_payments
# ON sum_payments.id = people.id
So knowing this we can pass the join_sources to ActiveRecord::QueryMethods#joins and let rails and arel handle the rest like so
current_account
.people
.includes(:payments)
.joins(payments_total.join_sources)
.where(:payments => { :date => #range })
.order("sum_payments.total DESC")
Which should result in SQL akin to
SELECT
-- ...
FROM
people
INNER JOIN payments ON payments.person_id = people.id
INNER JOIN ( SELECT payments.person_id,
SUM(payments.amount) as total
FROM payments
WHERE
payments.date BETWEEN -- ... AND ...
GROUP BY payments.person_id) sum_payments ON
sum_payments.person_id = people.id
WHERE
payments.date BETWEEN -- ... AND ..
ORDER BY
sum_payments.total DESC
This will show all the people having made payments in a given date range (along with those payments) sorted by the sum of those payments in descending order.
This is untested as I did not bother to set up a whole rails application but it should be functional.

ActiveRecord: Get all users who should be banned today

I have two models:
class User < ActiveRecord::Base
has_many :ban_messages, dependent: :destroy
end
class BanMessage < ActiveRecord::Base
belongs_to :user
end
Table BanMessage contains a field t.datetime :ban_date that stores the date, when user should be banned. User table contains a field status, that contains one of the status values (active, suspended, banned). When I send BanMessage to User, his status field changes from 'active' to 'suspended'. Also I can activate user back, so he would not be banned at ':ban_date', but his BanMessage wouldn't be destroyed.
So I need to create query for selecting all Users with status 'suspended', who have in their newest BanMessages records 'ban_date' field, with DateTime value, which is between 'DateTime.now.beginning_of_day' and 'DateTime.now.end_of_day'.
You can filter your users with where query followed by join method for associated ban_messages with where method to further filter with the date range.
User.where(status: 'suspended').joins(:ban_messages).where("ban_date >= ? AND ban_date <= ?", DateTime.now.beginning_of_day, DateTime.now.end_of_day )
Try something like this.
User.joins(:ban_messages).where(status: :suspended).where('ban_messages.ban_date BETWEEN :begin_time AND :end_date', begin_time: DateTime.now.beginning_of_day, end_time: DateTime.now.end_of_day)
Here's what I needed:
User.joins(:ban_messages)
.where('users.status = ?', :suspended)
.where('ban_messages.created_at = (SELECT MAX(ban_messages.created_at) FROM ban_messages WHERE ban_messages.user_id = users.id)')
.where('ban_messages.ban_date BETWEEN ? and ?', DateTime.now.beginning_of_day, DateTime.now.end_of_day)
.group('users.id')
UPDATE
MrYoshiji: The caveat in your code is that if you miss one day, then
the next day you won't see the people that should have been banned the
day before. You might want to get all User where the ban_date is
lesser than DateTime.now.end_of_day, either in the same view or
another one.
So final query might be something like this
User.joins(:ban_messages)
.where('users.status = ?', :suspended)
.where('ban_messages.created_at = (SELECT MAX(ban_messages.created_at) FROM ban_messages WHERE ban_messages.user_id = users.id)')
.where('ban_messages.ban_date < ?', DateTime.now.end_of_day)
.group('users.id')

Find associated model of all objects in an array

Using Rails 4, I have the following:
class Driver < ActiveRecord::Base
has_and_belongs_to_many :cars, dependent: :destroy
end
class Car < ActiveRecord::Base
has_and_belongs_to_many :drivers
end
I have a join table cars_drivers with car_id and patient_id.
I want to find drivers who are 30 years old and above (driver.age > 30), drives a Honda (car.brand = "Honda"), and sum the number of drivers found.
In a raw SQL fashion:
sql = "
SELECT
SUM (driver), age, brand
FROM cars_drivers
JOIN cars, drivers
ON cars_drivers.car_id = cars.id
ON cars_drivers.driver_id = drivers.id
where
age > 30
and brand = "Honda"
"
records_array = ActiveRecord::Base.connection.execute(sql)
This should count the cars:
Car.where(brand: BRAND).includes(:drivers).where('drivers.age > ?', AGE).count
This should count the drivers:
Driver.where('age > ?', AGE).includes(:cars).where('cars.brand = ?', BRAND).count
I do recommend not using has_and_belongs_to_many, it seems like you have a lot of logic between the Driver and Car but with this setup you cannot have validations, callbacks or extra fields.
I would create a join model called CarDriver or if there is something even more describing like Job or Delivery and save them in their own table. Here is an article on the subject.
Ended up with this:
#drivers =
Driver.
joins(:cars).
where(cars_drivers: {
driver_id: Driver.where('age > 30'),
complication_id: Car.find_by_model("Honda").id
}).
size

rails join on association and id

I want to achieve something that takes exactly 3 seconds on SQL, and I'm struggling with it for hours, I want to load all records and left join if it exists, if not, then don't give me the associated model.
the query I want to create is as follows:
"SELECT * FROM apartments LEFT JOIN comments ON apartments.id = comments.apartment_id AND comments.user_id = ?"
and when I call apartment.comments, it'll give me just the record (can only be one) for the specific user, not all the records for every user.
I tried
Apartment.joins("LEFT OUTER JOIN comments ON comments.apartment_id = apartments.id AND comments.user_id = #{user_id}")
but it doesn't work, as when I call apartments.comments it fires another query which returns all possible comments.
Apartment.includes(:comments).where("comments.user_id = ?", user_id)
doesn't work aswell, because it returns only apartments who has a comment from the specific user.
help is needed!
Maybe you could try this:
#app/models/apartment.rb
Class Apartment < ActiveRecord::Base
has_many :comments
scope :user, ->(id) { where("comments.user_id = ?", id }
end
#apartment = Apartment.find(params[:id])
#comments = #apartment.comments.user(user_id)

Resources