第一章:Room数据库简介——ORM概念、Room组件、优势归纳
各位同学,今天咱们来聊聊 Room。说实话,第一次接触 Android 本地存储的时候,我踩了不少坑。从早期的 SQLite 裸写 SQL,到后来用 GreenDAO、Realm,再到官方推出 Room,这个过程就像一部进化史。嗯,Room 现在基本是官方钦定的本地数据库方案了。为什么?因为它真的太懂 Android 开发者的痛点了。
1. ORM —— 说白了就是让对象和数据库“谈恋爱”
ORM 全称 Object-Relational Mapping,对象关系映射。你想想看,Java/Kotlin 里我们用的是对象(Object),数据库里存的是表(Table)里的行(Row)。中间要互相转换,写一堆 getter/setter、Cursor 遍历、字段赋值……以前我写过一段代码,光是把查询结果转成 Bean 就占了 30 行。这种“模板代码”不仅烦,还容易漏。
举个小例子:没有 ORM 时,保存一个 User 对象可能要这样:
// 传统的 SQLite 方式
ContentValues values = new ContentValues();
values.put("name", user.getName());
values.put("age", user.getAge());
db.insert("user", null, values);
有了 Room,你只需要:
@Insert
void insertUser(User user);
是不是清爽多了?我在早期项目里试过手动维护 ContentValues,一旦字段多了,经常写漏或写错类型。Room 的注解编译器会帮你检查 —— 写错了直接编译报错,不用等到运行时崩溃。
2. Room 三大组件 —— 分工明确,各司其职
Room 由 Database、Entity、DAO 三部分组成。我习惯把它们类比成一个餐厅:数据库是厨房,Entity 是菜单上的菜品描述,DAO 是服务员 —— 你告诉服务员想吃什么,他帮你从厨房端出来。
| 组件 | 角色 | 我的一点经验 |
|---|---|---|
| Database | 数据库持有者,负责创建/升级库,提供 DAO 实例 | 记得用单例,避免重复创建实例 |
| Entity | 数据表对应的 Kotlin/Java 类,每个字段对应一列 | 主键别忘加 @PrimaryKey,自增用 autoGenerate = true |
| DAO | 数据访问对象,定义了 CRUD 操作 | 尽量用 suspend 或 LiveData 返回,一步到位 |
2.1 Entity —— 数据表的样子
一个 Entity 就是一个数据类。比如我们的用户表:
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val age: Int
)
注意:字段名默认就是列名,如果需要别名可以用 @ColumnInfo(name = "user_name")。我曾经遇到过字段命名不规范,后来表结构改了,代码里忘了同步,编译时 Room 直接给了个红色波浪线 —— 省了不少坑。
2.2 DAO —— 操作数据库的接口
DAO 是一个接口,里面用注解标记 SQL 操作。
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): List<User>
@Insert
suspend fun insertUser(user: User)
@Delete
fun deleteUser(user: User)
}
我个人的习惯是:所有写操作都用 suspend 函数,配合 Kotlin 协程,避免在主线程搞事情。Room 默认不允许在主线程做数据库操作,除非你主动调用 allowMainThreadQueries() —— 但真的不建议,容易 ANR。
2.3 Database —— 中央仓库
需要创建一个抽象类继承 RoomDatabase,并关联 Entity 和 DAO。
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
fallbackToDestructiveMigration() 或者写 Migration 对象。我在一个线上项目里忘了写 Migration,结果用户升级后直接崩溃报错,被 PM 追着改了一个晚上……
3. Room 的优势 —— 为什么我抛弃了其他方案
- 编译时校验 SQL:写错表名或字段名,编译直接报红,而不是运行时闪退。这个太重要了,以前用原生 SQLite 写字符串拼接,一不留神就崩。
- 与 LiveData / Flow 无缝集成:DAO 返回 LiveData 或 Kotlin Flow,观察数据变化,UI 自动更新。省去了手动刷新列表的步骤。
- 支持协程:直接写
suspend函数,避免回调地狱。配合withContext(Dispatchers.IO)更安全。 - 自动处理线程:Room 强制要求数据库操作在后台线程,防止开发者不小心阻塞主线程。
- 支持 RxJava:虽然现在协程主流,但老项目也能接入。
- 迁移友好:提供 Migration 类,逐步修改表结构,而不用删库重来。
4. Room 知识体系一览
从上图可以看出,你只需要定义 Entity 和 DAO,然后告诉 Database 有哪些 Entity 和 DAO。剩下的 SQL 生成、事务管理、线程切换,Room 全部帮你搞定。我个人觉得,这种“约定大于配置”的设计思路,大大降低了本地存储的维护成本。
5. 快速体验:一个完整的例子
假设我们要做一个简单的笔记 App,存储标题和内容。
// Entity
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String,
val content: String
)
// DAO
@Dao
interface NoteDao {
@Insert
suspend fun insert(note: Note)
@Query("SELECT * FROM notes ORDER BY id DESC")
fun getAllNotes(): LiveData<List<Note>>
}
// Database
@Database(entities = [Note::class], version = 1)
abstract class NoteDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile
private var INSTANCE: NoteDatabase? = null
fun getInstance(context: Context): NoteDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
NoteDatabase::class.java,
"note_database"
).build()
INSTANCE = instance
instance
}
}
}
}
然后你就可以在 ViewModel 里这样使用:
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val dao = NoteDatabase.getInstance(application).noteDao()
val allNotes = dao.getAllNotes()
fun addNote(title: String, content: String) {
viewModelScope.launch {
dao.insert(Note(title = title, content = content))
}
}
}
所有操作都通过 DAO 进行,数据源变化后 LiveData 自动通知 UI 更新。是不是很顺滑?
6. 避坑小清单(来自我的血泪史)
- 不要在主线程运行数据库操作——除非你明确调用
allowMainThreadQueries(),但我建议永远不要这样做。 - Entity 字段类型避免使用非空集合——Room 不支持直接存储 List 等复杂类型,需要用
TypeConverter转换成 JSON 字符串。我曾经忘写 TypeConverter,编译报错找了半天。 - 数据库升级一定要写 Migration——即使只是加一个字段,也要写对应的 Migration 脚本。否则用户安装新版本后直接 crash。
- DAO 的返回值使用 LiveData 或 Flow 时,Room 会自动在后台线程查询,不需要再加
suspend。但如果是直接返回 List,则需要用协程或 Rx。 - 注意 @Database 的 entities 参数——所有用到的 Entity 都要列在里面,少一个就会编译报错。