プロジェクト

全般

プロフィール

Androidプログラミング-Room

Room概要

Roomライブラリは、Android端末内蔵のSQLiteデータベースに対するORM(Object Relation Mapper)を提供します。
基本構成は、Databaseクラス、Daoクラス、Entityクラスの3セットです。

android_room.png

Entityクラスは、SQLiteテーブルのレコードを格納するオブジェクトで、SQLiteテーブルのカラムに対応するフィールド(プロパティ)を持つクラスです。テーブルやカラムとの対応付けのためのアノテーションを付与します。

Daoクラスは、テーブルに対するCRUD操作のインタフェースとSQL文を定義するインタフェースです。

Databaseクラスは、SQLiteのデータベースファイルを開き、使用するDaoを参照します。

ビルド設定

Roomライブラリを使うには、モジュールのbuild.gradleファイルにRoomライブラリの依存関係を記述します。

次はKotlin言語を使用する場合の記述例です。

apply plugin: 'kotlin-kapt'

dependencies {
  implementation "androidx.room:room-runtime:2.2.5" 
  kapt "androidx.room:room-compiler:2.2.5" 
  implementation "androidx.room:room-ktx:2.2.5"  // optional - Kotlin extensions and coroutines support for Room
  testImplementation "androidx.room:room-testing:2.2.5" 
}

ここでは分かりやすさのためにバージョン番号を直書きしていますが、実際のプロジェクトでは一元的に変更できるよう変数に定義するのがよいです。

実装例

単純な計測温度のデータベース

きわめて単純な温度を計測し記録するデータベースを想定します。
スキーマは、一意IDと計測日時(年月日時分秒)、そして計測温度(浮動小数点数)から構成します。

スキーマ定義

SQLiteデータベース名(ファイル名) temperatures.db
テーブル名 Temperatures
カラム名 データ型 制約 内容
id INTEGER PRIMARY KEY, AUTOINCREMENT サロゲートキー
measured_at TEXT[^1] NOT NULL 計測日時
measurement REAL NOT NULL 計測温度

1 SQLiteは日時型を持たないので、SQLで関数を用いてTEXT、REAL、INTEGERのいずれかに変換して格納。

エンティティクラスの定義

  • クラスにはアノテーション @Entity を付与
  • テーブル名はこのクラス名になる。別名を使う場合は、@EntityアノテーションのtableName属性で指定
  • フィールドの1つを主キーとして定義する。フィールドにアノテーション @PrimaryKey を指定
    • IDを自動生成させる場合は@PrimaryKeyアノテーションにautoGenerate=true属性を指定
    • 複合主キーの定義は@EntityアノテーションのprimaryKeys属性で指定
  • カラム名はフィールド名になる。別名を使う場合は、@ColumnInfoアノテーションのname属性で指定
    • 永続化対象ではないフィールドがあるなら、Ignoreを指定
@Entity(tableName = "Temperatures")
data class TemperatureEntity(
    @PrimaryKey(autoGenerate = true)
    var id: Long,

    @ColumnInfo(name = "measured_at")
    val measuredAt: LocalDateTime,

    @ColumnInfo(name = "measurement")
    val measurement: Float
)
標準以外の型をデータベースに格納する場合、変換クラスを実装

LocalDateTime型はSQLiteには直接格納できないため、SQLiteのTEXT型で格納するものとします。この場合、LocalDateTime型とString型とを相互変換するクラスを作成する必要があります。

class LocalDateTimeConverter {
    @TypeConverter
    fun fromLocalDateTime(dateTime: LocalDateTime): String {
        return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
    }
    @TypeConverter
    fun toLocalDateTime(dateTimeText: String): LocalDateTime {
        return LocalDateTime.parse(dateTimeText) // Default format is ISO_LOCAL_DATE_TIME
    }
}
  • クラス名は任意、後述のデータベースクラスに @TypeConvertersアノテーションを付与し、属性にこの変換クラスを指定
  • 変換メソッドには @TypeConverterアノテーションを付与

DAOインタフェースの定義

  • インタフェースにはアノテーション @Dao を付与
  • データベースからレコードを読み出すメソッドには アノテーション @Query を付与
    • @Queryアノテーションの属性でSQL文(SELECT文)を指定
  • データベースへレコードを挿入するメソッドには アノテーション @Insert を付与
  • データベースのレコードのカラムの値を更新するメソッドには アノテーション @Update を付与
  • データベースのレコードを削除するメソッドにはアノテーション @Delete を付与
@Dao
interface TemperatureDao {
    @Insert
    fun insert(temperature: TemperatureEntity)
    @Update
    fun update(temperature: TemperatureEntity)
    @Query("SELECT * FROM Temperatures WHERE id = :key")
    fun get(key: Long): TemperatureEntity?
    @Query("SELECT * FROM Temperatures ORDER BY id DESC")
    fun getAllTemperatures(): LiveData<List<TemperatureEntity>>
}

@Insert は、引数に指定したエンティティクラスのインスタンスを、そのエンティティのテーブルに新しいレコードとしてINSERTします。引数に指定するエンティティインスタンスは、単一でも複数(配列あるいはコレクション)でも対応します。戻り値は、挿入されたレコードのROWIDです。ROWIDはSQLiteの機能で、隠れたカラムで一意な整数値が割当てられます。

@Update は、引数に指定したエンティティクラスのインスタンスの持つ値でそのインスタンスに対応するレコード(主キーが一致するレコード)を更新します。

データベースクラス

  • クラスにはアノテーション @Database を付与し、属性にはエンティティクラス、スキーマバージョンを指定
  • カスタム変換クラスを使用する場合、クラスにアノテーション @TypeConverters を付与し、属性に変換クラスを指定
  • abstractクラスで定義し、RoomDatabaseクラスを継承
  • Daoクラスをabstractプロパティで保持
  • インスタンス生成コストが高いので、一度インスタンスを生成すると以降はそのインスタンスを再利用するシングルトンパターンの一種で実装
    • 今回は companion object を使用。他にはトップレベル関数で getDatabase を定義する等の実装もあり(これの方がコードはシンプルかも)。
internal const val DATABASE_FILE_NAME = "temperatures.db" 

@Database(entities = [TemperatureEntity::class], version = 1)
@TypeConverters(LocalDateTimeConverter::class)
abstract class TemperatureDatabase : RoomDatabase() {
    abstract val temperatureDao: TemperatureDao

    companion object {
        @Volatile
        private var INSTANCE: TemperatureDatabase? = null

        fun getInstance(context: Context): TemperatureDatabase {
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        TemperatureDatabase::class.java,
                        DATABASE_FILE_NAME
                    ).fallbackToDestructiveMigration()
                        .build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}