コンテンツにスキップ

Kotlinでのリポジトリ実装例

リポジトリの概念を Kotlin で実装する際の注意点とコード例をまとめます。
リポジトリそのものの考え方を先に確認したい場合は、次の記事を参照してください。

Kotlin でリポジトリを実装するときは、次の点を意識します。

  • インターフェースはドメイン側に置く — アプリケーションサービスやドメインサービスは、DB の実装ではなくリポジトリの抽象に依存します。
  • 実装はインフラ層に置く — SQL、ORM、外部 API などの詳細はリポジトリ実装に閉じ込めます。
  • 返すものはドメインオブジェクトにする — DB レコードや DTO ではなく、エンティティや値オブジェクトとして復元して返します。
  • 更新に必要な取得に絞る — 画面表示や一覧検索の都合をリポジトリへ集めすぎないようにします。

ここでは、User エンティティを取得・保存する UserRepository を例にします。

ドメイン層のインターフェース

Section titled “ドメイン層のインターフェース”

リポジトリのインターフェースは、ドメイン層に置くことが多いです。

// domain
interface UserRepository {
fun findById(userId: UserId): User?
fun findByName(userName: UserName): User?
fun save(user: User)
}

アプリケーションサービスやドメインサービスは、この UserRepository インターフェースだけを知っています。
保存先が RDB なのか、外部 API なのか、ファイルなのかは利用側に見せません。

実際の DB アクセスは、インフラ層の実装クラスに書きます。

// infrastructure
class JdbcUserRepository(
private val userDao: UserDao
) : UserRepository {
override fun findById(userId: UserId): User? {
return userDao.selectById(userId.value)
?.toDomain()
}
override fun findByName(userName: UserName): User? {
return userDao.selectByName(userName.value)
?.toDomain()
}
override fun save(user: User) {
userDao.upsert(UserRecord.from(user))
}
}

DB のテーブル構造や SQL の都合は、JdbcUserRepository の内側に閉じ込めます。
これにより、DB の種類や ORM の使い方が変わっても、ドメイン層の考え方を変えずに済みます。

アプリケーションサービスから使う

Section titled “アプリケーションサービスから使う”

ユースケース側は、リポジトリを通じてドメインオブジェクトを取得し、ドメインメソッドを呼び出してから保存します。

class ChangeUserNameUseCase(
private val userRepository: UserRepository
) {
fun execute(userId: UserId, newName: UserName) {
val user = userRepository.findById(userId)
?: throw IllegalArgumentException("user not found")
user.changeName(newName)
userRepository.save(user)
}
}

ユースケースには「ユーザー名を変更する」という流れだけが残ります。
DB レコードから User を復元する処理や、保存時の SQL はリポジトリ実装に寄せます。


リポジトリの実装内で DAO や Mapper を使うことは問題ありません。
ただし、ドメイン層から DAO や DB レコードを直接見せないようにします。

// DAO に寄った名前
fun selectByUserName(userName: String): UserRecord?
// リポジトリとして扱いやすい名前
fun findByName(userName: UserName): User?

DAO は DB 操作、リポジトリはドメインオブジェクトの取得・保存というように役割を分けると、責務が見えやすくなります。


リポジトリは、DB レコードではなくドメインオブジェクトを返します。

// 避けたい例
fun findById(userId: UserId): UserRecord?
// よい例
fun findById(userId: UserId): User?

リポジトリの利用者が UserRecord を受け取ってしまうと、ドメインルールを通さずに値を扱えてしまいます。

何でも検索できるクラスにしない

Section titled “何でも検索できるクラスにしない”

リポジトリに検索条件を増やしすぎると、単なる検索用クラスになってしまいます。

interface UserRepository {
fun findById(userId: UserId): User?
fun findByName(userName: UserName): User?
fun findByEmail(email: Email): User?
fun findByStatus(status: UserStatus): List<User>
fun findByCreatedAtBetween(from: LocalDateTime, to: LocalDateTime): List<User>
}

すべてが悪いわけではありませんが、一覧表示や検索画面のための読み取り処理までリポジトリに集めると、ドメインモデルのための窓口という役割がぼやけます。

更新に必要な取得なのか、画面表示のための検索なのかを分けて考えると整理しやすくなります。

DDD では、リポジトリは多くの場合 集約ルート 単位で作ります。

たとえば OrderOrderItem を内部に持つ場合、外部から OrderItemRepository を直接使うのではなく、OrderRepository から Order 全体を取得し、Order のメソッドを通して明細を変更します。

集約については概念的な話が多いため、別の記事で整理します。
ここではまず、リポジトリは「ドメインオブジェクトを永続化の詳細から切り離す窓口」だと捉えておくと理解しやすいです。


Kotlin でリポジトリを実装するときは、ドメイン層にはインターフェースを置き、インフラ層に具体的な DB アクセスを閉じ込めます。

ポイントは次の 3 つです。

  1. インターフェースをドメイン側に置く — 利用側は永続化の詳細を知らなくてよい
  2. ドメインオブジェクトを返す — DB レコードではなく、エンティティや値オブジェクトとして扱う
  3. 検索責務を増やしすぎない — 更新に必要な取得と画面表示向けの読み取りを分けて考える