Unique on three different fields - ruby-on-rails-3

I am working on a Ruby on Rails web application. I want to validate the uniqueness of more than one field together. How can i do this?
For example: I have a model named waiting with three fields:
project_id category_id and user_id
I want to ensure that i won't have two identical rows in all three fields.

Why not just to:
validates_uniqueness_of :user_id, :scope => [:project_id, :category_id]
+
add_index :waitings, [:project_id, :category_id, :user_id], :unique => true
Read API:
http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of
http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_index

It's not pretty but this works for me:
class Waiting < ActiveRecord::Base
validate :must_be_unique
def must_be_unique
if self.class.where(project_id: project_id, category_id: category_id, user_id: user_id).exists?
errors.add(:base, 'Must be unique')
end
end
end
Of course you could just use a unique key in your db schema and then catch the relevant exceptions on the rare occasion you need to.

Related

How to reduce queries on associated object with Rails ActiveRecord

I have the following problem.
I have an Organization class that returns and array of "authorized user" emails and their associated User ID.
class Organization < ApplicationRecord
...
has_many :authorized_users
def authorized_user_opts
self.authorized_users.map do |authorized_user|
[authorized_user.email, authorized_user.user.id]
end
end
end
Then the AuthorizedUser class - note that we lookup the user via a find_by:
class AuthorizedUser < ApplicationRecord
...
def user
User.find_by(email: email)
end
end
And the User model:
class User < ApplicationRecord
validates :email, presence: true
end
This creates an extra query for each user to get their ID. Is there a way I can improve this query?
I thought about migrating the AuthorizedUser class to add a user_id field, but I'm wondering if there's a way to improve this just SQL instead of adding another field.
I think the addition of a user_id to AuthorizedUser is a decent choice, but if you must do it without, you should be able to use joins and includes:
authorized_users.joins("inner join users on users.email = authorized_users.email").includes(:users)
joins here is doing what an ActiveRecord association would do under the hood, and then includes eager loads the user objects in one query so that you don't have N queries for N users.
You might also be able to mess with the options on belongs_to which lets you specify the key that it uses under the hood. Something like:
# authorized_user.rb
belongs_to :user, foreign_key: 'email', primary_key: 'email'

Rails concatenate sql columns and create one to many relationship

I've got two models: Building and BuildingInfo. I want to relate the two tables using two columns townhall_level and name.
Ideally it will work like the following: Building.first.building_info For instance Building.first.townhall_level => 5 and Building.first.name => cannon, Building.first.building_info would access BuildingInfo.where(townhall_level: 5, name:"cannon".
What's the best way to do this? Can I create a third column which concatenates name and townhall_level? Could I also use that column to create the belongs_to and has_many relationship?
Simple and straightforward:
class Building < ActiveRecord::Base
def building_info
BuildingInfo.find_by(townhall_level: townhall_level, name: name)
end
end
It will be nil if nothing is found, and will return only the first record even if multiples are found. I also highly suggest that you add an index to the two columns through a migration:
add_index :building_infos, [:townhall_level, :name], name: 'building_infos_level_and_name'
Which will speed up searching, if you were concerned about performance.
mmm...I'm not sure this will work but you can do something like
class Building < ActiveRecord::Base
def self.bulding_info
BuildingInfo.find_by(townhall_level: townhall_level, name: name)
end
end
but I would really suggest you to put a building_info_id in the Building model and have a
class Building < ActiveRecord::Base
belongs_to :bulding_info
end

Rails 3 has_many :through accessing attributes

I am working with a has_many through for the first time, and despite a lot of reading here and in the guide I am not understanding the correct way to access attributes on the through table. My tables are the same as this example from another post.
class Product < ActiveRecord::Base
has_many :collaborators
has_many :users, :through => :collaborators
end
class User < ActiveRecord::Base
has_many :collaborators
has_many :products, :through => :collaborators
end
class Collaborator < ActiveRecord::Base
belongs_to :product
belongs_to :user
end
Assuming that the collaborators table has additional attributes, say hours_spent, what is the correct way to find the hours_spent from the collaborator table for a particular user and product?
When I have found my users via the product, and am iterating over them as in
#product.users.each do |user|
This seems to work
user.collaborator[0].hours_spent
I get the correct value, but since there should only be one collaborator record for each User/Product pair, the index is throwing me off, making me think I’m doing something wrong.
Thank you for reading!
EDIT
Perhaps I am not getting the has_many through concept. Maybe a MySQL example would help.
What I was thinking is that if I did
SELECT * FROM collaborators where user_id = 1;
I would expect a set (zero or more) as the result. Similarly
SELECT * FROM collaborators where product_id = 1;
would also give me a set, but
SELECT * FROM collaborators where user_id = 1 and product_id = 1;
would give at most 1 row.
If I am understanding properly, all 3 queries return a set. So I guess I need some kind of uniqueness constraint, but that would have to be a compound key of sorts, on both of the belongs to keys. Is that even possible? Is there a structure that better models this?
Thanks so much for the quick and helpful responses!
There may be a single database row per pair, but when considering a single user, that user can be associated to many products, so a user can have many rows in the collaborators table. Similarly, when considering a single product, that product can be associated to many users, so a product can have many rows in the collaborators table.
Also, instead of using user.collaborators[0].hours_spent, use user.collaborators.first.try(:hours_spent) (which may return null), if you only want the first collaborator's hours spent.
If a single user can only have one single product and a single product can only have a single user, then switch the has_many's to has_one's for everything.
Update: The preceding is the answer to the original question which has since been clarified via comments. See comments for detail and see comments on other answer by Peter.
Perhaps you should use has_and_belongs_to_many. If your Collaborator is used only to make link between User and Product without having more fields.
class Product < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :products
end
The beetween migration would be:
class CreateUsersProducts < ActiveRecord::Migration
def change
create_table "users_products", :id => false do |t|
t.integer :user_id
t.integer :product_id
end
end
end
After implementing this, what I found was that I think I had the correct relationships setup, I had to use the has_many :though as users could have many products, and it needed to be :through because there are additional attributes on the collaborator table. The sticking point was how to get there to only be a single Collaborator record for each user/product pair, and then how do I guarantee I got it. And to this point the answer I've found is it has to be done in code.
To make sure there is only a single record for each pair, I used
class Collaborator < ActiveRecord::Base
validates :product_id, :presence => true, :uniqueness => {:scope => [:user_id], :message => "This is a duplicate join"}
And then to make doubly sure I'm finding the right record, I have a scope
scope :collaboration_instance, lambda {|p_id, u_id| where("collaborations.product_id = ? && collaborations.user_id = ?", p_id, u_id)}
If someone has a more elegant solution, or just wants to improve this one, please post and I will change yours to the selected answer.

Simple has_many :through association

Pretty simple setup. I want to make sure my understanding of the ORM is correct.
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through => memberships
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, through => memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
Now when a user creates a group I want the membership record in the link table to get populated. It should be an atomic(transaction).
class GroupsController < ApplicationController
def create
#group = current_user.groups.build(params[:group])
if #group.save
flash[:notice] = "Group has been created."
redirect_to #group
else
flash[:alert] = "Group has not been created."
render :action => "new"
end
end
end
This doesn't work. The group gets saved but no membership record created in the link table. However using a create vs build works. Is that how it's supposed to work?
What's the best approach here?
This behaviour is by design. As you mentioned, you can either do #group = current_user.groups.create(params[:group]).
Or you can add an additional statement to create a record in the join model's table as :
#group = current_user.groups.build(params[:group])
if #group.save
#group.memberships.create(:user_id => current_user)
# redirect and notify
Well, the reason being simply building #group and saving it does not add an additional record in the join table.
Infact, in this case, #group = current_user.groups.build(params[:group]) is somewhat similar to #group = Group.new(params[:group]). The difference being, in the former case, current_user.groups will contain #group (you can try that in Groups#create before redirect) but doing current_user.reload followed by current_user.groups will yield [].
The best way to do this is somewhat similar to your approach. Have a simple create action as :
def create
#group = Group.new(params[:group])
# if else for save and redirect
However, for this to work the params hash submitted to Groups#create should include user_ids as :
"group"=>{"name"=>"new group", "user_ids"=>["1", "2", "3"]}, "commit"=>"Create Group"
May be that was the reason why #bruno077 was asking you to paste your view's code, so as to get an idea on user_ids params being passed.
So, if the new group form contains fields to select multiple users, then its simple create action as shown right above (because of the user_ids params). But if have a new group form with no options to select users, then you are better off using the first option (one using create).

Factory Girl sets wrong value for auto-increment id in association

I have two dependent models with one to many association
class Post < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
belongs_to :post
end
in my DBMS post_id in users table is defined as a foreign key
I wrote two cucumber scenarios - in first I(with factory_girl help) create two users and in second I also want to create two users but creation fails because factory_girl builds dependent post records with id set to one however in previous scenario two records in posts table were created and deleted so id value should start from 3 and not from 1. I used pry to debug this process and spotted that if I use FactoryGirl.build( :post ) it sets value properly but if I use FactoryGirl.build( :user ) value of the associated post.id is always 1. How can I make it buld records with proper id?
EDIT
FactoryGirl.define do
sequence :composite_id do |n|
"F98F3806-#{sprintf("%04X", n)}-4C96-879C-2C6F38213D8E"
end
# Post
factory :post do
sequence( :post_id ) { |n| "#{n}" }
some_field "Lorem Ipsum is simply dummy text of the printing a"
...
end
# User
factory :user do
user_id { FactoryGirl.generate( :composite_id )}
association :post_id, :factory => :post
...
end
end
That should be association :post. FactoryGirl will figure out the ID field.