Rails Search Self Referential Relationship - sql

I am trying to allow users to search through their own friends by email address. I'd like to do something like:
current_user.search('test#fake.com')
and have it return an array of current users friends that have that email address.
So I have a very basic friendship relationship set up on my user model
user.rb
has_many :friendships
has_many :friends, through: :friendships, source: :friend
has_many :inverse_friendships, class_name: 'Friendship', foreign_key: 'friend_id'
has_many :inverse_friends, through: :inverse_friendships, source: :user
friendship.rb
belongs_to :friend, class_name: 'User', foreign_key: 'friend_id'
belongs_to :user
I want to set up a method on my user model that can search through their friends by email address. It's not working so well
def search(query)
conditions = ['friends.user_id = ? AND email LIKE ? ', self.id, "%#{query}%"]
User.includes(:friends).where(conditions)
end
I guess I'm just not sure how to format my active record query / SQL here, since I am trying to search on the relations of a self referential model. Any one have any ideas?
Thanks!

Digital Cake is going in the right direction, but not exactly correct. A scope is a method of User, not user. What you need is:
def followers_by_email(email)
friends.where("email like ?", "%#{email}%")
end
This returns an ActiveRecord::Relation to which you can chain other conditions, order, paginate, etc as in
user.followers_by_email("me#example.com").order(:first_name).limit(10)

Good time to use active record scopes. http://guides.rubyonrails.org/active_record_querying.html#scopes
Heres a simple example
user.rb
scope :followers, friends.where(:published => true).order("created_at DESC").limit(150)
in your controller
#followers = User.followers

I seem to have some success with:
conditions = ['contacts.user_id = ? AND users.email LIKE ? ', self.id, "%#{query}%"]
User.includes(:inverse_friends).where(conditions)
Though it's strange that it works, I'm not entirely sure why it does.

Related

Rails has_many through query

I have many-to-many relationship with User, Attachment and Form model.
I want to reach to attachments not belongs to a user. I will try something like this but did not work.
Attachment.includes(:forms,:users).where.not('forms.user_id = ?', #user.id).references(:forms,:users)
I have tried more but did't find the correct one.
user.rb
has_many :forms
has_many :attachments, through: :forms
attachment.rb
has_many :forms
has_many :users, through: :forms
forms.rb
belongs_to :user
belongs_to :attachment
Update:
I m still finding an answer
Attachment.includes(:forms).where(forms: {user_id: user.id}).references(:forms)
is working but where.not returns empty
I think where.not only looks the attachments related to forms not all of them
It's actually pretty simple:
The first (sub)query you will need is to get all the attachments the user does have:
subquery = #user.attachments.select(:id)
Then you can easily get all the attachments that don't have the id's from the subquery.
Attachment.where.not(subquery)
# same as
Attachment.where.not(#user.attachments.select(:id))
Leading to the query:
SELECT "attachments".*
FROM "attachments"
WHERE ("attachments"."id" NOT IN (
SELECT "attachments"."id"
FROM "attachments"
INNER JOIN "forms"
ON "attachments"."id" = "forms"."attachment_id"
WHERE "forms"."user_id" = $1
))

Where conditions on ActiveRecord associations conflicting with each other in has_many through?

In my app (Rails 4.2.0.rc2), users can be either students or admins of a given institution. There's an association :admin_institutions on User that returns all the institutions the user is an admin of by checking their role in the join table. There's also an association :students on Institution that returns all the users who are students at that institution, again according to institution_users.role.
These associations work as expected, so I added an association :admin_students to User, meant to return all the students at all the institutions for which a given user is an admin.
class InstitutionUser < ActiveRecord::Base
belongs_to :institution
belongs_to :user
end
class Institution < ActiveRecord::Base
has_many :institution_users
has_many :users, :through => :institution_users
has_many :students, -> { where "institution_users.role = 'Student'" }, :through => :institution_users, source: :user
...
end
class User < ActiveRecord::Base
has_many :institution_users
has_many :admin_institutions, -> { where "institution_users.role = 'Admin'" }, through: :institution_users, source: :institution
has_many :admin_students, through: :admin_institutions, source: :students
...
end
However, :admin_students does not work as expected. It generates the following SQL:
SELECT "users".* FROM "users" INNER JOIN "institution_users" ON "users"."id" = "institution_users"."user_id" INNER JOIN "institutions" ON "institution_users"."institution_id" = "institutions"."id" INNER JOIN "institution_users" "institution_users_admin_students_join" ON "institutions"."id" = "institution_users_admin_students_join"."institution_id" WHERE "institution_users_admin_students_join"."user_id" = $1 AND (institution_users.role='Student') AND (institution_users.role = 'Admin') [["user_id", 190]]
Instead of looking for all the institutions where the user is an admin and selecting all their students, it seems to be looking for institutions where the user is BOTH a student and an admin, so it returns an empty collection.
Is there a way to write an association (as opposed to just a method) that will give me the results I want, without my conditions conflicting like this?
(Side note: Is this the expected behavior for this kind of association? If so, I'd really appreciate further insight into why ActiveRecord interprets it the way it does.)
This may not be the answer, but maybe it will lead to one.
I'm not a fan of the associations with hard-coded SQL:
-> { where "institution_users.role = 'Student'" }
They are definitely at least part of the problem because they cannot be interpreted by ActiveRecord to determine which table alias for institution_users to apply it to.
You can allow ActiveRecord that flexibility by referencing a class method of the InsitutionUser model:
def self.students
where(role: "Student")
end
This also keeps the InstitutionUser logic all in one place.
Then the association becomes:
has_many :students, -> {merge(InstitutionUser.students)}, :through => :institution_users, source: :user
Perhaps try it with this and see if that sorts it out for you, and if not it might get things going in the right direction.

Querying relationships of relationships in Rails

I am trying to create a query in Rails but am having some trouble creating the correct one. Below is my models with their relationships.
class User < ActiveRecord::Base
has_and_belongs_to_many :rsvps, class_name: 'Event'
has_many :albums
end
class Event < ActiveRecord::Base
has_many :albums
has_and_belongs_to_many :attendees, class_name: 'User'
end
class Album < ActiveRecord::Base
belongs_to :user
belongs_to :event
end
I need to get all events a user has "rsvp'ed" to that they haven't uploaded an album to yet. I can find out if a user has uploaded an album to a particular event using the following:
u = User.find(1)
e = Event.find(1)
e.albums.where(user_id: u.id)
I want to be able to run this query on each of the user's rsvp'ed albums. I know I could do something like this:
u.rsvps.delete_if { |e| !e.albums.where(user_id: u.id).blank? }
However, I want to do this all in one query instead of getting the rsvps and then iterating over them and deleting them when necessary.
In order to get all events a user has rsvp'ed to but haven't uploaded an album to yet, you can use the following, which (UPDATE) now also works when a user has not uploaded any albums.
#event_ids = Album.where(user_id: u.id).pluck(:event_id))
#event_ids.empty? ? u.rsvps : u.rsvps.where("id not in (?)", #event_ids)
In addition, this query should work as well.
u.rsvps.where.not(id: Album.where(user_id: u.id).pluck(:event_id))

Extra Role column in join table in Rails

I have users and companies in a many to many relationship by a join table which has a column for user Role. I'm not sure if this is the best way to have the model set up.
Now each user can have different roles depending on the company, what is the best way to design and access user's role using ActiveRecord associations?
I would like to return via JSON the user's role based on their current company and default to something if their company is nil OR their role has not been set (nil).
Update:
What I've got now after reading Many-to-many relationship with the same model in rails? which is a bit different (many to many on itself).
CompaniesUser
belongs_to :company
belongs_to :user
Company
has_many(:companies_users, :dependent => :destroy)
has_many :users, :through => :companies_users
User
has_one :company
has_many(:companies_users, :dependent => :destroy)
has_many :companies, :through => :companies_users
Appreciate any advice as I'm just starting to learn this!
What you have above is correct, in terms of the ActiveRecord relationships. If you'd like to read more on the subject I believe this is the best source: http://guides.rubyonrails.org/association_basics.html
One problem I see there is that CompaniesUsers should be in singular form: CompanyUser, and then in all cases where you use :companies_users use: :company_users
I am assuming here that the current company of the User is the last one assigned.
Now in order to serialize in JSON format you should add the following in your User ActiveRecord:
def serializable_hash(options = nil)
options ||= {}
h = super(options)
if(defined?self.company_users.last and defined?(self.company_users.last).role)
h[:role] = (self.company_users.last).role
else
h[:role] = 'default_value'
end
end

Unique Association :through

I have a many to many :through relationship between a set of classes like so:
class Company
has_many :shares
has_many :users, :through => :shares, :uniq => true
end
class User
has_many :shares
has_many :companys, :through => :shares, uniq => true
end
class Share
belongs_to :company
belongs_to :user
end
I want to ensure a unique relationship so that a user can only have one share in any one company, which is what I have tried to achieve using the "uniq" argument.
At first I thought this was working, however it seems the behaviour os the "uniq" is to filter on the SELECT of the record, not pre-INSERT so I still get duplicate records in the database, which becomes an issue if I want to start dealing with the :shares association directly, as calling user.shares will return duplicate records if they exist.
Can anyone help with an approach which would force truely uniq relationships? so that if I try adding the second relationships between a user and a company it will reject it and only keep the original?
Have you tried adding this to your Share class?
validates_uniqueness_of :user, scope: :company
Also, in your User class I think it should be:
has_many :companies, through: :shares
I hope that helps.