初探Jetpack(一) – ViewModel
初探Jetpack(二) – Lifecycles
初探Jetpack(三) – LiveData
Demo工程
Android 雖然自身攜帶SQLite,但是操作比較麻煩,而且如果再大型項目,會變得比較混亂且難以維護,除非你設計了一套非常好的架構和封裝。
當然,如果要操作簡單的話,郭老師的 Litepal 算不錯的,不過我們今天學習 google 在 Jetpack 中帶的組件 — ROOM ,今天一起來學習。
一 、ROOM 的基本使用
首先,ROOM 由 Entity ,Dao 和 Database 三個部分組成:
- Entity:用於定義iFeng準給實際數據的實體類,每個實體類都會在數據庫中對應一張表,並且表中的列是根據實體類的字段自動生成的。
- Dao :Dao 是數據訪問對象的意思,通常會在這裏對數據庫的各項操作進行封裝,比如 增刪查改,這樣,訪問數據時,就不用去管底層數據庫了,只需要跟Dao打交道即可
- Database:用於定義數據庫中的關鍵信息,包括版本號,包含哪些實體類以及 提供 Dao 的訪問層
如果你需要使用 ROOM ,需要在你的moudle build.gradle 添加插件
apply plugin: 'kotlin-kapt'
關聯依賴:
//room
implementation 'androidx.room:room-runtime:2.1.0'
kapt 'androidx.room:room-compiler:2.1.0'
kpt 爲註解的意思,相當於 java 的 annotationProcessor。
1.2 創建實體類
接着,我們創建一個實體類,User:
@Entity
data class UserData (
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") var lastName: String?
)
可以看到,我們用了 @Entity 註解,將它申明成一個實體類,然後添加了一個 id 字段,並使用 @PrimaryKey 註解將它設置成主鍵,然後參數使用 @ColumnInfo 註解,表示表的列。
1.3 設置 Dao
在數據庫中,最常見的就是 增刪查改了,但業務是千變萬化的,而 Dao 要做的事情就是覆蓋這些業務,這樣我們的邏輯就只要和 Dao 打交道,而不用去理會底層數據庫。
新建一個 UserDao ,注意必須是接口,然後編寫以下代碼:
@Dao
interface UserDao {
@Query("SELECT * FROM userdata")
fun getAll(): List<UserData>
@Update
fun updateUser(user: UserData)
@Query("SELECT * FROM userdata WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
fun findByName(first: String, last: String): UserData
@Insert
fun insertAll(vararg users: UserData)
@Delete
fun delete(user: UserData)
@Query("delete from UserData where last_name = :lastName")
fun deleteByLastName(lastName:String) :Int
}
可以看到,UserDao 用 @Dao 註解,ROOM 纔會識別成 Dao,並提供了 @Insert、@Update、 @Delete, @Query 四種對應的註解。
其中 @Insert 插入數據後,會返回自動生辰的主鍵 id,而特別要注意則是 @Query 註解,ROOM 無法知道我們要查詢哪些數據,因此必須編寫 SQL 語句纔行。
如果我們不是用實體類參數去 增刪改 數據,那麼也要編寫SQL 語句纔行,這個時候,不能使用 @Inset @Delete @update 註解,而都是使用 @Query 註解纔行,比如上面的 deleteByLastName 方法。
雖然要編寫 SQL 語句這點不太友好,但Room是編譯時動態檢查 SQL 語句的,也就是說,如果你的SQL 沒有寫對,編譯時就會報錯。
1.3 編寫 Database
Database 需要定義版本號啊,包含了哪些實體類,以及提供 Dao 層的訪問實例即可。新建一個 AppDatabase.kt ,代碼如下:
@Database(version = 1,entities = [UserData::class])
abstract class AppDataBase : RoomDatabase(){
abstract fun userDao() : UserDao
companion object{
private var instance : AppDataBase ? = null;
@Synchronized
fun getDatabase(context:Context) : AppDataBase{
instance?.let {
return it
}
val db = Room.databaseBuilder(
context.applicationContext,
AppDataBase::class.java, "AppDataBase"
).build()
return db.apply {
instance = this;
}
}
}
}
可以看到,AppDataBase 類的頭部使用了 @Database 的註解,並填寫了版本號,以及實體類,如果有多個實體類,用逗號隔開即可。
需要注意的是,AppDataBase 需要申明爲抽象類,並申明 Dao 的抽象方法,比如 userDao()。
接着,由於Dao 理論上應該就是一個單例模式,所以這裏用 companion objec 修飾,當 getDatabase 中,如果已經存在,則直接返回,如果不存在,則通過 Room.databaseBuilder 去創建build,它接收三個參數:
- context:這裏最好用 applicationContext 防止內存泄漏
- class 類型,這裏傳遞 AppDataBase::class.java
- 數據庫名
然後通過 apply 拿到實例,並賦值給 instance 即可。
ok,Room 的配置已經完成了,接着,我們在 xml 中添加 4 個按鈕:
<Button
android:id="@+id/addDataBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add data"/>
<Button
android:id="@+id/updateBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update data"/>
<Button
android:id="@+id/deleteBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="delete data"/>
<Button
android:id="@+id/queryBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="query data"/>
在 activity 中添加代碼:
val userDao = AppDataBase.getDatabase(this).userDao();
val user1 = UserData(1,"z","sr")
val user2 = UserData(2,"w","y")
//添加數據
addDataBtn.setOnClickListener{
thread {
userDao.insertAll(user1,user2)
}
}
//更新數據
updateBtn.setOnClickListener{
thread {
user2.lastName = "san"
userDao.updateUser(user2)
}
}
//刪除
deleteBtn.setOnClickListener{
thread {
// userDao.delete(user1)
userDao.deleteByLastName("sr")
}
}
//查詢
queryBtn.setOnClickListener{
thread {
for (user in userDao.getAll()){
Log.d(TAG, "zsr onCreate: "+user.toString())
}
}
}
ROOM 要求查詢數據庫在 線程中,所以這裏用了 thread{},如果你覺得需要在主線程中去更新,可以在配置中設置:
val db = Room.databaseBuilder(
context.applicationContext,
AppDataBase::class.java, "AppDataBase"
).allowMainThreadQueries().build()
點擊增加,然後按查詢:
點擊更新,然後查詢:
點擊刪除,然後查詢:
二、數據庫升級
ROOM 的數據庫升級比較麻煩,如果在測試階段,可以使用 fallbackToDestructiveMigration() 強制升級
val db = Room.databaseBuilder(
context.applicationContext,
AppDataBase::class.java, "AppDataBase"
).fallbackToDestructiveMigration().build()
2.1 、新增一張表
但,僅限於測試,如果我要增加一張表呢?比如增加一個 Book:
@Entity
data class Book(var name:String,var pages:Int) {
@PrimaryKey(autoGenerate = true)
var id:Long = 0;
}
並添加一個 BookDao 接口:
@Dao
interface BookDao {
@Insert
fun insertBook(book: Book)
@Query("select * from Book")
fun loadAllBooks() : List<Book>
}
然後修改 AppDataBase:
@Database(version = 2, entities = [UserData::class, Book::class])
abstract class AppDataBase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun bookDao(): BookDao
companion object {
val Migration1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("create table Book (id integer primary key autoincrement not null," +
"name text not null ,pages integer not null)")
}
}
private var instance: AppDataBase? = null;
@Synchronized
fun getDatabase(context: Context): AppDataBase {
instance?.let {
return it
}
val db = Room.databaseBuilder(
context.applicationContext,
AppDataBase::class.java, "AppDataBase"
).addMigrations(Migration1_2).build()
return db.apply {
instance = this;
}
}
}
}
可以看到,我們在 @Database 註解中,修改版本爲2,並添加 BookDao 類。
接着,實現一個 Migration 匿名類,重寫 migrate 方法,並在裏面編寫 SQL 語句,添加一個 Book 表;接着,在 Room.databaseBuilder() 那裏,通過 addMigrations(Migration1_2) 添加進去。
這樣,當 SQL 版本從1變到2 的時候,就會執行 Migration1_2 裏面的方法。
接着,調用一下:
val bookDao = AppDataBase.getDatabase(this).bookDao()
//添加數據
addDataBtn.setOnClickListener{
thread {
bookDao.insertBook(Book("android",100))
}
}
//查詢
queryBtn.setOnClickListener{
thread {
for (book in bookDao.loadAllBooks()){
Log.d(TAG, "zsr onCreate: "+book.toString())
}
}
}
點擊插入,並查詢:
2.2 、新增字段
但不是每次升級都升級一張表,假如要加贈一個字段呢?比如新增 Book 的作者名,author,修改 Book 類:
@Entity
data class Book(var name:String,var pages:Int,var author:String) {
@PrimaryKey(autoGenerate = true)
var id:Long = 0;
}
由於 Book 變動了,所以,AppDatabase 也需要改變:
@Database(version = 3, entities = [UserData::class, Book::class])
abstract class AppDataBase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun bookDao(): BookDao
companion object {
...
val Migration2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("alter table Book add column author text not null default 'unknown'")
}
}
private var instance: AppDataBase? = null;
@Synchronized
fun getDatabase(context: Context): AppDataBase {
instance?.let {
return it
}
val db = Room.databaseBuilder(
context.applicationContext,
AppDataBase::class.java, "AppDataBase"
).addMigrations(Migration1_2,Migration2_3).build()
return db.apply {
instance = this;
}
}
}
}
可以看到,版本改稱3,且增加了一個 Migration2_3 ,SQL 語句使用 alert 插入一個列。
這樣,我們就學習完成了。
參考:
第一行代碼,第三版
官網 ROOM