Rails: Duplicate records created at the same time - ruby-on-rails

There is something weird happening with my Rails app. A controller action is called and saves a record to a table each time a user visits a unique url I created before.
Unfortunately, sometimes two identical records are created instead of only one. I added a "validates_uniqueness_of" but it's not working.
My controller code:
class ShorturlController < ApplicationController
def show
#shorturl = ShortUrl.find_by_token(params[:id])
#card = Card.find(#shorturl.card_id)
#subscriber = BotUser.find_by_sender_id(params['u'])
#letter_campaign = Letter.find(#card.letter_id).campaign_name.downcase
if AnalyticClic.where(card_id: #card.id, short_url_id: #shorturl.id, bot_user_id: #subscriber.id).length != 0
#object = AnalyticClic.where(card_id: #card.id, short_url_id: #shorturl.id, bot_user_id: #subscriber.id)
#ccount = #object[0].clicks_count
#object.update(updated_at: Time.now, clicks_count: #ccount += 1)
else
AnalyticClic.create(card_id: #card.id, short_url_id: #shorturl.id, bot_user_id: #subscriber.id, clicks_count: "1".to_i)
end
#final_url = #card.cta_button_url
redirect_to #final_url, :status => 301
end
end
And the model:
class AnalyticClic < ApplicationRecord
validates_uniqueness_of :bot_user_id, scope: :card_id
end
Any idea why sometimes I have duplicated records? The if should prevent that as well as the validates_uniqueness_of.

First off, I believe your validation may need to look something like (although, TBH, your syntax may be fine):
class AnalyticClic < ApplicationRecord
validates :bot_user_id, uniqueness: { scope: :card_id }
end
Then, I think you should clean up your controller a bit. Something like:
class ShorturlController < ApplicationController
def show
#shorturl = ShortUrl.find_by_token(params[:id])
#card = Card.find(#shorturl.card_id)
#subscriber = BotUser.find_by_sender_id(params['u'])
#letter_campaign = Letter.find(#card.letter_id).campaign_name.downcase
analytic_clic.increment!(:click_count, by = 1)
#final_url = #card.cta_button_url
redirect_to #final_url, :status => 301
end
private
def analytic_clic
#analytic_clic ||= AnalyticClic.find_or_create_by(
card_id: #card.id,
short_url_id: #shorturl.id,
bot_user_id: #subscriber.id
)
end
end
A few IMPORTANT things to note:
You'll want to create an index that enforces uniqueness at the database level (as max pleaner says). I believe that'll look something like:
class AddIndexToAnalyticClic < ActiveRecord::Migration
def change
add_index :analytic_clics [:bot_user_id, :card_id], unique: true, name: :index_bot_user_card_id
end
end
You'll want to create a migration that sets :click_count to a default value of 0 on create (otherwise, you'll have a nil problem, I suspect).
And, you'll want to think about concurrency with increment! (see the docs)

You need to create unique index in your database table. There might be 2 processes getting to create condition together. The only way to stop these duplicate records is by having uniqueness constraint at DB level.

Related

Next Instance Method

In my blog app, I can call #article.comments.last. How do I create a next_comment method, one that always picks the next comment in line?
Update:
Also, how do I do the reverse, define a previous_comment method?
Update answer below.
Previous record:
class Comment < ActiveRecord::Base
def self.previous(comment, key = :id)
self.where("#{key} < ?", commend.send(key)).first
end
end
In order to define a "next", you must declare a sorting rule. There is no "next" without an order.
The order can be as simple as by primary key, or another field (e.g. a name). The following method should support both cases, and default to id"
class Comment < ActiveRecord::Base
def self.next(comment, key = :id)
self.where("#{key} > ?", commend.send(key)).first
end
end
You should call it on the chain and passing the comment instance, so that it can use the same relation you used to load the original comment
scope = #article.comments
last = scope.last
next = scope.next(last)
Another (maybe simpler) solution is to simply load two objects
current, next = #article.comments.take(2)
You can also make it a method
class Comment < ActiveRecord::Base
def self.first_and_next
# use all to create a scope in case you call
# the method directly on the Comment class
all.take(2)
end
end
current, next = #article.comments.first_and_next(2)
Given that you have a pagination gem, like will_paginate, this will work
# Article model
def next_comment
#page ||= 0
#page += 1
comments.page(#page).per(1).first
end
Or if you don't want to store the state
# Comment model
def next_comment
article.comments.where("id > ?", id).first
end
Dirty solution:
class Comment < ActiveRecord::Base
def next_comment
article.comments.where('id > ?', id).first
end
end

Recommended practice for passing current user to model

Given a model Orderstatus with attributes private_status:string, and private_status_history:json(I'm using Postgresql's json). I would like to record each status transition, together with the user who made the change.
Ideally it would be something like:
class Orderstatus < ActiveRecord::Base
after_save :track_changes
def track_changes
changes = self.changes
if self.private_status_changed?
self.private_status_history_will_change!
self.private_status_history.append({
type: changes[:private_status],
user: current_user.id
})
end
end
end
class OrderstatusController <ApplicationController
def update
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
#Desired behaviour (process not run with console)
status = Orderstatus.new(private_status:'one')
status.private_status #=> 'one'
status.private_status_history #=> []
status.update_attributes({:private_status=>'two'}) #=>true
status.private_status #=> 'two'
status.private_status_history #=> [{type:['one','two'],user:32]
What would be the recommended practice to achieve this? Apart from the usual one using Thread. Or maybe, any suggestion to refactor the structure of the app?
So, I finally settled for this option ( I hope it's not alarming to anyone :S)
class Orderstatus < ActiveRecord::Base
after_save :track_changes
attr_accessor :modifying_user
def track_changes
changes = self.changes
if self.private_status_changed?
newchange = {type:changes[:private_status],user: modifying_user.id}
self.update_column(:private_status_history,
self.private_status_history.append(newchange))
end
end
end
class OrderstatusController <ApplicationController
def update
#status.modifying_user = current_user # <---- HERE!
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
Notes:
- I pass the from the Controller to the Model through an instance attribute modifying_user of the class Orderstatus. That attribute is ofc not saved to the db.
- Change of method to append new changes to the history field. I.e. attr_will_change! + save to update_column + append

Elegant way so access a `belongs_to` -> `has_many` relationship

In my app, an user belongs_to a customer, and a customer has_many construction_sites. So when I want to show the current_user only his construction_sites, I have multiple possibilities of which none is elegant:
#construction_sites = ConstructionSite.where(customer: current_user.customer)
This works and looks good, except for the case that the user is not yet associated with a customer. Then, I get a PG::UndefinedColumn: ERROR: column construction_sites.customer does not exist error.
#construction_sites = ConstructionSite.where(customer_id: current_user.customer_id)
This seems to work fine on first sight, but again for the case that the user is not yet associated with a customer current_user.customer_id is nil and ConstructionSite.where(customer_id: nil) gets called which selects all (or all unassigned?) sites, which is not what I want.
unless...
unless current_user.customer.nil?
#construction_sites = ConstructionSite.where(customer: current_user.customer)
else
#construction_sites = []
end
Well this works, but does not look nice.
ConstructionSite.joins(customer: :users).where('users.id' => current_user.id)
works but does not look nice.
So, what is the most elegant solution to this problem?
Try using delegate keyword. Add this to your user model.
delegate :construction_sites, to: :customer, allow_nil: true
After that you can use statements like
current_user.construction_sites
Which I find the most elegant of all options.
def user_construction_sites
#construction_sites = []
#construction_sites = current_user.customer.construction_sites if current_user.customer.present?
#construction_sites
end
How about moving your logic to a named scope and putting in a guard clause?
class SomeController < ApplicationController
def some_action
#construction_sites = ConstructionSite.for(current_user)
end
end
class ConstructionSite < ActiveRecord::Base
def self.for(user)
return [] if user.customer.blank?
where(customer: user.customer)
end
end

rails 3 caching site settings with ability to reload cache without server restart and third-party tools

I need some solution to make following functionality in my RoR 3 site:
Site needs a user rating system, where users get points for performing some actions (like getting points for answering questions on stackoverflow).
The problems are:
1) I need ability to re-assign amount of points for some actions (not so often, but I can't restart Mongrel each time I need to re-assign, so in-code constants and YAML don't suit)
2) I can't use simple Active Record, because on 5000 users I'll do too many queries for each user action, so I need caching, and an ability to reset cache on re-assignment
3) I would like to make it without memcached or something like this, cause my server hardware is old enough.
Does anyone know such solution?
What about something like this ?
##points_loaded = false
##points
def load_action_points
if (File.ctime("setting.yml") < Time.now) || !##points_loaded
##points = YAML::load( File.open( 'setting.yml' ) )
##points_loaded = true
else
##points
end
or use A::B and cache the DB lookups
class ActionPoints < ActiveRecord::Base
extend ActiveSupport::Memoizable
def points
self.all
end
memoize :points
end
You also cache the points in the User model, something like this .. pseudocode...
class User < A::B
serialize :points
def after_save
points = PointCalculator(self).points
end
end
and....
class PointCalculator
def initialize(user)
##total_points = 0
user.actions.each do |action|
p = action.times * ActionPoints.find_by_action("did_something_cool").points
##total_points = ##total_points + p
end
end
def points
##total_points
end
end
I've found some simple solution, but it works only if you have one RoR server instance:
class Point < ActiveRecord::Base
##cache = {}
validates :name, :presence => true
validates :amount, :numericality => true, :presence => true
after_save :load_to_cache, :revaluate_users
def self.get_for(str)
##cache = Hash[Point.all.map { |point| [point.name.to_sym, point] }] if ##cache == {} #for cache initializing
##cache[str.to_sym].amount
end
private
def load_to_cache
##cache[self.name.to_sym] = self
end
def revaluate_users
#schedule some background method, that will update all users' scores
end
end
If you know a solution for multiple server instances without installing memcached, I will be very glad to see it.

Ruby attr_accessor setter does not get called during update_attributes on child object

I'm trying to add functionality to a project done in ruby, I'm unfamiliar with Ruby, but have managed to create a project review page that allows updates on the project task codes for a given monthly review.
My issue is that the client (my brother) has asked me to allow him to edit the scheduled hours for the next few months on "this" month's project review.
I've been able to show those values that don't belong to the child on the page, and I can get the usual child elements to update, but I cannot get the update to happen on the value I'm borrowing from the future month(s).
To get the page to show and not fail on update, I've added that attr_accessor (otherwise I had failures on update because the value didn't exist in the model.
an excerpt from my code is shown below. There are no errors, but there are also no updates to the variable reflected in the attr_accessor, I have tried testing with changes to the usual elements in the child object, those will get updated, but still no call to the attr_accessor "setter".
suggestions?
Thanks much,
Camille..
class Projectreview < ActiveRecord::Base
has_many :reviewcostelements
accepts_nested_attributes_for :reviewcostelements
end
class ProjectreviewsController < ApplicationController
def update
#projectreview = Projectreview.find(params[:id])
respond_to do |format|
if #projectreview.update_attributes(params[:projectreview])
format.html { redirect_to(#projectreview) }
end
end
end
end
class Reviewcostelement < ActiveRecord::Base
belongs_to :projectreview
attr_accessor :monthahead_hours1
def monthahead_hours1(newvalue) #this is the setter
#why do I never see this log message ??
logger.info('SETTER 1')
set_monthahead_hours(1, newvalue)
end
def monthahead_hours1 #this is the getter
get_monthahead_hours(1)
end
def update_attributes(attributes)
#never gets called!!!
logger.info('update_attributes values rce')
super(attributes)
end
def get_monthahead_hours(p_monthsahead)
#this works and returns the next month's scheduled_hours_this_month value
rce = Reviewcostelement.first(:joins => :projectreview,
:conditions => ["projectreviews.project_id = ?
and reviewcostelements.projecttaskcode_id =?
and projectreviews.month_start_at = ?", projectreview.project_id ,
projecttaskcode_id ,
projectreview.month_start_at.months_since(p_monthsahead)])
if rce
return rce.scheduled_hours_this_month
else
return 0
end
end
def set_monthahead_hours(p_monthsahead, newvalue)
#this never seems to get called
logger.info("set the month ahead hours")
rce = Reviewcostelement.first(:joins => :projectreview,
:conditions => ["projectreviews.project_id = ?
and reviewcostelements.projecttaskcode_id =?
and projectreviews.month_start_at = ?",
projectreview.project_id ,
projecttaskcode_id ,
projectreview.month_start_at.months_since(p_monthsahead)])
if rce
rce.scheduled_hours_this_month = newvalue
rce.save
end
end
end
The setter of the accessor looks like this:
def monthahead_hours1=(newvalue)
...
end
Note the equals (=) symbol.

Resources