Selecting models through Rails relations? - sql

I have these (simplified) models:
Address
public (Boolean)
has_one :group_member
Group
has_many :Group_Members
belongs_to :User
Group_Member
belongs_to :group
belongs_to :address
User
has_many :groups
I want to select all addresses where public is true and all the addresses which User can access through Groups.
I assume it's something along the lines of:
Address.where(public: TRUE).joins(:group_member)
Am I somewhere close?
I'm using Rails 4 and PostgreSQL as my database if it helps anyone.

I think this will work:
Address.joins(group_member: :group).where(is_public: true, groups: {user_id: 12345})
Let's break this down.
First we are calling on the Address model, b/c that is what we want to return.
.joins(:group_member) will join addresses to group_members via the Address has_one :group_member relation.
However, we actually want to go further, and join the group connected with the group_member, so we use a nested join, which is why it looks like this joins(group_member: :group) to indicate that we join address -> group_member, then group_member -> group.
Next the where clause. There are 2 conditions, we want:
Public addresses only, which is indicated as a column on the address, so we add this:
.where(is_public: true).
We want only where a particular user is connected with a group (and so its group_members and addresses). For this we need to add a nested where clause, like so groups: {user_id: 12345}.
The result of combining these is:
where(is_public: true, groups: {user_id: 12345})
So all together, the line of code above should get what you want.

Try the following -- it is not a single query but I think it is better than a single query with big join table:
public_addresses = Address.where(is_public: true)
# => get all public addresses
user_addresses = current_user.groups.includes(:group_members => :address).
# includes is to eager load records to avoid N+1 queries
flat_map{|g| g.group_members.map(&:address)}
# inner block returns array of addresses for each group
# flat_map converts the array of arrays to single level
# => get all addresses associated with the user
all_addresses = (public_addresses + user_addresses).uniq
# => remove duplicates
To speed up the query add indices for slower queries. For e.g
add_index :groups, :user_id
# this speeds up finding groups for a given user
add_index :group_members, :group_id
# this speeds up finding group_members for a given group
add_index :addresses, :group_member_id
# this speeds up finding addresses for a given group_member
Other Options is to get user_addresses using join tables
user_addresses = Address.joins(group_member: group).where(groups: {user_id: current_user.id} )

Related

query include methods rails

I have a Student model and that model has_many subscriptions. I need to return all students who have subscriptions. this object must contain only the student's name, email, phone and status, in addition to the subscription id and recurrence_code. I thought something more or less like this but I can't succeed in the query:
students = Student.all.includes(:subscriptions)
students.each do |student|
student.select(:id, :name, :email, :phone, :status, subscription: {
methods: [:id, :recurrency_code]}
end
This is a classic inner join scenario
class Student < ApplicationRecord
has_many :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :student
end
I find it helpful to break these problems into steps:
"Only Students where a Subscription record is present" is a standard inner join:
Student.joins(:subscriptions).uniq
"object must contain only the student's name, email, phone and status"
Student.joins(:subscriptions).select(:name, :email, :phone, :status).uniq
"in addition to the subscription id and recurrence_code"
students = Student.joins(:subscriptions)
.select(
'students.name, students.email,'\
'students.phone, students.status, '\
'subscriptions.id as subscription_id,'\
'subscriptions.recurrence_code as subscription_recurrence_code'
)
A few notes:
1. Using select with joins
#vee's SO Answer here points out:
If the column in select is not one of the attributes of the model on which the select is called on then those columns are not displayed. All of these attributes are still contained in the objects within AR::Relation and are accessible as any other public instance attributes.
This means if you load an individual record (e.g. students.first), you will only see the Student attributes by default. But you can still access the Subscription attributes by the as name you set in the query. E.g.:
students.first.subscription_recurrence_code
2. Use .uniq to eliminate duplicates.
Because of the has_many relationship, the query Student.joins(:subscriptions) will return a record for each subscription, which means each student will appear in the result the same number of times as they have subscriptions. Calling .uniq (short for unique) will remove duplicates from the result.
I'm agree with the Chiperific response, but I disagree to use the uniq method because it doesn't call the 'DISTINCT' in the SQL query.
Rails: uniq vs. distinct
For me it's better to use distinct. So the query could be as this:
Student.joins(:subscriptions).distinct.select(
:name, :email, :phone, :status,
'subscriptions.id AS subscription_id',
'subscriptions.recurrence_code'
)

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

Query for all of a specific record where its related resource all have the same value for a single attribute

class User < ActiveRecord::Base
has_many :memberships
# included columns
# id: integer
---------------------
Membership < ActiveRecord::Base
belongs_to :user
# included columns
# user_id: integer
# active: boolean
I'd like to be able to grab all users where all their memberships have 'active = false' in a single query. So far the best that I've been able to come up with is:
#grab possibles
users = User.joins(:memberships).where('memberships.active = false')
#select ones that satisfy condition
users.select{ |user| user.memberships.pluck(&:active).uniq == [false] }
which is not that great since I have to use ruby to pluck out the valid ones.
This can do the trick:
users_with_active_membership = User.joins(:memberships).where(memberships: { active: true })
users = User.where( 'users.id NOT IN (?)', users_with_active_membership.pluck(:id) )
I am not sure of the result but I expect it to be 2 nested queries, one selecting the User ids having an active membership, the other query to select the User not in this previous ids list.
I can't test it since I don't have an environment with these relationships. Can you try it and post the SQL query generated? (add .to_sql to see it)
Another way, I don't know which could be the most efficient:
User.where( 'users.id NOT IN (?)', Membership.where(active: true).group(:user_id).pluck(:user_id) )

Rails 3 Order Records By Grand-child Count

I'm trying to do some fairly complicated record sorting that I was having a bit of trouble with. I have three models:
class User < ActiveRecord::Base
has_many :registers
has_many :results, :through => :registers
#Find all the Users that exist as registrants for a tournament
scope :with_tournament_entrees, :include => :registers, :conditions => "registers.id IS NOT NULL"
end
Register
class Register < ActiveRecord::Base
belongs_to :user
has_many :results
end
Result
class Result < ActiveRecord::Base
belongs_to :register
end
Now on a Tournament result page I list all users by their total wins (wins is calculated through the results table). First thing first I find all users who have entered a tournament with the query:
User.with_tournament_entrees
With this I can simply loop through the returned users and query each individual record with the following to retrieve each users "Total Wins":
user.results.where("win = true").count()
However I would also like to take this a step further and order all of the users by their "Total Wins", and this is the best I could come up with:
User.with_tournament_entrees.select('SELECT *,
(SELECT count(*)
FROM results
INNER JOIN "registers"
ON "results"."register_id" = "registers"."id"
WHERE "registers"."user_id" = "users.id"
AND (win = true)
) AS total_wins
FROM users ORDER BY total_wins DESC')
I think it's close, but it doesn't actually order by the total_wins in descending order as I instruct it to. I'm using a PostgreSQL database.
Edit:
There's actually three selects taking place, the first occurs on User.with_tournament_entries which just performs a quick filter on the User table. If I ignore that and try
SELECT *, (SELECT count(*) FROM results INNER JOIN "registers" ON "results"."register_id" = "registers"."id" WHERE "registers"."user_id" = "users.id" AND (win = true)) AS total_wins FROM users ORDER BY total_wins DESC;
it fails in both PSQL and the ERB console. I get the error message:
PGError: ERROR: column "users.id" does not exist
I think this happens because the inner-select occurs before the outer-select so it doesn't have access to the user id before hand. Not sure how to give it access to all user ids before than inner select occurs but this isn't an issue when I do User.with_tournament_entires followed by the query.
In your SQL, "users.id" is quoted wrong -- it's telling Postgres to look for a column named, literally, "users.id".
It should be "users"."id", or, just users.id (you only need to quote it if you have a table/column name that conflicts with a postgres keyword, or have punctuation or something else unusual).