Railsアプリケーションで下記のusersテーブルのemailにユニーク制約をつけたい場合、どのように実装しますか?
- users
Field | Type | Null |
---|---|---|
id | bigint(20) | NO |
varchar(255) | NO |
だいたいの場合、下記2つのいずれか、または両方を実装するのではないでしょうか?
- RDBのemailカラムにユニークインデックスを付ける
- Userモデルにuniqueness validationを実装する
どちらの実装でもデータがユニークであることとをチェックしてくれるという意味では同じように感じますが、制約の完全性が全然違います。
一言で言うと、RDBのユニーク制約は100%保証されるが、Railsのuniquenessは突破される可能性があります。
この記事で使うユニーク制約
記事の最初に記載したusersテーブルを使います。emailをユニークにしたいとします。
DBでユニーク制約をかける場合はemalカラムにユニーク制約をつけます。
下記のコマンドでつけることができます。
ALTER UNIQUE INDEX idx_email ON users (email);
Railsでユニーク制約をかける場合はモデルクラスにvalidationを定義します。
class User < ApplicationRecord
validates :email, uniqueness: true
end
RDBのユニーク制約とRailsのユニークバリデーションの違い
Rails以外からの追加・更新には効かない
Railsだけで制約をかけた状態でmail: hoge@example.com
を追加します。
Railsで追加する場合は下記の通り。
# 追加OK
User.create!(email: 'hoge@example.com')
# uniq validationでエラー
User.create!(email: 'hoge@example.com')
validationを定義しているので重複して登録はできません。
ただ、DBは直接insertすることもできます。
-- 'hoge@example.com'が存在していても成功する
INSERT INTO users (email) values ('hoge@example.com');
DBから直接insertする以外にも、別アプリケーションが同じDBを使っている場合、そちらのアプリケーションでユニークチェックをしていなければ重複して追加することができます。
一方、DBにユニーク制約がついていたらどうでしょうか?
SQLで直接insertすることもできませんし、別アプリーケンションから追加することもできません!鉄壁です!!
ただ、そもそも1つのDBを複数のアプリケーションから参照しているのがよくない!
SQLで直接更新も運用で回避できる!!
という意見もあると思います。
おっしゃるとおりだと思います。運用でカバーできる範囲ですね!
DBの更新元がRailsだけでもユニーク制約が効かないパターン
たとえRailsからしか更新されないとしてもユニーク制約を突破することがあります。
Webアプリケーションでユーザー追加処理が同時にリクエストされた場合を考えます。
その場合、User.create!(email: 'hoge@example.com')
がほぼ同時に実行され、下記のように両方の追加処理が成功します。
# リクエスト1
User.create!(email: 'hoge@example.com')
# selectしても存在しないので追加する
# リクエスト2(リクエスト1と同時)
User.create!(email: 'hoge@example.com')
# create1のtransactionがまだ確定されておらず、selectしても存在しないので追加する
では複数ユーザーが同時にアクセスする可能性があるものだけ気をつければ良いのか??
いいえ、違います。
例えば、クリック連打などで同じユーザーが同じリクエストを複数送ってくることもありえますし、通信不調などでクライアントから2,3度同じリクエストが送られることもあります。
ほぼ起きないからRailsのvalidationだけでもいいんじゃね?
前述のようにRailsのvalidationが突破される発生確率は低いです。
ただ、プロダクトの規模が増えトランザクションが増えると発生確率は増えます。
そして、発生した時のめんどくささは半端ないです。
Railsでしかチェックできない制約もあると思うのでそれはRailsでやるしかないですが、ユニーク制約などDBで制約できるものはDBで制約するのが良いでしょう。
おまけ: 両方に制約つけとけば安心やな!
確かに両方につけておけば突破されることはありません。
ただ、制約を守るだけであればDBの制約だけで100%守れるのでRailsにvalidationをつける意味はないと思います。
実装&運用コストが増えるだけです。
実際にユニーク制約がついているカラムにvalidationを実装しているシーンをよく見かけますが、Railsで検知して制御したい場合を除いてチェックする必要はないと思います。
たまにソースだけで制約が見れるからRailsにもvalidationを書くという意見を聞きますが、コードリーディングのためだけに処理を実装するのは良くないと思いますし、そもそもバックエンドを開発するならDBスキーマも把握する必要があると思います。