Yesterday in mailing list of russian RoR group we have discussed following problem. MySQL database has unique index and model has validates_uniqueness_of constraint. Do we need to handle MySQL server exceptions or RoR’s validation will be enough?
I think we need, but to be sure I decided to make test. First, I created table with unique index:
1 2 3 4 5 6 7 8 9 10 11 12 | class CreateCustomers < ActiveRecord::Migration def self.up create_table :customers do |t| t.column :name, :string end add_index :customers, :name, :unique => true end def self.down drop_table :customers end end |
Then I wrote following model:
1 2 3 4 5 6 7 8 9 10 11 12 | class Customer < ActiveRecord::Base validates_presence_of :name, :if => lambda { |customer| unless @@created @@created = true Customer.create(:name => customer.name) end true } validates_uniqueness_of :name @@created = false end |
You can see two validation rules: one is uniqueness, which is the our primary goal. Second rule’s task is to create second customer between validation and insertion of first. Validations are processed in postorder, therefor validates_presence_of is going first. Now I’ve started script/console and typed:
1 | Customer.create(:name => 'name') |
You can find log file below. I think, you don’t need any comments.
1 2 3 4 5 6 7 | SHOW FIELDS FROM customers BEGIN SELECT * FROM customers WHERE (customers.name = 'name') LIMIT 1 SELECT * FROM customers WHERE (customers.name = 'name') LIMIT 1 INSERT INTO customers (`name`) VALUES('name') Mysql::Error: #23000Duplicate entry 'name' for key 2: INSERT INTO customers (`name`) VALUES('name') ROLLBACK |
Note: You right, this is broken logic, but I used it only to show what will be if two processes will insert record with the same name. You can replace code inside :if with sleep 10 and run two consoles to reproduce exception.