🍣

Userは論理削除にしておくべきかどうか

に公開

『システム要件をありのままに定義したい』エンジニアです。

Xで以下のような、Userは論理削除にしておくべきか削除済みユーザーのテーブルを用意するのかなど議論されていたので、私見をまとめてみます。

まずそもそも、正しい設計なんてシステム要件に依るだろうという正論はありつつ、たぶん開発初期で色々と要件が決まってない場合を想定しているんだろうと思います。

なので仮にこんな要件としてみましょう。

  • Userは退会することがある。退会ユーザーはログイン出来ないし一覧にも出てこない。
  • 退会したユーザーの個人情報は消しておきたいが、後で何か分析などするかもしれないので、とりあえず生年月日(日なし)だけ保持しておきたい。
  • OrderはUserに紐づいていて、Userが退会したときにOrderが消えると何か困るかもしれないので、とりあえず残しておきたい。
  • Userが誤って退会してしまい復帰したいこともあるかも?そうなったときに考えるけどOrderの紐づけは残ってると嬉しい。

一旦こんな感じの要件としてシステムを設計してみましょう。
ちなみに、僕はソフトウェアを設計するときにデータベースのテーブルから設計を始めるのはアンチパターンだと思っているので、ビジネスロジックの定義から始めます。

データ定義としてはこんな感じでしょうか。

sealed interface User {
    val id: UserId
    val birthYear: Int
    val birthMonth: Int
    val status: UserStatus
}

data class ActiveUser(
    override val id: UserId,
    val birthday: Any,
    val name: String,
    val email: String,
    val shippingAddress: String,
): User {
    override val birthYear: Int = getYear(birthday)
    override val birthMonth: Int = getMonth(birthday)
    override val status: UserStatus = UserStatus.ACTIVE
    fun archive(): ArchivedUser {
        return ArchivedUser(id, birthYear, birthMonth)
    }
}

data class ArchivedUser(
    override val id: UserId,
    override val birthYear: Int,
    override val birthMonth: Int,
): User {
    override val status: UserStatus = UserStatus.ARCHIVED
}

data class Order(
    val id: OrderId,
    val buyerId: UserId,
    val totalPrice: BigDecimal,
    val shippingAddress: String,
    val purchasedAt: OffsetDateTime,
)

このデータ定義なら、今現在の仕様を満たしつつ、今後来るかもしれない分析、例えば『20-30歳のUserのうち退会した人の割合』とか『配送先が東京都のOrderの平均金額』なども行えるでしょう。

あとはこれを満たすデータアクセスレイヤーを作れば良くて、例えばまだ開発初期であれば全部まぜて良くてこんな感じ。

interface UserRepository {
    // Activeな人のみ列挙する
    fun findUsers(): List<ActiveUser>
    
    // ログイン用
    fun findByEmail(email: String): ActiveUser?

    // Activeな人のみ作成・更新出来る
    fun save(user: ActiveUser)
    
    // 退会が済んだユーザーの保存用
    fun update(user: ArchivedUser)
}

テーブルも初期はUserテーブル一つだけで、退会時にはemailなどnullにするなどして論理削除しておいて良いと思います。
ビジネスロジック側でActiveUserはemailがあることが保証されているので問題ないです。

もし今後サービスが成長して、パフォーマンスの問題が出てきたり、分析チームがSQLをどう書けばいいか分からないとかなった時にはじめて、テーブル分割とかを考えればいいと思います。
物理削除も、「過去のOrderは消しても良い」とか「Orderは、金額は残しておきたいけど誰が買ったかは消すポリシーにした」とかなってから消せば良いと思います。

いずれにしろ、ビジネスロジックが固く設計されていればテーブル設計は後で考えれば良いかと。

Discussion