Lets put a bit of context on this question. Given an Ecommerce application in Ruby on Rails. Let's deal with 2 models for example. User and CreditCard.
My User is in the system after a registration no issue there.
CreditCard is a model with the credit card information (yes I know about PCI compliance but that's not the point here)
In the Credit Card model, I include a callback after_validation that will do a validation of the credit card against your bank.
Let me put some simple code here.
models/user.rb
class User < ActiveRecord::Base
enum :status, [:active, :banned]
has_one :credit_card
end
models/credit_card.rb
class CreditCard < ActiveRecord::Base
belongs_to :user
after_validation :validate_at_bank
def validate_at_bank
result = Bank.validate(info) #using active_merchant by exemple
unless result.success
errors.add {credit_card: "Bank doesn't validate"}
user.banned!
end
end
end
controllers/credit_cards_controller.rb
class CreditCardsController < ApplicationController
def create
#credit_card = CreditCard.new(credit_card_params) # from Strong Parameters
if #credit_card.save
render #success
else
render #failure
end
end
end
What causing me issue
It look like Rails opens a transaction in ActiveRecord when I'm doing a new. At this point nothing is send to the database.
When the bank reject the credit card, I want to ban the user. I do this by calling banned! Now I realised this update is going to the same transaction. I can see the update, but once the save doesn't go though, everything is rollback from both models. The credit card is not saved (that is good), the user is not saved (this is not good since I want to ban him)
I try to add a Transaction wrapper, but this only add a database checkpoint. I could create a delayed job for the ban, but this seems to me to be overkill. I could use a after_rollback callback, but I'm not sure this is the right way. I'm a bit surprise, I never caught this scenario before, leading me to believe that my patern is not correct or the point where I make this call is incorrect.
After much review and more digging I came up with 3 ways to handle this situation. Depending on the needs you have, one of them should be good for you.
Separate thread and new database connection
Calling the validate function before the save explictly
Send the task to be perform to a Delayed Job
Separate thread
The following answer shows how to do this operation.
https://stackoverflow.com/a/20743433/552443
This will work, but again not that nice and simple.
Call valid? before the save
This is a very quick solution. The issue is that a new developer could erase the valid? line thinking that the .save will do the job properly.
Calling Delayed Job
This could be any ActionJob provider. You can send the task to banned the user in a separate thread. Depending on your setup this is quite clean but not everybody needs DelayedJob.
If you see anything please add it to a new solution of the comments.
Related
I have several forms where users can send data for approval to admin. Once admin approves only then the approved data is created/updated in the real table.
(eg: a model with bank details for user. Once admin approves only then
the data is used). So how to structure the backend for this?
My initial thoughts:
1st approach
Let's say as per the eg above we have a banking model for saving the bank details.
def BankingModel
belongs_to :user
end
As it needs to be verified by admin initially it won't be saved to the banking model. Instead, I create an exact replica of the table that it is supposed to be saved and save it initially there
def ProposedBankingModel
belongs_to :user
end
Once admin click approves then the data is copied to the bankingmodel table.
Cons of this approach:
Unnecessary maintenance of a table for each table/data
need to create separate modal like proposed_some_table for each model that needs to change
I have a feeling this is a bit overkill
2nd approach
Here only one model will be used. So as per the example when sending a new create/update of a record the requested data will be saved on the service_request model itself.
def service_request
id: db_table_id,
request_model: banking_model// decides which model its for, here its banking model
requested_data:jsonb//contains data as json,
status: pending#shows approval status
end
Here the request type will decide which model to update & on approval by admin the json from the service_request will be used to fill the model of the request_type, here the BankingModel.
I found the second approach better. but I need to manually loop through the json for each field after approving for updating in the real table. But I think I can structure jsonb exactly based on the requestmodel type before saving which so it can look for :key :value for each table fields.
Is the direction am going right? Any suggestions?
Also is there a library available for doing the same in rails so I don't have to reinvent the wheel?
Thank you
The easiest way would to be to just add a ActiveRecord::Enum to the model:
class AddStatusToBankingModel < ActiveRecord::Migration[6.0]
def change
add_column :banking_models, :status, :integer, default: 0, index: true
end
end
Note default: 0.
class BankingModel
enum status: {
0: :pending,
1: :approved,
2: :on_hold # just an example
}
end
You can then approve a record by calling record.approved! or record.update(status: :approved). As to the rest of your question its kind of incomprehensible but I guess you could use nested attributes or the proxy pattern to approve a "banking model" that belongs to a service request.
Also is there a library available for doing the same in rails so I
don't have to reinvent the wheel? Thank you
Again somewhat unclear what you are actually doing. But you might not actually need any additional dependencies. If the logic of actually flipping the switch between pending and approved is complex you might want to look into state machines.
On creating a new user (in my user model) i want to create a stripe customer as well. The two actions must only be completed if they succeed together (like i don't want a customer without a user and vice versa). For this reason I figured it would be a good idea to wrap them in a transaction. However, I must not be doing it correctly. I do not believe I am properly overwriting the create method. If anyone has a suggestion as a better way to do this or what I am doing wrong it would be much appreciated. Thanks!
def create
User.transaction do
super
create_stripe_customer(self)
end
end
def destroy
User.transaction do
super
delete_stripe_customer(self)
end
end
I've done some research into your question and using after_create seems to be ok as long as an exception is raised if it fails. That will rollback the transaction as well. Just use the default callbacks.
Here is a good answer related to the question.
When should I save my models in rails? and who should be responsible for calling save, the model itself, or the caller?
Lets say I have (public)methods like udpate_points, update_level, etc. in my user model. There are 2 options:
The model/method is responsible for calling save . So each method will just call self.save.
The caller is responsible for calling save. So each method only updates the attributes but the caller calls user.save when it's done with the user.
The tradeoffs are fairly obvious:
In option #1 the model is guaranteed to save, but we call save multiple times per transaction.
In option #2 we call save only once per transaction, but the caller has to make sure to call save. For example team.leader.update_points would require me to call team.leader.save which is somewhat non-intuitive. This can get even more complicated if I have multiple methods operating on the same model object.
Adding a more specific info as per request:
update level looks at how many points the users has and updates the level of the user. The function also make a call to the facebook api to notify it that the user has achieved an new level, so I might potently execute it as a background job.
My favorite way of implementing stuff like this is using attr_accessors and model hooks. Here is an example:
class User < ActiveRecord::Base
attr_accessor :pts
after_validation :adjust_points, :on => :update
def adjust_points
if self.pts
self.points = self.pts / 3 #Put whatever code here that needs to be executed
end
end
end
Then in your controller you do something like this:
def update
User.find(params[:id]).update_attributes!(:pts => params[:points])
end
I have a User and a StripeCustomer model. Every User embeds one and accepts_nested_attributes_for StripeCustomer.
When creating a new user, I always create a corresponding StripeCustomer and if you provide either a CC or a coupon code, I create a subscription.
In my StripeCustomer:
attr_accessible :coupon_id, :stripe_card_token
What I'd like to do is, if the coupon is invalid, do:
errors.add :coupon_id, "bad coupon id"
So that normal rails controller patters like:
if #stripe_customer.save
....
else
....
end
will just work. And be able to use normal rails field_with_errors stuff for handling a bad coupon.
So the question is, at which active record callback should I call Stripe::Customer.create and save the stripe_customer_token?
I had it on before_create, because I want it done only if you are really going to persist the record. But this does strange things with valid? and worse, if you are going to create it via a User, the save of User and StripeCustomer actually succeeds even if you do errors.add in the before_create callback! I think the issue is that the save will only fail if you add errors and return false at before_validation.
That last part I'm not sure if it is a mongoid issue or not.
I could move it to before_validation :on => :create but then it would create a new Stripe::Customer even if I just called valid? which I don't want.
Anyway, I'm generically curious about what the best practices are with any model that is backed by or linked to a record on a remote service and how to handle errors.
Ok here is what I did, I split the calls to stripe into 2 callbacks, one at before_validation and one before_create (or before_update).
In the before_validation, I do whatever I can to check the uncontrolled inputs (directly from user) are valid. In the stripe case that just means the coupon code so I check with stripe that it is valid and add errors to :coupon_code as needed.
Actually creating/updating customers with stripe, I wait to do until before_create/before_update (I use two instead of just doing before_save because I handle these two cases differently). If there is an error then, I just don't handle the exception instead of trying to add to errors after validation which (a) doesn't really make any sense and (b) sort of works but fails to prevent saves on nested models (in mongoid anyway, which is very bad and strange).
This way I know by the time I get to persisting, that all the attributes are sound. Something could of course still fail but I've minimized my risk substantially. Now I can also do things like call valid? without worrying about creating records with stripe I didn't want.
In retrospect this seems pretty obvious.
I'm not sure I totally understand the scenario. you wrote:
Every User embeds one and accepts_nested_attributes_for StripeUser
Did you mean StripeCustomer?
So you have a User that has a Customer that holds the coupon info?
If so, I think it should be enough to accept nested attributed for the customer in the user, put the validation in the customer code and that's it.
See here
Let me know if I got your question wrong...
To preserve data integrity, I need to prevent some models from being modified after certain events. For example, a product shouldn't be allowed to be written off after it has been sold.
I've always implemented this in the controller, like so (pseudo-ish code):
def ProductsController < ApplicationController
before_filter require_product_not_sold, :only => [ :write_off ]
private
def require_product_not_sold
if #product.sold?
redirect_to #product, :error => "You can't write off a product that has been sold"
end
end
end
It just struck me that I could also do this in the model. Something like this:
def Product < ActiveRecord::Base
before_update :require_product_not_sold
private
def require_product_not_sold
if self.written_off_changed?
# Add an error, fail validation etc. Prevent the model from saving
end
end
end
Also consider that there may be several different events that require that a product has not been sold to take place.
I like the controller approach - you can set meaningful flash messages rather than adding validation errors. But it feels like this code should be in the model (eg if I wanted to use the model outside of my Rails app).
Am I doing it wrong?
What are the advantages of handling this in my model?
What are the disadvantages of handling this in my model?
If I handle it in the model, should I really be using validates rather than a callback? What's the cleanest way to handle it?
Thanks for your ideas :)
It seems like you already have this one covered, based on your question. Ideally a model should know how to guard its state, as data objects are typically designed with portability in mind (even when they'll never be used that way).
But in this case you want to prevent an action before the user even has access to the model. Using a model validation in this case means you're too late and the user has already gone farther than he should by having access to and attempting to write off a product which should never have been accessible based on its sold status.
So I guess the ideal answer is "both." The model "should" know how to protect itself, as a backup and in case it's ever used externally.
However in the real world we have different priorities and constraints, so I'd suggest implementing the change you listed if feasible, or saving it for the next project if not.
As far as using a model callback versus a validation, I think that's a trickier question but I'll go with a validation because you'd likely want to present a message to the user and validation is built for exactly that use (I'd consider this more of a friendly and expected user error than a hostile or security-related one which you might handle differently).
Is that along the lines of what you've been considering?