Android Room 数据库本地存储
引言
在现代 Android 开发中,本地数据持久化是构建响应式、离线优先应用的基础。Room 是 Google 推出的官方 ORM 框架,它构建在 SQLite 之上,提供了编译时检查、简化数据库操作以及与架构组件深度集成的能力。本教程将带你从零开始掌握 Room,学会如何定义表结构、操作数据、处理异步查询,并将数据库完美融入 Android 的 MVVM 架构中。
什么是 Room
Room 是一个抽象层,封装了 SQLite 数据库的繁琐操作,让开发者能够以面向对象的方式处理持久化数据。它提供了三大核心注解:
- @Entity:定义数据库中的一张表。
- @Dao:定义访问数据库的方法。
- @Database:声明数据库持有者,并作为底层连接的主要入口。
Room 会在编译时验证 SQL 语句,避免运行时出现语法错误,并自动生成实现代码。
为什么选择 Room
相比直接使用 SQLite 或其它 ORM 库,Room 具有以下优势:
- 编译时 SQL 校验:错误的查询在构建时就会被捕获,减少运行时崩溃。
- 与 LiveData 和 Flow 无缝集成:数据库的变化可以自动通知 UI 层,实现响应式数据流。
- 减少样板代码:使用注解即可自动生成增删改查代码。
- 支持数据迁移:提供结构化的版本升级方式。
- Kotlin 协程与 RxJava 友好:DAO 中的方法可以直接声明为挂起函数或返回响应式类型。
环境配置
首先在模块级 build.gradle 文件中添加 Room 依赖。以 Kotlin 与 KSP(Kotlin Symbol Processing)为例(推荐使用 KSP 以提升编译速度):
// 在项目根 build.gradle 中添加 KSP 插件
plugins {
id 'com.google.devtools.ksp' version '1.9.20-1.0.14' apply false
}
// 在模块 build.gradle 中应用插件并添加依赖
plugins {
id 'com.google.devtools.ksp'
}
android {
...
}
dependencies {
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
ksp "androidx.room:room-compiler:$room_version"
// 可选 - Kotlin 扩展和协程支持
implementation "androidx.room:room-ktx:$room_version"
}
同步项目后即可使用 Room。
定义数据实体(Entity)
每一个实体类对应数据库中的一张表。使用 @Entity 注解标记,类属性即为列。
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String,
val age: Int,
val email: String
)
常见配置:
- tableName:自定义表名,默认使用类名。
- @PrimaryKey:声明主键,
autoGenerate = true会使主键自增。 - @ColumnInfo(name = "column_name"):改变列名映射。
- @Ignore:忽略不希望持久化的字段。
创建数据访问对象(DAO)
DAO 是一个接口或抽象类,用于定义操作数据库的方法。Room 为其生成实现。
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
@Query("SELECT * FROM users ORDER BY id ASC")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
}
关键点:
- 使用
suspend函数确保操作在协程中执行,避免阻塞主线程。 - 返回
Flow类型的数据可以感知数据库内容的变化,实现响应式更新。 @Insert、@Update、@Delete开箱即用,复杂的查询写在@Query中。onConflict策略可指定冲突时的行为,如REPLACE表示替换旧数据。
定义数据库类
需要创建一个继承 RoomDatabase 的抽象类,使用 @Database 注解声明实体列表和版本号。
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
exportSchema = false 在开发阶段可以跳过导出数据库 schema,生产环境建议开启以便于迁移。
构建数据库实例
建议使用单例模式创建数据库,避免多次打开开销。通常放在 Application 类或依赖注入容器中。
import android.app.Application
import androidx.room.Room
class MyApplication : Application() {
val database: AppDatabase by lazy {
Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"app_database"
).build()
}
}
数据库名称随意指定,如 "app_database"。注意这里使用 lazy 确保只初始化一次。
执行增删改查操作
在 ViewModel 或 Repository 中使用 DAO 进行数据操作。下面展示结合 ViewModel 和协程的典型用法:
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val userDao = (application as MyApplication).database.userDao()
val allUsers: Flow<List<User>> = userDao.getAllUsers()
fun insertUser(user: User) = viewModelScope.launch {
userDao.insertUser(user)
}
fun deleteUser(user: User) = viewModelScope.launch {
userDao.deleteUser(user)
}
fun getUserById(id: Int) = viewModelScope.launch {
val user = userDao.getUserById(id)
// 处理取到的用户
}
}
视图层观察 allUsers 的 Flow 即可自动刷新列表:
lifecycleScope.launchWhenStarted {
viewModel.allUsers.collect { users ->
// 更新 RecyclerView 适配器
adapter.submitList(users)
}
}
处理复杂关系
Room 支持外键、嵌套对象以及多表查询。
一对多关系
假设一个用户有多本书,可以用 @Relation 辅助查询:
data class UserWithBooks(
@Embedded val user: User,
@Relation(
parentColumn = "id",
entityColumn = "userId"
)
val books: List<Book>
)
在 DAO 中查询:
@Transaction
@Query("SELECT * FROM users")
fun getUsersWithBooks(): Flow<List<UserWithBooks>>
@Transaction 确保关联数据一次性完整获取。
数据库版本迁移
随着应用升级,表结构可能发生变更。Room 提供 Migration 类来处理这一过程。
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE users ADD COLUMN phone TEXT NOT NULL DEFAULT ''")
}
}
// 构建数据库时加入迁移
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2)
.build()
版本号需递增,并在 @Database 中更新 version。若没有提供迁移路径,Room 会抛出异常;开发阶段可使用 fallbackToDestructiveMigration() 注意:这会清空数据库数据。
使用类型转换器(TypeConverter)
Room 只能存储基本数据类型,如需存储 Date、List 等复杂类型,需要使用 @TypeConverter。
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
并在 @Database 中注册:
@Database(entities = [User::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
测试 Room
Room 支持在内存中创建数据库进行测试,避免影响真实数据。
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
userDao = database.userDao()
}
@After
fun closeDb() {
database.close()
}
@Test
fun insertAndGetUser() = runTest {
val user = User(name = "Alice", age = 25, email = "alice@test.com")
userDao.insertUser(user)
val retrieved = userDao.getUserById(1)
assertEquals("Alice", retrieved?.name)
}
}
最佳实践
- 避免在主线程执行数据库操作:利用挂起函数或 Flow 自动转移工作线程。
- 单一数据库实例:使用单例或依赖注入(如 Dagger/Hilt)管理
AppDatabase实例。 - Schema 导出:在版本控制中保存数据库 schema 文件,便于团队协作和迁移对比。
- 谨慎使用
@Transaction:仅在需要原子性操作时使用,过多事务可能影响性能。 - 使用适当的返回类型:只读查询考虑返回
Flow或LiveData;一次性写入/查询使用suspend函数。
总结
Room 让 Android 本地存储变得安全、高效且优雅。通过注解驱动和编译时校验,显著降低了手动编写 SQLite 的错误概率。结合协程和 Flow,开发者可以轻松构建出离线优先且界面流畅的应用。从简单的 CRUD 到复杂的关联查询和版本迁移,Room 提供了完整的工具链。掌握 Room,是 Android 开发进阶的必经之路。