Rails: validate unique friendship relationships - ruby-on-rails

I'm following http://railscasts.com/episodes/163-self-referential-association
to make a friendship system.
#Friendship model
id | user_id | friend_id
The problem is
How can I make the combination of user_id and friend_id unique?
prevent creating user_id = 1, friend_id = 2 from user_id = 2. friend_id = 1
I find this model design is really bad. Can I model it to actually store the combine of 2 ids? Like friendship_id_combination = [1, 2], and then validate it.

You can use the following:
validate :users_are_not_already_friends
def users_are_not_already_friends
combinations = ["user_id = #{user_id} AND friend_id = #{friend_id}",
"user_id = #{friend_id} AND friend_id = #{user_id}"]
if User.where(combinations.join(' OR ')).exists?
self.errors.add(:user_id, 'Already friends!')
end
end
Update 2019:
Revised suggestion:
def users_are_not_already_friends
if User.where(user_id: friend_id, friend_id: user_id).exist?
|| User.where(user_id: user_id, friend_id: friend_id).exist?
self.errors.add(:user_id, 'Already friends!')
end
end
And I would strongly advise to add a DB constraint (unique index) based on the composite user_id and friend_id:
CREATE UNIQUE INDEX users_user_id_friend_id_key ON users (user_id, friend_id);

Yes. You are thinking along the right path. Perhaps the simplest solution is to use the database table to create the validation by making the combination of IDs a unique index.
class CreateFriendshipsUnq < ActiveRecord::Migration
def change
add_index :friendships, [:user_id, :friend_id], unique: true
end
end
I think this will do what you want.

Why you need this unique combination. Consider if you want to fetch friends of single user then in current scenario you can find it using simple association.
#user.friends
And if you don't want to add two keys then you need to fire two queries to find same details. One for your friends and other for who requested you as a friend.

Related

How to get serialized attribute in rails?

I have serialized column of Post model
serialize :user_ids
after save I can see in my console:
> p = Post.last
=> #<Post id: 30, title: "almost done2", created_at: "2017-05-08 15:09:40", updated_at: "2017-05-08 15:09:40", attachment: "LOGO_white.jpg", user_ids: [1, 2]>
I have permitted params :user_ids at my controller
def post_params
params.require(:post).permit(:title, :attachment, :user_ids)
end
All I need is to store #user_ids and copy to other model as atrribute,
but can't get instance like #user_ids = #Post.last.user_ids
Get error
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: users.post_id: SELECT "users".id FROM "users" WHERE "users"."post_id" = ?
Thanks for help!
I think you've chosen really wrong name for your serialized attribute, unfortunately!
See, Rails is trying to be helpful and relation_ids method is allowing you to call ids of all related object. Consider those two models
class User
belongs_to :post
end
class Post
has_many :users
end
You can call something like
Post.last.user_ids
Which will then try to find all users that belong to this post and give you back their ids.
Look at query that you get:
SELECT "users".id FROM "users" WHERE "users"."post_id" = ?
This is exactly what Rails is trying to do. Select USERS that belong to the post and give you their ids. However User does not have post_id column so this fails.
You're better off changing the name of your serialized column to something that won't confuse Rails. However, if you want to keep your column, you can override the method by putting this on the Post model
def user_ids
self[:user_ids]
end
This is non-ideal however, but will leave it up for you to decide what to do. My preference would be column rename, really. Will save you a lot of headeache

How to do bulk update in rails on habtm association

I have two models:
#user.rb
has_and_belongs_to_many :groups
#group.rb
has_and_belongs_to_many :users
I want to associate groups to users such that many users can be associated to many groups.
Front end side of the application is sending the user_ids and group_ids to the update method.
So in the UsersController, i've found out each user and associated that user with the groups.
def update
users = User.where(id: [325, 326])
users.each { |user| user.update(group_ids: [1, 2])}
end
I need to know the standard way of updating the associated records and what is the most efficient way of updating associated records?
There is a method update_all
def update
users = User.where(id: [325, 326])
users.update_all group_ids: [1, 2]
end
The simplest ActiveRecord-native approach is to create a model class for your join table and use that, e.g.:
class GroupsUser < ActiveRecord::Base; end
(Note that the model class is GroupsUser so that by convention ActiveRecord will know it's for the groups_users table. It's a little goofy but it works.)
Then you can use insert_all on the join table:
user_ids = [325, 326]
group_ids = [1, 2]
values = user_ids.map do |uid|
group_ids.map do |gid|
{ user_id: uid, group_id: gid }
end
end.flatten
# => [{user_id: 325, group_id: 1}, {user_id: 325, group_id: 2}, {user_id=>326, group_id: 1}, {user_id: 326, group_id: 2}]
GroupsUser.insert_all(values)
Note though that this is is insert_all, not update_all -- update_all doesn't really make sense for a join table. We're adding values, not replacing them. (At least on PostgreSQL and SQLite, if there's a unique index on the join table, duplicates will be ignored -- on MySQL you might need to jump through some more hoops to exclude existing relationships, unless you're deleting those anyway as below.)
If you want to replace any existing associations, you'll need to delete those explicitly:
ActiveRecord::Base.transaction do
GroupsUser.delete_all(user_id: user_ids)
# and/or GroupsUser.delete_all(group_id: group_ids), depending
GroupsUser.insert_all(values)
end
Also worth noting: there's no need to put GroupsUser in app/models/groups_user.rb; if this is the only code that uses it, you can put it right in the file where it's used. This is especially useful in migrations.

Joining two ActiveRecord associations on common attribute

Let's say I have a User model. User has 2 has_many associations, that is User has many pencils and has many cars. Cars and Pencils table has same attribute, :date, and separate such as :speed(car) and :length(pencil). I want to join a user's pencils and cars on their common attribute, :date, so that I have an array/relation [:date, :speed, :length]. How do I achieve that, I tried joins and merge but they were no use.
I'd definitely recommend getting this into a query rather than a loop, for efficiency's sake. I think this will work:
Car.joins(:user => :pencils).where("pencils.date = cars.date")
And if you want to reduce it to the array immediately:
Car.joins(:user => :pencils).where("pencils.date = cars.date").pluck("cars.date", "cars.speed", "pencils.length")
If you need to include matches where date is nil, you might need to add:
Car.joins(:user => :pencils).where("(pencils.date = cars.date) OR (pencils.date IS NULL AND cars.date IS NULL)")
Many more efficient options exist, but here is one possible approach:
class User < ActiveRecord::Base
def get_merged_array
dates = (cars.map(&:date) & pencils.map(&:date))
results = []
dates.each do |date|
cars.where(date: date).each do |car|
pencils.where(date: date).each do |pencil|
results << [date, car.speed, pencil.length]
end
end
end
results
end
end

How to enforce uniqueness of an entire row?

I've seen other SO questions like - How do you validate uniqueness of a pair of ids in Ruby on Rails? - which describes adding a scoped parameter to enforce uniqueness of a key pair, i.e. (from the answer)
validates_uniqueness_of :user_id, :scope => [:question_id]
My question is how do you do this kind of validation for an entire row of data?
In my case, I have five columns and the data should only be rejected if all five are the same. This data is not user entered and the table is essentially a join table (no id or timestamps).
My current thought is to search for a record with all of the column values and only create if the query returns nil but this seems like a bad work around. Is there an easier 'rails way' to do this?
You'll need to create a custom validator (http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations):
class TotallyUniqueValidator < ActiveModel::Validator
def validate(record)
if record.attributes_for_uniqueness.values.uniq.size == 1
record.errors[:base] << 'All fields must be unique!'
end
end
end
class User
validates_with TotallyUniqueValidator
def attributes_for_uniqueness
attributes.except :created_at, :updated_at, :id
end
end
The important line here is:
if record.attributes_for_uniqueness.values.uniq.size == 1
This will grab a hash of all the attributes you want to check for uniqueness (in this case everything except id and timestamps) and converts it to an array of just the values, then calls uniq on it which returns only uniq values and if the size is 1 then they were all the same value.
Update based on your comment that your table doesn't have an id or timestamps:
You can then simply do:
if record.attributes.except(:id).values.uniq.size == 1
...because I'm pretty sure it still has an id unless you're sure it doesn't then just remove the except part.
You can add a unique index to the table in a migration:
add_index :widgets, [:column1, :column2, :column3, :column4, :column5], unique: true
The resulting index will require that each combination of the 5 columns must be unique.

Make array of values from table attribute that matched another tables attribute

In my Ruby on Rails 3 app controller, I am trying to make an instance variable array to use in my edit view.
The User table has a user_id and reseller_id.
The Certificate table has a user_id.
I need to get the reseller_id(s) from the User table that have the user_id(s) in both User table and Certificate table.
Here is my User model:
class User < ActiveRecord::Base
attr_accessible :email, :name, :password, :password_confirmation, :remember_token, :reseller_id, :validate_code, :validate_url, :validated, :admin, :avatar
belongs_to :reseller
has_one :certificate
end
Here is my Certificate model:
class Certificate < ActiveRecord::Base
attr_accessible :attend, :pass, :user_id
validates :user_id, presence: true
end
Here is my controller, this seems to only store the last user_id in the Certificate table.
##train should be reseller.id(s) of all users in Certification table.
#certs = Certificate.all
#certs.each do |user|
#id = []
#id << user.user_id
#id.each do |id|
if User.find(id)
#train = []
#train << User.find(id).reseller_id
end
end
end
Thank you
1) Correct version of your code
#certs = Certificate.all
#reseller_id = [] # 1
#certs.each do |user|
id = user.user_id # 2
if u = User.find(id)
#reseller_id << u.reseller_id # 3
end
end
2) Rails way
Something like this
#reseller_id = User.joins(:certificates).select('users.reseller_id').map { |u| u['reseller_id']}
PS
Don't keep this code in controller please :-)
Well, first of all, you should not nest your ids' each block inside of the each block for certificates. You should build your ids array, then loop over it later. The reason you are only getting the last user_id, is because "#id" will only ever have a single element in it as your code is currently written. You will also run into the same problem with your "#train" array. Because you are declaring the array inside the iterator, it is getting re-created (with nothing in it) on every iteration. Using your existing code, this should work:
#certs = Certificate.all
#ids = []
#certs.each do |user|
#ids << user.user_id
end
#train = []
#ids.each do |id|
if User.find(id)
#train << User.find(id).reseller_id
end
end
A more Rubyish and concise way would be the following:
cert_user_ids = Certificate.all.map(&:user_id)
reseller_ids = cert_user_ids.map { |id| User.find(id).reseller_id rescue nil }.compact
Map is an enumerable method that returns an array of equal size to the first array. On each iteration, whatever the code inside the block returns "replaces" that element in the new array that is returned. In other words, it maps the values of one array to a new array of equal size. The first map function gets the user_ids of all certificates (using &:user_id is a shortcut for Certificate.all.map { |cert| cert.user_id } ) The second map function returns the "reseller_id" of the user, if a user is found. It returns nil if no user is found with that id. Finally, compact removes all nil values from the newly mapped array of reseller_ids, leaving just the reseller ids.
If you want to do this in the most efficient and railsy way possible, minimizing database calls and allowing the database to do most of the heavy lifting, you will likely want to use a join:
reseller_ids = User.joins(:certificates).all.map(&:reseller_id)
This grabs all users for which a certificate with that user's id exists. Then it utilizes map again to map the returned users to a new array that just contains user.reseller_id.
Ruby tends to be slower at this type of filtering than RDBM systems (like mysql), so it is best to delegate as much work as possible to the database.
(Note that this join will compare user.id to certificate.user_id by default. So, if your 'primary key' in your users table is named 'user_id', then this will not work. In order to get it to work, you should either use the standard "id" as the primary key, or you will need to specify that 'user_id' is your primary key in the User model)

Resources