class User < ApplicationRecord
has_and_belongs_to_many :profiles
def add_profile(profile)
self.profiles << profile unless self.profiles.include?(profile)
end
end
class Profile < ApplicationRecord
has_and_belongs_to_many :users
validates_uniqueness_of :linkedin_id, allow_nil: true
end
For some reason on production I get
ActiveRecord::RecordInvalid: Validation failed: Linkedin has already been taken
on
self.profiles << profile unless self.profiles.include?(profile) line.
And after this I have duplicates in User.profiles records.
What is the problem?
Using has_and_belongs_to_many is probably not the best option here.
If you have the quite common scenario where a user may attach several "profiles" or external OAuth credentials to their account you want a one to many relationship.
This example uses a generic uid column instead of linkedin_id so that you can use the exact same logic for Facebook, Twitter or any other sort of account.
class User < ActiveRecord::Base
has_many :profiles
end
class Profile < ActiveRecord::Base
belongs_to :user
end
This guarantees that a profile can only belong to a single user. You may want to add some additional uniqueness constraints.
class AddUserProviderIndexToProfiles < ActiveRecord::Migration
def change
add_index(:profiles, [:user_id, :provider], unique: true)
add_index(:profiles, [:uid, :provider], unique: true)
end
end
This enforces on the database level that a user can only have one profile with a given provider and that there may only be one profile with for a given :provider, :uid combination. Adding indexes in the database safeguards against race conditions and improves performance.
You will also want a application level validation to avoid the application crashing due to database driver errors!
class Profile < ActiveRecord::Base
belongs_to :user
validates_uniqueness_of :uid, scope: :provider
validates_uniqueness_of :user_id, scope: :provider
end
Related
If you are saving a has_many :through association at record creation time, how can you make sure the association has unique objects. Unique is defined by a custom set of attributes.
Considering:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
before_validation :ensure_unique_roles
private
def ensure_unique_roles
# I thought the following would work:
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but the above results in duplicate, and is also kind of wonky because it goes through ActiveRecord assignment operator for an association (which is likely the cause of it not working correctly)
# I tried also:
self.user_roles = []
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but this is also wonky because it clears out the user roles which may have auxiliary data associated with them
end
end
What is the best way to validate the user_roles and roles are unique based on arbitrary conditions on an association?
The best way to do this, especially if you're using a relational db, is to create a unique multi-column index on user_roles.
add_index :user_roles, [:user_id, :role_id], unique: true
And then gracefully handle when the role addition fails:
class User < ActiveRecord::Base
def try_add_unique_role(role)
self.roles << role
rescue WhateverYourDbUniqueIndexExceptionIs
# handle gracefully somehow
# (return false, raise your own application exception, etc, etc)
end
end
Relational DBs are designed to guarantee referential integrity, so use it for exactly that. Any ruby/rails-only solution will have race conditions and/or be really inefficient.
If you want to provide user-friendly messaging and check "just in case", just go ahead and check:
already_has_role = UserRole.exists?(user: user, role: prospective_role_additions)
You'll still have to handle the potential exception when you try to persist role addition, though.
Just do a multi-field validation. Something like:
class UserRole < ActiveRecord::Base
validates :user_id,
:role_id,
:project_id,
presence: true
validates :user_id, uniqueness: { scope: [:project_id, :role_id] }
belongs_to :user, :project, :role
end
Something like that will ensure that a user can have only one role for a given project - if that's what you're looking for.
As mentioned by Kache, you probably also want to do a db-level index. The whole migration might look something like:
class AddIndexToUserRole < ActiveRecord::Migration
def change
add_index :user_roles, [:user_id, :role_id, :project_id], unique: true, name: :index_unique_field_combination
end
end
The name: argument is optional but can be handy in case the concatenation of the field names gets too long (and throws an error).
This is my first question here in stackoverflow, so please bear with me hehe.
I have three models: User, Post, and Comments.
# user.rb
class User < ApplicationRecord
has_many :posts
has_many :comments
end
# post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments
end
# comments.rb
class Comment < ApplicationRecord
belongs_to :user
belongs_to :post
end
What i'm trying to achieve is to let the user comment only once in a post. If there is already an existing comment by the same user, it should not have accept the save/create.
So far, my idea is to create a model function where it iterate all of the exisiting post.comments.each then check all of the users, then if there is, then the comment is invalidated. Though I have no Idea how to do it.
If you have any idea (and perhaps the code snippets), please do share. Thanks in advance! :)
In your comment.rb
validates :user_id, uniqueness: { scope: [:post_id]}
This validation will make sure the combination user_id and post_id will be unique in your comments table.
Well, there are a couple of ways to do it.
Firstly, it is possible to validate uniqueness of user_id - post_id combinations in comments:
# app/models/comments.rb
class Comment < ActiveRecord::Base
# ...
validates_uniqueness_of :user_name, scope: :account_id
Another approach is to manually check for comment existance before creating a comment:
if Comment.exists?(user_id: current_user.id, post_id: params[:comment][:post_id])
# render error
else
Comment.create(params[:comment])
end
But in a concurrent environment both approaches may fail. If your application is using a concurrent server like puma or unicorn you will also need a database constraint to prevent creation of duplicated records. The migration will be as follows:
add_index :comments, [:user_id, :post_id], unique: true
I want to change has_many association behaviour
considering this basic data model
class Skill < ActiveRecord::Base
has_many :users, through: :skills_users
has_many :skills_users
end
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, validate: true
has_many :skills_users
end
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
end
For adding a new skill we can easily do that :
john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')
john.skills << tidy
but if you do this twice we obtain a duplicate skill for this user
An possibility to prevent that is to check before adding
john.skills << tidy unless john.skills.include?(tidy)
But this is quite mean...
We can as well change ActiveRecord::Associations::CollectionProxy#<< behaviour like
module InvalidModelIgnoredSilently
def <<(*records)
super(records.to_a.keep_if { |r| !!include?(r) })
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
to force CollectionProxy to ignore transparently adding duplicate records.
But I'm not happy with that.
We can add a validation on extra validation on SkillsUser
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
validates :user, uniqueness: { scope: :skill }
end
but in this case adding twice will raise up ActiveRecord::RecordInvalid and again we have to check before adding
or make a uglier hack on CollectionProxy
module InvalidModelIgnoredSilently
def <<(*records)
super(valid_records(records))
end
private
def valid_records(records)
records.with_object([]).each do |record, _valid_records|
begin
proxy_association.dup.concat(record)
_valid_records << record
rescue ActiveRecord::RecordInvalid
end
end
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
But I'm still not happy with that.
To me the ideal and maybe missing methods on CollectionProxy are :
john.skills.push(tidy)
=> false
and
john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
Any idea how I can do that nicely?
-- EDIT --
A way I found to avoid throwing Exception is throwing an Exception!
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, before_add: :check_presence
has_many :skills_users
private
def check_presence(skill)
raise ActiveRecord::Rollback if skills.include?(skill)
end
end
Isn't based on validations, neither a generic solution, but can help...
Perhaps i'm not understanding the problem but here is what I'd do:
Add a constraint on the DB level to make sure the data is clean, no matter how things are implemented
Make sure that skill is not added multiple times (on the client)
Can you show me the migration that created your SkillsUser table.
the better if you show me the indexes of SkillsUser table that you have.
i usually use has_and_belongs_to_many instead of has_many - through.
try to add this migration
$ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
no need for validations if you have double index "skills_users".
hope it helps you.
I have this structure
class Organization
has_many :clients
end
class Client
belongs_to :organization
has_many :contacts
end
class Contact
belongs_to :client
belongs_to :organization
end
How can I make sure that when client is assigned to a contact he is a child of a specific organization and not allow a client from another organization to be assigned ?
While searching I did find a scope parameter can be added but that seems not to be evaluated when client_id is assigned.
Update
Here is an example from Rails Docs :
validates :name, uniqueness: { scope: :year,message: "should happen once per year" }
I'm looking for something like "if client is set it must be in Organization.clients"
class Contact
#...
validate :client_organization
def client_organization
unless client.nil?
unless organization == client.organization
errors.add(:organization, "can't be different for client.")
end
end
end
end
I'm coming up against a tricky challenge. Let me explain what I'm trying to make happen. If a user logs into my app with Facebook, I scrape all their facebook friends UIDs and store these as the users 'facebook_friends'. Then, once logged in, the user sees a list of events that are upcoming, and I want to check on each event if any of the attendees match a UID of the user's facebook friends and highlight this to them.
I've opted to create the Event.rb model as follows:
class Event < ActiveRecord::Base
# id :integer(11)
has_many :attendances, as: :attendable
has_many :attendees
def which_facebook_friends_are_coming_for(user)
matches = []
self.attendees.each do |attendee|
matches << user.facebook_friends.where("friend_uid=?", attendee.facebook_id)
end
return matches
end
end
You can see that I've created the which_facebook_friends_are_coming_for(user) method, but it strikes me as incredibily inefficient. When I run it from the console, it does work, but if I try and dump it in any form (like YAML), I get told can't dump anonymous module. I'm presuming this is because now the 'matches' holder isn't a class as such (when it should be FacebookFriends).
There must be a better way to do this and I'd love some suggestions.
For reference, the other classes look like this:
class User < ActiveRecord::Base
# id :integer(11)
has_many :attendances, foreign_key: :attendee_id, :dependent => :destroy
has_many :facebook_friends
end
class FacebookFriend < ActiveRecord::Base
# user_id :integer(11)
# friend_uid :string
# friend_name :string
belongs_to :user
end
class Attendance < ActiveRecord::Base
# attendee_id :integer(11)
# attendable_type :string
# attendable_id :integer(11)
belongs_to :attendable, polymorphic: true
belongs_to :attendee, class_name: "User"
end
What about something like that:
def which_facebook_friends_are_coming_for(user)
self.attendees.map(&:facebook_id) & user.facebook_friends.map(&:friend_uid)
end
The & operator simply returns the intersection of two arrays