天天看點

Android 最實用的Room入門

文章目錄

    • 一. 簡介與導入
    • 二 . 應用
        • 1. 利用注解entity定義實體類
        • 2. 定義Dao,用于操作資料,進行增删改查
        • 3. 定義database
        • 4. 資料庫的更新與降級
        • 5. 表關聯
    • 三. 其他可能會用的一點技巧
        • TypeConverter
        • Embedded
        • rxjava2
        • 補充

一. 簡介與導入

Andorid官方中推薦Room代替SQlite,是以新的項目中直接舍棄了以前用的第三那方架構greenDao

Room由三部分組成,并且用三個注解标注:

Entity: 這個注解表示的是實體類,代表的是資料庫中的表,每一個實體類都是一張表

Dao:改注解标注的是一個接口,接口中封裝的是操作資料庫的方法,比如增删改查

database:這個注解标注的是一個資料的持有者,他是一個抽象類,并且持有一個接口dao的抽象方法,還需在這個抽象類中添加上所有實體的辨別,另外他需要繼承RoomDatabase。其實原理是在編譯階段,根據注解,就會編譯出具體的實作類,是以對于我們來說這就簡單的很多,因為所有的代碼都是編譯器根據規則生成的。

是以我們需要再gradle中加入:

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
           

需要依賴:

implementation "androidx.room:room-runtime:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
implementation "androidx.room:room-ktx:2.3.0"
           

加入kapt的目的就是編譯注解。kapt是一個注解處理插件

二 . 應用

1. 利用注解entity定義實體類

比如:

@Entity(tableName = "student")
data class Student(
    val name: String,
    val age:String,
) {
    @PrimaryKey(autoGenerate = true)
    var id:Long = 0L
}
           

Entity注解标記這個類是一個表,可以自定義表明tableName,如果不指定的話預設是類的名字,一個表中必須指定一個主鍵,如果不指定主鍵編譯會報錯,可以使用注解@PrimaryKey 指定某一個屬性為主鍵,其中autoGenerate表示的是自增,如果不在屬性上标記主鍵的話,可以再@Entity上面指定,比如指定姓名為主鍵:

也可以指定聯合主鍵:

Entity這個注解還有其他的幾個值,但是不怎麼常用

Index[] indices() default {}; 索引,可以為表添加索引
boolean inheritSuperIndices() default false; 表示的是父類的索引是否可以被目前的類繼承
ForeignKey[] foreignKeys() default {}; 需要依賴的外鍵,現在基本上沒人會用外鍵
,都是根據邏輯關系控制具體的資料
String[] ignoredColumns() default {}; 可以忽略的字段
           

當資料量很大的時候,可以為了快速查找,可以為某一列或者多列添加索引,不過一般應該用不到,主要是Android端一般也不會存儲特别大的資料量

@Entity(tableName = "student", 
indices = [Index("name"), Index(value = ["name", "age"])])
           

還有一個就是外鍵,用處不大,一般沒人用,了解下就好:

@Entity(tableName = "student",
    foreignKeys = [ForeignKey(entity = ClassRoom::class,parentColumns = ["id"],
        childColumns = ["classRoomId"])])
           

parentColumns 指的是所依賴的表的主鍵,childColumns 目前表中的所依賴的表的id

預設情況下,Entity标注的類中的屬性值都是表中的一列,如果某一列不需要存儲在表中,可以用ignoredColumns 進行忽略,不過一般都是用單獨的注解進行忽略,比如:

@Ignore
val classRoomId:String
           

如果列不想用預設的屬性名字的話可以用@ColumnInfo進行指定:

@ColumnInfo(name = "age")
val age:String,
           

ColumnInfo這個注解下面有幾個屬性可以使用:

String defaultValue() default VALUE_UNSPECIFIED; 設定預設值
boolean index() default false; 是不是索引列
@Collate int collate() default UNSPECIFIED; 列的排列順序
           

設定預設值可能會用,其他的兩個基本上也不會用

2. 定義Dao,用于操作資料,進行增删改查

定義的dao是一個接口,用Dao進行标注,例如查找所有的學生:

@Dao
interface StudentDao {
    @Query("select * from student")
    fun findAll():List<Student>
}
           

如果根據條件查詢的話,那可可以進行将值傳過來,寫where語句例如:

@Query("select * from student where id = :id ")
fun findById(id:Long): Student
           

:後面的參數即為方法中的參數,需要多少傳多少即可

其實這樣一個方法的上面隻要寫sql就好了,還是很簡單的,在比如,相求 總的資料條數:

@Query("select count(*) from student")
fun findCount():Int
           

是不是很簡單,當然如果你想聯合查詢某幾個表,隻是保留某幾個字段的話,那就需要指定列名,并且和你傳回的實體類的屬性一一對應即可,比如:

@Query("select s.name as studentName, r.class_name as roomName from student as s  left join class_room as r on r.class_id = s.roomId")
fun getAllName():List<StudentRoom>
data class StudentRoom(val studentName:String,val roomName:String) {}
           

insert也很簡單,直接用@Insert注解即可

@Insert
fun insertStudent(student: Student)
           

如果想确定是不是插入成功的的話,可以加一個傳回參數的,傳回的是你插入的id吧

當然可以批量插入,用可變參數或者list都可以,傳回的結果也是一個list,表示每一個插入成功的id

其實編譯之後,可以點進去看看源碼的:

@Override
public long insertStudent(final Student student) {
  __db.assertNotSuspendingTransaction();
  __db.beginTransaction();
  try {
    long _result = __insertionAdapterOfStudent.insertAndReturnId(student);
    __db.setTransactionSuccessful();
    return _result;
  } finally {
    __db.endTransaction();
  }
}
           

明顯傳回的是ID

也可以用可變參數,kotlin是vararg表示可變參數

更新和删除的話,可以這麼幹:

@Update
fun updateStudent(student: Student)
@Delete
fun deleteStudent(student: Student)
           

都是根據主鍵進行更新和删除的,但是這裡有一個問題是,更新的話就對于這個主鍵的一行資料全部更新了,是以有時候隻需要更新某一個字段的話,就需要用另外一種方法了,使用sql語句

比如 我隻想更新學生的名字:

@Query("update student set name = :name where id = :id")
fun updateNameForStudent(name:String,id:Long)
           

同理,delete其實也可以寫sql語句

3. 定義database

有了表和操作方法,沒有資料庫,是以我們需要定義一個資料庫,資料庫中指定該資料庫中有哪些表和操作方法,需要一個抽象方法,并繼承RoomDatabase,如下:

@Database(entities = [Student::class, ClassRoom::class], version = 1)
abstract class AppDatabase : RoomDatabase(){

    abstract fun studentDao(): StudentDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase?= null
        fun getInstance(context: Context): AppDatabase =
            INSTANCE?: synchronized(this){
                INSTANCE?:buildDatabase(context).also {
                    INSTANCE = it
                }
            }

        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
                .allowMainThreadQueries()
                .build()
    }
}
           

entities 指定所有的實體類,version指定目前資料庫的版本,并且裡面有一個抽象方法,傳回值就是你的dao,這就可以了,剩下的編譯插件會自動給你實作。接下來就是建立資料庫了,建立書資料庫的方法是這個:

Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
                .allowMainThreadQueries()
                .build()
           

指定你的資料庫抽象類和資料庫的名稱,并建立,這個地方法你可以再任何用到資料庫的地方執行,但是通常我們會寫再database中,并改造成單例模式,這是避免有多個資料的引用,避免資源的浪費,到這兒基本就結束了,剩下的就是調用了, .allowMainThreadQueries() 這個方法事運作在主線程中調用,如果不加的話,在主線程調用會報錯的,比如插入一個學生:

AppDatabase.getInstance(this).studentDao()
.insertStudent(Student("Marry","24",1))
           

4. 資料庫的更新與降級

表的增加,表的修改,是避免不了的,是以這個時候需要表的更新

比如我增加了一個位址表的話

就應該,加上address這個實體類,并将版本号更新

@Database(entities = [Student::class, ClassRoom::class, Address::class],
 version = 2)
           

如果隻是這樣,那app就挂了,還需要加上一些其他的處理,有時候更新的時候,可能需要删除某些資料,并将資料遷移等等,還需要一個方法:

Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
    .addMigrations(MIGRATION_1_2)
    .allowMainThreadQueries()
    .build()
    
     private val MIGRATION_1_2 = object : Migration(1,2){
            override fun migrate(database: SupportSQLiteDatabase) {
            }
        }
           

每更新一次,都需要加一個這個方法,比如2到3

private fun buildDatabase(context: Context) =
            Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                .allowMainThreadQueries()
                .build()


        private val MIGRATION_1_2 = object : Migration(1,2){
            override fun migrate(database: SupportSQLiteDatabase) {
             
            }
        }

        private val MIGRATION_2_3 = object : Migration(2,3){
            override fun migrate(database: SupportSQLiteDatabase) {
            }
        }
           

更新的規則是每升一級都要有一個相應的方法,如果直接第一個版本更新到第三個版本的話,預設會先執行更新到2,然後更新到3

另外還有一個特别需要注意的問題是,如果添加了新表的話,不隻需要在database這個類中entities 指定,還需要再更新的時候去寫sql建立表,才行,否則會報錯。

比如我想加一個address的表,

@Database(entities = [Student::class,
 ClassRoom::class, Address::class], version = 2)
 private val MIGRATION_1_2 = object : Migration(1,2){
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE IF NOT EXISTS `address` (`addressId` INTEGER NOT NULL, `addressName` TEXT NOT NULL, PRIMARY KEY(`addressId`))")
    }
}
           

這樣才行,另外不要自己寫這個sql語句,去你自動生成的建立語句copy,否則,自己寫如果和自動生成的create語句不一緻的話,也會報錯。

預設生成的create的語句在你的database_impl這個類中

public void createAllTables(SupportSQLiteDatabase _db) {
  _db.execSQL("CREATE TABLE IF NOT EXISTS `student` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `age` TEXT NOT NULL, `roomId` INTEGER NOT NULL)");
  _db.execSQL("CREATE TABLE IF NOT EXISTS `class_room` (`class_id` INTEGER NOT NULL, `class_name` TEXT NOT NULL, PRIMARY KEY(`class_id`))");
  _db.execSQL("CREATE TABLE IF NOT EXISTS `address` (`addressId` INTEGER NOT NULL, `addressName` TEXT NOT NULL, PRIMARY KEY(`addressId`))");
  _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
  _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7cbdd6263025181ec070edd36e1118eb')");
           

另外更新添加字段的時候,一定不要忘了 添加預設值,否則也會有問題的

如果資料庫要降級的話需要添加這.fallbackToDestructiveMigration(),預設删除所有的表,重新建立,當然所有的資料也就沒了

Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
    .fallbackToDestructiveMigration()
    .allowMainThreadQueries()
    .build()
           

5. 表關聯

Room也是關系型資料庫,是以表和表之間可以有一對一的關系,一對多的關系,多對多的關系

一對一的關系

比如一個學生對應一個教室:根據這個學生查出所對應的教室:

學生表:

@Entity(tableName = "student")
data class Student(
    val name: String,
    val age:String,
    val roomId:Long
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id:Long = 0L
}
           

教室表:

@Entity(tableName = "class_room")
data class ClassRoom(@PrimaryKey val class_id:Long, var class_name:String) {}
           

查詢所對應的關系表:

data class StudentRoom(
    @Embedded val student: Student,
    @Relation(
       parentColumn = "roomId",
        entityColumn = "class_id"
    )
    val classRoom: ClassRoom
) {}
           

對應的查詢語句:

@Query("select * from student")
fun getAllStudent():List<StudentRoom>
           

多對一的的話,那就是一個教室對應多個學生,相對來說也很簡單:

data class StudentRoom(
    @Embedded val student: Student,
    @Relation(
       parentColumn = "roomId",
        entityColumn = "class_id"
    )
    val classRoom: ClassRoom
) {}
           

查詢語句:

@Query("select * from class_room")
fun getAllRoom():List<RoomStudent>
           

還有一種多對多的關系,暫時先不看了,也用不到

三. 其他可能會用的一點技巧

TypeConverter

有時候資料庫中的一些資料是無法存儲的,比我Student,每一個學生都有些朋友,朋友很多,我隻想存儲下他的名字,那就是一個list:

data class Student(
    val name: String,
    val age:String,
    val roomId:Long,
    val friend:List<String>
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id:Long = 0L
}
           

這個時候肯定會報錯,因為資料庫不明白你這個list是什麼東西,是以我們可以對他進行轉換,比如說轉成string類型,是以就有了一個注解TypeConverter:

class MyConverters {

    @TypeConverter
    fun listToString(value:List<String>):String{
        val sb = StringBuilder()
        value.forEach {
            sb.append(",").append(it)
        }
        return sb.toString().substring(1)
    }

    @TypeConverter
    fun stringToList(value:String):List<String>{
        return value.split(",")
    }
}
           

定義一個類,類中有兩個方法,一個是轉string,一個是轉list,這兩個一定要成對出現,用于實作自動轉換,接着在實體類上标注下即可:

@Entity(tableName = "student")
@TypeConverters(MyConverters::class)
data class Student(
    val name: String,
    val age:String,
    val roomId:Long,
    val friend:List<String>
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id:Long = 0L
}
           

系統會自動轉list和string,比如insert語句中,生成的代碼:

_tmp = __myConverters.listToString(value.getFriend());
 public void bind(SupportSQLiteStatement stmt, Student value) {
    stmt.bindLong(1, value.getId());
    if (value.getName() == null) {
      stmt.bindNull(2);
    } else {
      stmt.bindString(2, value.getName());
    }
    if (value.getAge() == null) {
      stmt.bindNull(3);
    } else {
      stmt.bindString(3, value.getAge());
    }
    stmt.bindLong(4, value.getRoomId());
    final String _tmp;
    _tmp = __myConverters.listToString(value.getFriend());
    if (_tmp == null) {
      stmt.bindNull(5);
    } else {
      stmt.bindString(5, _tmp);
    }
  }
};
           

Embedded

還有一個注解 可能也會用到 @Embedded,這個表示嵌套對象

比如Student中我使用@Embedded 嵌套一個實體類,這個實體類就是一個普通的類

@Entity(tableName = "student")
data class Student(
    val name: String,
    val age:String,
    val roomId:Long,
    @Embedded val test: Test
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id:Long = 0L
}

data class Test(val testName:String) {
}
           

test中表的字段會在建立student表的時候建立出來,是以student會多出testName這一列。

rxjava2

還可以使用rxjava,進行異步操作,需要引入依賴

比如:

@Query("select * from student")
fun getAllStudent():Observable<List<StudentRoom>>
           

補充

Andorid中contentProvider是非常常用的四大主件之一,用于提供資料,當我們使用了room之後,怎麼對應contentProvider中的查詢,删除,修改呢?

AppDatabase.getInstance(context!!.applicationContext).openHelper
            .writableDatabase.query(SupportSQLiteQueryBuilder
                .builder("student")
                .selection(selection, selectionArgs)
                .columns(projection).orderBy(sortOrder).create())
           

用到的是SupportSQLiteQueryBuilder,進行怎删改語句的建立,用來接收傳過來的參數,是不是很簡單?

再比如update: