最近 Rails を使い始めたのですが、気になったことがあったのでメモっときます。
モデルに一意性バリデーションをつけても厳密にはチェックされません。もしかしたら Rails では常識なのかもしれませんが、Rails 初心者なので気になってしまいました。
普通に Rails アプリを作ります。User モデルに login 属性をつけてます。
% rails new hoge -d mysql % cd hoge % vi config/database.yml % rails g scaffold user login % rake db:create db:migrate
User モデルに一意性バリデーションつけます。
% vi app/models/user.rb
class User < ActiveRecord::Base attr_accessible :login validates_uniqueness_of :login end
ユーザーを作ってみます。
% rails c Loading development environment (Rails 3.2.3) irb(main):001:0> User.create(login:'hoge') (0.1ms) BEGIN User Exists (0.7ms) SELECT 1 FROM `users` WHERE `users`.`login` = BINARY 'hoge' LIMIT 1 SQL (0.3ms) INSERT INTO `users` (`created_at`, `login`, `updated_at`) VALUES ('2012-06-02 08:01:32', 'hoge', '2012-06-02 08:01:32') (40.5ms) COMMIT => #<User id: 1, login: "hoge", created_at: "2012-06-02 08:01:32", updated_at: "2012-06-02 08:01:32">
もう一度同じユーザーを作ってみます。
irb(main):002:0> User.create(login:'hoge') (0.3ms) BEGIN User Exists (0.6ms) SELECT 1 FROM `users` WHERE `users`.`login` = BINARY 'hoge' LIMIT 1 (0.3ms) ROLLBACK => #<User id: nil, login: "hoge", created_at: nil, updated_at: nil>
今度はロールバックされました。
発行されるクエリを見ると、INSERT の前に SELECT で値が既存かどうかチェックしてるのがわかります。
しかし、これだと SELECT と INSERT の間に他のクライアントから同じ値が登録されてしまうと重複登録されてしまいます。
なので本当に一意性を保証したい場合は、データベース側でも一意性制約をつける必要があります。
% vi db/migrate/20120602095609_add_index_to_users.rb
class AddIndexToUsers < ActiveRecord::Migration def change add_index :users, :login, :unique=>true end end
% rake db:migrate
これでデータベースに重複登録されることはなくなったはずです。
試しにレコードの登録を削除を繰り返してみます。
irb(main):004:0> loop{User.create(login:'hoge'); User.delete_all(login:'hoge')}
これを複数の端末から同時に動かしてしばらくすると、いずれかの端末で次のエラーが発生します。
(0.1ms) BEGIN User Exists (0.3ms) SELECT 1 FROM `users` WHERE `users`.`login` = BINARY 'hoge' LIMIT 1 SQL (78.2ms) INSERT INTO `users` (`created_at`, `login`, `updated_at`) VALUES ('2012-06-02 08:13:41', 'hoge', '2012-06-02 08:13:41') Mysql2::Error: Duplicate entry 'hoge' for key 'index_users_on_login': INSERT INTO `users` (`created_at`, `login`, `updated_at`) VALUES ('2012-06-02 08:13:41', 'hoge', '2012-06-02 08:13:41') (198.5ms) ROLLBACK
SELECT でレコードが存在しなかったので INSERT しようとしたら、その間に他のクライアントから登録されてしまって重複エラーになりました。
なお、このエラーはバリデーションのチェックにはならず、そのままアプリがエラーになってしまいます。エラーにせず、一意性のバリデーションのチェックに引っかかったのと同じようにする方法はどうやらないみたいです。ほとんど発生しないから考えないって感じでしょうか。
それにしても、テーブル構造をデータベースから取得して自動的にモデルを作ってくれたりする割には、データベースのユニーク制約をちゃんと扱ってくれないのはイマイチな気がします。