Android Jetpack ROOM數據庫用法介紹

ROOM數據庫用法介紹

room 數據庫有三個比較重要的部分java

  • @Database:定義一個數據庫, 同時必須使用entities屬性定義包含哪些表;使用version屬性表示數據庫的版本號,用於數據庫升級使用;同時對於Dao的實例化也是定義在@Database所註解的class內
  • @Dao:定義操做數據庫表的各類api(好比:對錶的增刪改查)
  • @Entity(實體類):定義一個table,實體類的每一個屬性 表示table的每一個字段, 除非你用@Ignore 註解

@Database 註解定義一個數據庫

  • 定義一個抽象類繼承RoomDatabase
  • 使用@Database註解這個抽象類,同時使用entities屬性配置表,version配置版本號
  • 定義一系列的Dao層的抽象方法

而後build後,會自動生成 繼承AppDataBase 的 AppDataBase_Impl 類,並自動實現全部的抽象方法android

@Database(entities = [User::class, Course::class, Teacher::class, UserJoinCourse::class, IDCard::class], version = 1)
abstract class AppDataBase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun teacherDao(): TeacherDao
    abstract fun courseDao(): CourseDao
    abstract fun userJoinCourseDao(): UserJoinCourseDao
    abstract fun idCardDao(): IDCardDao
}
複製代碼

@Dao 註解定義一個數據庫

  • 定義一個接口或抽象類,並使用@Dao註解這個類
  • 定義各類操做表的抽象方法,並使用@Query等註解對應的抽象方法

而後build後,會自動生成 繼承 UserDao 的 UserDao_Impl 類,並自動實現全部的抽象方法git

@Dao
abstract class UserDao {

    @Query("select * from tab_user")
    abstract fun getAll(): List<User>

    @Query("select * from tab_user where uid = :uid")
    abstract fun getById(uid: Long): User?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    abstract fun insert(vararg users: User): LongArray

    @Update
    abstract fun update(user: User)

    @Delete
    abstract fun delete(vararg users: User): Int
}
複製代碼

@Entity註解定義table

@Entity(tableName = "tab_user")
data class User(
    @ColumnInfo(name = "uid") // 定義列的名字
    @PrimaryKey(autoGenerate = true) // 標示主鍵,並自增加
    var uid: Long?,

    @ColumnInfo // 若是沒有指定列的名字,則使用字段名稱
    var username: String?,

    // @ColumnInfo 是非必須的,room 默認會將全部class的全部字段定義table的列
    var age: Int = 0
)
複製代碼

定義聯合主鍵

若是你須要使用多個字段一塊兒看成主鍵,則須要使用@Entity註解中的primaryKeys屬性定義聯合主鍵github

@Entity(primaryKeys = arrayOf("firstName", "lastName"))
data class User(
    val firstName: String?,
    val lastName: String?
)
複製代碼

忽略某個字段

可使用@Ignore註解sql

@Entity(primaryKeys = arrayOf("firstName", "lastName"))
data class User(
    val firstName: String?,
    val lastName: String?,
    @Ignore val picture: Bitmap?
)
複製代碼

也可使用@Entity註解中的ignoredColumns屬性數據庫

@Entity(
primaryKeys = ["firstName", "lastName"],
ignoredColumns = ["picture"]
)
data class User(
    val firstName: String?,
    val lastName: String?,
    val picture: Bitmap?
)
複製代碼

添加索引

使用@Entity註解中的indices屬性添加索引api

@Entity(indices = [Index(value = ["lastName", "address"])])
data class User(
    @PrimaryKey val id: Int,
    val firstName: String?,
    val address: String?,
    val lastName: String?
)
複製代碼

定義外鍵關聯

使用@Entity註解中的foreignKeys屬性能夠定義兩個表之間的外鍵關聯app

@Entity(tableName = "tab_course")
data class Course(
    @ColumnInfo(name = "cid") @PrimaryKey(autoGenerate = true) var cid: Long? = null,
    @ColumnInfo var name: String
)

@Entity(tableName = "tab_teacher", foreignKeys = [ForeignKey(
    entity = Course::class,
    childColumns = ["cid"], // tab_teacher的列名
    parentColumns = ["cid"] // 關聯的tab_course表的主鍵列名
	)], indices = [Index("cid")]
)
data class Teacher(
    @ColumnInfo(name = "tid") @PrimaryKey(autoGenerate = true) var tid: Long? = null,
    var name: String,
    var cid: Long? = null
)
複製代碼

一對一的關聯關係

使用 外鍵關聯 + 惟一約束 表示 一對一的關聯關係ide

好比:一個用戶只能有一張身份證,一張身份證只能被一個用戶擁有函數

// 定義user表
@Entity(tableName = "tab_user")
data class User(
    @ColumnInfo(name = "uid")
    @PrimaryKey(autoGenerate = true)
    var uid: Long?,
    @ColumnInfo
    var username: String?,
    var age: Int = 0
)

// 定義身份證表,並與user表創建一對一的關聯關係
@Entity(
    tableName = "tab_id_card",
    foreignKeys = [ForeignKey(
        entity = User::class,
        parentColumns = ["uid"],
        childColumns = ["uid"]
    )],
    indices = [
        Index("_uuid", unique = true),
        Index("uid", unique = true) // 標示惟一約束,則與tab_user是一對一的關係
    ]
)
class IDCard(
    @PrimaryKey(autoGenerate = true) var id: Long?,
    @ColumnInfo(name = "_uuid")
    var uuid: String,
    var startTime: String,
    var expireTime: String,
    @ColumnInfo(name = "uid")
    var userId: Long?
)
複製代碼

我的建議爲了方便操做,通常在user表下定一個用 @Ignore 註解的idCard字段,好比:

@Entity(tableName = "tab_user")
data class User(
    @ColumnInfo(name = "uid")
    @PrimaryKey(autoGenerate = true)
    var uid: Long?,
    @ColumnInfo
    var username: String?,
    var age: Int = 0
) {
	//爲了方便操做,因此定義一個idCard字段
    @Ignore var idCard: IDCard? = null
}
複製代碼

而後UserDao的實現以下:

@Dao
abstract class UserDao {

    @Query("select * from tab_user where uid = :uid")
    abstract fun getById(uid: Long): User?
    
    // 查詢user時,同時也查詢idcard
    fun getByIdWithIdCard(uid: Long): User? {
        val user = getById(uid)
        user?.let {
            it.idCard = AppDataBase.getInstance().idCardDao().getByForeignKey(it.uid!!)
        }
        return user
    }
}

@Dao
abstract class IDCardDao {
    @Query("select * from tab_id_card where uid in (:uid)")
    abstract fun getByForeignKey(uid: Long): IDCard?
    
    ...
}
複製代碼

一對多的關聯關係

使用 外鍵關聯 表示 一對多的關聯關係

好比:一個老師只能教一門 課程,一門課程 能夠 被多個老師教

// 定義課程table
@Entity(tableName = "tab_course")
data class Course(
    @ColumnInfo(name = "cid") @PrimaryKey(autoGenerate = true) var cid: Long? = null,
    @ColumnInfo var name: String
) {
    // 同理 爲了 方便操做,定義一個teachers字段
    @Ignore
    var teachers: List<Teacher>? = null
}

//定義老師table,並與tab_course創建一對多的關係
@Entity(tableName = "tab_teacher", foreignKeys = [ForeignKey(
    entity = Course::class,
    childColumns = ["cid"],
    parentColumns = ["cid"]
)], indices = [Index("cid")]
)
data class Teacher(
    @ColumnInfo(name = "tid") @PrimaryKey(autoGenerate = true) var tid: Long? = null,
    var name: String,
    var cid: Long? = null
) {
    // 同理 爲了 方便操做,定義一個course字段
    @Ignore
    var course: Course? = null
}
複製代碼

而後爲了方便操做,CourseDao 和 TeacherDao的實現以下:

@Dao
abstract class CourseDao {
    @Query("select * from tab_course where cid = :cid")
    abstract fun getById(cid: Long): Course?

    fun getByIdWithTeacher(cid: Long): Course? {
        return getById(cid)?.apply {
            this.teachers = AppDataBase.getInstance().teacherDao().getByForeignKey(this.cid!!)
            this.teachers?.forEach {
                it.course = this
            }
        }
    }
    ...
}

@Dao
abstract class TeacherDao {
    @Query("select * from tab_teacher where tid = :tid")
    abstract fun getById(tid: Long): Teacher?

    @Query("select * from tab_teacher where cid = :cid")
    abstract fun getByForeignKey(cid: Long): List<Teacher>
    ...
}
複製代碼

多對多的關聯關係

多對多的關聯關係,須要一箇中間表來表示

好比:一個user能夠學多門課程,一門課程也能夠被多個user學習

中間表結構以下:

@Entity(
    tableName = "tab_user_join_course",
    indices = [Index(value = ["uid", "cid"], unique = true)],
    foreignKeys = [
        // 外鍵關聯user表
        ForeignKey(entity = User::class, childColumns = ["uid"], parentColumns = ["uid"], onDelete = ForeignKey.CASCADE),
        // 外鍵關聯課程表
        ForeignKey(entity = Course::class, childColumns = ["cid"], parentColumns = ["cid"])
    ]
)
data class UserJoinCourse(
    @PrimaryKey(autoGenerate = true) var id: Long? = null,
    @ColumnInfo(name = "uid") var uid: Long,
    @ColumnInfo(name = "cid") var cid: Long
)
複製代碼

UserJoinCourseDao的實現以下:

@Dao
interface UserJoinCourseDao {

    @Query("""
        select * from tab_user
        inner join tab_user_join_course on tab_user.uid = tab_user_join_course.uid
        where tab_user_join_course.cid = :cid
    """)
    fun getUsersByCourseId(cid: Long): List<User>

    @Query("""
        select * from tab_course
        inner join tab_user_join_course on tab_course.cid = tab_user_join_course.cid
        where tab_user_join_course.uid = :uid
    """)
    fun getCoursesByUserId(uid: Long): List<Course>

    @Insert
    fun insert(vararg userJoinCourses: UserJoinCourse)
}
複製代碼

@Relation註解

@Relation註解用在查詢表的數據時,自動查詢關聯的其它數據

@Relation註解 不能用於@Entity註解的實體類中

@Relation註解 只能用於一對多(返回值必須是一個集合)

好比:先定義一個CourseWithTeacher類

class CourseWithTeacher (
    @Embedded var course: Course,

    @Relation(
        // entity 標示關聯查詢的表(非必須),默認匹配返回類型的表
        entity = Teacher::class,
        // parentColumn 表示 Course 表中的字段(能夠是Course表中的任意字段)
        // entityColumn 表示 Teacher表的 用於 查詢的字段(能夠是Teacher表中的任意字段)
        // 最後的子查詢語句是 (example:SELECT `tid`,`name`,`cid` FROM `tab_teacher` where :entityColumn in [:parentColumn])
        parentColumn = "cid",
        entityColumn = "cid")
    var teachers: List<Teacher>
)
複製代碼

而後修改 CourseDao的實現

@Dao
abstract class CourseDao {
    @Query("select * from tab_course")
    abstract fun getAll(): List<CourseWithTeacher>
}
複製代碼

及聯刪除策略

當兩張表有外鍵關聯關係的時候,好比tab_usertab_id_card表用ForeignKey實現了一對一的關聯關係,當刪除tab_user表的一條數據時,若是這條數據被tab_id_card關聯,則刪除失敗,會報 android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed錯誤;這時能夠給ForeignKey添加onDelete屬性配置及聯刪除策略

  • ForeignKey.NO_ACTION: 默認策略,不會作任何處理,發現刪除的數據被關聯,則直接報錯

  • ForeignKey.RESTRICT : 同NO_ACTION效果同樣, 但它會先檢查約束

  • ForeignKey.SET_NULL :tab_user表刪除一條數據時,則將對應的tab_id_card表的uid的值設置成NULL

  • ForeignKey.SET_DEFAULT :tab_user表刪除一條數據時,則將對應的tab_id_card表的uid的值設置成默認值,可是因爲room暫時沒辦法給column設置默認值,因此仍是會設置成 NULL

  • ForeignKey.CASCADEtab_user表刪除一條數據時,則同時也會刪除tab_id_card表的所對應的數據

    @Entity(
          tableName = "tab_id_card",
          foreignKeys = [ForeignKey(
              entity = User::class,
              parentColumns = ["uid"],
              childColumns = ["uid"],
              onDelete = ForeignKey.CASCADE
          )],
          indices = [
              Index("_uuid", unique = true),
              Index("uid", unique = true) // 標示惟一約束,則與tab_user是一對一的關係
          ]
      )
      class IDCard(
          @PrimaryKey(autoGenerate = true) var id: Long?,
          @ColumnInfo(name = "_uuid")
          var uuid: String,
          var startTime: String,
          var expireTime: String,
          @ColumnInfo(name = "uid")
          var userId: Long? = null
      )
    複製代碼

及聯更新策略 同 刪除策略一致,只是經過onUpdate配置

數據庫升級或降級

room 數據庫升級規則以下

好比在app的版本迭代過程當中version(版本號)經歷了一、二、三、4的變動

使用Migration配置每一個版本的更新規則,其構造函數必須指定startVersionendVersion

代碼實現以下:

private fun createAppDataBase(context: Context): AppDataBase {
    return Room.databaseBuilder(context, AppDataBase::class.java, "db_example")
        .addMigrations(object : Migration(1, 2) { // 從1升級到2的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 1-2===")
                // do something
            }
        }).addMigrations(object : Migration(2, 3) {// 從2升級到3的實現
            override fun migrate(database: SupportSQLiteDatabase) {
            Log.i("AppDataBase", "===Migration 2-3===")
                // do something
            }
        })
        .addMigrations(object : Migration(3, 4) {// 從3升級到4的實現
            override fun migrate(database: SupportSQLiteDatabase) {
            Log.i("AppDataBase", "===Migration 3-4===")
                // do something
            }
        })
        .addMigrations(object : Migration(1, 3) {// 從1升級到3的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 1-3===")
                // do something
            }
        }).addMigrations(object : Migration(2, 4) {// 從2升級到4的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 2-4===")
                // do something
            }
        })
        .build()
}
複製代碼
當前app數據庫version 最新的app數據庫version 升級規則 打印結果
1 4 先從1升級到3,再從3升級到4 ===Migration 1-3===
===Migration 3-4===
2 4 直接從2升級到4 ===Migration 2-4===
3 4 從3升級到4 ===Migration 3-4===
4 4 不變

總結規則以下(以當前version == 1,最新version == 4 爲例 ):

  • 先從當前 version 做爲 startVersion, 匹配最大的endVersion(即:先從1升級到3)
  • 而後再以上面匹配的endVersion最爲startVersion,又匹配最大的endVersion(即:再從3升級到4)

若是沒有匹配到對應的升級Migration配置怎麼辦呢?

默認狀況下,若是沒有匹配到升級策略,則app 直接 crash

爲了防止crash,可添加fallbackToDestructiveMigration方法配置 直接刪除全部的表,從新建立表

private fun createAppDataBase(context: Context): AppDataBase {
    return Room.databaseBuilder(context, AppDataBase::class.java, "db_example")
        .addMigrations(object : Migration(1, 2) { // 從1升級到2的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 1-2===")
                // do something
            }
         ...   
         // 若是沒有匹配到Migration,則直接刪除全部的表,從新建立表
        .fallbackToDestructiveMigration()
        .build()
}
複製代碼

指定版本號刪表重建

例如當前version 是 1或2, 升級到 4 很是麻煩,工做量太大,還不如直接刪庫重建,這個時候就能夠調用fallbackToDestructiveMigrationFrom方法指定當前version是多少的時候刪表重建

private fun createAppDataBase(context: Context): AppDataBase {
    return Room.databaseBuilder(context, AppDataBase::class.java, "db_example")
        .addMigrations(object : Migration(3, 4) {// 從1升級到3的實現
            override fun migrate(database: SupportSQLiteDatabase) {
            Log.i("AppDataBase", "===Migration 3-4===")
                // do something
            }
        })
        // 若是沒有匹配到Migration,則直接刪除全部的表,從新建立表
        .fallbackToDestructiveMigration()
        // 須要配合fallbackToDestructiveMigration方法使用,指定當前`version` 是 1或2,則直接刪除全部的表,從新建立表
        .fallbackToDestructiveMigrationFrom(1, 2)
        .build()
}
複製代碼

room 數據庫降級規則以下

好比在app的版本迭代過程當中version(版本號)經歷了一、二、三、4的變動,當前是4,須要降級到1

private fun createAppDataBase(context: Context): AppDataBase {
    return Room.databaseBuilder(context, AppDataBase::class.java, "db_example")
        .addMigrations(object : Migration(4, 3) { // 從4降級到3的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 4-3===")
                // do something
            }
        }).addMigrations(object : Migration(3, 2) {// 從3降級到2的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 3-2===")
                // do something
            }
        })
        .addMigrations(object : Migration(2, 1) {// 從2降級到1的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 2-1===")
                // do something
            }
        })
        .addMigrations(object : Migration(4, 2) {// 從4降級到2的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 4-2===")
                // do something
            }
        }).addMigrations(object : Migration(3, 1) {// 從3降級到1的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 3-1===")
                // do something
            }
        })
        .build()
}
複製代碼
當前app數據庫version 降級到app數據庫version 升級規則 打印結果
4 1 先從4降級到2,再從2降級到1 ===Migration 4-2===
===Migration 2-1===
4 2 直接從4降級到2 ===Migration 4-2===
4 3 從4降級到3 ===Migration 4-3===
4 4 不變

總結規則以下(以當前version == 4,降級到version == 1 爲例 ):

  • 先從當前 version 做爲 startVersion, 匹配最小的endVersion(即:先從4降級到2)
  • 而後再以上面匹配的endVersion最爲startVersion,又匹配最小的endVersion(即:再從2降級到1)

同理若是沒有匹配到降級規則,默認也會crash;能夠經過fallbackToDestructiveMigrationOnDowngrade方法配置刪表重建,但不能指定version刪表重建

private fun createAppDataBase(context: Context): AppDataBase {
    return Room.databaseBuilder(context, AppDataBase::class.java, "db_example")
        .addMigrations(object : Migration(4, 3) { // 從4降級到3的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 4-3===")
                // do something
            }
        })
        // 若是沒有匹配到降級Migration,則刪表重建
        .fallbackToDestructiveMigrationOnDowngrade()
        .build()
}
複製代碼

數據庫升級經常使用方法

從1 升級到 2,添加一張表tab_test表

private fun createAppDataBase(context: Context): AppDataBase {
    return Room.databaseBuilder(context, AppDataBase::class.java, "db_example")
        .addMigrations(object : Migration(1, 2) { // 從1升級到2的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 1-2===")
                database.execSQL("""
                    CREATE TABLE IF NOT EXISTS `tab_test` (
                        `uid` INTEGER PRIMARY KEY AUTOINCREMENT,
                        `username` TEXT,
                        `age` INTEGER NOT NULL
                    )
                """.trimIndent())
            }
        })
        .fallbackToDestructiveMigration()
        .fallbackToDestructiveMigrationOnDowngrade()
        .build()
}
複製代碼

從2 升級到 3,給tab_test表添加 desc 字段

private fun createAppDataBase(context: Context): AppDataBase {
    return Room.databaseBuilder(context, AppDataBase::class.java, "db_example")
        .addMigrations(object : Migration(1, 2) { // 從1升級到2的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 1-2===")
                database.execSQL("""
                    CREATE TABLE IF NOT EXISTS `tab_test` (
                        `uid` INTEGER PRIMARY KEY AUTOINCREMENT,
                        `username` TEXT,
                        `age` INTEGER NOT NULL
                    )
                """.trimIndent())
            }
        })
        .addMigrations(object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 2-3===")
                database.execSQL("ALTER TABLE `tab_test` ADD COLUMN `desc` TEXT")
            }
        })
        .fallbackToDestructiveMigration()
        .fallbackToDestructiveMigrationOnDowngrade()
        .build()
}
複製代碼

從3 升級到 4,給tab_test表 desc 字段 重命名爲 desc2

private fun createAppDataBase(context: Context): AppDataBase {
    return Room.databaseBuilder(context, AppDataBase::class.java, "db_example")
        .addMigrations(object : Migration(1, 2) { // 從1升級到2的實現
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 1-2===")
                database.execSQL("""
                    CREATE TABLE IF NOT EXISTS `tab_test` (
                        `uid` INTEGER PRIMARY KEY AUTOINCREMENT,
                        `username` TEXT,
                        `age` INTEGER NOT NULL
                    )
                """.trimIndent())
            }
        })
        .addMigrations(object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 2-3===")
                database.execSQL("ALTER TABLE `tab_test` ADD COLUMN `desc` TEXT")
            }
        })
        .addMigrations(object : Migration(3, 4) {
            override fun migrate(database: SupportSQLiteDatabase) {
                Log.i("AppDataBase", "===Migration 3-4===")
                // 重命名tmp_tab_test
                database.execSQL("ALTER TABLE `tab_test` RENAME TO `tmp_tab_test`")
                // 從新建立表tab_test
                database.execSQL("""
                    CREATE TABLE IF NOT EXISTS `tab_test` (
                        `uid` INTEGER PRIMARY KEY AUTOINCREMENT,
                        `username` TEXT,
                        `age` INTEGER NOT NULL,
                        `desc2` TEXT
                    )
                """.trimIndent())
                // 將表tmp_tab_test的數據複製到tab_test
                database.execSQL("insert into `tab_test` select * from `tmp_tab_test`")
                // 刪除tmp_tab_test表
                database.execSQL("drop table `tmp_tab_test`")
            }
        })
        .fallbackToDestructiveMigration()
        .fallbackToDestructiveMigrationOnDowngrade()
        .build()
}
複製代碼

gradle 依賴

implementation "androidx.room:room-runtime:2.1.0"
kapt "androidx.room:room-compiler:2.1.0" // For Kotlin use kapt instead of annotationProcessor
複製代碼

其它建議

強烈建議使用 facebookstetho library 配合調試你的數據庫

###快速查詢tab_user表的建表語句(方便升級時建表使用)

SELECT sql FROM sqlite_master WHERE name='tab_user';
複製代碼
相關文章
相關標籤/搜索