activerecord: update object attribute without using transaction - mysql

Here is my code:
class UserTopicVisit < ActiveRecord::Base
belongs_to :user
belongs_to :topic
def self.log_last_user_topic_visit(user_id, topic_id)
visit = UserTopicVisit.find_or_create_by_user_id_and_topic_id(user_id, topic_id)
visit.update_attributes!(:last_visit => Time.now.to_formatted_s(:db))
end
end
Which is self-explainig.
The problems is: I need to be able to update object attribute without transaction, by simple mysql query. How can I accomplish that using activerecord?

update_attributes uses a transaction, but oddly, save doesn't, so if you want callbacks and all that jazz without the transaction you can do:
visit.last_visit = Time.now.to_formatted_s(:db)
visit.save!
Under the hood, update_attributes actually just calls attributes= and then save.

Maybe you are looking for update-column
Updates a single attribute of an object, without calling save
Validation is skipped.
Callbacks are skipped.
updated_at/updated_on column is not updated if that column is available.

Related

Delete some record and then rollback if condition fails in rails

Problem?
I have a Rails5 application. I have two models. Team and Players.
The association is has_many & belongs_to in between them.
class Team < ApplicationRecord
has_many :players
end
class Player < ApplicationRecord
belongs_to :team
end
Now, I want to perform destroy players before updating the team model. The condition is like below
def update
#team.players.destroy_all if a
........
if b
.... some code.....
elsif c
#team.players.destroy_all
end
if #team.update_attributes(team_params)
redirect_to teams_path
else
... some code..
render :edit
end
end
Note
In team_params, I have players_attributes, so each time if a new entry is there I need to remove all old entries and then #team.update_attributes will insert the entries in players.
Expectations
If #team.update_attributes(team_parmas) fails, then #team.players should be rolled back.
Things I tried
I tried adding Transactions in the first line of update method but it doesn't work.
You can use a gem like paper trail to keep a history, but the nature of rails is to not be able to roll back transactions after committed. One way to mock this would be to keep the attributes in a transient collection of hashes and re-save them if you need the records once more. You could have logic like
team_players = #team.players.map(&:attributes)
Then if you need to 'roll back'
team_players.each do |team_player|
TeamPlayer.create(team_player)
end
This will only work with basic attributes. If you have other model relations you will have to handle them with attributes also.

Cannot destroy records for some models in rails

I have a model with 2 associations and no validations, which can be created but if I want to destroy it, I get error " ActiveRecord::RecordNotDestroyed"
So I tried directly in rails create new record for this Model and then run destroy, but it rollbacks without any error message. If I do destroy! I get the error. I enabled mysql query logging but it looks it doesnt even get to delete part so I assume rails is somehow preventing from destroying this model.
As a test I created dummy model without any association, with just one string value. Same issue in rails console. I have to note that .delete method works, but .destroy not
Then I started to analyze in my rails app (I took it over from somebody so I am still not 100% familiar) and in rails console tried to destroy some records for different models and realized some I am able to destroy, some not.
Is there any way how to find out why I am not able to destroy those records? It is really strange for my Testmodel without any validation or association. Is there anywhere to look for more info? I tried this:
testrecord.destroy; testrecord.errors but in #messages I have empty {}
Here is my code for model where I found an issue (EinvoiceContact) and also my Testmodel
class EinvoiceContact < ActiveRecord::Base
belongs_to :customer
has_many :building
end
class Customer < ActiveRecord::Base
has_many :einvoice_contacts, dependent: :destroy
accepts_nested_attributes_for :einvoice_contacts, allow_destroy: true, reject_if: :empty_einvoice_contact
end
class Testmodel < ActiveRecord::Base
end
and here is output from rails and mysql log
> irb(main):045:0> Testmodel.find(1).destroy\r Testmodel Load (0.4ms)
> SELECT `testmodels`.* FROM `testmodels` WHERE `testmodels`.`id` = 1
> LIMIT 1 (0.1ms) BEGIN (0.1ms) ROLLBACK
> => false
>
>
> 2019-12-18T21:05:42.455378Z 33 Query SELECT `testmodels`.* FROM
> `testmodels` WHERE `testmodels`.`id` = 1 LIMIT 1
> 2019-12-18T21:05:42.456559Z 33 Query BEGIN
> 2019-12-18T21:05:42.458261Z 33 Query ROLLBACK
Any help where to look for more details, or where this can be set is appreciated. This app uses CanCan but I have set
can :manage, :all
Rails is 4.2.8 with ruby 2.1.4 (I know it is old app, I just took it over and maintain), mysql
thanks
It seems that Testmodel has some associations.
Try this in the console and check the output.
Testmodel.reflect_on_all_associations
Does Testmodel.find(1) return anything?
Try Testmodel.find(1).destroy! (with exclamation point) to see if it tells you anything more.
Problem solved, there was this code added in lib folder which prevents to destroy anything unless I set in model can_destroy to true
thanks all for help
class ActiveRecord::Base
def can_destroy?
false
end
before_destroy do
can_destroy?
end

Rails find object id on has_many association

I'm trying to get the transaction id associated with the current_user but rails shos this error below
error:
ActiveRecord::RecordNotFound (Couldn't find Transaction with id=92
[WHERE "transactions"."user_id" = 24])
iv'e tried use where and just the transaction and with a conditional comparing the transaction.user_id with the current_user.id but show error!
someone have any hint about this kind of issue?
model user
user has_many transactions
model transaction
transaction belongs to user
transaction controller
def new
#transaction = Transaction.new
end
def create
#transaction = Transaction.build_user
end
def show
#transaction = current_user.transactions.find(params[:id])
end
As many pointed in comments build_user does indeed create a new User instance (without saving it). Furthermore I believe build_user is not a class method but an instance method instead. Therefore Transaction.build_user should be raising 'undefined method' exception. Also, build_user returns a User instance, so even if you're code was #transaction = Transaction.new.build_user #transaction would be a newly created User instance.
And again, what others already pointed out in comments, your transaction wouldn't necessarily be associated to the current_user for you to fetch it like this in subsequent calls during the same session, rather, and supposing you properly built and saved the user, you would need to login as this 'new user' in order to.

How to delete a record only if it exists in Rails?

I want to delete the tokens I created for a post, when I am deleting the post itself. But if the token does not exist, I don't want Rails to throw an error and stop executing.
Right now, this is how I go about it. I think this code is way too chunky. Is there any neat way to accomplish the same?
DownloadToken.find_by_post_id(post.id).destroy unless DownloadToken.find_by_post_id(#post.id).nil?
This is one way(old syntax)
DownloadToken.find_by_post_id(post.id).try(:destroy)
Newer syntax in rails:
DownloadToken.find_by(id: post.id).try(:destroy)
With Ruby's safe navigation operator:
DownloadToken.find_by(id: post.id)&.destroy
Look at your Post model; post.rb
Ie.
Class Post < ActiveRecord::Base
has_many :download_tokens, dependent: :destroy
end
Now when you delete a Post object, whatever the association; has_many, has_one, it will find the destroy the dependent also. In this case the DownloadToken(s)
DownloadToken.find_by_post_id(post.id)&.destroy
Executes destroy only if the query result is not nil. It's the abbreviated version of:
token = DownloadToken.find_by_post_id(post.id)
token.destroy if token
If you are certain you 'll handle the post deletion with its destroy method, than you can follow Jay's answer and it will work just fine.
If you will use the delete method on post you need some extra functionality to handle the download_tokens.
class DownloadToken < ActiveRecord::Base
def self.remove_post_tokens(the_post_id)
where(post_id: the_post_id).destroy_all
end
end
so your sequence will be:
id = post.id
post.delete #or post.destroy
DownloadToken.remove_post_tokens(id)
That scenario is not purely academic, because the dependent destroy action can be really expensive, eg if you have too many download_tokens, so you would not want it to be in the same transaction as post's destruction.

Rails: Creating A record and then reading it instantly issue

I am creating a record and then pushing the id of the newly created record in a queue, from the after create filter.
From another script I am reading the ids from the queue and reading the db record for the ids instantly.
record = Model.find(id)# this is giving error: Couldn't find record with ID 8732
I am using rails 2.3.14 with mysql2 gem.
What you are experience is known as a race-condition.
Your second script or worker library is trying to access the record before it has been entirely written ("committed"), as ilan pointed out.
A common solution to this problem is using an after_commit callback instead of after_create / after_save etc.
An example from the Article on Rails BestPractices.
Before:
class Notification < ActiveRecord::Base
after_create :asyns_send_notification
def async_send_notification
NotificationWorker.async_send_notification({:notification_id => id})
end
end
class NotificationWorker < Workling::Base
def send_notification(params)
notification = Notification.find(params[:notification_id])
user = notification.user
# send notification to user's friends by email
end
end
After refactoring using after_commit lifecycle hooks:
class Notification < ActiveRecord::Base
after_commit :asyns_send_notification, :on => :create
def async_send_notification
NotificationWorker.async_send_notification({:notification_id => id})
end
end
Further reading: after_commit in the Rails API docs.
Maybe the query result "SELECT * FROM Model WHERE id=8732" is on cache.
You should try to "reload" the query :
record = Model.find_by_id(id, true)
The reason has to do with transaction isolation levels. Although you can read the entry that you just inserted, another process can't until the transaction is committed. This commit happens after the controller returns.