Вчера в русскоязычной рассылке по RoR обсуждалась следующая проблема. MySQL поддерживает уникальные индексы, а модели – ограничение validates_uniqueness_of. Нужно ли нам обрабатывать исключения MySQL, или валидации RoR достаточно?
Я считаю, что обрабатывать нужно, но для убедительности решил провести тест. Для начала я создал таблицу с уникальным индексом:
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 |
Затем я описал следующую модель:
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 |
Вы видите два правила валидации: первое это проверка уникальности, наша основная цель. Задачей второго правила является создание второго объекта модели между валидацией и вставкой в базу первого объекта. Правила валидации отрабатывают в обратном порядке, потому validates_presence_of идет первым. Осталось запустить script/console и набрать:
1 | Customer.create(:name => 'name') |
Вы можете найти лог ниже. Думаю, комментарии излишни.
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 |
Примечание: Вы правы, это неправильная логика, но я использовал ее только для того, чтобы показать, что может произойти, если два процесса одновременно добавят запись с одинаковым полем name. Вы можете заменить код внутри :if на sleep 10 и запустить две консоли, чтобы воспроизвести исключение.
