Room数据库:Entity、DAO、Database、CRUD与类型转换器
说实话,Room 是我在 Android 开发中最喜欢的本地存储方案。没有之一。
以前用 SQLite 原生 API,写个查询得拼字符串,稍不留神就拼错了。后来用 GreenDAO、Realm,各有各的坑。直到 Google 推出了 Room,我才觉得「嗯,这才是 Android 该有的数据库姿势」。
今天我们就来彻底搞懂 Room 的四个核心组件:Entity、DAO、Database,以及类型转换器。我会结合我自己的踩坑经历,帮你避开那些常见的雷区。
1. Entity:数据表的 Kotlin 化身
Entity 说白了就是一张数据库表的 Kotlin 类映射。你定义一个数据类,加上 @Entity 注解,Room 就自动帮你建表。
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val age: Int,
val email: String?
)
这里有几个细节我特别想强调:
- @PrimaryKey 必须要有,不然 Room 会报错。我刚开始学的时候忘了加,编译直接挂了。
- autoGenerate = true 让主键自增,省心省力。
- 可空类型 对应数据库的 NULL 字段。比如 email 我用了 String?,表示允许为空。
2. DAO:数据访问的接口层
DAO 是 Data Access Object 的缩写。它就是一个接口,你定义好方法,Room 帮你生成实现代码。
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("SELECT * FROM users ORDER BY id DESC")
suspend fun getAllUsers(): List<User>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
}
注意看,我用了 suspend 关键字。为什么?因为 Room 不允许在主线程执行数据库操作,用协程是最优雅的解决方案。
我曾经见过有人用 AsyncTask 去调 Room,代码写得又臭又长。你想想看,用协程它不香吗?
3. Database:数据库的入口
Database 是一个抽象类,继承 RoomDatabase。它负责创建数据库实例,并暴露 DAO 对象。
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
这里有个经典的单例模式。为什么要用 @Volatile 和 synchronized?说白了就是为了线程安全。多线程同时调用 getDatabase 时,保证只创建一个实例。
我个人的习惯是:exportSchema = false 在开发阶段可以关掉,避免生成一堆 schema 文件。但上线前建议打开,方便以后做数据库迁移。
4. CRUD 操作实战
有了 Entity、DAO、Database,我们来实际跑一遍增删改查。
// 在 ViewModel 或 Repository 中
class UserRepository(private val userDao: UserDao) {
suspend fun addUser(name: String, age: Int, email: String?) {
val user = User(name = name, age = age, email = email)
userDao.insert(user)
}
suspend fun updateUserAge(userId: Int, newAge: Int) {
val user = userDao.getUserById(userId)
user?.let {
userDao.update(it.copy(age = newAge))
}
}
suspend fun deleteUser(userId: Int) {
val user = userDao.getUserById(userId)
user?.let { userDao.delete(it) }
}
suspend fun getAllUsers(): List<User> {
return userDao.getAllUsers()
}
}
你看,整个流程非常清晰:
- 构造 Entity 对象
- 调用 DAO 方法
- Room 自动转成 SQL 语句执行
我记得第一次用 Room 做 CRUD 时,最大的感受就是「终于不用写 SQL 了」。虽然 @Query 还是要写,但增删改基本零代码。
5. 类型转换器:处理复杂类型
Room 默认只支持基本类型和 String。如果你存 List、Date 这类复杂类型,就需要类型转换器。
举个例子,我想存用户的爱好列表:
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val hobbies: List<String> // Room 不认识 List!
)
直接这么写,编译会报错。怎么办?写个转换器:
class Converters {
@TypeConverter
fun fromHobbyList(value: List<String>): String {
return value.joinToString(",")
}
@TypeConverter
fun toHobbyList(value: String): List<String> {
return value.split(",").filter { it.isNotEmpty() }
}
}
然后在 Database 上注册:
@Database(
entities = [User::class],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
// ...
}
这样 Room 就知道怎么存 List 了——转成逗号分隔的字符串存进去,读出来再还原成 List。
6. 避坑总结
最后,把我这些年踩过的坑分享给你:
- 数据库版本升级: 改了 Entity 结构后,记得升级 version 号,并写 Migration。不然直接 crash。
- 主键不能为 null: 如果用 autoGenerate,主键类型用 Int 而不是 Int?。否则 Room 会报错。
- DAO 方法参数顺序: @Query 里的参数名要和方法参数名一致。我试过拼写错误,编译不报错,运行才报。
- Context 用 applicationContext: 创建 Database 时一定要用 applicationContext,防止内存泄漏。
Room 其实不难,难的是把细节处理好。你只要把 Entity、DAO、Database 这三层理清楚,再配好类型转换器,本地存储这块基本就稳了。
好了,今天就聊到这里。希望这些实战经验对你有帮助。
公众号:蓝海资料掘金营,微信deep3321