はじめに
とりあえずIDは、第3章で取り上げられているアンチパターンですが、Railsをはじめとした複数のフレームワークで利用されています。サロゲートキー(代理キー、代替キー)とも呼ばれます。後続の文章ではサロゲートキーという名称を使います。
SQLアンチパターンの比較的前半に記載があるので、システム開発経験の浅い人が本書を読み、とりあえずIDはアンチパターンなのだと認識してしまうケースがあり、定期的に話題に上がってきます。SQLアンチパターンの発行は2013年01月と十年以上も前です(原著は2010年6月)。当時と今では状況も変わってきており、フレームワークも増え、知見が溜まってきています。
この話題に関しては、他の記事でもメリット、デメリットの比較だけだったり、明確に「とりあえずID」に対して賛成・反対の立場をとっている記事もあったりしますが、本記事では、「とりあえずID」に対して賛成の立場(デメリットを上回るメリットが大きい)で書こうと思います。
サロゲートキーとは簡単に説明すると、システムが自動的に付与するキーを主キーとするRDBの設計手法です。シンプルにauto incrementを用いて自動で1つずつ増やしていく方法や、あるいはUUIDを用いた方法などがあります。
語り尽くされた話題ではあると思いますが、自分の整理のために記載しようと思います。
ナチュラルキーと比較した際のサロゲートキーのメリット
サロゲートキーを用いる利点について、いくつか挙げてみたいと思います。
変化に強い
RDBを論理的に設計した時に人為的に作成したキーを主キーにします。例えば、社員番号、部署コードなどです。これらは、一見すると設計時には不変のものでキーできると思うかもしれません。しかし、人間が考えたコードなので、同じ社員だったとしても、途中から変えたいというケースは発生します。例えば会社統合などで社員番号の生成ロジックが変わった、部署の統廃合があったなど、理由は様々です。私はRDB設計において人間が考えた値に関しては、今後変わりうる可能性があると思って設計すべきだと考えています。一方で、サロゲートキーを用いると、それはシステムが自動生成したもので、その値を人間が変更するようなものではありません。
例えば、サロゲートキーを用いずに複合主キーを用いた場合だと、そのテーブルにリレーションを貼りたい場合、リレーション先のテーブルにも複合主キーを設定しないといけなく、カラム数が増えます。前述の様にそれらのキーを変更したいとなった場合は、複数のテーブルにまたがって変更する必要があります。しかも同時に変更しないと整合性が取れなくなるので、上手にトランザクションを用いたり、システムメンテナンスを伴って実施する必要がでてきます。サロゲートキーを用いれば必要な変更は1つのカラムでそのカラムにはリレーションは貼られてないので変化の影響を小さくできます。
実装の簡便さ
例えば次の様なカテゴリーテーブル(categories)と商品テーブル(products)をもつ2つのテーブルを考えてみます。このシステムでは複数の企業(company)向けに提供しており、カテゴリーは企業ごとに持てるものとします。この時、カテゴリーテーブルの主キーは複合主キーで company_code
とcategory_code
になります。
company_code | category_code | category_name |
---|---|---|
COMPANY1 | CATEGORY1 | 生鮮食品 |
COMPANY2 | CATEGORY2 | 野菜・生鮮食品 |
COMPANY3 | CATEGORY1 | 惣菜 |
カテゴリーテーブルに対し、そのカテゴリーに対して複数の商品が所属できるとします。その場合の商品テーブルを以下の様に設定するとします。商品(product)も企業ごとに持つことができるので、このテーブルにおける主キーは同様に複合主キーで product_code
とcompany_code
になります。
product_code | company_code | category_code | product_name |
---|---|---|---|
PRODUCT1 | COMPANY1 | CATEGORY1 | 牛バラ肉 |
PRODUCT2 | COMPANY2 | CATEGORY2 | レタス |
PRODUCT3 | COMPANY3 | CATEGORY1 | から揚げ |
ここで、例えば、COMPANY1とCOMPANY2のCATEGORY1とCATEGORY2の商品を取得したいとします。その場合のSQLは以下の様になります。CATEGORY1というcategory_codeがCOMPANY3でも利用されているので、company_codeとcategory_codeの両方で絞り込む必要があります。取得したいカテゴリーが増えるにつれ、ORでどんどん接続されることになります。
SELECT
*
FROM
products
WHERE
company_code = 'COMPANY1' AND category_code = 'CATEGORY1'
OR
company_code = 'COMPANY2' AND category_code = 'CATEGORY2'
サロゲートキーを使った場合だと、カテゴリーテーブル(categories)と商品テーブル(products)はそれぞれ以下の様になります。
id | company_id | category_code | category_name |
---|---|---|---|
1 | 1 | CATEGORY1 | 生鮮食品 |
2 | 2 | CATEGORY2 | 野菜・生鮮食品 |
3 | 3 | CATEGORY1 | 惣菜 |
id | category_id | product_code | roduct_name |
---|---|---|---|
1 | 1 | PRODUCT1 | 牛バラ肉 |
2 | 2 | PRODUCT2 | レタス |
3 | 3 | PRODUCT3 | から揚げ |
SQLは以下の様にシンプルになります。例としてはもう少し良い例があると思いますが、IN
が使いにくくなるというのがお伝えしたかったことになります。
SELECT
*
FROM
products
WHERE
category_id in (1, 2)
そのほかにも実装時のメリットがあると思いますが、おって記載できればと思います。
サロゲートキーを用いることに対して指摘されている点について
ここでは、サロゲートキーを用いることに対して、SQLアンチパターンの中や、他記事にて言及されていることに関して
重複行を許可してしまうことに関して
SQLアンチパターンには重複業を許可してしまうことがデメリットとして挙げていますが、これはシンプルに複合ユニーク制約をかければ済む話です。ことの時、そのテーブル単体でみればサロゲートキーは意味のないものになりますが、前述のように他テーブルとのリレーションを持つ時には非常に役に立つものになります。
中間テーブルにサロゲートキーは不要なのではないか
例えば、usersテーブルとproductsテーブルがあり、ユーザーのお気に入りの商品を管理するuser_favorite_productsがあるとします。この時、user_favorite_productsに必要なカラムは、user_id
とproduct_id
になります。
このようなテーブルに対してまでサロゲートキーを設定する必要はないのではないかという意見です。多くの場合、設計時点では不要なのだと思います。しかし、user_favarote_productsテーブルに新たなカラムを追加したいとなり、単純にユーザーのお気に入りの商品だけを管理するテーブルで無くなってきた場合は、サロゲートキーが有効になってきます。もちろん、必要になったタイミングで列を追加するのでも良いと思いますが、実装上もディスクサイズ的にもそこまで大きな影響はないと思うので最初からあっても全く問題ないと思います。開発者にシステム全体として必ずidカラム(サロゲートキー)があるという頭があれば、逆に存在しないことが開発生産性を下げる原因にもなります。
idカラムはどのテーブルの主キーかわからないため、意味を持たせた方がいいのではないか
たとえばproductsテーブルのサロゲートキーをid
とすると、カラム名を見ただけではどのテーブルの主キーかわからないという指摘です。この指摘はもっともで、たとえばRailsのActiveRecordでは、標準ではid
カラムが使われます。ただし、より分かりやすくするためにカラム名をproduct_id
にすることも可能です。USING
が使いやすいなどのメリットもあるため、この指摘には賛成です。
シーケンシャルなサロゲートキーを使う際に注意すべきこと
サロゲートキーを使う際に、シーケンシャルなキーを使う方法方と、uuidなどのシーケンシャルでないキーを使う方法があります。シーケンシャルなキーを使う場合に注意しなければいけないことがあります。それは、場合によっては表に連番IDが出ないようにする必要があるということです。たとえば申し込み完了画面に番号があれば申し込み数が把握できてしまいますし、POSTリクエストでシーケンシャルなキーを使った時に意図しない番号であった場合に、たとえアプリケーション的には処理を行わなかったとしても、レスポンスのHTTPのステータスが400番台と500番台で違っていることで攻撃者にヒントを与えてしまうということもありえます。この場合でも、例えば管理画面で社内の人しか見ないような場合であれば大きな問題にならないでしょう。システム要件に応じて許容できる範囲は変わってきます。
ここで「表に出ないようにする」ということはURLや画面上だけではなく、例えばFORMのhiddenに入っているとか、ブラウザの開発者ツールのネットワークで見た時に把握できるといったすぐには見れない箇所も含めてになります。本格的には脆弱性試験などを行なってもらって把握する必要があると思いますが、普段から気をつけておく必要があります。
最後に
この記事では、設計時点では問題ないが今後変更の可能性があり、その時のために変化に強い設計をしておく必要があり、サロゲートキーを用いた方がいいという旨の文を記載をいくつかしてきました。ソフトウェアには、YAGNI原則(You Ain't Gonna Need It 原則)というものがあり、「必要なもの以外を実装するな」というものです。本記事での主張はYAGNI原則に反しているかもしれません。しかし、リレーショナルデータベース設計においては、リリース後の変更がソースコードと比較して難しいことが多いため、設計段階である程度考慮しておいた方が良いケースが多いです。
私の経歴としては、新卒で入ったSIer時代にIPAのデータベーススペシャリストの資格をとり、Web系に転職しRailsでの開発をしてきました。前述の通りRailsは連番をidとして用います。データベーススペシャリストの試験では当時はサロゲートキーを使ったモデリングはあまりありませんでしたので、最初見たときは戸惑いを覚えました。その後C#のプロジェクトやもともとRails以外で作られていてRailsに移行したプロジェクトでサロゲートキーが使われていないものを扱う経験をして、実装者として感じたことは、サロゲートキーがあった方が圧倒的に実装がしやすい、つまり、高い品質を保ちながら多くの機能を実装できると感じました。
サロゲートキー論争の原因がどこにあるのかは分かりませんが、データモデリングの理論的な世界と実装という実践の世界には一定の乖離があります。私たちが行うべきはいかにスピーディーに高品質のものをユーザーに届けるかです。そこには理論的な理想だけでは決して解決しないものもあります。それを踏まえると「とりあえずID」はベストプラクティスなのではないかと思うのです。