Rails: join query on association with class_name - sql

class A < ActiveRecord::Base
has_one :b, class_name: "Something::B"
end
module Something
class B < ActiveRecord::Base
end
end
Assuming above class structure with actual table names a and something_b, I want to create the following SQL query.
SELECT "a".* FROM "a"
INNER JOIN "something_b" ON
"something_b"."a_id" = "a"."id"
WHERE "something_b"."some_column" = "some_value" LIMIT 1
I tried something along the lines of
A.joins(:b).find_by(b: { some_column: 'some_value' })
but the resulting query is as follows, which has "b" instead of "something_b" in the WHERE clause.
SELECT "a".* FROM "a"
INNER JOIN "something_b" ON
"something_b"."a_id" = "a"."id"
WHERE "b"."some_column" = "some_value" LIMIT 1
Is there a way to do it without explicitly specifying the table name as follows?
A.joins(:b).find_by(something_b: { some_column: 'some_value' })

You can try one of the following solution.
In module you can define the following method
def self.table_name_prefix
'something_'
end
or
In class you can set table name as
self.table_name = 'something_b'

If your model name and the table names do not match rails conventions, which in its simplest form table_name = SomeClass.name.pluralize.downcase, then you need to define the actual table name in your model.
module Something
class B < ActiveRecord::Base
self.table_name = 'something_b'
end
end
Above change should pick up the right table name in to the generated query.

Related

Get most recent records from deeply nested model

Say I have 3 models:
ModelA has many ModelB
ModelB has many ModelC
I'm querying ModelA, but in ModelC I have multiple ones of the same type, let's say I have 3 but I only need the most recently one.
I tried to do something like this...
records = ModelA.where(some query).includes ModelB includes ModelC
// convert activerecord collection to array
records = records.to_a
records.each do |record|
record.modelBs.each do |modelB|
filter the modelCs i don't need
modelB.modelCs = filteredModelCs
end
end
return records
but instead of merely returning the array of records, an UPDATE sql query is run and the db records are modified. this is a surprise because i never used the .save method and i thought i had converted the collection from an active record collection to an array
How can I filter deeply nested records without modifying the db records? then i can return the filtered result
Assigning a list of instances to a has_many collection with = will immediately persist the changes to the database.
Instead, I would try to solve this with more specific associations like this:
class A
has_many :bs
has_many(:cs, through: :bs)
has_one :recent_c, -> { order(created_at: :desc).limit(1) }, source: :cs
class B
has_many :cs
With those associations, I would expect the following to work:
as = A.where(some query).includes(:recent_c)
as.each do |a|
a.recent_c # returns the most recent c for this a
end
If I got you right, you want to get a collection of latest Cs, which are connected to Bs, which are connected to certain A-relation? If so, you can do something like that (considering you have tables as, bs and cs):
class A < ApplicationRecord
has_many :bs
end
class B < ApplicationRecord
belongs_to :a
has_many :cs
end
class C < ApplicationRecord
belongs_to :b
scope :recent_for_bs, -> { joins(
<<-sql
INNER JOIN (SELECT b_id, MAX(id) AS max_id FROM cs GROUP BY b_id) recent_cs
ON cs.b_id = recent_cs.b_id AND cs.id = recent_cs.max_id
sql
) }
end
And then you would query Cs like that:
C.recent_for_bs.joins(b: :a).merge(A.where(some_query))
You get recent Cs, inner join them with Bs and As and then get records connected to your A-relation by merging it.

Find an existing messaging group when given the potential members

I have an application where users can create messaging groups. MessageGroups have members through MessageMemberships. MessageMemberships belongs to a 'profile', which is polymorphic due to their being different types of 'profiles' in the db.
MessageGroup
class MessageGroup < ApplicationRecord
has_many :message_memberships, dependent: :destroy
has_many :coach_profiles, through: :message_memberships, source: :profile, source_type: "CoachProfile"
has_many :parent_profiles, through: :message_memberships, source: :profile, source_type: "ParentProfile"
has_many :customers, through: :message_memberships, source: :profile, source_type: "Customer"
end
MessageMembership
class MessageMembership < ApplicationRecord
belongs_to :message_group
belongs_to :profile, polymorphic: true
end
In my UI, I'd like to be able to first query to see if a messaging group exists with exactly x members so I can use that, rather than creating an entirely new messaging group (similar to how Slack or iMessages will find you an existing thread).
How would you go about querying that?
The code (not tested) below assumes:
You have (or can add) a message_memberships_count counter_cache column to the message_groups table. (and maybe adding an index to the message_memberships_count column to speed up the query)
You have proper unique indexing in the message_memberships table that will prevent a profile from being added to the same message_group multiple times
How it works:
There is a loop that will do multiple inner joins on the same table to ensure that the association exists for each profile
The query will then check that the total number of members in the group is equal to the number of profiles
class MessageGroup < ApplicationRecord
...
def self.for_profiles(profiles)
query = "SELECT \"message_groups\".* FROM \"message_groups\""
profiles.each do |profile|
klass = profile.class.name
# provide an alias to the table to prevent `PG::DuplicateAlias: ERROR
table_alias = "message_memberships_#{Digest::SHA1.hexdigest("#{klass}_#{profile.id}")[0..6]}"
query += " INNER JOIN \"message_memberships\" \"#{table_alias}\" ON \"#{table_alias}\".\"message_group_id\" = \"message_groups\".\"id\" AND \"#{table_alias}\".\"profile_type\" = #{klass} AND \"#{table_alias}\".\"profile_id\" = #{profile.id}"
end
query += " where \"message_groups\".\"message_memberships_count\" = #{profiles.length}"
self.find_by_sql(query)
end
end
Based on #AbM's answer I arrived at the following. This has the same assumptions as the previous answer, counter cache and unique indexing should be in place.
def self.find_direct_with_profiles!(profiles)
# Not present, some authorization checks that may raise (hence the bang method name)
# Loop through the profiles and join them all together so we get a join that contains
# all the data we need in order to filter it down
join = ""
conditions = ""
profiles.each_with_index do |profile, index|
klass = profile.class.name
# provide an alias to the table to prevent `PG::DuplicateAlias: ERROR
table_alias = "message_memberships_#{Digest::SHA1.hexdigest("#{klass}_#{profile.id}")[0..6]}"
join += " INNER JOIN \"message_memberships\" \"#{table_alias}\" ON \"#{table_alias}\".\"message_group_id\" = \"message_groups\".\"id\""
condition_join = index == 0 ? 'where' : ' and'
conditions += "#{condition_join} \"#{table_alias}\".\"profile_type\" = '#{klass}' and \"#{table_alias}\".\"profile_id\" = #{profile.id}"
end
# Add one
size_conditional = " and \"message_groups\".\"message_memberships_count\" = #{profiles.size}"
# Add any other conditions you may need
conditions += "#{size_conditional}"
query = "SELECT \"message_groups\".* FROM \"message_groups\" #{join} #{conditions}"
# find_by_sql returns an array with hydrated models from the select statement. In this case I am just grabbing the first one to match other finder active record method conventions
self.find_by_sql(query).first
end

SQL + Rails - is it posible to get a collection of objects with the values of attributes from other table columns

class Baby
belongs_to :child
attr :is_public
scope :public, includes(:child).merge(Child.public).where('babies.is_public IS TRUE')
end
class Child
belongs_to :parent
attr :is_public
scope: public, where ???
def is_public; read_attribute(:is_public).blank? ? self.parent.is_public : super(); end
end
class Parent
has_many :children
attr :is_public
end
Is it possible to get a collection of objects of Child
where if the value of attribute_a of child is NULL
it should get the value from parents.attribute_a
in one sql statement
I think, basic SQL logic should by like this:
SELECT c.*,p.* FROM child c
INNER JOIN parent p ON p.id = c.parent_id
LEFT JOIN baby b on b.id = c.baby_id
WHERE b.id IS NULL
if you want to use SQL query instead of ruby class, you can use:
query_result = ActiveRecord::Base.connection.execute('Your query')
Hope this helps

Rails activerecord inner join a custom object

I have classes:
class Want < ActiveRecord::Base
has_many :cached_buy_offers, dependent: :destroy
end
class CachedBuyOffer < ActiveRecord::Base
belongs_to :want
end
So, I can do
Want.joins(:cached_buy_offers)
which works as expected.
I want to generate following sql:
select * from wants
inner join
(select cached_buy_offers.want_id, max(buy_offer_cents) as max_buy_offer, count(cached_buy_offers.want_id) as buy_offer_count
from cached_buy_offers
where cached_buy_offers.want_id in (1,2,3,4)
group by cached_buy_offers.want_id
order by max_buy_offer) as cached_buy_offers
on cached_buy_offers.want_id = wants.id
Inner sql query can be generated using:
ids = [1,2,3,4]
CachedBuyOffer.select('cached_buy_offers.want_id, max(buy_offer_cents) as max_buy_offer, count(cached_buy_offers.want_id) as buy_offer_count').where('cached_buy_offers.want_id in (?)',ids).group('cached_buy_offers.want_id').order('max_buy_offer')
But when I try to do this:
Want.joins(CachedBuyOffer.select ..... the above activerecord inner query)
throws an error RuntimeError: unknown class: CachedBuyOffer
How can I generate the required sql?
You can use Arel.sql.
ids = [1,2,3,4]
cached_buy_offer_subquery = CachedBuyOffer
.select('cached_buy_offers.want_id,
max(buy_offer_cents) as max_buy_offer,
count(cached_buy_offers.want_id) as buy_offer_count')
.where('cached_buy_offers.want_id in (?)',ids)
.group('cached_buy_offers.want_id')
.order('max_buy_offer').to_sql
Want.joins("INNER JOIN (#{Arel.sql(cached_buy_offer_subquery)}) cached_buy_offers ON cached_buy_offers.want_id = wants.id")
.joins takes an association key as an argument, such as Want.joins(:cached_buy_offer). You can chain queries off of that!

Custom select on join table

I'm using rails 3.2, and trying to use ActiveRecord to query my database.
I have 2 activerecord models, Admin and Order:
class Admin < ActiveRecord::Base
attr_accessible :email, :name
has_many :orders
class Order < ActiveRecord::Base
attr_accessible :operation
belongs_to :admin
In my case, order.operation is a string, representing order type. I'm trying to build a query giving me three columns:
admin.name, admin.orders.where(:operation => 'bonus').count, admin.orders.where(:operation => 'gift').count
Is there a way to fit it in a single query?
EDITED:
here's raw sql to get what I need:
SELECT t_a.admin_id_com, t_a.name, gb_f_f_gb_f_fi.bonus_count, gb_f_f_gb_f_fi.gifts_count
FROM (
SELECT f_fil.admin_id_gifts, f_fil.gifts_count, f_filt.admin_id_bonus, f_filt.bonus_count
FROM (
SELECT admin_id as admin_id_gifts, count(distinct id) as gifts_count FROM orders WHERE operation = 'new gifts!' GROUP BY admin_id_gifts)
f_fil LEFT OUTER JOIN (
SELECT admin_id as admin_id_bonus, count(distinct id) as bonus_count FROM orders WHERE operation = 'new bonuses!' GROUP BY admin_id_bonus)
f_filt ON (f_fil.admin_id_gifts = f_filt.admin_id_bonus))
gb_f_f_gb_f_fi LEFT OUTER JOIN (
SELECT id AS admin_id_com, t_ad.name FROM admins t_ad) t_a ON (gb_f_f_gb_f_fi.admin_id_gifts = t_a.admin_id_com)
Is it possible to buid a query like that using ActiveRecord?
Try this:
#admins = Admin.joins(:orders).
select("admins.id, admins.name, orders.id,
SUM((orders.operation = 'bonus')::integer) AS bonus_count,
SUM((orders.operation = 'gift')::integer) AS gift_count ").
group("admins.id ")
# access each columns as
admin.name, admin.bonus_count, admin.gift_count
Other option is to use eager loading, it will use two queries but might be faster
#admins = Admin.includes(:orders)
# in admin.rb
def orders_count(type)
# Don't use where here as it would make a separate query instead of using eager loading records
orders.select{|x| x.operation == type}.count
end
# access each columns as
admin.name, admin.orders_count("bonus"), admin.orders_count("gift")