RoomDatabaseを使ってデータベースを活用したAndroidアプリを開発する!

Androidアプリ開発

今回は、AndroidアプリでRoomDatabaseを活用するための基礎知識と設定方法について解説します。

クラス作成の手順から、スレッドセーフなデータベースの扱い方まで、実際に使える知識を身につけていきましょう!データベースの活用で、アプリの機能がさらに広がること間違いなし!

Androidのデータベースの基本

Androidアプリ開発では、ユーザーが入力した情報やアプリの設定、操作履歴などのデータを永続的に保存するためにデータベースを使用します。Androidで使用されるデータベースはSQLiteです。SQLiteは、リレーショナルデータベース管理システム(RDBMS)で、Androidデバイス上にローカルなデータベースを構築できます。

Androidでのデータベースの特徴

  1. ローカルストレージ: SQLiteはデバイス内のストレージに保存され、インターネット接続なしでも利用できます。アプリごとに独立したデータベースが管理され、他のアプリから直接アクセスされることはありません。
  2. リレーショナルデータベース: SQLiteはリレーショナルデータベースの一種で、表(テーブル)でデータを管理し、SQL(Structured Query Language)を使ってデータを操作します。テーブルは行と列で構成され、効率的にデータを操作できるのが特徴です。
  3. 軽量でコンパクト: SQLiteは軽量で、データベースサーバーを必要とせず、ファイルとしてデバイス内に格納されるため、リソースの少ない環境でも動作します。これにより、モバイルデバイス上でのデータ管理に適しています。

RoomDatabaseの登場

Androidでは、SQLiteのコードを直接書く代わりに、RoomDatabase というライブラリが登場し、SQLiteをより使いやすくするためのラッパーとして利用されています。RoomDatabaseは、コードの安全性や保守性を高め、効率的なデータベース操作を可能にします。

RoomDatabaseの構成要素

RoomDatabaseは、エンティティ(Entity)、DAO、データベースクラスの3つの要素で構成されます。各要素がそれぞれ異なる役割を持っており、これらを組み合わせることで効率的なデータ管理が可能になります。

エンティティ(Entity)

Roomのデータベース内に格納されるデータを表現するクラスで、通常1つのデータベーステーブルに対応します。エンティティクラスの各プロパティがテーブルの列に対応し、データ構造が明確に管理されます。

@Entity(tableName = "users")
public class User {
    
    @PrimaryKey(autoGenerate = true)
    public int id;
    
    public String name;
    
    public int age;
}

クラスに @Entity アノテーションを付与することで、Roomにこのクラスがデータベースのテーブルであることを示します。

エンティティクラスで使用するアノテーション

@Entity

クラスに付与することで、このクラスがデータベース内のテーブルであることを示します。

:

@Entity(tableName = "users", indices = {@Index(value = "email", unique = true)})
public class User {
// クラス内容
}
プロパティ概要
tableName テーブル名を指定。指定しない場合はクラス名がテーブル名として使用される。
indicesインデックスを作成する際に使用。検索速度の向上やデータの一意性を保証するために利用される。
primaryKeys複数のカラムを主キーにする場合に使用。
foreignKeys外部キーを指定する際に使用する。
@PrimaryKey

主キーを指定するアノテーションです。データベース内で各レコードを一意に識別するために使用されます。

:

@PrimaryKey(autoGenerate = true)
private int id;
プロパティ概要
autoGeneratetrueを指定すると、Roomが主キーを自動的に生成する。
@ColumnInfo

カラム(列)に関する詳細を指定するためのアノテーションです。カラム名や型、エンコーディングを指定できます。

:

@ColumnInfo(name = "user_name", typeAffinity = ColumnInfo.TEXT)
private String name;
プロパティ概要
nameカラム名を指定します。デフォルトでは、変数名がそのままカラム名になります。
typeAffinity カラムのデータ型(INTEGER、TEXT、REAL、BLOB)を指定します。
defaultValue デフォルト値を指定できます。

@Ignore

Roomがデータベースに格納しないフィールドやコンストラクタに指定するアノテーションです。計算結果を格納する一時的なフィールドなど、データベースに保存したくない場合に使用します。

:

@Ignore
private int tempValue; // データベースには保存しない
@Embedded

他のオブジェクトをエンティティに埋め込む際に使用するアノテーションです。オブジェクトのプロパティがエンティティのカラムとして扱われます。

  • 用途: 住所や名前など、複数のフィールドが一緒に関連付けられる構造に便利です。

:

@Embedded
private Address address;
@ForeignKey

外部キーの設定を行うアノテーションで、他のテーブルと関係を持たせる際に使用します。

:

@ForeignKey(entity = Department.class,
            parentColumns = "id",
            childColumns = "departmentId",
            onDelete = ForeignKey.CASCADE)
private int departmentId;
プロパティ概要
autoGenerate参照するエンティティクラスを指定。
parentColumns参照先のテーブルのカラム名を指定。
childColumns現在のテーブルのカラム名を指定。
onDelete親レコードが削除された際の動作(CASCADE、NO ACTION など)を指定。

カラムの用途に合わせてアノテーションを付与しましょう♪

DAO(データアクセスオブジェクト)

エンティティへの操作(SQL)を定義するインターフェースで、RoomではこのDAOを介してデータベース操作が行われます。DAOを通じて、データの挿入、更新、削除、取得といった操作を簡単に定義でき、直接SQLコードを書く必要がないため、コードの安全性と保守性が向上します。

@Dao
public interface UserDao {    
    // データの挿入
    @Insert
    void insertUser(User user);
    
    // データの削除
    @Delete
    void deleteUser(User user);
    
    // 全ユーザーの取得
    @Query("SELECT * FROM users")
    LiveData<List<User>> getAllUsers();
}

DAOインターフェースには@Daoアノテーションを付与し、メソッドにはクエリを表すアノテーション(@Insert@Delete@Queryなど)を付けます。

DAOで使用するアノテーション

@Insert

データの挿入操作を定義します。Roomが自動でSQLのINSERT文を生成します。複数のエンティティを一度に挿入できるよう、メソッドの引数にリストを渡すことも可能です。

// 単一のユーザーを挿入
@Insert
void insertUser(User user);

// 複数のユーザーを一度に挿入
@Insert
void insertUsers(List<User> users);
@Delete

データの削除操作を定義します。引数に指定したエンティティを削除します。

// 単一のユーザーを削除
@Delete
void deleteUser(User user);

// 複数のユーザーを一度に削除
@Delete
void deleteUsers(List<User> users);

@Deleteで更新する場合ID(主キー)が必須となります。

@Update

データの更新操作を定義します。引数に指定したエンティティのデータを更新します。更新対象となるレコードは、エンティティの主キーによって自動的に判断されます。

// 単一のユーザーを更新
@Update
void updateUser(User user);

// 複数のユーザーを一度に更新
@Update
void updateUsers(List<User> users);

@Updateで更新する場合ID(主キー)が必須となります。
@Updateアノテーションを使ってエンティティ全体を更新する際、nullの値を持つフィールドもデータベースに反映します。

@Query

任意のSQLクエリを指定できるアノテーションで、データの取得やフィルタリングなど、複雑な操作を定義できます。データの検索、特定の条件に基づいたデータの取得などに使用します。

// すべてのユーザーを取得
@Query("SELECT * FROM users")
List<User> getAllUsers();

// 年齢が指定された値以上のユーザーを取得
@Query("SELECT * FROM users WHERE age > :minAge")
List<User> getUsersOlderThan(int minAge);

// ユーザー名が一致するユーザーを取得
@Query("SELECT * FROM users WHERE name = :userName")
User getUserByName(String userName);

// すべてのユーザーの年齢を一斉に更新
@Query("UPDATE users SET age = :newAge")
void updateAllUserAges(int newAge);

// 特定のIDのユーザーを削除
@Query("DELETE FROM users WHERE id = :userId")
void deleteUserById(int userId);

@Queryの利用タイミング

@Queryアノテーションは柔軟で便利ですが、使い方を見極める必要があります。以下の指針を参考に、他のアノテーション(@Insert、@Delete、@Update)と使い分けると良いでしょう。

1. データの登録が目的か?

データの登録を行う場合、@Insertアノテーションを使用するのがよいでしょう。@InsertはRoomが自動的にSQLのINSERT文を生成してくれるため、効率的に登録が可能です。主キーが自動生成される場合は、RoomがIDを自動的に挿入してくれます。

  • 使い分け: 単純なデータの追加は@Insert。特定条件付きの挿入が必要な場合には@Queryが適しています。

2. 不要なデータの削除か?

不要なデータを削除する際は、@Deleteアノテーションが便利です。@Deleteは指定されたエンティティのインスタンスに基づいて、Roomが自動的にDELETE文を生成してくれます。

  • 使い分け: 単純な削除は@Delete。条件付きで削除したい場合は@Query(例: 年齢が特定の値以上のユーザーのみ削除)を使用します。

3. UI上で非表示にしたいがデータは保持したいか?

データを削除せずに、UI上で非表示にしたい場合は「論理削除」が有効です。論理削除とは、削除フラグ(例: isDeleted)を追加して、削除する代わりにそのフラグを更新する方法です。論理削除を行う場合、@Updateまたは@Queryでフラグを更新します。

  • 使い分け: フィールドを論理的に更新する場合は@Update。フラグの条件で表示・非表示を切り替えるクエリには@Queryを使用します。

4. データを更新したいか?

データの更新を行いたい場合は、@Updateアノテーションが基本です。Roomは自動的に主キーに基づいてエンティティ全体を更新してくれます。

  • 使い分け: 単純な更新(全項目の更新)は@Update。特定フィールドのみの更新や条件付き更新には@Queryを使用します。

5. データの取得や特定の条件での操作が必要か?

@Queryは、特定の条件に基づいたデータ取得や複雑な操作が必要な場合に最適です。Roomでは@Queryを使ってSQL文を直接記述することで、詳細なデータ操作が可能になります。例えば、特定の年齢以上のユーザーのみを取得する場合などです。

使い分けのまとめ
  • @Insert / @Delete / @Update: 単純なデータの追加、削除、全項目の更新に適しています。
  • @Query: 条件付きの操作や部分的な更新、複雑なデータの取得など、カスタムクエリが必要な場合に適しています。

戻り値に関して

Roomでは、データの操作(挿入・更新・削除)と取得の際に異なる戻り値を指定します。特にデータ取得用の@Queryアノテーションでは、用途に応じた戻り値を指定する必要があります。

挿入・更新・削除の戻り値

挿入(@Insert)、更新(@Update)、削除(@Delete)では基本的に戻り値としてvoidを指定します。データの変更が無事に完了したことを示すための戻り値は特に必要ない場合が多いためです。

挿入や更新時に自動生成されたIDを取得したい場合や、操作の結果を確認したい場合にはlongintなどの戻り値を指定することもできます。

: 挿入後に自動生成されたIDを取得

@Insert long insertUser(User user);
データ取得(@Query)の戻り値

データを取得する@Queryアノテーションでは、さまざまな戻り値を指定することができます。データのタイプやリアクティブ性に応じて、以下のような戻り値が使用されます。

エンティティクラス
取得するデータが単一のエンティティインスタンスである場合、エンティティクラスを戻り値として指定します。

: IDに一致する単一のユーザーを取得

@Query("SELECT * FROM users WHERE id = :id")
 User getUserById(int id);

リスト型
複数のデータを取得する場合、エンティティクラスをListでラップした型を指定します。: 全ユーザーをリストで取得javaコードをコピーする

@Query("SELECT * FROM users") 
List<User> getAllUsers();

LiveData / Flow
データの変更に応じて自動的にUIを更新したい場合、LiveDataを戻り値に指定します。これにより、データが更新されるたびにUIもリアクティブに変更されます。

: 全ユーザーをリアクティブに取得

@Query("SELECT * FROM users") 
LiveData<List<User>> getAllUsersLive();

特定のカラムを取得する場合
エンティティ全体ではなく、特定のカラムのみを取得したい場合、カラム名と一致するフィールドを持つクラス、もしくはそのカラムの型を戻り値として指定します。

: 全ユーザーの名前のみを取得

@Query("SELECT name FROM users")
 List<String> getAllUserNames();

Cursor
より細かなデータ操作が必要な場合、Cursorを戻り値として指定することも可能です。Roomではあまり使用されませんが、特定のシステムAPIと連携する際に便利です。

:

@Query("SELECT * FROM users") 
Cursor getAllUsersCursor();
戻り値の選択ポイント
  • 単一のデータ取得: 単一のエンティティインスタンスまたはカラム値を返す場合、エンティティクラスまたはその型を戻り値として指定します。
  • 複数データ取得: リストで取得するか、リアクティブに取得する場合はListLiveData<List>Flow<List>を選びます。
  • 特定のカラム取得: 必要なカラムのみを取得して負荷を軽減したい場合、該当カラムの型またはそれを保持するクラスを戻り値として指定します。

Roomの戻り値は、データ操作の効率性やアプリのパフォーマンスを左右するので、用途に応じて適切に選択しましょう!

データベースクラス

RoomDatabaseは、Roomを使用するためのデータベース全体を管理する抽象クラスです。このクラスを継承して作成され、アプリ全体で共通して使用するDAOのインスタンスをまとめて管理します。エンティティやDAOを登録し、アプリ内でデータベース操作を行う中心的な役割を担います。

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    // DAOを取得する抽象メソッド
    public abstract UserDao userDao();

    // シングルトンとして利用するためのインスタンス
    private static AppDatabase appDatabase;

    // データベースインスタンスを取得するメソッド
    public static synchronized AppDatabase getInstance(Context context) {
        if (appDatabase == null) {
            // スレッドセーフなシングルトンパターン
            synchronized (AppDatabase.class) {
                if (appDatabase == null) {
                    appDatabase = Room.databaseBuilder(context.getApplicationContext(),
                        AppDatabase.class, "app_database")
                        .fallbackToDestructiveMigration() // スキーマ変更時に再構築
                        .build();
                }
            }
        }
        return appDatabase;
    }
}

データベースクラスには@Databaseアノテーションを使用してエンティティクラスやデータベースのバージョンを指定し、Roomデータベースとして設定します。

プロパティ概要
entitiesデータベースに含めるエンティティクラスのリストを指定。
versionデータベースのバージョンを指定。データベースのスキーマが変更されるときはバージョンを増やす必要がある。

データベースクラスのフィールド変数

DAOメソッド
UserDaoを取得するための抽象メソッドuserDao()を定義しています。データベース内の操作は、このDAOを通じて行われます。

public abstract UserDao userDao();

シングルトンインスタンス
AppDatabaseのインスタンスを保持する静的な変数appDatabaseを定義し、シングルトンとして利用するようにしています。

private static AppDatabase appDatabase;

データベースクラスのメソッド

getInstanceメソッド
getInstanceメソッドはAppDatabaseのインスタンスを取得するための静的メソッドです。
synchronizedを使用してスレッドセーフにすることで、複数のスレッドが同時にこのメソッドにアクセスしても、1つのインスタンスが生成されるようにしています。

public static synchronized AppDatabase getInstance(Context context) {
    //処理
}

スレッドセーフにすることで、複数のスレッドから同時にアクセスがあってもデータの一貫性を保ち、クラッシュやデータ破損を防ぎ、安全にデータ操作を行うことが可能となります。

ダブルチェックロッキング
if (appDatabase == null) のチェックを2回行うことで、効率的にインスタンスの重複生成を防ぎます。

if (appDatabase == null) {
    // スレッドセーフなシングルトンパターン
    synchronized (AppDatabase.class) {
        if (appDatabase == null) {
            //処理
        }
}

Room.databaseBuilderメソッド
Room.databaseBuilderを使用して、Roomデータベースインスタンスを構築しています。
context.getApplicationContext()を使用することで、アクティビティのライフサイクルに依存しないコンテキストを使用し、アプリ全体でのデータベースの寿命が保証されます。

fallbackToDestructiveMigration()を指定することで、データベースのスキーマが変更された場合にデータを破棄し、データベースを再構築します。開発時には便利なオプションですが、リリース時にはバックアップやマイグレーションの検討が必要となります。

appDatabase = Room.databaseBuilder(context.getApplicationContext(),
                    AppDatabase.class, "app_database")
                    .fallbackToDestructiveMigration() // スキーマ変更時に再構築
                    .build();

Room.databaseBuilder()メソッド

引数クラス概要
第一引数Context contextcontext.getApplicationContext()を使用してアプリケーションのコンテキストを指定する。
第二引数Class databaseClassデータベースのクラス型を指定。
RoomDatabaseを継承したクラスを指定し、データベースのスキーマやDAOの情報がRoomに認識されるようにする。
第三引数String databaseNameデータベースのファイル名を指定。
この名前は、デバイス内に保存されるデータベースファイルの名前として使用される。

Roomデータベースをリリースする際には、スキーマ変更が発生すると、データベース構造の違いが原因でアプリがクラッシュしたり、ユーザーデータが失われるリスクがあります。そのため、スキーマ変更への対策として、データのバックアップやマイグレーションを適切に検討することが重要です。

RoomDatabaseの利用方法

1.データベースクラスのインスタンスを取得する

AppDatabase.getInstance()メソッドを呼び出し、データベースのインスタンスを取得します。データベースインスタンスはシングルトンとして管理するため、getInstance()メソッドで一度作成したインスタンスが再利用されます。

AppDatabase db = AppDatabase.getInstance(getApplicationContext());

2. DAOインスタンスを取得する

データベースインスタンスからDAO(Data Access Object)のインスタンスを取得します。

UserDao userDao = db.userDao();

3. データ操作を実行する

DAOを通してデータベース操作を行います。

User user = new User("Alice", 25);
userDao.insertUser(user)

RoomDatabaseを利用するうえでの注意点

RoomDatabaseは、メインスレッドでのデータベース操作ができないため、バックグラウンドスレッド(非同期)で利用する必要があります。

メインスレッドでデータベースアクセスを行うと、Cannot access database on the main threadというエラーが発生します。アプリのパフォーマンスを保つためにも、データベース操作は常にバックグラウンドスレッドで行いましょう!

非同期処理について詳しく知りたい方は、以下の記事を参考にしてください。

まとめ

いかがでしたでしょうか?

RoomDatabaseを利用するには、クラスの作成やデータベースクラスの構築など、さまざまな準備が必要です。また、データベースの更新やスキーマ変更時には、データの保持方法なども考慮する必要があります。

しかし、データベース操作を習得すると、アプリで扱える機能の幅が広がり、より高度なアプリ開発が可能になります。積極的に挑戦して、データベース操作をマスターしていきましょう!

タイトルとURLをコピーしました