1. doyaaaaaken

    Posted

    doyaaaaaken
Changes in title
+型安全にIDの型を宣言する実装パターン(Kotlinの場合)
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,140 @@
+# IDをLongやStrnigで宣言した場合の問題点
+
+Kotlinに限らない話ですが、アプリケーション内でIDを扱うとき、Long型、String型のようなGeneralな型を使うと、実装ミスで恐ろしいセキュリティバグにつながる可能性があります。
+例えば以下のようなケースを考えます。
+
+※ 以降のコードはすべてKotlinで書かれていますが、Kotlinに限らない別のプログラム言語に通じる一般的な話だと思っています。
+
+```kotlin
+data class User(val id: Long)
+data class Company(val id: Long)
+
+val userId = 1L
+val companyId = 100L
+//userIdとcompanyIdを間違えて指定!!別ユーザのデータが取得されてしまうセキュリティ事故に!
+val user = User(companyId)
+```
+
+普通にテストしていればバグは防げるだろうと思われる人もいるかもしれませんが、
+分岐の通りにくい条件だったり、
+あるいはたまたま動くケース(上の例だとuserId=100の人がたまたまいた場合。ローカル環境のテストデータでidをauto incrementにしているとよく起きる)でテスト時に見逃してしまったりあると思います。
+
+対処法がライトな順に書いていきます。
+
+# 対応方法
+
+### 1. idを使わない
+
+クソリプ感がややある回答ですw
+ユニークに判別すべき理由がなければidは必要ないため、idを使わないというのも選択肢になります。
+
+なおここまでで話しているIdはサロゲートキーと呼ばれる「データ的には意味がないが、あるエンティティをユニークに判別するための値」を指しているのですが、ナチュラルキーと呼ばれる「データ的な意味としてあるエンティティをユニークに判別できる値または複数値の組み合わせ(例えばUserにおけるemail, Employeeにおける)」を使えばサロゲートキーIdは不要という考え方もできます。
+
+とはいえ、この方法を取ることはほぼないと思います。
+
+### 2. UUIDを使う
+
+UUIDをidとして利用すれば、仮に前述したようなバグがあったとしても重複することは無いので安心という考え方です。
+以下例だとuserId(仮に6f2fd261-8250-45db-9198-037df55c982aとします)の代わりに、companyid(仮に069d4c2e-e1c5-4cae-b689-a9919d87e2e6とします)を使ってUserを定義していますが、そのUUIDのUserがいる可能性はほぼない([これ](https://qiita.com/ta_ta_ta_miya/items/1f8f71db3c1bf2dfb7ea)によると230京分の1)のでバグにはなりにくいという考え方です。
+
+* メリット
+ * idはまず衝突しないという、セキュリティ面での安心感
+* デメリット
+ * DBのデータとしてすでにIdが入っている場合、列をUUID型に変えるのは難しい(既存のIdも許容するためIdの列の型はString型にし、その列にUUIDを入れていくという形になる)
+ * URLにidが入る場合、UUIDを使うとURLが異常に長くなる(ただし、これについてはQitaのこの記事のURLのようにUUIDv4ではないものを使えば短くできる)
+
+```kotlin
+
+import java.util.UUID
+
+data class User(val id: UUID)
+data class Company(val id: UUID)
+
+val userId = UUID.randomUUID()
+val companyId = UUID.randomUUID()
+//userIdとcompanyIdを間違えて指定!!バグではあるが、UUIDが被る可能性は無いに等しいので安心。
+val user = User(companyId)
+```
+
+### 3. 各々のIdを表すWrapperクラスを定義する
+
+以下のようなやつです。どのプログラム言語でも使える汎用的な技です。
+
+```kotlin
+data class UserId(val raw: Long)
+data class CompanyId(val raw: Long)
+
+data class User(val id: UserId)
+data class Company(val id: CompanyId)
+
+val userId = UserId(1L)
+val companyId = CompanyId(100L)
+
+//userIdとcompanyIdを間違えて指定すると、コンパイルエラーで検知できる!!
+val user = User(companyId)
+```
+
+* メリット
+ * 型安全なのでコンパイルエラーで検知できる
+ * 文法的に簡単でKotlinに詳しくない人でも一目瞭然
+ * 今までこの方式で書いていなかったとしても、この方式への移行は安全にできる(コードの変更のみでDBのデータ型は変わらない。)
+* デメリット
+ * Idを振りたいものができるたびにIdを定義しなければいけないので毎回コードを書かなければいけない
+
+### 4. 統一的なId型を作り、それを利用して各Id型を利用する
+
+以下のようにIdentifier型を作って使うことで型安全にします。
+
+```
+data class Identifier<EntityT, RawT>(val raw: RawT)
+
+data class User(val id: Identifier<User, Long>)
+data class Company(val id: Identifier<Company, Long>)
+
+val userId = Identifier<User, Long>(1L)
+val companyId = Identifier<Company, Long>(100L)
+
+//userIdとcompanyIdを間違えて指定すると、コンパイルエラーで検知できる!!
+val user = User(companyId)
+```
+
+上のコードだとコードがわかりづらいので、typealiasを利用してUserId, CompanyId型を定義すると
+グッとわかりやすくなります。
+
+```
+data class Identifier<EntityT, RawT>(val raw: RawT)
+
+typealias UserId = Identifier<User, Long>
+typealias CompanyId = Identifier<Company, Long>
+
+data class User(val id: UserId)
+data class Company(val id: CompanyId)
+
+val userId = UserId(1L)
+val companyId = CompanyId(100L)
+
+//userIdとcompanyIdを間違えて指定すると、コンパイルエラーで検知できる!!
+val user = User(companyId)
+```
+
+個人的にはこの方法が一番いいのではないかと思っています。
+
+* メリット
+ * 型安全なのでコンパイルエラーで検知できる
+ * 短くシンプルで、可読性が非常に高い
+ * 今までこの方式で書いていなかったとしても、この方式への移行は安全にできる(コードの変更のみでDBのデータ型は変わらない。)
+* デメリット
+ * Idを振りたいものができるたびにIdを定義しなければいけないので毎回コードを書かなければいけない
+
+# 終わりに
+
+上で紹介した方式はどれか一つを選択するのではなく、Idをどの程度安全にしたいかによって複数の方式を柔軟に選択するのがいいのではないかと思います。
+例えばUserIdは値を間違えると重大な事故になりうるので型安全にする、といったような判断です。
+
+もしかすると抜け漏れているパターンやもっと良いパターンがあるかもしれません。
+そういったパターンを考えている人の参考になれば幸いです。
+
+# 参考
+
+[Enforcing type safety of IDs in Kotlin](https://www.lordcodes.com/articles/enforcing-type-safety-of-identifiers-in-kotlin) というタイトルのブログ記事の内容を一部参考にしてこの問題について考えてみました。
+