Room数据库:Entity、DAO、Database、CRUD与类型转换器

说实话,Room 是我在 Android 开发中最喜欢的本地存储方案。没有之一。

以前用 SQLite 原生 API,写个查询得拼字符串,稍不留神就拼错了。后来用 GreenDAO、Realm,各有各的坑。直到 Google 推出了 Room,我才觉得「嗯,这才是 Android 该有的数据库姿势」。

今天我们就来彻底搞懂 Room 的四个核心组件:Entity、DAO、Database,以及类型转换器。我会结合我自己的踩坑经历,帮你避开那些常见的雷区。

Room 数据库核心架构 RoomDatabase Entity(实体) DAO(数据访问) @Entity 注解的表映射 @PrimaryKey 主键 @Insert / @Update / @Delete @Query 自定义查询 @TypeConverter 类型转换器

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?,表示允许为空。
💡 我的习惯: 所有 Entity 类我都放在 entity 包下,命名用单数形式(User 而不是 Users)。这样代码结构清晰,找起来方便。

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,代码写得又臭又长。你想想看,用协程它不香吗?

⚠️ 避坑指南: 我曾经在 @Query 里写 SQL 语句时,把表名写错了。Room 编译期会检查 SQL 语法,但不会检查表名是否存在。所以写完后最好跑一次测试,确保表名、字段名都对得上。

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()
    }
}

你看,整个流程非常清晰:

  1. 构造 Entity 对象
  2. 调用 DAO 方法
  3. 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。

🔑 核心要点: 类型转换器本质上是「序列化/反序列化」的桥梁。你存什么格式都行,JSON、逗号分隔、甚至二进制都 OK。但建议用 JSON,可读性更好,也方便以后做数据迁移。

6. 避坑总结

最后,把我这些年踩过的坑分享给你:

  • 数据库版本升级: 改了 Entity 结构后,记得升级 version 号,并写 Migration。不然直接 crash。
  • 主键不能为 null: 如果用 autoGenerate,主键类型用 Int 而不是 Int?。否则 Room 会报错。
  • DAO 方法参数顺序: @Query 里的参数名要和方法参数名一致。我试过拼写错误,编译不报错,运行才报。
  • Context 用 applicationContext: 创建 Database 时一定要用 applicationContext,防止内存泄漏。

Room 其实不难,难的是把细节处理好。你只要把 Entity、DAO、Database 这三层理清楚,再配好类型转换器,本地存储这块基本就稳了。

好了,今天就聊到这里。希望这些实战经验对你有帮助。


公众号:蓝海资料掘金营,微信deep3321