Room DAO を使用してデータにアクセスする

Room 永続ライブラリを使用してアプリのデータを保存する場合、データアクセス オブジェクト(DAO)を定義して、保存対象のデータを操作します。各 DAO は、アプリのデータベースへの抽象アクセスを可能にする関数を備えています。コンパイル時に、Room は定義した DAO の実装を自動的に生成します。

クエリビルダーやダイレクト クエリではなく DAO を使用してアプリのデータベースにアクセスすることで、重要なアーキテクチャ原則である関心の分離を維持できます。DAO を使用することで、アプリをテストするときにデータベース アクセスをモックすることもできます。

DAO の仕組み

各 DAO は、インターフェースまたは抽象クラスとして定義できます。基本的なユースケースでは、通常はインターフェースを使用します。いずれの場合も、DAO には常に @Dao アノテーションを付ける必要があります。DAO にプロパティはありませんが、アプリのデータベース内のデータを操作する 1 つ以上の関数を定義します。

次のコードは、Room データベースで User オブジェクトを挿入、削除、選択する関数を定義する DAO の例です。

@Dao
interface UserDao {
    @Insert
    suspend fun insertAll(vararg users: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM user")
    suspend fun getAll(): List<User>
}

データベース操作を定義する DAO 関数には、次の 2 種類があります。

  • SQL コードを記述せずにデータベースの行を挿入、更新、削除できるコンビニエンス関数。
  • データベースを操作するための独自の SQL クエリを記述できるクエリ関数。

以降のセクションでは、アプリに必要なデータベース操作を定義するために両方のタイプの DAO 関数を使用する方法を示します。

コンビニエンス関数

Room には、SQL ステートメントを記述せずに挿入、更新、削除を行う関数を定義するための便利なアノテーションが用意されています。

より複雑な挿入、更新、削除を定義する必要がある場合や、データベース内のデータにクエリを行う必要がある場合は、代わりにクエリ関数を使用します。

挿入

@Insert アノテーションを使用すると、データベース内の適切なテーブルにパラメータを挿入する関数を定義できます。次のコードは、1 つ以上の User オブジェクトをデータベースに挿入する有効な @Insert 関数の例を示しています。

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)

    @Insert
    suspend fun insertBothUsers(user1: User, user2: User)

    @Insert
    suspend fun insertUsersAndFriends(user: User, friends: List<User>)
}

@Insert 関数の各パラメータは、@Entity アノテーションが付けられた Room データ エンティティ クラスのインスタンスか、データ エンティティ クラスのインスタンスのコレクションである必要があります。@Insert 関数が呼び出されると、Room は、渡された各エンティティ インスタンスを、対応するデータベース テーブルに挿入します。

@Insert 関数が単一のパラメータを受け取る場合、Long 値(挿入されたアイテムの新しい rowId)を返すことができます。パラメータが配列またはコレクションの場合、代わりに Long 値の配列またはコレクションを返す必要があり、各値は挿入されたアイテムのいずれかの rowId となります。rowId 値の返し方の詳細については、@Insert アノテーションのリファレンス ドキュメントと、rowid テーブルの SQLite ドキュメントをご覧ください。

更新

@Update アノテーションを使用すると、データベース テーブルの特定の行を更新する関数を定義できます。@Insert 関数と同様に、@Update 関数はデータ エンティティ インスタンスをパラメータとして受け取ります。次のコードは、データベース内の 1 つ以上の User オブジェクトを更新しようとする @Update 関数の例を示しています。

@Dao
interface UserDao {
    @Update
    suspend fun updateUsers(vararg users: User)
}

Room は主キーを使用して、引数のエンティティ インスタンスをデータベースの行と照合します。同じ主キーを持つ行がない場合、Room は変更を行いません。

@Update 関数は、必要に応じて、正常に更新された行数を示す Int 値を返すことができます。

削除

@Delete アノテーションを使用すると、データベース テーブルから特定の行を削除する関数を定義できます。@Insert 関数と同様に、@Delete 関数はデータ エンティティ インスタンスをパラメータとして受け取ります。次のコードは、データベースから 1 つ以上の User オブジェクトを削除しようとする @Delete 関数の例を示しています。

@Dao
interface UserDao {
    @Delete
    suspend fun deleteUsers(vararg users: User)
}

Room は主キーを使用して、引数のエンティティ インスタンスをデータベースの行と照合します。同じ主キーを持つ行がない場合、Room は変更を行いません。

@Delete 関数は、必要に応じて、正常に削除された行数を示す Int 値を返すことができます。

Upsert

@Upsert アノテーションを使用すると、一致する行がない場合にエンティティ インスタンスを挿入する関数を定義できます。また、同じ主キーを持つ行がすでに存在する場合は、その行を更新できます。

@Insert 関数や @Update 関数と同様に、@Upsert 関数はデータ エンティティ インスタンスをパラメータとして受け取ります。次のコードは、データベース内の 1 つ以上の User オブジェクトを upsert しようとする @Upsert 関数の例を示しています。

@Dao
interface UserDao {
    @Upsert
    suspend fun upsertUsers(vararg users: User)
}

@Upsert 関数が単一のパラメータを受け取る場合、Long 値を返すことができます。新しい行が挿入された場合は、新しく挿入された行の rowId を返します。既存の行が更新された場合は、-1 を返します。パラメータが配列やコレクションの場合は、代わりに Long 値の配列やコレクションが返されます。

クエリ関数

@Query アノテーションを使用すると、SQL ステートメントを記述して DAO 関数として公開できます。これらのクエリ関数は、アプリのデータベースからデータをクエリする場合や、より複雑な挿入、更新、削除を行う必要がある場合に使用できます。

Room はコンパイル時に SQL クエリを検証します。つまり、クエリに問題がある場合、ランタイム エラーではなくコンパイル エラーが発生します。

単純なクエリ

次のコードは、SELECT クエリを使用してデータベース内のすべての User オブジェクトを返す関数を定義しています。

@Query("SELECT * FROM user")
suspend fun loadAllUsers(): List<User>

以降のセクションでは、一般的なユースケースに合わせてこの例を変更する方法を示します。

テーブルの列のサブセットを返す

ほとんどの場合、クエリを行うテーブルの列のサブセットを返すだけで済みます。たとえば、あるユーザーに関する詳細情報をすべて UI に表示するのではなく、氏名のみを表示する場合があります。リソースを節約してクエリの実行を効率化するには、必要なプロパティのみをクエリします。

Room では、結果列のセットを返されるオブジェクトにマッピングできる限り、どのようなクエリからもデータ オブジェクトを返すことができます。たとえば、ユーザーの氏名を保持する次のオブジェクトを定義できます。

data class NameTuple(
    @ColumnInfo(name = "first_name") val firstName: String,
    @ColumnInfo(name = "last_name") val lastName: String
)

その後、クエリ関数からデータ オブジェクトを返すことができます。

@Query("SELECT first_name, last_name FROM user")
suspend fun loadFullName(): List<NameTuple>

クエリは first_name 列と last_name 列の値を返すため、Room はこれらの値を NameTuple クラスのプロパティにマッピングします。返されたオブジェクトのプロパティにマッピングされない列をクエリが返した場合、Room は警告を表示します。

前の例では、カスタム データクラスを使用して列のサブセットを取得していますが、クエリが 2 つまたは 3 つの列を返す場合、Room は便宜上 kotlin.Pairkotlin.Triple を返すこともサポートしています。これらの型を使用する場合、列はクエリ ステートメントで定義された順序でマッピングされるため、SELECT ステートメントの列の順序は、Pair または Triple の型の順序と一致する必要があります。

クエリにシンプルなパラメータを渡す

ほとんどの場合、DAO 関数は、フィルタ処理を行えるようにパラメータを受け入れる必要があります。Room では、関数パラメータをクエリでバインド パラメータとして使用することをサポートしています。

たとえば、次のコードは、特定の年齢以上のユーザーをすべて返す関数を定義しています。

@Query("SELECT * FROM user WHERE age > :minAge")
suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>

次のコードに示すように、1 回のクエリで複数のパラメータを渡すことも、同じパラメータを複数回参照することもできます。

@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

@Query(
    """
    SELECT * FROM user
    WHERE first_name LIKE :search OR last_name LIKE :search
    """
)
suspend fun findUserWithName(search: String): List<User>

クエリにパラメータのコレクションを渡す

DAO 関数によっては、実行時までわからない可変数のパラメータを渡すことが必要になる場合があります。パラメータがコレクションを表す場合、値の数に基づいて実行時に自動的に展開されます。

たとえば、次のコードは、リージョンのサブセットからすべてのユーザーに関する情報を返す関数を定義しています。

@Query("SELECT * FROM user WHERE region IN (:regions)")
suspend fun loadUsersFromRegions(regions: List<String>): List<User>

複数のテーブルにクエリする

クエリによっては、結果を計算するために複数のテーブルにアクセスすることが必要になる場合があります。SQL クエリで JOIN 句を使用すると、複数のテーブルを参照できます。

次のコードは、3 つのテーブルを結合して特定のユーザーに現在貸し出されている本を返す関数を定義しています。

@Query(
    """
    SELECT * FROM book
    INNER JOIN loan ON loan.book_id = book.id
    INNER JOIN user ON user.id = loan.user_id
    WHERE user.name LIKE :userName
    """
)
suspend fun findBooksBorrowedByName(userName: String): List<Book>

結合された複数のテーブルから列のサブセットを返すデータ オブジェクトを定義することもできます。詳細については、テーブルの列のサブセットを返すをご覧ください。次のコードは、ユーザー名とユーザーが借りた本の名前を返す関数を持つ DAO を定義しています。

interface UserBookDao {
    @Query(
        """
        SELECT user.name AS userName, book.name AS bookName
        FROM user, book
        WHERE user.id = book.user_id
        """
    )
    fun loadUserAndBookNames(): Flow<List<UserBook>>
}

data class UserBook(val userName: String, val bookName: String)

マルチマップを返す

結合オペレーションでは、マルチマップを返すクエリ関数を作成して、追加のデータクラスを定義せずに複数のテーブルの列をクエリすることもできます。

複数のテーブルにクエリするの例で考えてみます。User インスタンスと Book インスタンスのペアを保持するカスタム データクラスのインスタンスのリストを返す代わりに、UserBook のマッピングをクエリ関数から直接返すことができます。

@Query(
    """
    SELECT * FROM user
    JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNames(): Map<User, List<Book>>

クエリ関数がマルチマップを返す場合、GROUP BY 句を使用したクエリを作成することで、高度な計算やフィルタリングを行うために SQL の機能を利用できます。たとえば、本を 3 冊以上借りたユーザーのみを返すように loadUserAndBookNames 関数を変更できます。

@Query(
    """
    SELECT * FROM user
    JOIN book ON user.id = book.user_id
    GROUP BY user.name HAVING COUNT(book.id) >= 3
    """
)
suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>

オブジェクト全体をマッピングする必要がない場合は、戻り値の型の汎用パラメータで @MapColumn アノテーションを使用することで、クエリ内の特定の列間のマッピングを返すこともできます。

@Query(
    """
    SELECT user.name AS username, book.name AS bookname FROM user
    JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNamesColumns(): Map<
    @MapColumn(columnName = "username") String,
    List<@MapColumn(columnName = "bookname") String>
    >

特別な戻り値の型

Room には、他の API ライブラリと統合するための特別な戻り値の型が用意されています。

ページング ライブラリによるページングされたクエリ

Room は、ページング ライブラリとの統合により、ページングされたクエリをサポートしています。Paging 3 の戻り値の型を使用するには、データベースまたは DAO で Paging の戻り値の型コンバータを登録する必要があります。

  1. ビルド構成に androidx.room3:room3-paging アーティファクトを含めます。
  2. @Database または @Dao の宣言に @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) アノテーションを付けます。

登録すると、DAO は Paging 3 で使用する PagingSource オブジェクトを返すことができます。

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM users WHERE label LIKE :query")
    fun pagingSource(query: String): PagingSource<Int, User>
}

PagingSource の型パラメータの選択について詳しくは、キーと値の型を選択するをご覧ください。

データベースへの直接接続アクセス

アプリのロジックでデータベース接続への直接的な下位レベルのアクセスが必要な場合は、代わりに Room の接続 API を使用できます。読み取り専用オペレーションの場合は useReaderConnectionRoomDatabase インスタンスでの書き込みオペレーションの場合は useWriterConnection を使用して接続を取得し、usePrepared を使用してステートメントを実行できます。

val result: List<Pair<Long, String>> =
    roomDatabase.useReaderConnection { connection ->
        connection.usePrepared(
            "SELECT * FROM user WHERE age > :minAge LIMIT 5"
        ) { stmt ->
            // Bind arguments if needed
            stmt.bindLong(1, minAge.toLong())
            buildList {
                // Step through the results
                while (stmt.step()) {
                    add(stmt.getLong(0) to stmt.getText(1))
                }
            }
        }
    }

接続で低レベルのデータベース トランザクションを直接実行する必要がある場合は、useWriterConnection ブロック内の Transactor インスタンスで immediateTransactiondeferredTransactionexclusiveTransaction ヘルパー関数を使用できます。

roomDatabase.useWriterConnection { transactor ->
    transactor.immediateTransaction {
        // Perform transactional database operations using transactor
    }
}

または、トランザクションで高レベルの DAO オペレーションのみを実行する必要がある場合は、RoomDatabase インスタンスで withReadTransaction または withWriteTransaction ヘルパー拡張関数を使用します。

// Perform transactional read operations (DEFERRED transaction)
val userCount = roomDatabase.withReadTransaction {
    userDao.countUsers()
}

// Perform transactional write operations (IMMEDIATE transaction)
roomDatabase.withWriteTransaction {
    userDao.insert(newUser)
    userDao.update(existingUser)
}