Hi all, in theese days I am working on a new little feature on a Rails project. I have to generate a random and unique token for every new record of a model.
Unfortunately I have encountered a strange error ActiveRecord::StatetementInvalid. Let’s see some code and understand why this happens.
I have created a simple project (Git Repo) with a single model, User, that has email and token as columns. I want to generate automatically a unique random token for each user, so in my db migration I have added an index on the token column:
... add_index :users, :token, unique: true ...
And in my model I have added an after_create callback:
class User < ActiveRecord::Base after_create :generate_token private def generate_token update_column :token, SecureRandom.hex(8) end end
This works great and generates a random token, but at the moment there is no control on its uniqueness; Rails simply raises an ActiveRecord::RecordNotUnique error if the token is not unique.
Well, I can rescue this error and retry to generate a new token.
... def generate_token update_column :token, SecureRandom.hex(8) rescue ActiveRecord::RecordNotUnique retry end ...
Now I have a model that retries generating a new token until it is unique, Great!
Oh yeah, works great; until you switch from the little cute SQLite to the super powered PostgreSQL!
With PostgreSQL this code ends in another error ActiveRecord::StatetementInvalid raised after a PostgreSQL error PG:InFailedSqlTransaction.
With PostgreSQL I can’t run a new SQL query if a previous query, in the same transaction, fails.
So if I want to preserve the behavior of my model I have to use a different callback.
The problem is that I am making an invalid update_column during a transaction, to solve this I can use the after_commit callback. When the record is saved I regenerate the token until it is unique, and this happens out of the previous transaction.
class User < ActiveRecord::Base after_commit :generate_token, on: :create private def generate_token update_column :token, SecureRandom.hex(8) rescue ActiveRecord::RecordNotUnique retry end end
The only one cons of this solution are the tests, to make it pass I need to use the database_cleaner gem with :truncation as strategy. Otherwise in tests Rails does not fire the after_commit trigger.
RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :truncation DatabaseCleaner.clean_with(:truncation) end config.around(:each) do |example| DatabaseCleaner.cleaning do example.run end end end
There is another couple of different solutions to archive this result, maybe a trigger at the database level or something else, but for now this one works.
To avoid an almost impossible, but possible, infinite loop I’ve added a retry limit, that you can find in the Repo.
You can find also an another branch sqlite with the SQLite version of the code.
I hope this article has been helpful, bye!
Leave a Reply