前言:
🏀在我們日常開發中,經常要和資料打交道,是以存儲資料是很重要的事。Android從最開始使用SQLite作為資料庫存儲資料,再到許多的開源的資料庫,例如QRMLite,DBFlow,郭霖大佬開發的Litepal等等,都是為了友善SQLite的使用而出現的,因為SQLite的使用繁瑣且容易出錯。Google當然也意識到了SQLite的一些問題,于是在Jetpack元件中推出了Room,本質上Room也是在SQLite上提供了一層封裝。因為它官方元件的身份,和良好的開發體驗,現在逐漸成為了最主流的資料庫ORM架構。
🌟Room官方文檔:<https://developer.android.google.cn/jetpack/androidx/releases/room>
🌟SQL文法教程:<https://www.runoob.com/sqlite/sqlite-tutorial.html>
🚀本文代碼位址:<https://github.com/taxze6/Jetpack_learn/tree/main/Jetpack_basic_learn/room>
為什麼要使用Room?Room具有什麼優勢?
Room在SQLite上提供了一個抽象層,以便在充分利用SQLite的強大功能的同時,能夠享有更強健的資料庫通路機制。
Room的具體優勢:
- 有可以最大限度減少重複和容易出錯的樣闆代碼的注解
- 簡化了資料庫遷移路徑
- 針對編譯期
的文法檢查SQL
- API設計友好,更容易上手,了解
- 與
語句的使用更加貼近,能夠降低學習成本SQL
- 對
、RxJava
、LiveData
協程等都支援Kotlin
Room具有三個主要子產品
- Entity:
用來表示資料庫中的一個表。需要使用Entity
注解,其中的參數為表名。@Entity(tableName = "XXX")
- Dao:資料庫通路對象,用于通路和管理資料(增删改查)。在使用時需要
注解@DAO
- Database:它作為資料庫持有者,用
注解和@Database
擴充的類Room Database
如何使用Room呢?
①添加依賴
最近更新時間(文章釋出時的最新版本) | 穩定版 | Alpha 版 |
2022 年 6 月 1 日 | 2.4.2 | 2.5.0-alpha02 |
plugins {
...
id 'kotlin-kapt'
}
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt 'androidx.room:room-compiler:$room_version'
②建立
Entity
實體類,用來表示資料庫中的一張表(table)
@Entity(tableName = "user")
data class UserEntity(
//主鍵定義需要用到@PrimaryKey(autoGenerate = true)注解,autoGenerate參數決定是否自增長
@PrimaryKey(autoGenerate = true) val id:Int = 0, //預設值為0
@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT) val name:String?,
@ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER) val age:Int?
)
其中,每個表的字段都要加上
@ColumnInfo(name = "xxx", typeAffinity = ColumnInfo.xxx)
,
name
屬性表示這張表中的字段名,
typeAffinity
表示改字段的資料類型。
其他常用注解:
- @Ignore:
中的所有屬性都會被持久化到資料庫,除非使用Entity
@Ignore
@Ignore val name: String?
- @ForeignKey:外鍵限制,不同于目前存在的大多數ORM庫,Room不支援Entitiy對象間的直接引用。Google也做出了解釋,具體原因請檢視:<https://developer.android.com/training/data-storage/room/referencing-data>,不過
允許通過外鍵來表示Room
之間的關系。Entity
我們文章後面再談,先講簡單的使用。ForeignKey
- @Embedded:實體類中引用其他實體類,在某些情況下,對于一張表的資料,我們用多個
類來表示,是以在這種情況下,我們可以使用POJO
注解嵌套對象。Embedded
③建立資料通路對象(Dao)處理增删改查
@Dao
interface UserDao {
//添加使用者
@Insert
fun addUser(vararg userEntity: UserEntity)
//删除使用者
@Delete
fun deleteUser(vararg userEntity: UserEntity)
//更新使用者
@Update
fun updateUser(vararg userEntity: UserEntity)
//查找使用者
//傳回user表中所有的資料
@Query("select * from user")
fun queryUser(): List<UserEntity>
}
Dao
負責提供通路
DB
的
API
,我們每一張表都需要一個
Dao
。在這裡使用
@Dao
注解定義
Dao
類。
-
,@Insert
需要傳一個@Delete
進去entity()
Class<?> entity() default Object.class;
-
則是需要傳遞@Query
語句SQL
public @interface Query {
//要運作的SQL語句
String value();
}
☀注意:Room會在編譯期基于Dao自動生成具體的實作類,UserDao_Impl(實作增删改查的方法)。
🔥Dao所有的方法調研都在目前線程進行,需要避免在UI線程中直接通路!
④建立Room database
@Database(entities = [UserEntity::class], version = 1)
abstract class UserDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
通過
Room.databaseBuilder()
或者
Room.inMemoryDatabaseBuilder()
擷取
Database
執行個體
val db = Room.databaseBuilder(
applicationContext,
UserDatabase::class.java, "userDb"
).build()
☀注意:建立的成本較高,是以我們最好使用單例的
Database
,避免反複建立執行個體所帶來的開銷。
Database
單例模式建立Database:
@Database(entities = [UserEntity::class], version = 1)
abstract class UserDatabase : RoomDatabase() {
abstract fun getUserDao(): UserDao
companion object {
@Volatile
private var INSTANCE: UserDatabase? = null
@JvmStatic
fun getInstance(context: Context): UserDatabase {
val tmpInstance = INSTANCE
if (tmpInstance != null) {
return tmpInstance
}
//鎖
synchronized(this) {
val instance =
Room.databaseBuilder(context, UserDatabase::class.java, "userDb").build()
INSTANCE = instance
return instance
}
}
}
}
⑤在Activity中使用,進行一些可視化操作
activity_main:
<LinearLayout
...
tools:context=".MainActivity"
android:orientation="vertical">
<Button
android:id="@+id/btn_add"
...
android:text="增加一條資料"/>
<Button
android:id="@+id/btn_delete"
...
android:text="删除一條資料"/>
<Button
android:id="@+id/btn_update"
...
android:text="更新一條資料"/>
<Button
android:id="@+id/btn_query_all"
...
android:text="查新所有資料"/>
</LinearLayout>
MainActivity:
private const val TAG = "My_MainActivity"
class MainActivity : AppCompatActivity() {
private val userDao by lazy {
UserDatabase.getInstance(this).getUserDao()
}
private lateinit var btnAdd: Button
private lateinit var btnDelete: Button
private lateinit var btnUpdate: Button
private lateinit var btnQueryAll: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
//添加資料
btnAdd.setOnClickListener {
//資料庫的增删改查必須在子線程,當然也可以在協程中操作
Thread {
val entity = UserEntity(name = "Taxze", age = 18)
userDao.addUser(entity)
}.start()
}
//查詢資料
btnQueryAll.setOnClickListener {
Thread {
val userList = userDao.queryUser()
userList.forEach {
Log.d(TAG, "查詢到的資料為:$it")
}
}.start()
}
//修改資料
btnUpdate.setOnClickListener {
Thread {
userDao.updateUser(UserEntity(2, "Taxzeeeeee", 18))
}.start()
}
//删除資料
btnDelete.setOnClickListener {
Thread {
userDao.deleteUser(UserEntity(2, null, null))
}.start()
}
}
//初始化
private fun init() {
btnAdd = findViewById(R.id.btn_add)
btnDelete = findViewById(R.id.btn_delete)
btnUpdate = findViewById(R.id.btn_update)
btnQueryAll = findViewById(R.id.btn_query_all)
}
}
結果:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAnYldHL0FWby9mZvwFN4ETMfdHLkVGepZ2XtxSZ6l2clJ3LcV2Zh1Wa9M3clN2byBXLzN3btgHL9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsQTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5yM3ATNykTZ5U2NyUTYjBDZyYzXwUDMwATM1EzLcdDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
到這裡我們已經講完了Room的最基本的使用,如果隻是一些非常簡單的業務,你看到這裡已經可以去寫代碼了,但是還有一些進階的操作需講解一下,繼續往下看吧!
資料庫的更新
Room在2021 年 4 月 21 日釋出的版本 2.4.0-alpha01中開始支援自動遷移,不過很多朋友反應還是有很多問題,建議手動遷移,當然如果你使用的是更低的版本隻能手動遷移啦。
具體資訊請參考:<https://developer.android.google.cn/training/data-storage/room/migrating-db-versions#manual>
具體如何更新資料庫呢?下面我們一步一步來實作吧!
①修改資料庫版本
在
UserDatabase
檔案中修改
version
,将其變為2(原來是1)
在此時,我們需要想一想,我們要對資料庫做什麼更新操作呢?
我們這裡為了示範就給資料庫增加一張成績表:
@Database(entities = [UserEntity::class,ScoreEntity::class], version = 2)
添加表:
@Entity(tableName = "score")
data class ScoreEntity(
@PrimaryKey(autoGenerate = true) var id: Int = 0,
@ColumnInfo(name = "userScore")
var userScore: Int
)
②建立對應的Dao,ScoreDao
@Dao
interface ScoreDao {
@Insert
fun insertUserScore(vararg scoreEntity: ScoreEntity)
@Query("select * from score")
fun queryUserScoreData():List<ScoreEntity>
}
③在Database中添加遷移
@Database(entities = [UserEntity::class,ScoreEntity::class], version = 2)
abstract class UserDatabase : RoomDatabase() {
abstract fun getUserDao(): UserDao
//添加一個Dao
abstract fun getScoreDao():ScoreDao
companion object {
//變量名最好為xxx版本遷移到xxx版本
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
create table userScore(
id integer primary key autoincrement not null,
userScore integer not null)
""".trimIndent()
)
}
}
@Volatile
private var INSTANCE: UserDatabase? = null
@JvmStatic
fun getInstance(context: Context): UserDatabase {
...
synchronized(this) {
val instance =
Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"userDb"
)
.addMigrations(MIGRATION_1_2)
.build()
INSTANCE = instance
return instance
}
}
}
}
④使用更新後的資料
在xml布局中添加兩個Button:
<Button
android:id="@+id/btn_add_user_score"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="增加user的score資料"/>
<Button
android:id="@+id/btn_query_user_score"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="查詢user的score資料"/>
在MainActivity中加入:
private val userScoreDao by lazy {
UserDatabase.getInstance(this).getScoreDao()
}
...
private lateinit var btnAddUserScore: Button
private lateinit var btnQueryUserScore: Button
...
btnAddUserScore = findViewById(R.id.btn_add_user_score)
btnQueryUserScore = findViewById(R.id.btn_query_user_score)
...
btnAddUserScore.setOnClickListener {
Thread{
userScoreDao.insertUserScore(ScoreEntity(userScore = 100))
}.start()
}
btnQueryUserScore.setOnClickListener {
Thread{
userScoreDao.queryUserScoreData().forEach{
Log.d(TAG,"userScore表的資料為:$it")
}
}.start()
}
這樣對資料庫的一次手動遷移就完成啦!💪
如果你想繼續更新,就重複之前的步驟,然後将2→3
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
.... 再一次新的操作
""".trimIndent()
)
}
}
...
.addMigrations(MIGRATION_1_2,MIGRATION_2_3)
②更優雅的修改資料
在上面的修改資料操作中,我們是需要填入每個字段的值的,但是,大部分情況,我們是不會全部知道的,比如我們不知道
User
的
age
,那麼我們的
age
字段就填個
Null
嗎?
val entity = UserEntity(name = "Taxze", age = null)
這顯然是不合适的!
當我們隻想修改使用者名的時,卻又不知道age的值的時候,我們需要怎麼修改呢?
⑴建立UpdateNameBean
class UpdateNameBean(var id:Int,var name:String)
⑵在Dao中加入新的方法
@Update(entity = UserEntity::class)
fun updataUser2(vararg updataNameBean:UpdateNameBean)
⑶然後在使用時隻需要傳入id,和name即可
userDao.updateUser2(updataNameBean(2, "Taxzeeeeee"))
當然你也可以給使用者類建立多個構造方法,并給這些構造方法添加
@lgnore
③詳解@Insert 插入
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg userEntity: UserEntity)
}
其中
onConflict
用于設定當事務中遇到沖突時的政策。
有如下一些參數可以選擇:
- OnConflictStrategy.REPLACE : 替換舊值,繼續目前事務
- OnConflictStrategy.NONE : 忽略沖突,繼續目前事務
- OnConflictStrategy.ABORT : 復原
④@Query 指定參數查詢
每次都查表的全部資訊這也不是事啊,我們要用到where條件來指定參數查詢。
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<UserEntity>
}
大家可以自己學習一下SQL文法~
⑤多表查詢
很多業務情況下,我們是需要同時在多張表中進行查詢的。
@Dao
interface UserDao {
@Query(
"SELECT * FROM user " +
"INNER JOIN score ON score.id = user.id " +
"WHERE user.name LIKE :userName"
)
fun findUsersScoreId(userName: String): List<UserEntity>
}
⑥@Embedded内嵌對象
我們可以使用@Embedded注解,将一個Entity作為屬性内嵌到另外一個Entity,然後我們就可以像通路Column一樣去通路内嵌的Entity啦。
data class Score(
val id:Int?,
val score:String?,
)
@Entity(tableName = "user")
data class UserEntity(
@PrimaryKey(autoGenerate = true) val id:Int = 0,
.....
@Embedded val score: Score?
)
⑦使用
@Relation
注解和
foreignkeys
注解來描述
Entity
之間更複雜的關系
可以實作一對多,多對多的關系
⑧預填充資料庫
可以檢視官方文檔:<https://developer.android.google.cn/training/data-storage/room/prepopulate>
⑨類型轉換器 TypeConverter
....
Room配合LiveData和ViewModel
下面我們通過一個
Room
+
LiveData
+
ViewModel
的例子來完成這篇文章的學習吧
話不多說,先上效果圖:
①建立UserEntity
@Entity(tableName = "user")
data class UserEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT) val name: String?,
@ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER) val age: Int?,
)
②建立對應的Dao
@Dao
interface UserDao {
//添加使用者
@Insert
fun addUser(vararg userEntity: UserEntity)
//查找使用者
//傳回user表中所有的資料,使用LiveData
@Query("select * from user")
fun getUserData(): LiveData<List<UserEntity>>
}
③建立對應的Database
代碼在最開始的例子中已經給出了。
④建立ViewModel
class UserViewModel(userDao: UserDao):ViewModel(){
var userLivedata = userDao.getUserData()
}
⑤建立UserViewModelFactory
我們在
UserViewModel
類中傳遞了
UserDao
參數,是以我們需要有這麼個類實作
ViewModelProvider.Factory
接口,以便于将
UserDao
在執行個體化時傳入。
class UserViewModelFactory(private val userDao: UserDao) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return UserViewModel(userDao) as T
}
}
⑥編輯xml
activity_main
:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientatinotallow="vertical"
tools:cnotallow=".MainActivity">
<EditText
android:id="@+id/user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="請輸入UserName" />
<EditText
android:id="@+id/user_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="請輸入UserAge" />
<Button
android:id="@+id/btn_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加一條user資料" />
<ListView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
建立一個
simple_list_item.xml
,用于展示每一條使用者資料
<?xml versinotallow="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/userText"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
⑦在MainActivity中調用
class MainActivity : AppCompatActivity() {
private var userList: MutableList<UserEntity> = arrayListOf()
private lateinit var arrayAdapter: ArrayAdapter<UserEntity>
private val userDao by lazy {
UserDatabase.getInstance(this).getUserDao()
}
lateinit var viewModel: UserViewModel
private lateinit var listView: ListView
private lateinit var editUserName: EditText
private lateinit var editUserAge: EditText
private lateinit var addButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
arrayAdapter = ArrayAdapter(this, R.layout.simple_list_item, userList)
listView.adapter = arrayAdapter
//執行個體化UserViewModel,并監聽LiveData的變化。
viewModel =
ViewModelProvider(this, UserViewModelFactory(userDao)).get(UserViewModel::class.java)
viewModel.userLivedata.observe(this, Observer {
userList.clear()
userList.addAll(it)
arrayAdapter.notifyDataSetChanged()
})
addButton.setOnClickListener {
addClick()
}
}
//初始化控件
private fun init() {
editUserName = findViewById(R.id.user_name)
editUserAge = findViewById(R.id.user_age)
addButton = findViewById(R.id.btn_add)
listView = findViewById(R.id.recycler_view)
}
fun addClick() {
if (editUserName.text.toString() == "" || editUserAge.text.toString() == "") {
Toast.makeText(this, "姓名或年齡不能為空", Toast.LENGTH_SHORT).show()
return
}
val user = UserEntity(
name = editUserName.text.toString(),
age = editUserAge.text.toString().toInt()
)
thread {
userDao.addUser(user)
}
}
}
這樣一個簡單的Room配合LiveData和ViewModel實作頁面自動更新的Demo就完成啦🌹具體代碼可以檢視
Git
倉庫😉
尾述
看完這篇文章,相信你已經發現Room雖然看上去還是有些繁瑣,但是相比較于SQLite還是簡單不少了,Room還能幫你檢測SQL是否正确哈哈。這篇文章已經很詳細的講了Jetpack Room的大部分用法,不過在看完文章後,你仍需多多實踐,相信你很快就可以掌握Room啦😺 因為我本人能力也有限,文章有不對的地方歡迎指出,有問題歡迎在評論區留言讨論~
關于我
Hello,我是Taxze,如果您覺得文章對您有價值,希望您能給我的文章點個❤️,也歡迎關注我的部落格。
如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?😝
基礎系列:
2022 · 讓我帶你Jetpack架構元件從入門到精通 — Lifecycle
學會使用LiveData和ViewModel,我相信會讓你在寫業務時變得輕松🌞
當你真的學會DataBinding後,你會發現“這玩意真香”!
Navigation — 這麼好用的跳轉管理架構你确定不來看看?
Room:又要寫業務代碼了?看看我吧,給你飛一般的感覺!(本文🌟)
以下部分還在碼字,趕緊點個收藏吧🔥
2022 · 讓我帶你Jetpack架構元件從入門到精通 — Paging3
2022 · 讓我帶你Jetpack架構元件從入門到精通 — WorkManager
2022 · 讓我帶你Jetpack架構元件從入門到精通 — ViewPager2
2022 · 讓我帶你Jetpack架構元件從入門到精通 — 登入注冊頁面實戰(MVVM)
進階系列: