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

Room 永続ライブラリを使用してアプリのデータにアクセスするには、データ アクセス オブジェクト(DAO)を使用します。この Dao オブジェクト セットは、Room の主要コンポーネントであり、各 DAO は、アプリのデータベースへの抽象アクセスを可能にするメソッドを備えています。

クエリビルダーやダイレクト クエリではなく DAO クラスを使用してデータベースにアクセスすることで、データベース アーキテクチャの各種コンポーネントを分離できます。 また、DAO を使用すると、アプリをテストする際、簡単にデータベース アクセスを模倣できます。

DAO は、インターフェース クラスとしても抽象クラスとしても利用できます。抽象クラスの場合、必要に応じて、唯一のパラメータとして RoomDatabase を取るコンストラクタを指定できます。コンパイル時に Room によって各 DAO 実装が作成されます。

注: UI が長期間ロックされる可能性があるため、ビルダーで allowMainThreadQueries() を呼び出していない限り、Room では、メインスレッド上でのデータベース アクセスはサポートされていません。非同期クエリ(LiveData または Flowable のインスタンスを返すクエリ)は、必要に応じてバックグラウンド スレッドで非同期にクエリを実行するため、このルールから除外されます。

コンビニエンス メソッドを定義する

DAO クラスを使用すると、さまざまなコンビニエンス クエリを表現できます。 このドキュメントでは、よく利用される例について説明します。

挿入

DAO メソッドを作成して @Insert のアノテーションを付けた場合、すべてのパラメータを単一のトランザクション内でデータベースに挿入する実装が Room によって作成されます。

クエリの例を次のコード スニペットに示します。

Kotlin

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

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

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

Java

    @Dao
    public interface MyDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        public void insertUsers(User... users);

        @Insert
        public void insertBothUsers(User user1, User user2);

        @Insert
        public void insertUsersAndFriends(User user, List<User> friends);
    }
    

@Insert メソッドが 1 つのパラメータだけを受け取る場合、挿入されるアイテムの新しい rowId になる long を返すことができます。パラメータが配列やコレクションの場合は、long[]List<Long> が返されます。

詳細については、@Insert アノテーションのリファレンス ドキュメントと rowid テーブルの SQLite ドキュメントをご覧ください。

更新

Update コンビニエンス メソッドは、パラメータとして指定されたデータベース内のエンティティのセットを変更します。このメソッドは、各エンティティの主キーをマッチングするクエリを使用します。

このメソッドを定義する方法を次のコード スニペットに示します。

Kotlin

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

Java

    @Dao
    public interface MyDao {
        @Update
        public void updateUsers(User... users);
    }
    

通常は必要ありませんが、データベース内で更新された行数を示す int 値をこのメソッドで返すこともできます。

削除

Delete コンビニエンス メソッドは、パラメータとして指定されたデータベース内のエンティティのセットを削除します。このメソッドは、主キーを使用して、削除するエンティティを検索します。

このメソッドを定義する方法を次のコード スニペットに示します。

Kotlin

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

Java

    @Dao
    public interface MyDao {
        @Delete
        public void deleteUsers(User... users);
    }
    

通常は必要ありませんが、データベースから削除された行数を示す int 値をこのメソッドで返すこともできます。

情報のクエリ

@Query は、DAO クラス内で使用される主要なアノテーションです。データベースに対して読み書き処理を実行できます。各 @Query メソッドはコンパイル時に検証されるため、クエリに問題があると、ランタイム エラーの代わりにコンパイル エラーが発生します。

Room はクエリの戻り値についても検証を行い、戻りオブジェクトのフィールド名がクエリ応答内の対応列名と一致しない場合、次の 2 つの方法のいずれかでアラートを通知します。

  • 一部のフィールド名だけが一致している場合は、警告を表示します。
  • フィールド名が 1 つも一致しない場合は、エラーを表示します。

シンプルなクエリ

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user")
        fun loadAllUsers(): Array<User>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user")
        public User[] loadAllUsers();
    }
    

上記は、すべてのユーザーを読み込む非常にシンプルなクエリです。Room はコンパイル時に、user テーブル内のすべての列をクエリしていることを認識します。クエリ内に構文エラーが含まれている場合や、データベース内に user テーブルが存在しない場合、Room はアプリのコンパイル時にエラー メッセージを表示します。

クエリにパラメータを渡す

たとえば、特定の年齢よりも年上のユーザーだけを表示するなど、フィルタリング処理を実行する際はほとんどの場合、パラメータをクエリに渡す必要があります。このタスクを実現するには、Room アノテーション内でメソッド パラメータを使用します。次のコード スニペットをご覧ください。

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge")
        fun loadAllUsersOlderThan(minAge: Int): Array<User>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge")
        public User[] loadAllUsersOlderThan(int minAge);
    }
    

コンパイル時にこのクエリを処理する際、Room は :minAge バインド パラメータと minAge メソッド パラメータをマッチングします。Room は、パラメータ名を使用してマッチングを実行します。不一致が存在する場合、アプリのコンパイル時にエラーが発生します。

1 回のクエリ内で複数のパラメータを渡すことも、それを複数回参照することもできます。次のコード スニペットをご覧ください。

Kotlin

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

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

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
        public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

        @Query("SELECT * FROM user WHERE first_name LIKE :search " +
               "OR last_name LIKE :search")
        public List<User> findUserWithName(String search);
    }
    

列のサブセットを返す

ほとんどの場合、取得する必要があるのは、エンティティの一部のフィールドだけです。たとえば、UI に表示するのは、ユーザーのすべての詳細情報ではなく、ユーザーの姓名だけという場合があります。このような場合に、アプリの UI に表示する列だけを取得することで、貴重なリソースを節約し、クエリを迅速に完了することができます。

Room では、結果列のセットを戻りオブジェクトにマッピングできる限り、クエリから Java ベースのオブジェクトを返すことができます。たとえば、次のプレーン オールド Java ベース オブジェクト(POJO)を作成することで、ユーザーの姓名を取得できます。

Kotlin

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

Java

    public class NameTuple {
        @ColumnInfo(name = "first_name")
        public String firstName;

        @ColumnInfo(name = "last_name")
        @NonNull
        public String lastName;
    }
    

この POJO は、次のクエリメソッドで使用できます。

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user")
        fun loadFullName(): List<NameTuple>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user")
        public List<NameTuple> loadFullName();
    }
    

Room は、このクエリが first_name 列と last_name 列の値を返し、その値を NameTuple クラスのフィールドにマッピングできることを認識します。その結果、Room は適切なコードを生成できます。クエリが返す列数が多すぎる場合や、NameTuple クラス内に存在しない列を返した場合は、警告が表示されます。

引数のコレクションを渡す

クエリによっては、実行時までパラメータの正確な数がわからず、数が可変のパラメータを渡すことが必要になる場合があります。たとえば、地域のサブセットからすべてのユーザーに関する情報を取得することがあります。Room は、パラメータがコレクションを示している場合はそのことを認識し、供給されたパラメータの数に基づいて実行時に自動的にコレクションを展開します。

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        public List<NameTuple> loadUsersFromRegions(List<String> regions);
    }
    

ダイレクト カーソル アクセス

アプリのロジックが戻り行に直接アクセスする必要がある場合は、クエリから Cursor オブジェクトを返します。次のコード スニペットをご覧ください。

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
        fun loadRawUsersOlderThan(minAge: Int): Cursor
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
        public Cursor loadRawUsersOlderThan(int minAge);
    }
    

注: Cursor API を使用する方法はおすすめしません。Cursor API では、行が存在するかどうか、行にどのような値が含まれているのかを知ることができません。この機能を使用するのは、カーソルを必要とし、簡単にはリファクタリングできないコードがすでに存在する場合に限るようにしてください。

複数のテーブルのクエリを行う

クエリによっては、結果を計算するために複数のテーブルにアクセスすることが必要になる場合があります。Room の場合、任意のクエリを記述することが可能で、テーブルを結合することもできます。 また、応答が監視可能なデータ型(FlowableLiveData など)である場合、Room は、クエリ内で参照されているすべてのテーブルを対象に、無効なものがないかモニタリングします。

テーブル結合を実行して、書籍を借りているユーザーを格納しているテーブルと現在貸出中の書籍に関するデータを格納しているテーブルの情報を統合する方法を、次のコード スニペットに示します。

Kotlin

    @Dao
    interface MyDao {
        @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"
        )
        fun findBooksBorrowedByNameSync(userName: String): List<Book>
    }
    

Java

    @Dao
    public interface MyDao {
        @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")
       public List<Book> findBooksBorrowedByNameSync(String userName);
    }
    

また、このようなクエリから POJO を返すこともできます。たとえば、次のようにユーザーとペットの名前を読み込むクエリを記述できます。

Kotlin

    @Dao
    interface MyDao {
        @Query(
            "SELECT user.name AS userName, pet.name AS petName " +
            "FROM user, pet " +
            "WHERE user.id = pet.user_id"
        )
        fun loadUserAndPetNames(): LiveData<List<UserPet>>

        // You can also define this class in a separate file.
        data class UserPet(val userName: String?, val petName: String?)
    }
    

Java

    @Dao
    public interface MyDao {
       @Query("SELECT user.name AS userName, pet.name AS petName " +
              "FROM user, pet " +
              "WHERE user.id = pet.user_id")
       public LiveData<List<UserPet>> loadUserAndPetNames();

       // You can also define this class in a separate file, as long as you add the
       // "public" access modifier.
       static class UserPet {
           public String userName;
           public String petName;
       }
    }
    

クエリの戻り値の型

Room は、特定のフレームワークや API との相互運用のための特殊な戻り値の型など、クエリメソッドのさまざまな戻り値の型をサポートしています。次の表に、クエリタイプとフレームワークに基づいて、該当する戻り値の型を示します。

クエリタイプ コルーチン RxJava Guava Lifecycle
監視可能な読み取り Flow<T> Flowable<T>Publisher<T>Observable<T> なし LiveData<T>
ワンショット読み取り suspend fun Single<T>Maybe<T> ListenableFuture<T> なし
ワンショット書き込み suspend fun Single<T>Maybe<T>Completable<T> ListenableFuture<T> なし

フローを使用したリアクティブ クエリ

Room 2.2 以降では、Kotlin の Flow 機能を使用して、アプリの UI を最新の状態に保つことができます。基になるデータが変更されたときに UI を自動的に更新するには、Flow オブジェクトを返すクエリメソッドを記述します。

@Query("SELECT * FROM User")
    fun getAllUsers(): Flow<List<User>>
    

テーブル内のデータが変更されるたびに、返された Flow オブジェクトは再びクエリをトリガーし、結果セット全体が再出力されます。

Flow を使用したリアクティブ クエリには、1 つの重要な制限があります。それは、テーブル内の行が更新されるたびに、その行が結果セットに含まれているかどうかにかかわらず、Flow オブジェクトはクエリを再実行するということです。実際のクエリ結果が変更された場合にのみ UI が通知されるようにするには、返された Flow オブジェクトに distinctUntilChanged() 演算子を適用します。

@Dao
    abstract class UsersDao {
        @Query("SELECT * FROM User WHERE username = :username")
        abstract fun getUser(username: String): Flow<User>

        fun getUserDistinctUntilChanged(username:String) =
               getUser(username).distinctUntilChanged()
    }
    

Kotlin コルーチンを使用した非同期クエリ

DAO メソッドに suspend Kotlin キーワードを追加すると、Kotlin コルーチン機能を使用して非同期化できます。これにより、メインスレッド上で実行される可能性がなくなります。

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

        @Update
        suspend fun updateUsers(vararg users: User)

        @Delete
        suspend fun deleteUsers(vararg users: User)

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

この方法は、@Transaction アノテーション付きの DAO メソッドにも当てはまります。この機能を使用して、別の DAO メソッドから一時停止データベース メソッドを作成できます。いずれのメソッドも、単一のデータベース トランザクション内で実行されます。

@Dao
    abstract class UsersDao {
        @Transaction
        open suspend fun setLoggedInUser(loggedInUser: User) {
            deleteUser(loggedInUser)
            insertUser(loggedInUser)
        }

        @Query("DELETE FROM users")
        abstract fun deleteUser(user: User)

        @Insert
        abstract suspend fun insertUser(user: User)
    }
    

アプリ内で Kotlin コルーチンを使用する方法については、Kotlin コルーチンを使用してアプリのパフォーマンスを改善するをご覧ください。

LiveData を使用した監視可能なクエリ

クエリを実行しているとき、データの変更に応じてアプリの UI を自動的に更新したいことがよくあります。これを実現するには、クエリメソッドの説明で LiveData 型の戻り値を使用します。データベースが更新されると、LiveData の更新に必要なすべてのコードが Room によって生成されます。

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
    }
    

RxJava を使用したリアクティブ クエリ

Room は、RxJava2 型の戻り値を以下のようにサポートしています。

  • @Query メソッド: Room は、PublisherFlowableObservable 型の戻り値をサポートしています。
  • @Insert メソッド、@Update メソッド、@Delete メソッド: Room 2.1.0 以降は、CompletableSingle<T>Maybe<T> 型の戻り値をサポートしています。

この機能を使用するには、最新バージョンの rxjava2 アーティファクトをアプリの build.gradle ファイル内に組み込みます。

app/build.gradle

    dependencies {
        def room_version = "2.1.0"
        implementation 'androidx.room:room-rxjava2:$room_version'
    }
    

このライブラリの現在のバージョンを確認するには、バージョンのページで Room に関する情報をご覧ください。

このような戻り値の型の使用例を次のコード スニペットに示します。

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT * from user where id = :id LIMIT 1")
        fun loadUserById(id: Int): Flowable<User>

        // Emits the number of users added to the database.
        @Insert
        fun insertLargeNumberOfUsers(users: List<User>): Maybe<Int>

        // Makes sure that the operation finishes successfully.
        @Insert
        fun insertLargeNumberOfUsers(varargs users: User): Completable

        /* Emits the number of users removed from the database. Always emits at
           least one user. */
        @Delete
        fun deleteAllUsers(users: List<User>): Single<Int>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * from user where id = :id LIMIT 1")
        public Flowable<User> loadUserById(int id);

        // Emits the number of users added to the database.
        @Insert
        public Maybe<Integer> insertLargeNumberOfUsers(List<User> users);

        // Makes sure that the operation finishes successfully.
        @Insert
        public Completable insertLargeNumberOfUsers(User... users);

        /* Emits the number of users removed from the database. Always emits at
           least one user. */
        @Delete
        public Single<Integer> deleteUsers(List<User> users);
    }
    

詳細については、Google Developers の Room および RxJava をご覧ください。

参考情報

Room DAO を使用してデータにアクセスする方法について詳しくは、以下の参考情報をご確認ください。

サンプル

コードラボ

ブログ