Make sure has_many :through association is unique on creation - ruby-on-rails

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).

Related

Uniqueness of has_many association

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

Reset PK number based on association

I have a Post and Comments table.
Post has many comments, and Comment belongs to a post.
I want to have primary keys which start at 1 when I create a comment for a Post, so that I can access comments in a REST-ful manner, e.g:
/posts/1/comments/1
/posts/1/comments/2
/posts/2/comments/1
/posts/2/comments/2
How can I achieve that with Rails 3?
I am using MySQL as a database.
Bonus: I am using the Sequel ORM; an approach compatible with Sequel, not only ActiveRecord, would be awesome.
Well, you can't use id for this, as id is a primary key here. What you can do is to add an extra field to your database table like comment_number and make it unique in the scope of the post:
#migration
def change
add_column :comments, :comment_number, :integer, null: false
add_index :comments, [:post_id, :comment_number], unique: true
end
#Class
class Comment < ActiveRecord::Base
belongs_to :post
validates :post_id, presence: true
validates :comment_number, uniqueness: { scope: :post_id }
end
Now with this in place you need to ensure this column is populated:
class Comment < ActiveRecord::Base
#...
before_create :assign_comment_number
private
def assign_comment_number
self.comment_number = (self.class.max(:comment_number) || 0) + 1
end
end
Last step is to tell rails to use this column instead of id. To do this you need to override to_param method:
class Comment < ActiveRecord::Base
#...
def to_param
comment_number
end
end
Update:
One more thing, it would be really useful to make this field read-only:
class Comment < ActiveRecord::Base
attr_readonly :comment_id
end
Also after rethinking having uniqueness validation on comment_number makes very little sense having it is assigned after validations are run. Most likely you should just get rid of it and rely on database index.
Even having this validation, there is still a possible condition race. I would probably override save method to handle constraint validation exception with retry a couple of time to ensure this won't break application flow. But this is a topic for another question.
Another option without changing models:
get 'posts/:id/comments/:comment_id', to: 'posts#get_comment'
And in the posts controller:
def get_comment
#comment = post.find(params[:id]).comments[params[:comment_id] -1]
end
Asumptions: Comments bookmarks might change if coments deletion is allowed.

Rails: destroying multiple user-to-user associations

I have a model called Block that has a blocker_id (a user_id) and a blocked_user_id field (also a user_id). The Block model lets one user block another. When one user blocks another, I want it to destroy the Relationship between them using a before_save method for the Block class. The Relationship table has a follower_id and a followed_id.
This is where things get tricky. I know I could achieve this goal by using multiple return if Relationship.xyz.nil? statements and then using multiple Relationship.find_by(follower_id: , followed_id: ).destroy statements, but this gets to be way over complicated because each blocker and blocked_user could be either the follower and followed id, both, or neither. Is there any easier way to do this?
Here's my models for reference: (also the Block class has a blocked_post field, which I'm having no trouble with)
class Block < ActiveRecord::Base
validates :blocker_id, presence: true
validates :blocked_user_id, uniqueness: {scope: :blocker_id}, allow_nil: true
validates :blocked_post_id, uniqueness: {scope: :blocker_id}, allow_nil: true
validate :blocked_user_or_post
after_validation :validate_block
before_save :destroy_blocked_relationships
belongs_to(
:blocker,
class_name: "User"
)
has_one(
:blocked_user,
class_name: "User"
)
has_one(
:blocked_post,
class_name: "Post"
)
private
def blocked_user_or_post
blocked_user_id.blank? ^ blocked_post_id.blank?
end
def validate_block
if blocked_user_id.present?
!(blocker_id == blocked_user_id)
elsif blocked_post_id.present?
blocked_post = Post.find_by(id: self.blocked_post_id).user_id
!(blocker_id == blocked_post)
else
false
end
end
def destroy_blocked_relationships
#my over-complex code was here
end
end
relationship.rb:
class Relationship < ActiveRecord::Base
validates :follower_id, :followed_id, presence: {message: 'Need an eligible follower and followee id'}
validates :followed_id, uniqueness: { scope: :follower_id}
belongs_to(
:follower,
class_name: "User"
)
belongs_to(
:followed,
class_name: "User"
)
end
If there is any way to do this that doesn't require massive amounts of code, I'd really like to know. Thanks in advance.
I'm not sure of your exact use case, but my thoughts about a system where people can follow each other, it seems that the blocker would always be the person being followed. If this is the case, here's an implementation:
def destroy_blocked_relationships
Relationship.where(follower_id:blocked_user_id, followed_id:blocker_id).destroy_all
true
end
If it makes sense to also block someone from being being followed, you could add this:
Relationship.where(follower_id:blocker_id, followed_id:blocked_user_id).destory_all
Here it is all together, and stopping the save of the Block if there are no relationships:
before_save :destroy_blocked_relationships
def destroy_blocked_relationships
relationships = Relationship.where("(follower_id = ? AND followed_id = ?) OR (followed_id = ? AND follower_id = ? )", blocked_user_id, blocker_id, blocked_user_id, blocker_id)
relationships.destroy_all
relationships.present? # Omit this line if the save should continue regardless
end
here is my understanding:
a Block is a relationship between two users, OR between a User and a Post
when a Block is created between user A and Post X, an implicit block is also created between User A and User B, where User B is Post X's author
Consider making two models, BlockedPost and BlockedUser. Then, make two #make methods. This makes all the related logic easier to reason about.
# class BlockedPost
def make(user, post)
transaction do
create!(user: user, post: post)
BlockedUser.make(user, post.author)
end
end
# class BlockedUser
def make(user, blocked_user)
transaction do
create!(user: user, blocked_user: blocked_user)
Relationship.where(follower: user, following: blocked_user).destroy_all
Relationship.where(follower: blocked_user, following: user).destroy_all
end
end

In Rails, how can I create group of users as another association, such as "members"?

I am trying to create a special relationship between two existing models, User and Dwelling. A Dwelling has only one owner (Dwelling belongs_to :user, User has_one :dwelling) at the time of creation. But other Users can be added to this Dwelling as Roomies (there is no model created for this now, Roomie is a conceptual relationship).
I don't think I need a separate model but rather a special relationship with the existing models, but I could be wrong. I think the reference needs to be made with user_id from the Users table. I'm not really sure where to start this. Thank you for any and all help!
For example:
Dwelling1
user_id: 1
roomies: [1, 2, 3, 4]
Where 1, 2, 3, 4 are user_ids.
Updated Models
Dwelling Model
# dwelling.rb
class Dwelling < ActiveRecord::Base
attr_accessible :street_address, :city, :state, :zip, :nickname
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_many :roomies, :class_name => "User"
validates :street_address, presence: true
validates :city, presence: true
validates :state, presence: true
validates :zip, presence: true
end
User Model
# user.rb
class User < ActiveRecord::Base
attr_accessible :email, :first_name, :last_name, :password, :password_confirmation, :zip
has_secure_password
before_save { |user| user.email = email.downcase }
before_save :create_remember_token
belongs_to :dwelling
has_many :properties, :class_name => "Dwelling", :foreign_key => "owner_id"
validates :first_name, presence: true, length: { maximum: 50 }
...
Updated Dwelling Create Action
#dwellings_controller.rb
...
def create
#dwelling = current_user.properties.build(params[:dwelling])
if #dwelling.save
current_user.dwelling = #dwelling
if current_user.save
flash[:success] = "Woohoo! Your dwelling has been created. Welcome home!"
redirect_to current_user
else
render 'new'
end
end
end
...
My answer assumes you only want a user to be a roomie at one dwelling. If you want a user to be a roomie at more than one dwelling, I think #ari's answer is good, although I might opt for has_and_belongs_to_many instead of has_many :through.
Now for my answer:
I would set it up so that a dwelling belongs_to an owner and has_many roomies (including possibly the owner, but not necessarily).
You can use the User model both for owners and roomies. You don't need any additional tables or models, you just need to setup the proper relationships by using the :class_name and :foreign_key options.
In your Dwelling model:
# dwelling.rb
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_many :roomies, :class_name => "User"
In your User model:
# user.rb
belongs_to :dwelling # This is where the user lives
has_many :properties, :class_name => "Dwelling", :foreign_key => "owner_id" # This is the dwellings the user owns
In your dwellings table you need an owner_id column to store the user_id of the owner
In your users table you need a dwelling_id to store the dwelling_id of the dwelling where the user lives.
To answer your question in the comments regarding the controller:
If you want to setup current_user as the owner of the new dwelling, do this:
#dwelling = current_user.properties.build(params[:dwelling])
....
If you want to setup the current_user as the owner AND a roomie of the new dwelling, do this:
#dwelling = current_user.properties.build(params[:dwelling]
if #dwelling.save
current_user.dwelling = #dwelling
if current_user.save
# flash and redirect go here
else
# It's not clear why this wouldn't save, but you'll to determine
# What to do in such a case.
end
else
...
end
The trickiest part of above is handling the case that the dwelling is valid and saves, but for some unrelated reason the current_user can't be saved. Depending on your application, you may want the dwelling to save anyway, even if you can't assign the current_user as a roomie. Or, you might want the dwelling not to be saved --- if so, you'd need to use a model transaction, which is bit beyond the scope of this question.
Your controller code didn't work because saving the Dwelling doesn't actually update the current_user record to store the dwelling_id. Your code would be equivalent to the following:
#dwelling = Dwelling.new(params[:dwelling])
current_user.dwelling = #dwelling
if #dwelling.save
...
Note that current_user is never saved, so the current_user.dwelling = #dwelling line is useless.
This might seem counter-intuitive, but the bottom line is that build_dwelling isn't actually setting up things in memory as you might expect. You'd achieve more intuitive results if you saved the model you're building from rather than the model you're building:
#dwelling = current_user.build_dwelling(params[:dwelling])
if current_user.save # This will save the dwelling (if it is valid)
However, this (by default) won't save the dwelling if it has validation errors unless you turn :autosave on for the association, which is also a bit beyond the scope of this question. I really wouldn't recommend this approach.
Update:
Here is a more detailed code snippet:**
# dwellings_controller.rb
def create
#dwelling = current_user.properties.build(params[:dwelling])
if #dwelling.save
# The current user is now the owner, but we also want to try to assign
# his as a roomie:
current_user.dwelling = #dwelling
if current_user.save
flash[:notice] = "You have successfully created a dwelling"
else
# For some reason, current_user couldn't be assigned as a roomie at the
# dwelling. This could be for several reasons such as validations on the
# user model that prevent the current_user from being saved.
flash[:notice] = "You have successfully created a dwelling, but we could not assign you to it as a roomie"
end
redirect_to current_user
else
# Dwelling could not be saved, so re-display the creation form:
render :new
end
end
When a dwelling saves successfully, the current user will be the owner (owner_id in the database). However, if the current_user doesn't save, you'll need to decide how your application should respond to that. In the example above, I allow the dwelling to be saved (i.e. I don't rollback its creation), but I inform the user that he couldn't be assigned as a roomie. When this happens, it's most likely other code in your application causing the problem. You could examine the errors of current_user to see why. Or, you could use current_user.save! instead of current_user.save temporarily to troubleshoot.
Another way to do all of this is with an after_create callback in the Dwelling model. In many ways that would be a cleaner and simpler way to do it. However, catching the case when the current_user can't be saved could be even uglier than the method above, depending on how you want to handle it.
I believe the bottom line is that the current_user.save code is causing some problems. You'll need to diagnose why, and then determine what your application should do in that case. There are several ways to handle this, including at least the following
Put everything in a transaction block, and use current_use.save! instead of current_user.save so that an exception is raised and neither the user or dwelling is saved.
Save the dwelling, but inform the user that he isn't a roomie (As above)
Instead of saving the current_user, use update_column (which avoids callbacks, validations, etc.).
I believe the current problems you're experiencing are essentially unrelated to the original question. If you need further assistance, it might be best to break it off as a separate question.
You could do this by storing Roomie ids as a column in Dwelling
Make a migration:
class AddRoomiesToDwelling < ActiveRecord::Migration
def self.up
add_column :dwelling, :roomies, :text
end
def self.down
remove_column :dwelling, :roomies
end
end
In your Dwelling model:
class Dwelling < ActiveRecord::Base
serialize :roomies
end
You can then set the roomie ids with:
roomie_ids = [1, 2, 3, 4]
#dwelling.roomies = {:ids => roomie_ids}
#dwelling.save!
Taken from the Saving arrays, hashes, and other non-mappable objects in text columns section of this
You have two possible options.
Depending on your plan, it might be clearer for the dwelling to have_one owner instead of the owner having one dwelling. Then the dwelling would also be able to have users. You can add a column to User called dwelling_id and then you could do dwelling has_many users.
Another option would be to use the "has_many through" association. This means you would need to create a new model that would keep track of this association, say "Relationship.rb", which would belong to both User and Dwelling (and have columns for both for them). Then you would be able to write code like this:
//in Dwelling.rb
has_many :roomies, through: :relationships, source: :user
//in User.rb
has_many :dwellings, through: :relationships
This would let users also join more than one dwelling.

rails validate a belongs_to relation

Given a simple relationship where Person has_many Telephones. And a telephone only contains a telephonenumber which must be unique!
class Telephone < ActiveRecord::Base
validates_presence_of :contact_id
belongs_to :contact
validates :telephone, {:presence => true, :uniqueness => true}
end
class Contact < ActiveRecord::Base
has_many :telephones
validates_associated :telephones
has_many :emails
has_many :addresses
validates_presence_of :firstname
accepts_nested_attributes_for :telephones, :allow_destroy=>true
validates_presence_of :lastname
end
test "telephone number must be unique" do
john = contacts :johndoe #johndoe is a person with 1 existing number
2.times do
john.telephones.build :telephone=> "123" # 123 doesnt exist yet
end
puts Telephone.count # this gives 1
john.save
puts Telephone.count # this gives 3 !!!! ???
assert not(john.valid?) # This validates unless I remove the save above
end
Can someone explain the outcome of this test.
just calling valid? fails, but that is mentioned in the rdoc (must save first)
saving first does make valid? pass
BUT now I actually have 3 records in the database which breaks my unique requirement.
Is there a better way to do this? I don't understand the outcome of this test, it really goes against my expectations.
Ok if you read the ruby documentation you will notice that they mention that validating a model is not sufficient for uniqueness. YOU MUST use database unique constraints whenever possible. Otherwise it is possible when using two processes/threads/whatever that both will do a validation check, pass as unique, and then insert same values.
tl;dr: Add a unique constraint to the db column.

Resources