ActiveRecord is not reloading nested object after it's updated inside a transaction - ruby-on-rails

I'm using Rails 4 with Oracle 12c and I need to update the status of an User, and then use the new status in a validation for another model I also need to update:
class User
has_many :posts
def custom_update!(new_status)
relevant_posts = user.posts.active_or_something
ActiveRecord::Base.transaction do
update!(status: new_status)
relevant_posts.each { |post| post.update_stuff! }
end
end
end
class Post
belongs_to :user
validate :pesky_validation
def update_stuff!
# I can call this from other places, so I also need a transaction here
ActiveRecord::Base.transaction do
update!(some_stuff: 'Some Value')
end
end
def pesky_validation
if user.status == OLD_STATUS
errors.add(:base, 'Nope')
end
end
end
However, this is failing and I receive the validation error from pesky_validation, because the user inside Post doesn't have the updated status.
The problem is, when I first update the user, the already instantiated users inside the relevant_posts variable are not yet updated, and normally all I'd need to fix this was to call reload, however, maybe because I'm inside a transaction, this is not working, and pesky_validation is failing.
relevant_users.first.user.reload, for example, reloads the user to the same old status it had before the update, and I'm assuming it's because the transaction is not yet committed. How can I solve this and update all references to the new status?

Related

Deleting a User relation using Rails Models and Interactors

I have a question regarding the deletion of a User. Note that I'm not using Devise since my app is an API.
What I need to do
So I have a User model, I can delete this User with no issues. The user belongs to many other associations regarding Bank Accounts, Transactions, you name it.
When I delete the User, it's able to be deleted but its associations are not. Note that I'm using soft_deletion which means it gets in an INACTIVE state. And by saying that the "associations aren't being deleted" means that I just need to DISABLE specific associations when the User has been deleted or gets INACTIVE
What I currently have
user.rb model file
class User < ApplicationRecord
has_many :bank_accounts
def soft_deletion!
update!(status: "DELETED",
deleted_at: Time.now)
end
end
delete_user.rb interactor file
module UserRequests
class DeleteUser < BaseInteractor
def call
require_context(:id)
remove_user
context.message = 'user record deleted'
end
def remove_user
user = context.user
context.user.soft_deletion! #<- This is the method I have on my model, which works!
#below I removed all user invites in case there's one
user_invite = UserInvite.joined.where(
"lower(email) = '#{user.email.downcase}'"
)&.first
user_invite&.update(status: "CANCELLED")
return if user.user.present?
user.update!(status: "INACTIVE")
end
end
end
So giving a bit more context of what happened up there. My App is an API, on my frontend I remove the user and it actually works, and so what I need to do next is delete the user AND delete a bank_account that's associated with my user.
What I've been trying to do (This fails so hard, and I need some help )
I honestly don't know how to interact between interactions on Rails, that's the reason of my question here.
delete_user.rb interactor file
module UserRequests
class DeleteUser < BaseInteractor
def call
require_context(:id)
remove_user
soft_delete_transaction_account #method to delete bank account
context.message = 'user record deleted'
end
#since there's an association I believe in adding a method to verify if there's a bank
account.
def soft_delete_bank_account
context.account = context.user.bank_accounts.find_by_id(context.id)
fail_with_error!('account', 'does not exist') unless
context.account.present?
context.account.update!(deleted: true,
deleted_at: Time.now)
end
def remove_user
user = context.user
context.user.soft_deletion! #<- This is the method I have on my model, which works!
context.user.soft_delete_transaction_account #<- Then I add that method here so the bank account can be deleted while the user gets deleted!
#below I removed all user invites in case there's one
user_invite = UserInvite.joined.where(
"lower(email) = '#{user.email.downcase}'"
)&.first
user_invite&.update(status: "CANCELLED")
return if user.user.present?
user.update!(status: "INACTIVE")
end
end
end
ERROR LOG of my code:
NoMethodError - undefined method `soft_delete_bank_account' for #<User:0x00007f8284d660b0>:
app/interactors/admin_requests/delete_user.rb:47:in `remove_user'
app/interactors/admin_requests/delete_user.rb:9:in `call'
app/controllers/admin_controller.rb:18:in `destroy'
I would appreciate your help on this!

Rails before_destroy callback db changes always rolled back

I'm trying to prevent deletion of models from the db and pretty much follow this guide (see 9.2.5.3 Exercise Your Paranoia with before_destroy) from a Rails 4 book.
I have a simple model:
class User < ActiveRecord::Base
before_destroy do
update_attribute(:deleted_at, Time.current)
false
end
and in the controller:
def destroy
#user = User.find(params[:id])
# #user.update!(deleted_at: Time.zone.now) # if I do it here it works
#user.destroy # if I also comment this line...
render :show
end
The callback gets called and the attribute gets set, but then the database transaction always gets rolled back. It I leave out the returning of false the model gets deleted because the execution of delete is not halted.
As you can see in the comments I can get it to work but what I really want to do is use a Service Object and put the logic out of the controller.
if your callback returns false the transaction will always be rollbacked.
For what you want you should not call to the destroy method on your arel object.
Instead, make your own method like soft_destroy or something like that and update your attribute.
And to prevent others from calling the destroy method on your arel object, just add a callback raising and exception for instance.
Your model is just an object. If you really want to change the concept of destroy, change it:
def destroy
condition ? alt_action : super
end

How Rails: If a project has tasks it should not be deleted: how can I fix this?

Hi I have a project and each project has tasks. A task belongs to a project. Before I delete a project I want to check if there are related tasks. If there are tasks I don't want to delete the project. If there are no associated tasks, the project should be deleted. Can you please help me with the code? What am I missing?
class Project < ActiveRecord::Base
before_destroy :check_tasks
def check_tasks
if Project.find(params[:id]).tasks
flash[:notice] = 'This project has tasks.'
redirect_to :action => 'list_projects'
end
end
end
Return false from the before_destroy method to prevent the instance from being destroyed.
The method should also return a meaningful error for troubleshooting.
class Project < ActiveRecord::Base
before_destroy :check_tasks
def check_tasks
if self.tasks.any?
errors.add_to_base "Project has tasks and cannot be destroyed."
return false
end
end
end
Note: flash[:notice] and params[:attr_name] can only be used from controllers.
You have a couple of problems here.
You don't (or shouldn't) have access to the params variable (it's available in controllers and views only, unless you're passing it to the model, which is probably not what you want).
Your if checks against project.tasks which is an array - even an empty array evaluates to true, so your other code branch will never occur no matter if the project has tasks or not.
You should probably be setting error messages for the view from your ProjectsController#destroy action, not in your model.
Solutions:
Change Project.find(params[:id]) to self - you want to check the tasks for every instance of the Project.
Change the check in your if statement from if self.tasks to if self.tasks.any? which returns the value you want (false if the array is empty, true otherwise).
Move the flash[:notice] from your model to your controller, as well as the redirect_to call, where they belong. This means your check_tasks method can be changed to the following:
code:
def check_tasks
return !self.tasks.any?
end
Should the check be self instead? (not sure where you getting the params[:id] from).
Haven't checked this out yet though - but since I need something similar for my Users model I'll see how that works out and get back to you.
class Project < ActiveRecord::Base
before_destroy :check_tasks
private
def check_tasks
#edited
if tasks.empty?
false
end
end

Use before_destroy to delete another model's entry?

So I have a model, let's call it Notes. On the notes, you can but several posts. So the notes model has the line:
has_many :posts
And the posts model has the line
belongs_to :note
Now, when a User destroys a post, I want for the note to be destroyed IF it not longer has any other posts.
I thought i would write this code into the post model with before_destroy:
def delete_note_if_last_post
if self.note.posts.count == 1
self.note.destroy
end
end
This doesn't work. It shuts down the server based on an "Illegal Instruction". Is there some way to accomplish what I am trying to do?
EDIT: changed the code, as I noticed an error, and now the problem is slightly different.
you can return false to prevent a model from destruction in before_destroy filter
before_destroy :has_no_post
then in has_no_post
def has_no_post
#You can prevent this from deletion by using these options
#Option1 return false on certain condition
return false if posts.any?
#or add an error to errors
errors << "Can not delete note if it has post" if posts.any?
#raise an exception
raise "Cant delete ..." if blah blah
end
I would suggest putting this kind of logic into an observer. Something like
class PostObserver < ActiveRecord::Observer
def after_destroy(post)
note = Note.find(post.note_id)
note.destroy if note.posts.count == 0
end
end
You'd have to register the observer in your config/application.rb file as well. One thing to note is that if your callback returns any value that can be evaluated as false (e.g. nil or false) the rest of your callbacks will not run.

How to save something to the database after failed ActiveRecord validations?

Basically what I want to do is to log an action on MyModel in the table of MyModelLog. Here's some pseudo code:
class MyModel < ActiveRecord::Base
validate :something
def something
# test
errors.add(:data, "bug!!")
end
end
I also have a model looking like this:
class MyModelLog < ActiveRecord::Base
def self.log_something
self.create(:log => "something happened")
end
end
In order to log I tried to :
Add MyModelLog.log_something in the something method of MyModel
Call MyModelLog.log_something on the after_validation callback of MyModel
In both cases the creation is rolled back when the validation fails because it's in the validation transaction. Of course I also want to log when validations fail. I don't really want to log in a file or somewhere else than the database because I need the relationships of log entries with other models and ability to do requests.
What are my options?
Nested transactions do seem to work in MySQL.
Here is what I tried on a freshly generated rails (with MySQL) project:
./script/generate model Event title:string --skip-timestamps --skip-fixture
./script/generate model EventLog error_message:text --skip-fixture
class Event < ActiveRecord::Base
validates_presence_of :title
after_validation_on_create :log_errors
def log_errors
EventLog.log_error(self) if errors.on(:title).present?
end
end
class EventLog < ActiveRecord::Base
def self.log_error(event)
connection.execute('BEGIN') # If I do transaction do then it doesn't work.
create :error_message => event.errors.on(:title)
connection.execute('COMMIT')
end
end
# And then in script/console:
>> Event.new.save
=> false
>> EventLog.all
=> [#<EventLog id: 1, error_message: "can't be blank", created_at: "2010-10-22 13:17:41", updated_at: "2010-10-22 13:17:41">]
>> Event.all
=> []
Maybe I have over simplified it, or missing some point.
Would this be a good fit for an Observer? I'm not sure, but I'm hoping that exists outside of the transaction... I have a similar need where I might want to delete a record on update...
I've solved a problem like this by taking advantage of Ruby's variable scoping. Basically I declared an error variable outside of a transaction block then catch, store log message, and raise the error again.
It looks something like this:
def something
error = nil
ActiveRecord::Base.transaction do
begin
# place codez here
rescue ActiveRecord::Rollback => e
error = e.message
raise ActiveRecord::Rollback
end
end
MyModelLog.log_something(error) unless error.nil?
end
By declaring the error variable outside of the transaction scope the contents of the variable persist even after the transaction has exited.
I am not sure if it applies to you, but i assume you are trying to save/create a model from your controller. In the controller it is easy to check the outcome of that action, and you most likely already do to provide the user with a useful flash; so you could easily log an appropriate message there.
I am also assuming you do not use any explicit transactions, so if you handle it in the controller, it is outside of the transaction (every save and destroy work in their own transaction).
What do you think?
MyModelLog.log_something should be done using a different connection.
You can make MyModelLog model always use a different connection by using establish_connection.
class MyModelLog < ActiveRecord::Base
establish_connection Rails.env # Use different connection
def self.log_something
self.create(:log => "something happened")
end
end
Not sure if this is the right way to do logging!!
You could use a nested transaction. This way the code in your callback executes in a different transaction than the failing validation. The Rails documentations for ActiveRecord::Transactions::ClassMethods discusses how this is done.

Resources