ID型はソフトウェア上で一意になるようにした方がいい

Last Updated on 2023年12月15日 by lemonade

概要

DBやその他のストレージを使用するソフトウェアでは、一般に連番、UUID、または任意の文字列をデータのキーとして使用します。強い静的型を持つ言語では、ID値を値オブジェクトでラップし、それぞれのエンティティに対するID型を作成することがよくあります。ID型を用意することで、型による安全性が高まり、永続化層やドメインサービス間でのデータの不整合を防ぐことができます。また、型を使用することで、単にidという引数名でも、どのIDが必要かが明確になります。

IDの型を作成する例

以下は、User型にUser.ID型を使用したSwiftの例です。

import Foundation

/// ユーザー
struct User: Hashable, Sendable {
  /// ユーザー識別子
  var id: ID
  
  struct ID: Hashable, Sendable {
    var value: UUID
    init(_ value: UUID = .init()) {
      self.value = value
    }
  }
}

/// ユーザークエリサービス
final class UserQueryService {
  func find(_ id: User.ID) async throws -> User? {
    try await UserModel.find(id.value, on: db)?.toEntity
  }
}

この方法では、リポジトリのfindメソッドのIDをUser.IDに制限することができます。

特定のエンティティに対する一意のIDの問題点

ソフトウェア内で一意でなく、特定のエンティティに対して一意なIDを使用すると、常にその親エンティティのIDも同時に使用する必要が生じます。

例えば、以下はユーザーに対して一意な画像IDを使用した場合のSwiftの例です。

import Foundation

struct Image: Hashable, Sendable {
  var id: ID
  var data: Data

  struct ID: Hashable, Sendable {
    var value: String
  }
}

この場合、ユーザーAがImage.ID("hello")を持ち、ユーザーBがImage.ID("hello")を持つことが可能ですが、ユーザーAは同じIDの画像を2つ持つことはできません。

しかし、この実装には大きな問題があります。画像のIDを扱う際に常にユーザーIDを付随させる必要があり、扱いが煩雑になります。

例えば、以下のImageQueryServiceクラス、ImageRepositoryクラスでは、画像に関する各操作にユーザーIDが必要です。

final class ImageQueryService {
  func find(_ imageID: Image.ID, of userID: User.ID) async throws -> Image? { ... }

}
final class ImageRepository {
  func create(_ image: Image, of userID: User.ID) async throws { ... }
  func update(_ image: Image, of userID: User.ID) async throws { ... }
  func delete(_ imageID: Image.ID, of userID: User.ID) async throws { ... }
}

この実装では、複数ユーザーの画像を一括で扱うことが困難になります。たとえば、以下のようなメソッドは利用できません。

final class ImageQueryService {
  func deleteAll(_ imageIDs: Set<Image.ID>) async throws
}

解決策

解決策としては、IDに親エンティティのIDも含めることが考えられます。

struct Image: Hashable, Sendable {
  var id: ID
  var data: Data

  struct ID: Hashable, Sendable {
    var userID: User.ID
    var value: String
  }
}

この方法により、IDがソフトウェア全体で一意になるため、集合として扱うことが容易になり、User.IDを常に渡す必要がなくなります。

Leave a Comment

CAPTCHA