Rails4 validate uniqueness of multiple columns - ruby-on-rails

I have a model Friendship where :friend_id is also a foreign key as :user_id
friendships
id | user_id | friend_id
I'm aware that validating the uniqueness of the two fields would be something like this
validates :user_id, :uniqueness => {:scope => :friend_id}
But is there a way to validate
user_id = 1, friend_id = 3
user_id = 3, friend_id = 1
so that only user_id = 1, friend_id = 3 is stored?

Join Model
Looks like you're using a join model with Rails - as mentioned in the comments, you'll want to change this to friendship (not friend).
Rails has two types of join mechanism - has_many :through and has_and_belongs_to_many - both allow you to associate many-to-many records, like this:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :friendships
has_many :friends, through: :friendships
end
#app/models/friendship.rb
Class Friendship < ActiveRecord::Base
belongs_to :friend, class: "User", foreign_key: "friend_id"
belongs_to :user
end
#friendships
id | user_id | friend_id | created_at | updated_at
I would recommend you have a friendship model (as opposed to a model-less HABTM setup), because HMT allows you to add extra data, such as created_at, updated_at, permissions, etc)
Self Referential
I would also use a self-referential model. This will allow you to keep a single model (for User), and then call #user.friends without an issue.
Whether my above association definitions are correct is another matter; this is how you'd want to set them up.
--
Validation
In terms of validating your associations, I believe you'll be able to use indexes in your DB. This is used often for HABTM tables -
#Migration
# Adding the index can massively speed up join tables. Don't use the
# unique if you allow duplicates.
add_index(:friendships, [:user_id, :friend_id], :unique => true)
This will work for your original case, but not for the second

Related

Choosing between has_many :through and has_and_belongs_to_many association

I’m new to programming and have difficulty determining the model association I need. I already have two tables: Organizations and Users with a 1:many relationship, since an organization has multiple users/members. Not all users are a member of an organization though but that’s not a problem.
# In the organization model:
has_many :users
# In the user model:
belongs_to :organization, inverse_of: :users
# In the users migration file:
t.references :organization, index: true, foreign_key: true
I need to extend this with a moderator function: A user can be a moderator for multiple organizations and an organization can have multiple users as their moderator. So there will then be two separate reasons for a relationship between the user and organization model (user as a member of an organization and user as a moderator of an organization; no need to a member before you can be a moderator).
I read here about the two different possible ways to create the association, namely has_many :through or has_and_belongs_to_many.
The simplest rule of thumb is that you should set up a has_many
:through relationship if you need to work with the relationship model
as an independent entity. If you don't need to do anything with the
relationship model, it may be simpler to set up a
has_and_belongs_to_many relationship (though you'll need to remember
to create the joining table in the database).
You should use has_many :through if you need validations, callbacks,
or extra attributes on the join model.
I'm not sure what is meant by "if you need to work with the relationship model as an independent entity". In my use case is a moderator will have additional rights. The association will, I belief, be used in two ways: 1) displaying the moderators for an organization, and displaying the organizations that a user is the moderator of; and 2) look up if a user has moderator rights to allow him these rights. So I don't belief I will need validations (organization will itself be able to select users as moderator) or extra attributes. I'm not sure about callbacks (don't know exactly what they are).
So in short, which one of the two forms of association is the best association in my use case?
"if you need to work with the relationship model as an independent entity" means you will be able to operate on the Relationship model as any other ActiveRecord model, i.e you can create, save, destroy etc. records or assign various types of attributes to the relationship.
Suppose, for example, that a user could have different types of relationship with the organization (e.g. moderator, employee, stockholder et). Using the :through association, you would have:
class Relationship < ActiveRecord::Base
belongs_to :user
belongs_to :organization
end
class User < ActiveRecord::Base
has_many :organizations, through: relationships
end
class Organization < ActiveRecord::Base
has_many :users, through: relationships
end
# schema.rb
ActiveRecord::Schema.define(:version => 20150511223747) do
# . . . .
create_table "relationships", :force => true do |t|
t.string "relationship_type"
t.integer "user_id"
t.integer "organization_id"
t.datetime "created_at"
t.datetime "updated_at"
end
end
# things you can do:
org = Organization.first
moderators_or_employees = org.users.where("relationship_type = 'moderator' OR relationship_type = 'employee'")
new_moderators = org.users.where("relationship_type = 'moderator' AND DATE(relationships.created_at) > '2015-01-01'")
These are things you could not do with the has_and_belongs_to_many association since you have no way of querying on relationship type or created_at. Because of these limitations, I have often found that the :through relationship, although a bit more complicated, gives you a lot more flexibility.
My nutshell explanation when people ask:
habtm – only a join table, e.g., two ids
hm:t – a join table with meta data about the join relationship, e.g., two ids and other data like times, counts, etc.

rails 4 HABTM relation and extra fields on join table

What I have (pseudo code):
model Document
column :title
HABTM :users
model User
column :name
HABTM :documents
Document has users (being approvers for document, either approve or not), and in this context join table should have extra column approved for each user.
jointable
user_id, document_id, approved
1 , 1 , true
2 , 1 , false
What I want is basically:
contract.approvers => returns users but with possibility to =>
contract.approvers.first.approve(:true) => and it updates JOINtable approve column to TRUE.
Answer right for this situation is optional, will appreciate advises on schema too (or maybe i should use other type of relation?).
HABTM has been deprecated a while ago, I think it is just a reference to has many through now.
Either way
join table name = DocumentReview
Document
has_many :document_reviews
has_many :users, through: :document_reviews
User
has_many :document_reviews
has_many :documents, through: :document_reviews
I don't understand how contract fits into this, i think you are saying that a document is a contract?
I would put the approve method in a separate class
class DocumentSignOff
def initialize(user, document)
#document_review = DocumentReview.find_by(user: user,document: document)
end
def approve!
#maybe more logic and such
#document_review.udpate(approved: true)
end
end
end

Rails ActiveRecord how to order by a custom named association

Ok so have created 2 models User and Following. Where User has a username attribute and Following has 2 attributes which are User associations: user_id, following_user_id. I have set up these associations in the respective models and all works good.
class User < ActiveRecord::Base
has_many :followings, dependent: :destroy
has_many :followers, :class_name => 'Following', :foreign_key => 'following_user_id', dependent: :destroy
end
class Following < ActiveRecord::Base
belongs_to :user
belongs_to :following_user, :class_name => 'User', :foreign_key => 'following_user_id'
end
Now I need to order the results when doing an ActiveRecord query by the username. I can achieve this easily for the straight-up User association (user_id) with the following code which will return to me a list of Followings ordered by the username of the association belonging to user_id:
Following.where(:user_id => 47).includes(:user).order("users.username ASC")
The problem is I cannot achieve the same result for ordering by the other association (following_user_id). I have added the association to the .includes call but i get an error because active record is looking for the association on a table titled following_users
Following.where(:user_id => 47).includes(:user => :followers).order("following_users.username ASC")
I have tried changing the association name in the .order call to names I set up in the user model as followers, followings but none work, it still is looking for a table with those titles. I have also tried user.username, but this will order based off the other association such as in the first example.
How can I order ActiveRecord results by following_user.username?
That is because there is no following_users table in your SQL query.
You will need to manually join it like so:
Following.
joins("
INNER JOIN users AS following_users ON
following_users.id = followings.following_user_id
").
where(user_id: 47). # use "followings.user_id" if necessary
includes(user: :followers).
order("following_users.username ASC")
To fetch Following rows that don't have a following_user_id, simply use an OUTER JOIN.
Alternatively, you can do this in Ruby rather than SQL, if you can afford the speed and memory cost:
Following.
where(user_id: 47). # use "followings.user_id" if necessary
includes(:following_user, {user: :followers}).
sort_by{ |f| f.following_user.try(:username).to_s }
Just FYI: That try is in case of a missing following_user and the to_s is to ensure that strings are compared for sorting. Otherwise, nil when compared with a String will crash.

has_many through help, what am I doing wrong?

My models are like this:
User
has_and_belongs_to_many :Roles
Role
has_and_belongs_to_many :Users
tables:
roles_users
user_id
role_id
roleGroups
id
role_id
some_column
Now I want to create another association on the User model, that will be a collection of all roleGroups the user belongs to.
i.e. if the user is in roles with id's 1 and 2, then fetch all RoleGroups where role_id = 1 and 2.
I think I need to use a through because it is based on the user's Role association right?
I tried:
User
has_many :RoleGroups, :through => :Roles
Role
has_many :RoleGroups, :through => :User
But I get an error saying:
ActiveRecord::HasManyThroughSourceAssociationMacroError: Invalid source reflection macro :has_many :through for has_many :RoleGroups, :through => :Roles. Use :source to specify the source reflection.
Update
Ok my models look like this now:
User
habtm :Roles
has_many :RoleGroups, :through => :Roles
Role
habtm :Users
has_many :RoleGroups
RoleGroup
belongs_to :Role
mysql tables:
roles_users
user_id
role_id
role_groups
id
role_id
col3
col4
..
If I do:
u = User.find(1)
u.Roles (works fine)
u.RoleGroups #see error
Error message:
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'roles.user_id' in 'where clause': SELECT `role_groups`.* FROM `role_groups` INNER JOIN `roles` ON `role_groups`.role_id = `roles`.id WHERE ((`roles`.user_id = 1))
You're looking for the has_and_belongs_to_many association.
You can't do this the way you're thinking. I'm not sure why you're capitalizing your associations, but there are a few other things wrong, too.
First, RoleGroups hangs off of Role (via has_many, but more on that in a sec), which means you don't have a direct connection between User and RoleGroup.
Second, it appears from your updated explanation that each RoleGroup can have more than one Role, which is normal, but in your code Role has_many :role_groups, which means each role can have more than one role group. This is counterintuitive naming, but perhaps intentional. I'm going to assume that your role groups contain multiple roles rather than the other way around.
Third, you can't use a HABTM as a :through model. HABTM only uses a table in the database, not a Rails model, so the information in your roles_users table can't be used directly; has_many :foos :through => :bars requires an actual Bar model.
# Models
User
habtm :roles
Role
habtm :users
belongs_to :role_group # add role.role_group_id attribute
RoleGroup
has_many :roles # remove role_groups.role_id attribute
# Console
u = User.find(1) # get user
r = u.roles # get user roles
u.roles.collect { |role| role.role_group.name }
# ["Administrative","Editorial","User"]

HABTM - uniqueness constraint

I have two models with a HABTM relationship - User and Role.
user - has_and_belongs_to_many :roles
role - belongs_to :user
I want to add a uniqueness constraint in the join (users_roles table) that says the user_id and role_id must be unique. In Rails, would look like:
validates_uniqueness_of :user, :scope => [:role]
Of course, in Rails, we don't usually have a model to represent the join relationship in a HABTM association.
So my question is where is the best place to add the constraint?
You can add uniqueness to join table
add_index :users_roles, [ :user_id, :role_id ], :unique => true, :name => 'by_user_and_role'
see In a join table, what's the best workaround for Rails' absence of a composite key?
Your database will raise an exception then, which you have to handle.
I don't know any ready to use rails validation for this case, but you can add your own validation like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :roles, :before_add => :validates_role
I would just silently drop the database call and report success.
def validates_role(role)
raise ActiveRecord::Rollback if self.roles.include? role
end
ActiveRecord::Rollback is internally captured but not reraised.
Edit
Don't use the part where I'm adding custom validation. It kinda works but there is better alternatives.
Use :uniq option on association as #Spyros suggested in another answer:
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies, :uniq => true, :read_only => true
end
(this code snippet is from Rails Guides v.3). Read up on Rails Guides v 3.2.13 look for 4.4.2.19 :uniq
Rails Guide v.4 specifically warns against using include? for checking for uniqueness because of possible race conditions.
The part about adding an index to join table stays.
In Rails 5 you'll want to use distinct instead of uniq
Also, try this for ensuring uniqueness
has_and_belongs_to_many :foos, -> { distinct } do
def << (value)
super value rescue ActiveRecord::RecordNotUnique
end
end
I think that using :uniq => true would ensure that you get no duplicate objects. But, if you want to check on whether a duplicate exists before writing a second one to your db, i would probably use find_or_create_by_name_and_description(...).
(Of course name and description are your column values)
I prefer
class User < ActiveRecord::Base
has_and_belongs_to_many :roles, -> { uniq }
end
other options reference here

Resources