Android數(shù)據(jù)庫(kù)Room的實(shí)際使用過程總結(jié)
前言
最近我負(fù)責(zé)開發(fā)一個(gè)基于Android系統(tǒng)的平板應(yīng)用程序,在項(xiàng)目中涉及到數(shù)據(jù)庫(kù)操作的部分,我們最終決定采用Room數(shù)據(jù)庫(kù)框架來(lái)實(shí)現(xiàn)。在實(shí)際使用過程中,我遇到了一些挑戰(zhàn)和問題,現(xiàn)在我想將這些經(jīng)驗(yàn)記錄下來(lái),以便未來(lái)參考和改進(jìn)。
一、Room的基本使用
1.項(xiàng)目配置
在開發(fā)這個(gè)Android項(xiàng)目時(shí),我決定將數(shù)據(jù)庫(kù)操作代碼獨(dú)立成一個(gè)模塊,這樣做有助于保持代碼的整潔和模塊化。在這個(gè)模塊中,我選擇了Kotlin作為編程語(yǔ)言,并使用了Kotlin 1.5.21版本。為了支持Kotlin開發(fā)和編譯,我需要在項(xiàng)目中包含兩個(gè)插件:kotlin-android 和 kotlin-kapt。這兩個(gè)插件分別負(fù)責(zé)Kotlin代碼的Android特定功能支持和注解處理,確保代碼能夠正確編譯和運(yùn)行。
plugins { id 'com.android.library' id 'kotlin-android' id 'kotlin-kapt' }
采用了Room框架,具體版本為2.3.0。由于Room框架在不同版本之間可能存在API差異,因此在這里特別指出我所使用的版本,以便于在遇到問題時(shí)能夠準(zhǔn)確地查找和解決問題,同時(shí)也使用到了協(xié)程,所有依賴如下:
dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // Room數(shù)據(jù)庫(kù)版本 def room_version = "2.3.0" implementation "androidx.room:room-runtime:$room_version" // Kapt kapt "androidx.room:room-compiler:$room_version" // room-ktx implementation "androidx.room:room-ktx:$room_version" // 協(xié)程 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" }
2.創(chuàng)建實(shí)體類(Entity)
在Room數(shù)據(jù)庫(kù)框架中,實(shí)體類是用來(lái)映射數(shù)據(jù)庫(kù)表的。每個(gè)實(shí)體類代表一個(gè)數(shù)據(jù)庫(kù)表,而實(shí)體類的屬性則對(duì)應(yīng)表中的列。以下是一個(gè)使用Kotlin語(yǔ)言編寫的Airport實(shí)體類的示例代碼,其中id字段被標(biāo)記為主鍵:
import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "airports") // 指定表名 data class Airport( @PrimaryKey(autoGenerate = true) val id: Int, // 主鍵,自動(dòng)生成 val name: String, // 機(jī)場(chǎng)名稱 val city: String, // 所在城市 val country: String // 所在國(guó)家 )
3.創(chuàng)建數(shù)據(jù)訪問對(duì)象(DAO - Data Access Object)
DAO 用于定義訪問數(shù)據(jù)庫(kù)的方法,比如插入、查詢、更新、刪除等操作。以下是針對(duì) Airport實(shí)體類創(chuàng)建的 AirportDao示例:
import androidx.room.Dao import androidx.room.Insert import androidx.room.Query import androidx.room.Update import androidx.room.Delete @Dao interface AirportDao { // 插入單個(gè)Airport對(duì)象 @Insert suspend fun insert(airport: Airport) // 插入多個(gè)Airport對(duì)象 @Insert suspend fun insertAll(vararg airports: Airport) // 根據(jù)ID查詢Airport對(duì)象 @Query("SELECT * FROM airports WHERE id = :id") suspend fun getAirportById(id: Int): Airport? // 更新Airport對(duì)象 @Update suspend fun update(airport: Airport) // 刪除單個(gè)Airport對(duì)象 @Delete suspend fun delete(airport: Airport) // 刪除所有Airport對(duì)象 @Query("DELETE FROM airports") suspend fun deleteAll() }
在這個(gè)AirportDao接口中:
- @Dao 注解標(biāo)記這個(gè)接口為一個(gè)DAO接口。
- @Insert注解用于定義插入操作的方法。insert()方法可以插入單個(gè)Airport對(duì)象,而insertAll()方法可以插入多個(gè)Airport對(duì)象。
- @Query 注解用于定義自定義SQL查詢的方法。getAirportById()方法通過ID查詢單個(gè)Airport對(duì)象。
- @Update 注解用于定義更新操作的方法。update()方法更新一個(gè)Airport對(duì)象。
- @Delete注解用于定義刪除操作的方法。delete()方法可以刪除單個(gè)Airport對(duì)象,而deleteAll()方法可以刪除所有Airport對(duì)象。
- suspend關(guān)鍵字用于標(biāo)記一個(gè)函數(shù)為掛起函數(shù)(suspend function),這是Kotlin協(xié)程(coroutines)的一個(gè)重要特性。掛起函數(shù)可以暫停和恢復(fù)其執(zhí)行,而不會(huì)阻塞線程
這些方法定義了與Airport實(shí)體類對(duì)應(yīng)的數(shù)據(jù)庫(kù)表進(jìn)行交互的基本操作。Room框架會(huì)在編譯時(shí)自動(dòng)實(shí)現(xiàn)這些接口方法,開發(fā)者無(wú)需手動(dòng)編寫實(shí)現(xiàn)代碼。
4. 創(chuàng)建數(shù)據(jù)庫(kù)抽象類(Database)
在Room數(shù)據(jù)庫(kù)框架中,你需要?jiǎng)?chuàng)建一個(gè)繼承自RoomDatabase的抽象類,這個(gè)類將作為數(shù)據(jù)庫(kù)的訪問入口,并定義與實(shí)體類和DAO的關(guān)聯(lián)。以下是一個(gè)示例代碼,展示了如何創(chuàng)建這樣的數(shù)據(jù)庫(kù)類,并與Airport實(shí)體類和AirportDao接口關(guān)聯(lián):
import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters // 定義數(shù)據(jù)庫(kù)的版本 @Database(entities = [Airport::class], version = 1, exportSchema = false) @TypeConverters(YourTypeConverters::class) // 如果有自定義類型轉(zhuǎn)換器,在這里指定 abstract class AppDatabase : RoomDatabase() { // 提供獲取DAO實(shí)例的方法 abstract fun airportDao(): AirportDao // Companion object to create an instance of AppDatabase companion object { // Singleton instance of the database @Volatile private var instance: AppDatabase? = null // Method to get the database instance fun getInstance(context: Context): AppDatabase { return instance ?: synchronized(this) { instance ?: buildDatabase(context).also { inst -> instance = inst } } } // Method to build the database private fun buildDatabase(context: Context): AppDatabase { return Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ).build() } } }
在這個(gè)AppDatabase類中:
- @Database注解定義了數(shù)據(jù)庫(kù)包含的實(shí)體類(entities)和數(shù)據(jù)庫(kù)版本(version)。exportSchema屬性用于控制是否導(dǎo)出數(shù)據(jù)庫(kù)的schema文件,通常在開發(fā)階段設(shè)置為true,而在生產(chǎn)環(huán)境中設(shè)置為false。
- @TypeConverters注解用于指定一個(gè)或多個(gè)類,這些類包含自定義的類型轉(zhuǎn)換器,如果需要將非標(biāo)準(zhǔn)類型(如Date或URL)存儲(chǔ)在數(shù)據(jù)庫(kù)中,這些轉(zhuǎn)換器是必需的。
- abstract fun airportDao(): AirportDao提供了一個(gè)抽象方法,用于獲取AirportDao的實(shí)例,這樣我們就可以在數(shù)據(jù)庫(kù)類中執(zhí)行對(duì)Airport表的操作。
- companion
object提供了一個(gè)單例模式的實(shí)現(xiàn),用于獲取AppDatabase的實(shí)例。getInstance()方法確保整個(gè)應(yīng)用中只有一個(gè)數(shù)據(jù)庫(kù)實(shí)例被創(chuàng)建。buildDatabase()方法用于實(shí)際構(gòu)建和配置數(shù)據(jù)庫(kù)。
**請(qǐng)注意!**你需要將YourTypeConverters::class替換為實(shí)際包含自定義類型轉(zhuǎn)換器的類的名稱,如果你沒有自定義類型轉(zhuǎn)換器,可以省略@TypeConverters注解。此外,context參數(shù)需要從你的應(yīng)用上下文傳遞給getInstance()方法,以確保數(shù)據(jù)庫(kù)正確地與應(yīng)用的生命周期關(guān)聯(lián)。
5. 使用數(shù)據(jù)庫(kù)
在Android的Activity中使用數(shù)據(jù)庫(kù)進(jìn)行操作時(shí),可以在協(xié)程中執(zhí)行這些操作,以避免阻塞主線程。以下是在Activity中使用協(xié)程與Room數(shù)據(jù)庫(kù)進(jìn)行交互的簡(jiǎn)單示例代碼片段:
import android.os.Bundle import android.util.Log import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider import kotlinx.coroutines.* class AirportActivity : AppCompatActivity() { private val viewModel by viewModels<AirportViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_airport) // 啟動(dòng)協(xié)程來(lái)插入數(shù)據(jù) lifecycleScope.launch { viewModel.insertAirport(Airport(0, "Moonshot International", "Shanghai", "China")) } // 啟動(dòng)協(xié)程來(lái)查詢數(shù)據(jù) lifecycleScope.launch { val airport = viewModel.getAirportById(1) // 假設(shè)ID為1 airport.observe(this@AirportActivity, { airport -> Log.d("AirportActivity", "Airport Name: ${airport?.name}") }) } } }
在這個(gè)Activity中:
- lifecycleScope是一個(gè)與Activity生命周期綁定的協(xié)程作用域,它確保協(xié)程在Activity銷毀時(shí)取消。
- launch是一個(gè)協(xié)程構(gòu)建器,用于啟動(dòng)一個(gè)新的協(xié)程。
- insertAirport方法在協(xié)程中被調(diào)用,用于插入新的機(jī)場(chǎng)信息。
getAirportById方法返回一個(gè)LiveData<Airport?>對(duì)象,它在協(xié)程中被觀察,以便在機(jī)場(chǎng)信息變化時(shí)更新UI。
請(qǐng)注意,AirportViewModel需要正確實(shí)現(xiàn),并且包含insertAirport和getAirportById方法。這些方法應(yīng)該在ViewModel中使用viewModelScope而不是lifecycleScope,因?yàn)関iewModelScope是與ViewModel的生命周期綁定的,而不是Activity。
以下是AirportViewModel的示例實(shí)現(xiàn):
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.* class AirportViewModel : ViewModel() { private val database = AppDatabase.getInstance(applicationContext) // 假設(shè)這是全局可訪問的context private val airportDao = database.airportDao() fun insertAirport(airport: Airport) { viewModelScope.launch { airportDao.insert(airport) } } fun getAirportById(id: Int): LiveData<Airport?> { return liveData(viewModelScope.coroutineContext + Dispatchers.IO) { emit(airportDao.getAirportById(id)) } } }
至此Room簡(jiǎn)單的使用已經(jīng)說(shuō)完了,這些步驟構(gòu)成了Room數(shù)據(jù)庫(kù)在Android應(yīng)用中的簡(jiǎn)單使用流程。Room提供了一個(gè)抽象層,幫助開發(fā)者以更聲明式和類型安全的方式進(jìn)行數(shù)據(jù)庫(kù)操作,同時(shí)利用協(xié)程簡(jiǎn)化了異步編程。
二、Room使用過程遇到的問題
1.聲明表中字段可以為null
import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "airports") // 指定表名 data class Airport( @PrimaryKey(autoGenerate = true) val id: Int, // 主鍵,自動(dòng)生成 val name: String, // 機(jī)場(chǎng)名稱 val city: String, // 所在城市 val country: String // 所在國(guó)家 )
如果在使用Room數(shù)據(jù)庫(kù)時(shí),需要在實(shí)體類中允許某些字段存儲(chǔ)空值,可以直接將這些字段聲明為可空類型。這樣,即使在插入數(shù)據(jù)時(shí)這些字段的值為空,數(shù)據(jù)庫(kù)操作也能正常進(jìn)行。具體來(lái)說(shuō),只需在實(shí)體類中將相應(yīng)的變量聲明為String?、Int?等可空類型,Room就會(huì)允許這些字段在數(shù)據(jù)庫(kù)中存儲(chǔ)空值,代碼如下:
import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "airports") // 指定表名 class Airport { @PrimaryKey(autoGenerate = true) var id: Int = 0 // 主鍵,自動(dòng)生成 var name: String? = null // 機(jī)場(chǎng)名稱 var city: String? = null// 所在城市 var country: String? = null // 所在國(guó)家 }
2.數(shù)據(jù)庫(kù)升級(jí)
當(dāng)你在Room數(shù)據(jù)庫(kù)的實(shí)體類中添加了一個(gè)新的字段后,如果在運(yùn)行應(yīng)用時(shí)遇到了崩潰,并且出現(xiàn)了異常信息,這通常是因?yàn)镽oom數(shù)據(jù)庫(kù)的遷移問題。Room需要知道如何處理數(shù)據(jù)庫(kù)結(jié)構(gòu)的變化,比如添加、刪除或修改字段。如果沒有正確處理這些變化,Room在嘗試訪問數(shù)據(jù)庫(kù)時(shí)就會(huì)拋出異常,異常信息如下:
Room cannot verify the data integrity, Looks like vou’ve changed schema but forgot to update the version number, You can simply . fix this by increasing the version number.
遇到數(shù)據(jù)庫(kù)結(jié)構(gòu)變更時(shí),通常有兩種處理方法:
第一種卸載并重新安裝應(yīng)用:這是一種簡(jiǎn)單直接的方法,通過卸載應(yīng)用再重新安裝,應(yīng)用將創(chuàng)建全新的數(shù)據(jù)庫(kù),從而自動(dòng)包含所有新的表結(jié)構(gòu)和字段變更。另一種方法是進(jìn)行數(shù)據(jù)庫(kù)升級(jí),下面是數(shù)據(jù)庫(kù)升級(jí)的步驟:
- 更新實(shí)體類:在實(shí)體類中添加新的字段。例如,如果你想為Airport實(shí)體類添加一個(gè)新字段,你可以直接聲明這個(gè)字段為可空類型,如val newField: String? = null。
- 增加數(shù)據(jù)庫(kù)版本號(hào):在@Database注解中增加版本號(hào)。例如,如果你的數(shù)據(jù)庫(kù)當(dāng)前版本是1,那么在添加新字段后,將版本號(hào)增加到2:@Database(entities = [Airport::class], version = 2) 。
- 創(chuàng)建Migration遷移類:在RoomDatabase中定義Migration對(duì)象,指定如何從舊版本遷移到新版本。例如,為Airport實(shí)體類添加新字段的遷移可以這樣定義:
val MIGRATION_1_2: Migration = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { // 執(zhí)行SQL database.execSQL("ALTER TABLE airports ADD COLUMN newField TEXT") } }
- 將Migration添加到數(shù)據(jù)庫(kù)構(gòu)建中:在構(gòu)建數(shù)據(jù)庫(kù)時(shí),通過addMigrations方法添加Migration對(duì)象。例如
val database = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ).addMigrations(MIGRATION_1_2).build()
這樣,當(dāng)應(yīng)用啟動(dòng)時(shí),Room會(huì)自動(dòng)執(zhí)行Migration中定義的遷移操作。
通過這些步驟,你可以平滑地將Room數(shù)據(jù)庫(kù)升級(jí)到新版本,同時(shí)添加新的字段。如果用戶之前安裝的數(shù)據(jù)庫(kù)版本較低,Room會(huì)按照定義的Migration順序依次執(zhí)行,直到達(dá)到最新的數(shù)據(jù)庫(kù)版本。
3.如何關(guān)聯(lián)外鍵ForeignKey
發(fā)現(xiàn)有些人不知道什么是外鍵:這里簡(jiǎn)單說(shuō)明一下:
外鍵的主要作用如下:
建立關(guān)聯(lián)關(guān)系:用于在不同表之間建立聯(lián)系,清晰體現(xiàn)實(shí)體之間的對(duì)應(yīng)關(guān)系,如機(jī)場(chǎng)與跑道的所屬關(guān)系,方便進(jìn)行多表聯(lián)合查詢等操作。
維護(hù)數(shù)據(jù)完整性:防止在插入或更新數(shù)據(jù)時(shí)出現(xiàn)無(wú)效數(shù)據(jù),確保子表中的外鍵值在父表的主鍵值中存在或?yàn)?NULL,保證數(shù)據(jù)的準(zhǔn)確性和一致性。
實(shí)現(xiàn)級(jí)聯(lián)操作:定義級(jí)聯(lián)規(guī)則,當(dāng)父表中的記錄發(fā)生刪除或更新時(shí),子表中對(duì)應(yīng)的記錄可按規(guī)則自動(dòng)進(jìn)行相應(yīng)操作,確保數(shù)據(jù)在不同表之間的協(xié)調(diào)一致。
約束數(shù)據(jù)變化:通過參照完整性約束,控制數(shù)據(jù)在表之間的更新和刪除傳播方式,保證數(shù)據(jù)的修改和更新符合特定的業(yè)務(wù)邏輯和要求。
在 Room 中聲明外鍵可以通過在實(shí)體類中使用@ForeignKey注解來(lái)實(shí)現(xiàn)。以下是一個(gè)示例,展示了如何在機(jī)場(chǎng)表和機(jī)場(chǎng)跑道表之間聲明外鍵關(guān)聯(lián):
- 定義機(jī)場(chǎng)表實(shí)體類
import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "airport") data class Airport( @PrimaryKey(autoGenerate = true) var airportId: Int = 0, var airportName: String = "" )
- 定義機(jī)場(chǎng)跑道表實(shí)體類(Runway)并聲明外鍵
import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey @Entity( tableName = "runway", foreignKeys = [ForeignKey( entity = Airport::class, parentColumns = ["airportId"], childColumns = ["airportIdFK"], onDelete = ForeignKey.CASCADE )] ) data class Runway( @PrimaryKey(autoGenerate = true) var runwayId: Int = 0, var airportIdFK: Int = 0, var runwayName: String = "" )
在上述 Runway 實(shí)體類中,使用 @ForeignKey 注解來(lái)聲明外鍵關(guān)系,各參數(shù)含義和 Java 版本中的一致:
- entity:指定關(guān)聯(lián)的實(shí)體類型,這里關(guān)聯(lián)的是 Airport 類。
- parentColumns:表示在關(guān)聯(lián)的父實(shí)體(即 Airport)中對(duì)應(yīng)的主鍵列名,此處為 airportId。
- childColumns:代表在當(dāng)前實(shí)體(Runway)中作為外鍵的列名,也就是 airportIdFK。
- onDelete:定義了當(dāng)父表(Airport 表)中對(duì)應(yīng)的主鍵記錄被刪除時(shí)的行為,這里設(shè)置為 CASCADE,意味著級(jí)聯(lián)刪除,比如刪除某個(gè)機(jī)場(chǎng)記錄時(shí),與之關(guān)聯(lián)的跑道記錄也會(huì)自動(dòng)刪除。除了 CASCADE 之外,還有以下幾種常見的類型及其含義:
-NO_ACTION含義:當(dāng)父表中的記錄被刪除或更新時(shí),子表中的外鍵列不做任何操作,這可能會(huì)導(dǎo)致子表中的外鍵引用無(wú)效的父鍵值,從而產(chǎn)生孤立的數(shù)據(jù),破壞數(shù)據(jù)的完整性。
示例:在 文章表 和 評(píng)論表 的關(guān)聯(lián)中,如果使用 NO_ACTION,當(dāng)一篇文章被刪除時(shí),評(píng)論表中對(duì)應(yīng)的文章外鍵值不會(huì)改變,仍然保留原來(lái)的文章 ID,即使該文章已經(jīng)不存在了,這就導(dǎo)致了評(píng)論表中的這些評(píng)論與實(shí)際不存在的文章產(chǎn)生了孤立的關(guān)聯(lián)。
SET_NULL含義:當(dāng)父表中的記錄被刪除或更新時(shí),子表中對(duì)應(yīng)的外鍵列的值將被設(shè)置為 NULL。
示例:假設(shè)有 用戶表 和 訂單表,訂單表 中的 用戶ID 是外鍵關(guān)聯(lián)到 用戶表 的主鍵。當(dāng)一個(gè)用戶被刪除時(shí),該用戶的所有訂單記錄中的 用戶ID 字段將被設(shè)置為 NULL,表示這些訂單與任何用戶都不再關(guān)聯(lián),但訂單記錄本身仍然保留在 訂單表 中。
SET_DEFAULT含義:當(dāng)父表中的記錄被刪除或更新時(shí),子表中對(duì)應(yīng)的外鍵列的值將被設(shè)置為其默認(rèn)值。
示例:若 訂單表 中的 用戶ID 外鍵字段有一個(gè)默認(rèn)值為 0,當(dāng)關(guān)聯(lián)的用戶被刪除時(shí),該用戶的所有訂單記錄中的 用戶ID 將被設(shè)置為 0,以此來(lái)表示一種特殊的狀態(tài)或無(wú)關(guān)聯(lián)的情況。
RESTRICT含義:當(dāng)父表中的記錄被刪除或更新時(shí),如果子表中存在對(duì)應(yīng)的關(guān)聯(lián)記錄,則拒絕父表的刪除或更新操作,從而防止出現(xiàn)孤立的子記錄,確保數(shù)據(jù)的一致性和完整性。
示例:在 部門表 和 員工表 的關(guān)系中,員工表 通過外鍵關(guān)聯(lián)到 部門表 的主鍵。如果試圖刪除一個(gè)部門,而該部門下還有員工,那么由于 RESTRICT 約束,數(shù)據(jù)庫(kù)將不允許執(zhí)行這個(gè)刪除操作,避免出現(xiàn)員工所屬部門不存在的不合理情況。
4.使用事務(wù)@Transaction
在 Room 中,事務(wù)是一種重要的機(jī)制,用于確保多個(gè)數(shù)據(jù)庫(kù)操作的原子性,即要么所有操作都成功執(zhí)行,數(shù)據(jù)庫(kù)狀態(tài)被完整更新;要么所有操作都失敗回滾,數(shù)據(jù)庫(kù)保持初始狀態(tài),從而有效地維護(hù)數(shù)據(jù)的一致性。以下是關(guān)于 Room 中事務(wù)的詳細(xì)介紹:
事務(wù)的必要性
- 在實(shí)際的數(shù)據(jù)庫(kù)操作中,常常會(huì)有多個(gè)相關(guān)的操作需要作為一個(gè)整體來(lái)執(zhí)行。例如,在一個(gè)銀行轉(zhuǎn)賬系統(tǒng)中,從一個(gè)賬戶扣除一定金額并在另一個(gè)賬戶增加相應(yīng)金額,這兩個(gè)操作必須同時(shí)成功或同時(shí)失敗,否則就會(huì)導(dǎo)致數(shù)據(jù)不一致,如賬戶余額出現(xiàn)錯(cuò)誤等問題。事務(wù)機(jī)制正是為了滿足這種需求而設(shè)計(jì)的,它能夠保證在復(fù)雜的操作場(chǎng)景下數(shù)據(jù)的準(zhǔn)確性和完整性。
使用方法
以下是一個(gè)使用事務(wù)進(jìn)行多表查詢的例子,還以Airport和Runway這兩個(gè)實(shí)體類為例,它們之間存在關(guān)聯(lián)關(guān)系。
@Dao interface AirportRunwayDao { @Query("SELECT * FROM airports WHERE id = :airportId") fun getAirport(airportId: Int): Airport? @Query("SELECT * FROM runways WHERE airportId = :airportId") fun getRunways(airportId: Int): List<Runway> // 事務(wù)性查詢操作 @Transaction fun getAirportWithRunways(airportId: Int): Pair<Airport?, List<Runway>> { // 這里的代碼將在一個(gè)事務(wù)中執(zhí)行 val airport = getAirport(airportId) val runways = getRunways(airportId) return Pair(airport, runways) } }
5.數(shù)據(jù)庫(kù)文件的位置
在Room數(shù)據(jù)庫(kù)中,創(chuàng)建AppDatabase對(duì)象時(shí),可以指定數(shù)據(jù)庫(kù)文件的名稱,這個(gè)名稱也是數(shù)據(jù)庫(kù)文件的名字。默認(rèn)情況下,Room數(shù)據(jù)庫(kù)文件存儲(chǔ)在應(yīng)用的內(nèi)部存儲(chǔ)目錄下的特定子目錄中。如果需要更改數(shù)據(jù)庫(kù)文件的存儲(chǔ)位置,可以通過指定具體的文件路徑來(lái)實(shí)現(xiàn)。這樣,數(shù)據(jù)庫(kù)文件就會(huì)被創(chuàng)建在指定的路徑下,而不是默認(rèn)的內(nèi)部存儲(chǔ)位置。代碼如下:
private fun buildDatabase(context: Context): AppDatabase { val dbPath = "${context.getExternalFilesDir(null)?.absolutePath}/database/test.db" return Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, dbPath ).build() }
這樣數(shù)據(jù)庫(kù)文件存在的位置,就會(huì)放到指定目錄下。
6.打開已存在的數(shù)據(jù)庫(kù)
在大多數(shù)應(yīng)用場(chǎng)景中,Room數(shù)據(jù)庫(kù)的標(biāo)準(zhǔn)使用方法已經(jīng)足夠。但在本次項(xiàng)目中,我們需要軟件具備打開本地已有數(shù)據(jù)庫(kù)文件或?qū)胪獠繑?shù)據(jù)庫(kù)文件的功能。操作步驟與常規(guī)配置相似:
- 創(chuàng)建實(shí)體類(Entity):定義一個(gè)實(shí)體類來(lái)映射數(shù)據(jù)庫(kù)中的表結(jié)構(gòu)。
- 創(chuàng)建數(shù)據(jù)訪問對(duì)象(DAO):定義一個(gè)接口,用于執(zhí)行數(shù)據(jù)庫(kù)的增刪改查等操作。
與常規(guī)使用的主要區(qū)別在于,需要將待打開的數(shù)據(jù)庫(kù)文件放置在指定目錄下。在初始化Room數(shù)據(jù)庫(kù)時(shí),指定數(shù)據(jù)庫(kù)文件的路徑:
- 如果指定路徑下已存在數(shù)據(jù)庫(kù)文件,Room將直接使用該文件。
- 如果指定路徑下沒有數(shù)據(jù)庫(kù)文件,Room將創(chuàng)建一個(gè)新的數(shù)據(jù)庫(kù)文件。
這樣,我們就能夠?qū)崿F(xiàn)對(duì)本地或外部數(shù)據(jù)庫(kù)文件的訪問和管理。
打開外部數(shù)據(jù)時(shí)遇到的問題
當(dāng)遇到
IllegalStateException: Pre-packaged database has an invalid schema: airport
Expected…
…表結(jié)構(gòu)信息
Found:
…表結(jié)構(gòu)信息
當(dāng)遇到類似 “IllagelStateException: Pre-packaged database has an invalid schena: Excepted… Found:” 這樣的報(bào)錯(cuò)時(shí),其背后的原因通常是預(yù)打包數(shù)據(jù)庫(kù)(也就是你準(zhǔn)備打開的外部數(shù)據(jù)文件對(duì)應(yīng)的數(shù)據(jù)庫(kù))的架構(gòu)與 Room 所期望的架構(gòu)出現(xiàn)了不匹配的情況。
那這里所說(shuō)的數(shù)據(jù)庫(kù)架構(gòu),涵蓋了表結(jié)構(gòu)、列定義以及約束等多個(gè)方面的內(nèi)容。常見的導(dǎo)致架構(gòu)不匹配的因素有以下幾種:
- 一是字段可空聲明不一致。比如在 Room中通過實(shí)體類定義某個(gè)字段是非空的,但在預(yù)打包數(shù)據(jù)庫(kù)里對(duì)應(yīng)的該字段卻允許為空,或者反之,這種差異就會(huì)造成架構(gòu)不一致。
- 二是數(shù)據(jù)類型不一致。可能在實(shí)體類中定義某個(gè)字段為 Integer 類型,然而預(yù)打包數(shù)據(jù)庫(kù)里對(duì)應(yīng)列的數(shù)據(jù)類型卻是TEXT,不同的數(shù)據(jù)類型設(shè)置會(huì)讓 Room 在驗(yàn)證數(shù)據(jù)庫(kù)架構(gòu)時(shí)判定為不匹配。
- 三是外鍵約束不一致。例如在 Room 的實(shí)體類中定義了兩張表之間通過外鍵建立了特定的關(guān)聯(lián)關(guān)系,并且設(shè)置了相應(yīng)的外鍵約束規(guī)則,像刪除操作時(shí)的級(jí)聯(lián)方式等,但在預(yù)打包數(shù)據(jù)庫(kù)里對(duì)應(yīng)的表之間的外鍵約束情況與之不同,這同樣會(huì)引發(fā)架構(gòu)方面的問題。
當(dāng)出現(xiàn)這類報(bào)錯(cuò)后,我們需要仔細(xì)對(duì)比異常日志里呈現(xiàn)的兩個(gè)表結(jié)構(gòu),查找究竟是哪個(gè)地方出現(xiàn)了不一致的情況。一旦發(fā)現(xiàn)了問題所在,接下來(lái)就要采取相應(yīng)的解決措施。要么對(duì) Room 中的 Entity 實(shí)體類進(jìn)行修改,使其表結(jié)構(gòu)、字段定義以及約束等各方面與預(yù)打包數(shù)據(jù)庫(kù)的實(shí)際架構(gòu)相符;要么對(duì)預(yù)打包的數(shù)據(jù)庫(kù)文件本身進(jìn)行調(diào)整,從而讓二者的結(jié)構(gòu)能夠達(dá)成一致。只有在確保這兩個(gè)結(jié)構(gòu)完全一致的前提下,才能夠成功連接數(shù)據(jù)庫(kù),避免出現(xiàn)上述的報(bào)錯(cuò)情況。
總結(jié)
到此這篇關(guān)于Android數(shù)據(jù)庫(kù)Room的實(shí)際使用的文章就介紹到這了,更多相關(guān)Android數(shù)據(jù)庫(kù)Room使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android實(shí)現(xiàn)圖片疊加效果的兩種方法
這篇文章主要介紹了Android實(shí)現(xiàn)圖片疊加效果的兩種方法,結(jié)合實(shí)例形式分析了Android實(shí)現(xiàn)圖片疊加效果的兩種操作方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-08-08android實(shí)現(xiàn)RecyclerView列表單選功能
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)RecyclerView列表單選功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07一些有效的Android啟動(dòng)優(yōu)化策略分享
在當(dāng)今激烈競(jìng)爭(zhēng)的移動(dòng)應(yīng)用市場(chǎng),應(yīng)用的啟動(dòng)速度直接影響著用戶的第一印象和滿意度,Android的啟動(dòng)優(yōu)化是開發(fā)者必須關(guān)注的關(guān)鍵領(lǐng)域,本文將詳細(xì)介紹一些強(qiáng)大有效的Android啟動(dòng)優(yōu)化策略,幫助你優(yōu)化應(yīng)用的啟動(dòng)過程,為用戶創(chuàng)造更出色的體驗(yàn),需要的朋友可以參考下2023-08-08android實(shí)現(xiàn)一鍵鎖屏和一鍵卸載的方法實(shí)例
這篇文章主要給大家介紹了關(guān)于android如何實(shí)現(xiàn)一鍵鎖屏和一鍵卸載的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-05-05全面解析Android中對(duì)EditText輸入實(shí)現(xiàn)監(jiān)聽的方法
這篇文章主要介紹了Android中對(duì)EditText輸入實(shí)現(xiàn)監(jiān)聽的方法,包括一個(gè)仿iOS的帶清除功能的ClearEditText輸入框控件的詳細(xì)使用介紹,需要的朋友可以參考下2016-04-04Android在項(xiàng)目中接入騰訊TBS瀏覽器WebView的教程與注意的地方
今天小編就為大家分享一篇關(guān)于Android在項(xiàng)目中接入騰訊TBS瀏覽器WebView的教程與注意的地方,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10