Get most recent records from deeply nested model - sql

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.

Related

Rails and SQL - get related by all elements from array, entries

I have something like this:
duplicates = ['a','b','c','d']
if duplicates.length > 4
Photo.includes(:tags).where('tags.name IN (?)',duplicates)
.references(:tags).limit(15).each do |f|
returned_array.push(f.id)
end
end
duplicates is an array of tags that were duplicated with other Photo tags
What I want is to get Photo which includes all tags from duplicates array, but right now I get every Photo that include at least one tag from array.
THANKS FOR ANSWERS:
I try them and somethings starts to work but wasn't too clear for me and take some time to execute.
Today I make it creating arrays, compare them, take duplicates which exist in array more than X times and finally have uniq array of photos ids.
If you want to find photos that have all the given tags you just need to apply a GROUP and use HAVING to set a condition on the group:
class Photo
def self.with_tags(*names)
t = Tag.arel_table
joins(:tags)
.where(tags: { name: names })
.group(:id)
.having(t[:id].count.eq(tags.length)) # COUNT(tags.id) = ?
end
end
This is somewhat like a WHERE clause but it applies to the group. Using .gteq (>=) instead of .eq will give you records that can have all the tags in the list but may have more.
A better way to solve this is to use a better domain model that doesn't allow duplicates in the first place:
class Photo < ApplicationRecord
has_many :taggings
has_many :tags, through: :taggings
end
class Tag < ApplicationRecord
has_many :taggings
has_many :photos, through: :taggings
validates :name,
uniqueness: true,
presenece: true
end
class Tagging < ApplicationRecord
belongs_to :photo
belongs_to :tag
validates :tag_id,
uniqueness: { scope: :photo_id }
end
By adding unique indexes on tags.name and a compound index on taggings.tag_id and taggings.photo_id duplicates cannot be created.
The issue as I see it is that you're only doing one join, which means that you have to specify that tags.name is within the list of duplicates.
You could solve this in two places:
In the database query
In you application code
For your example the query is something like "find all records in the photos table which also have a relation to a specific set of records in the tags table". So we need to join the photos table to the tags table, and also specify that the only tags we join are those within the duplicate list.
We can use a inner join for this
select photos.* from photos
inner join tags as d1 on d1.name = 'a' and d1.photo_id = photos.id
inner join tags as d2 on d2.name = 'b' and d2.photo_id = photos.id
inner join tags as d3 on d3.name = 'c' and d3.photo_id = photos.id
inner join tags as d4 on d4.name = 'd' and d4.photo_id = photos.id
In ActiveRecord it seems we can't specify aliases for joins, but we can chain queries, so we can do something like this:
query = Photo
duplicate.each_with_index do |tag, index|
join_name = "d#{index}"
query = query.joins("inner join tags as #{join_name} on #{join_name}.name = '#{tag}' and #{join_name}.photo_id = photos.id")
end
Ugly, but gets the job done. I'm sure there would be a better way using arel instead - but it demonstrates how to construct a SQL query to find all photos that have a relation to all of the duplicate tags.
The other method is to extent what you have and filter in the application. As you already have the photos that has at least one of the tags, you could just select those which have all the tags.
Photo
.includes(:tags)
.joins(:tags)
.where('tags.name IN (?)',duplicates)
.select do |photo|
(duplicates - photo.tags.map(&:name)).empty?
end
(duplicates - photo.tags.map(&:name)).empty? takes the duplicates array and removes all occurrences of any item that is also in the photo tags. If this returns an empty array then we know that the tags in the photo had all the duplicate tags as well.
This could have performance issues if the duplicates array is large, since it could potentially return all photos from the database.

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

How to select only unique entries from a join table

I would like to make my block of code more efficient. I have two models and a join table for them. They both have a has_many :through relationship. Some parts belong to multiple groups, some only belong to one. I need to get the records that belong to only one group and in the most efficient manner as there are thousands of parts. Here are my models:
part.rb
class Part < ActiveRecord::Base
attr_accessible :name,
:group_ids
has_many :part_groups, dependent: :destroy
has_many :groups, through: :part_groups, select: 'groups.*, part_groups.*'
end
group.rb
class Group < ActiveRecord::Base
attr_accessible :name,
:part_ids
has_many :part_groups, dependent: :destroy
has_many :parts, through: :part_groups, select: 'parts.*, part_groups.*'
end
part_group.rb
class PartGroup < ActiveRecord::Base
attr_accessible :part_id,
:group_id
belongs_to :part
belongs_to :group
end
What I would like to be able to do is get all the parts that belong only to Group A and only to Group B, but not ones that belong to both A & B. After struggling with this for hours and getting nowhere I'm using this as a stop gap:
#groupA = []
#groupB = []
Part.all.each do |part|
if part.group_ids.length == 1
if part.group_ids.first == 1
#groupA.push(part)
elsif part.group_ids.first == 2
#groupB.push(part)
end
end
end
This obviously isn't scalable as there will be many groups. I've tried various methods of join and include that I've been googling but so far nothing has worked.
I am also new to rails , So as far i understand this is the structure of your tables.
parts
Id | Name
groups
Id | Name
part_groups
Id | part_id | group_id
So you can do the following,
Group.find(1).parts // Parts belong to group A
Group.find(2).parts // Parts belong to group B
so this may give parts that belong to other groups also.
Objective is to get parts that belongs only to group A and only to group B
Try for
Group.find(1).parts.collect{|row| row if row.groups.count==1}.flatten
I think this is better approach than yours , because am traversing only those parts which belong to group1.
The raw sql for this could look like
select parts.* from parts
inner join part_groups on parts.id = part_groups.part_id
left outer join part_groups as group_b on group_b.part_id = parts.id and group_b.group_id = 456
where group_b.id is null and part_groups.group_id = 123
Assuming that group a had id 123 and group b had id 456.
What this does is try to join the part_groups table twice (so an alias needs to be used the second time), once where group_id matches group A and once against group B. The use of the left join allows us to require that the second join (against B) produces no rows.
Activerecord doesn't provide much assistance for this, other than allowing you to pass arbitrary sql fragments to joins, so you end up with something like
Part.select("parts.*").
.joins(:part_groups).
.joins("left outer join part_groups as group_b on group_b.group_id = #{groupb.id} and group_b.part_id = parts.id").
.where(:part_groups => {:group_id => groupa.id}).where("group_b.id is null")
Arel (which underpins the query generation part of active record) can generate this sort of query but this isn't exposed directly.

Query a 3-way relationship in Active Record

I'm trying to figure out how to query this relationship without using find_by_sql
class User < ActiveRecord::Base
has_many :lists
end
class List < ActiveRecord::Base
has_many :list_items
belongs_to :user
end
class ListItem < ActiveRecord::Base
belongs_to :list
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :list_items
end
this should be what we are using but How would I do this not by find_by_sql
in user.rb
def self.find_users_who_like_by_item_id item_id
find_by_sql(["select u.* from users u, lists l, list_items li where l.list_type_id=10 and li.item_id=? and l.user_id=u.id and li.list_id=l.id", item_id])
end
I've tried several different includes / joins / merge scenarios but am not able to get at what I'm trying to do.
thx
It's a bit difficult to tell exactly what query you're trying to do here, but it looks like you want the user records where the user has a list with a particular list_type_id and containing a particular item. That would look approximately like this:
User.joins(:lists => [:list_items]).where('lists.list_type_id = ? and list_items.item_id = ?', list_type_id, item_id)
This causes ActiveRecord to execute a query like the following:
SELECT "users".* FROM "users" INNER JOIN "lists" ON "lists"."user_id" = "users"."id" INNER JOIN "list_items" ON "list_items"."list_id" = "lists"."id" WHERE (lists.list_type_id = 10 and list_items.item_id = 6)
and return the resulting collection of User objects.

Complex Query with Has many and belongs to for RAILS 3

I am trying to do a QUERY in my controller to get a list of suppliers with a category ID.
I have my models set up like this.
class Supplier < ActiveRecord::Base
has_and_belongs_to_many :sub_categories
end
class Category < ActiveRecord::Base
has_many :sub_categories
end
class SubCategory < ActiveRecord::Base
belongs_to :category
has_and_belongs_to_many :suppliers
end
A supplier can have Many sub_categories that are under one single category. So i can grab the category of a supplier by doing this.
#supplier.sub_categories.first.category.name
This returns the category that the supplier comes under because they have to have at least 1 sub category which is then linked to a category.
What i am trying to do is by passing a category_id i wish to return all suppliers that come under that category.
I had it written like this but it doesnt seem to be working.
#category = Category.find(params[:category_id])
#suppliers = Supplier.where('sub_category.first.category.id = ?', #category.id)
i get the following sql error
Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '.id = 20)' at line 1: SELECT `suppliers`.* FROM `suppliers` WHERE (sub_category.first.category.id = 20)
Well, that's certainly an sql error. The stuff inside the where() call gets translated directly to SQL, and that's not sq;l. :)
You need to join tables together. I'm assuming there's a sub_category_suppliers table that completes the habtm association. (BTW, I much prefer to use has_many :through exclusively)
I think it would be something like this:
Supplier.joins(:sub_category_suppliers => :sub_categories).
where('sub_categories.category_id =?', #category.id).
group('suppliers.id')
As Caley Woods suggested, this should be placed in the Supplier model as a scope:
scope :by_category, lambda { |category_id|
joins(:sub_category_suppliers => :sub_categories).
where('sub_categories.category_id =?', category_id).
group('suppliers.id')
}
and then called as Supplier.by_category(#category.id)