Reputation: 7070
I have a user model where the user has a friendships of another user. The friendship model uses friend with a class_name of User. Everything seems to be working properly. However, I think that I'm just trying to patch together the functionality and not following best procedures.
In my controller, friendship controller, I have it where the current_user can add a friend. However, I do not want them to add the same friend twice.
user = current_user.id
friend = params[:friend_id]
temp_friendship = Friendship.where('(user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)', user,friend,friend,user)
if !temp_friendship.present?
@friendship = current_user.friendships.build(:friend_id => params[:friend_id])
if @friendship.save
redirect_to current_user, :notice => "Added friend."
else
redirect_to current_user, :alert => "Unable to add friend."
end
else
redirect_to current_user, :alert => "Already a friend."
end
This code all works great. However, it seems like I'm making unnecessary calls back to the database. Is there any way to optimize this controller call, either validation through the model or anything like that?
I have tried doing this, but it only returns a validation error if I already initiated the friend. If someone added me as a friend (where the friend_id would be my user's ID) it would not raise any errors.
validates_uniqueness_of :user_id, :scope => :friend_id
validates_uniqueness_of :friend_id, :scope => :user_id
Upvotes: 0
Views: 480
Reputation: 7937
What you do here is basically :
That sounded familiar to you and you decide to try a mean to implement that as a uniqueness validation ; but that's no solution, you're actually doing exactly what #validates_uniquess
does : check all ids, then save.
On that point, you can't do better, you've already reduced problem to its smallest steps. So even if you could translate this in symmetric scoped uniqueness rules, it would still fire two database queries (actually, it would fire three using two #validates_uniqueness_of
).
On that point, there are several things you can do. That's no joke : it will save time when you'll have to read the whole controller quickly later, way after you write it.
First, your temp_friendship query could be a scope and a model method. That would be their place and it probably will prove useful.
Second, the redirection if friendship exists could be a before filter, which would leave the action way more legible :
class User < ActiveRecord::Base
has_many :friends, through: :friendship
scope :friend_with, ->( other ) do
other = other.id if other.is_a?( User )
where( '(friendships.user_id = users.id AND friendships.friend_id = ?) OR (friendships.user_id = ? AND friendships.friend_id = users.id)', other, other ).includes( :frienships )
end
def friend_with?( other )
User.where( id: id ).friend_with( other ).any?
end
end
class FriendshipsController < ApplicationController
before_filter :check_friendship, only: :create
def create
@friendship = current_user.friendships.build( friend_id: params[:friend_id] )
if @friendship.save
redirect_to current_user, notice: 'Added friend.'
else
redirect_to current_user, alert: 'Unable to add friend.'
end
end
private
def check_friendship
redirect_to( current_user, alert: 'Already a friend' ) if current_user.friend_with?( params[ :friend_id ] )
end
end
Upvotes: 1
Reputation: 52386
Another option here is to remove the validation from the Rails app and enforce uniqueness in the database.
It's unconventional, but removes the need for the extra queries and is completely safe if a way that an intra-row validation enforced by an application outside the database never is. (Hence the comments in the Rails Guides about backing up ActiveRecord validation with database validation.
You would add to the save of the model a rescue step to handle uniqueness errors thrown by the RDBMS, and treat them as validation failures. there's good information on rescuing database erros here: Rails 3 ignore Postgres unique constraint exception.
I just raise this as another option, so just evaluate it to see if the unconventional-code trade-off is worth it for you.
I would really like to see this sort of methodology encapsulated in activerecord. Maybe I should roll my sleeves up ...
Upvotes: 1
Reputation: 9700
A custom validator could be used to check for either 'A is a friend of B' or 'B is a friend of A' as a case of 'friendship already exists'. The total number of database hits probably doesn't go down, though - the validator is just going to perform a similar check to what you already wrote. It's probably still a better way, since it moves the logic out of your controller, but I wouldn't expect any performance improvement.
Upvotes: 0