I have 3 models in my rails app
class Contact < ActiveRecord::Base
belongs_to :survey, counter_cache: :contact_count
belongs_to :voter
has_many :contact_attempts
end
class Survey < ActiveRecord::Base
has_many :questions
has_many :contacts
end
class Voter < ActiveRecord::Base
has_many :contacts
end
the Contact consists of the voter_id and a survey_id. The Logic of my app is that a there can only be one contact for a voter in any given survey.
right now I am using the following code to enforce this logic. I query the contacts table for records matching the given voter_id and survey_id. if does not exist then it is created. otherwise it does nothing.
if !Contact.exists?(:survey_id => survey, :voter_id => voter)
c = Contact.new
c.survey_id = survey
c.voter_id = voter
c.save
end
Obviously this requires a select and a insert query to create 1 potential contact. When I am adding potentially thousands of contacts at once.
Right now I'm using Resque to allow this run in the background and away from the ui thread. What can I do to speed this up, and make it more efficient?
You can do the following:
Contact.where(survey_id: survey,voter_id: voter).first_or_create
You should add first a database index to force this condition at the lowest level as possible:
add_index :contacts, [:voter_id, :survey_id], unique: true
Then you should add an uniqueness validation at an ActiveRecord level:
validates_uniqueness_of :voter_id, scope: [:survey_id]
Then contact.save will return false if a contact exists for a specified voter and survey.
UPDATE: If you create the index, then the uniqueness validation will run pretty fast.
See if those links can help you.
Those links are for rails 4.0.2, but you can change in the api docks
From the apidock: first_or_create, find_or_create_by
From the Rails Guide: find-or-create-by
It would be better if you let MySQL to handle it.
Create a migration and add a composite unique key to survey_id, voter_id
add_index :contact, [:survey_id, :voter_id], :unique=> true
Now
Contact.create(:survey_id=>survey, :voter_id=>voter_id)
Will create new record only if there is no duplicates.
Related
I have a simple task list app that has users and lists on it, the users are managed by Devise, and can create task lists, as well as favorite lists created by other users, or by themself. The relation of ownership between users and lists were easy to establish, but I am having trouble setting up the relation of a user favoriting a list. I envision it being a many-to-many relation after all, a user can favorite many lists and a list can be favorited by many users, this relationship happening on top of another already existing one-to-many relationship of list ownership by a user gave me some pause as to whether this is good practice to do, but I proceeded with my attempt regardless.
Currently I have two models, one for the user, and one for the list, and I tried to create a migration for the favorites by running rails g migration CreateJoinTableFavorites users lists, which resulted in the following migration
class CreateJoinTableFavorites < ActiveRecord::Migration[5.2]
def change
create_join_table :users, :lists do |t|
t.index [:user_id, :list_id] <-- I uncommented this line
# t.index [:list_id, :user_id]
t.timestamps <-- I added this line
end
end
end
I thought this would create a table named "Favorites" that would automatically link users and lists, but instead it created a table called "lists_users". Now I am stuck as to what to do next. I have read that I need to create a model for this join table, but I don't know how to go about doing that. What command do I run? rails g model Favorites? rails g model ListsUsers? do I also inform the fields I want to add such as rails g model Favorites user_id:integer list_id:integer, or is there another better way to do it such as perhaps rails g model Favorites user:references list:references? What's the best practice here
Beyond that, I have added a button inside my list#show view for the user to click to add that list to their favorites, and had some trouble routing it. What I did was create a button like this:
<%= button_to 'Add to favorites', add_favorites_path({list_id: #list.id}), method: :post %>
as well as a new route:
post 'add_favorites', to: 'lists#add_favorites'
Though this I managed to have access to the list id and user id in that action, now I don't know how to proceed to create the "favorite" database entry in my lists_users table. To illustrate, I'll paste here my "add_favorite" action
def add_favorites
user_id = current_user.id
list_id = params[:list_id]
#TODO: create the relation in lists_items table
end
I'm aware that I can't get this to work without the model for the join table, but even if I had that model, I haven't had much luck searching for what to do within the controller to create that relation. Anyway, my models are as follows:
class List < ApplicationRecord
belongs_to :user
has_many :users, through: :lists_users
end
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :lists
has_many :lists, through: :lists_users
end
So to summarize, I am aware that I am missing a model for the join table, and would like a step-by-step as to how to create it, what name to give it, etc, as well as how to proceed within my action in my controller to create a new favorite entry
There are two ways to create a many-to-many relation in Rails. What you're doing seems to conflate the two, which I suspect is the source of your problem.
Briefly, the two methods are:
1) has_many :other_models, through: :relation or
2) has_and_belongs_to_many :other_models
The main difference being that the "has_many through" method expects the join table to be a separate model which can be handled independently of this relationship if need be, while the "has_and_belongs_to_many" method does not require the join table to have a corresponding model. In the latter case, you will not be able to deal with the join table independently. (This makes timestamps on the join table useless, by the way.)
Which method you should go with depends on your use case. The docs summarize the criteria nicely:
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). (emphasis added)
Now for your question: When you use create_join_table, you're treating it as though you're setting things up for a has_and_belongs_to_many relation. create_join_table will create a table named "#{table1}_#{table2}" with ids pointing to those tables. It alphabetizes them too, which is why you got "lists_users" instead of "users_lists". This is in fact the standard naming convention for rails join tables if you are planning on using has_and_belongs_to_many, and generally shouldn't be renamed.
If you really want to use has_and_belongs_to_many, keep the migration with the create_join_table and just do the following in your models:
# user.rb
class User
has_and_belongs_to_many :lists
end
# list.rb
class List
has_and_belongs_to_many :users
end
And voila. No Favorite model is needed, and rails is smart enough to handle the relationships through the table on its own. Although a bit easier, the downside is, as stated above, that you won't be able to deal with the join table as an independent model. (Again, timestamps on the join table are useless in this case, as Rails won't set them.)
Edit: Since you can't directly touch lists_users, you'd create relationships by setting the lists relation on a user, or by setting the users relation on lists, like so:
def add_favorites
list = List.find(params[:list_id])
current_user.lists << list # creates the corresponding entry in lists_users
# Don't forget to test how this works when the current_user has already favorited a list!
# If you want to prevent that from happening, try
# current_user.lists << list unless current_user.lists.include?(list)
# Alternatively you can do the assignment in reverse:
# list.users << current_user
# Again, because the join table is not an independent model, Rails won't be able to do much to other columns on lists_users out of the box.
# This includes timestamps
end
On the other hand, if you want to use "has_many through", don't use create_join_table. If you're using has_many through, the join table should be thought of almost as an entirely separate model, that just happens to have two foreign keys and tie two other models together in a many-to-many relationship. In this case, you'd do something like:
# migration
class CreateFavorites < ActiveRecord::Migration[5.2]
def change
create_table :favorites do |t|
t.references :list
t.references :user
t.timestamps
end
end
end
# user.rb
class User
has_many :favorites
has_many :lists, through: :favorites
end
# list.rb
class List
has_many :favorites
has_many :users, through: :favorites
end
# favorite.rb
class Favorite
belongs_to :list
belongs_to :user
end
# controller
def add_favorites
# You actually have a Favorite model in this case, while you don't in the other. The Favorite model can be more or less independent of the List and User, and can be given other attributes like timestamps.
# It's the rails methods like `save`, `create`, and `update` that set timestamps, so this will track those for you as any other model.
Favorite.create(list_id: params[:list_id], user: current_user)
end
You might want to reflect on which method to use. Again, this really depends on your use case, and on the criteria above. Personally, when I'm not sure, I prefer the "has_many through" method as it gives you more tools to work with and is generally more flexible.
You may try following :
class User
has_and_belongs_to_many :lists
end
class List
has_and_belongs_to_many :users
end
class CreateUsersAndLists
def change
create_table :users do |t|
# Code
end
create_table :lists do |t|
# Code
end
create_table :users_lists id: false do |t|
t.belongs_to :user, index: true
t.belongs_to :list, index: true
t.boolean :is_favourite
end
end
end
I am having trouble with retaining only a single record in a has_one relationship
Example
Categories and question
A question belongs_to a category, and a category has_one question
In reality as seen in a system i am building even though the has_one relationship exists, there comes a case where i have multiple questions that belong to a category.
Shouldn't the has_one relationship limit them to just one record? If not, then how can i make sure that i always keep one record?
EDIT
Please note that question model has a uniqueness rule on the ID of category and STILL i found cases of multiple records that point to same category ID.
How is that possible?
EDIT 2
Overview of the models
class Event < ApplicationRecord
has_one :travel_time, :inverse_of => :event, dependent: :destroy
end
class TravelTime < ApplicationRecord
belongs_to :event, :inverse_of => :travel_time
validates_uniqueness_of :event_id, allow_nil: true
end
EDIT 3
method in event model used to save the travel time record
def store_travel_times(body)
travel_times = self.build_travel_time
# get travel times
...
if !travel_times.save
logger.error "..."
end
end
Query used to find the multiple records
2017-05-04T13:39:15.277063 #50567] DEBUG -- : TravelTime Load (0.4ms) SELECT "travel_times".* FROM "travel_times" WHERE "travel_times"."event_id" = $1 [["event_id", 3105]]
As discussed in the comments, create db level constraint to check uniqueness of a column and avoid multiple records creation. You need to create a migration file with below code:
add_index :travel_times, :event_id, unique: true
Hope it helps.
I am building a job board in rails based on PostgreSQL. I want to count and display the amount of job offers per employer on the index of the employer page. What is the code for this kind of count?
I created a nested resource, and associated my employer and offer model, by:
class Employer < ActiveRecord::Base
has_many :offers, dependent: :delete_all
end
You should use counter_cache i.e. adding an extra column(offer_count) in employer table & update the counter while making entry in offer table. For more details , check counter_cache in http://guides.rubyonrails.org/association_basics.html
Your migration should like
def change
add_column :employers, :offers_count, :integer, default: 0
Employer.reset_column_information
Employer.all.each do |p|
Employer.update_counters p.id, :offers_count => p.offers.length
end
end
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
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