Room入门:添加依赖、定义实体、创建DAO接口、集成Database

我在带团队做第一个离线App的时候,一开始用的是裸SQLite,写完发现维护成本太高。后来切到Room,说实话,真香。咱今天就把Room入门的全流程走一遍,从依赖到跑通,一步不落。

添加Room依赖

Room其实是Google对SQLite的一层封装,但它跟Jetpack全家桶深度绑定。第一步,在build.gradle(模块级别)里加上依赖。

注意:Room需要用注解处理器生成代码,所以必须同时加KSP(推荐)或KAPT。
dependencies {
    // Room核心库
    implementation("androidx.room:room-runtime:2.6.1")
    // KSP处理器(如果用Kotlin 1.9+)
    ksp("androidx.room:room-compiler:2.6.1")
    // 可选:Kotlin协程支持
    implementation("androidx.room:room-ktx:2.6.1")
}

我刚开始用Room时,踩过一个坑:版本号没和Kotlin插件匹配。比如Room 2.5.x需要KSP 1.8.x,否则编译直接报错。你想想看,折腾半小时才发现是版本不兼容。后来我习惯去Room官方版本说明页面确认对应关系。

还有一点:如果你项目用的是Java,那就用annotationProcessor替代KSP;但咱们做Android新项目基本都用Kotlin,所以直接上KSP就好。

小技巧:把Room版本号提取到build.gradle的ext块里,方便统一升级。比如:
ext.room_version = "2.6.1"

定义实体(Entity)

实体就是一张表。说白了,一个Kotlin数据类加上@Entity注解,Room就能把它变成建表语句。我一般在entity包下放这个类。

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    @ColumnInfo(name = "user_name")
    val name: String,
    val age: Int,
    val email: String?
)

这里有几个点值得注意:

  • 主键必填——每个Entity必须至少有一个@PrimaryKey,否则编译报错。
  • 列名可自定义——用@ColumnInfo可以改数据库里的字段名,但实体属性名才是Kotlin里用的。
  • 可空类型——email: String?对应数据库里的NULL列。我早期犯过错,把该可空的字段写成,结果存NULL时报错。
避坑:我曾经在定义@Entity的时候,忘了给主键加autoGenerate = true,结果每次插入手动设ID,导致主键冲突。记住:自增主键就用autoGenerate。

如果表里有嵌套对象,Room还支持@Embedded。不过入门阶段先不用,等后面讲关系查询再展开。

创建DAO接口

DAO是Data Access Object,用来定义增删改查。Room提倡用接口(interface)而非抽象类,因为这样更轻量。我给你看一个典型的例子:

@Dao
interface UserDao {
    @Insert
    suspend fun insertUser(user: User)

    @Update
    suspend fun updateUser(user: User)

    @Delete
    suspend fun deleteUser(user: User)

    @Query("SELECT * FROM users ORDER BY id DESC")
    fun getAllUsers(): Flow<List<User>>

    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserById(userId: Long): User?
}

注意看:

  • 挂起函数——Room支持Kotlin协程,用suspend让数据库操作在后台线程跑。
  • 返回Flow——查询用Flow,数据变化时自动通知UI。这是Room和DataStore共通的优势。
  • 参数绑定——:userId会自动匹配方法参数,Room帮你防注入。

我个人的习惯是:每个DAO只对应一个实体,除非有特殊关联查询。这样代码好维护,也符合单一职责。你不用追求一个DAO管所有表,拆开反而清晰。

为什么用接口?Room会在编译时自动生成接口的实现类(比如UserDao_Impl),接口比抽象类更容易做动态代理。而且你不需要写任何实现代码,省事儿。

集成Database

最后一大步:定义@Database抽象类,把实体和DAO串起来。这是Room的入口,也是你App里的数据库实例。

@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
            }
        }
    }
}

关键点拆开说:

  • entities数组——把你所有的Table类都列进去。我这只放了一个User,以后再加。
  • version & exportSchema——版本号用于迁移;exportSchema建议生产环境设为true,然后把Schema文件保存到代码里,方便查历史变更。
  • 单例模式——INSTANCE加@Volatile和synchronized,保证多线程下只有一个数据库实例。我见过有人忘了加,然后Crash报“Room database instance already closed”。
  • context.applicationContext——这里一定要用ApplicationContext,避免Activity泄漏。
提示:如果只是开发阶段,可以加.fallbackToDestructiveMigration()——当数据库版本升级时,Room直接删表重建。但上生产环境前一定要写迁移脚本,否则用户数据全丢。

到这里,Room的三件套就齐了:Entity、DAO、Database。你可以在Application.onCreate()里初始化:

class MyApp : Application() {
    lateinit var database: AppDatabase

    override fun onCreate() {
        super.onCreate()
        database = AppDatabase.getDatabase(this)
    }
}

然后通过userDao去操作数据库。比如在ViewModel里注入,配合Repository模式,完美。

Room核心组件一览

组件作用入门要点
@Entity定义表结构必须指定主键,列名可选
@Dao定义数据操作接口 + suspend / Flow
@Database数据库入口单例、版本号、迁移策略
Room.databaseBuilder创建实例传ApplicationContext
Room 入门核心流程 添加依赖 room-runtime + KSP 定义Entity @Entity data class 创建DAO @Dao interface 集成Database @Database + 单例 编译生成 _Impl 自动生成 使用CRUD suspend / Flow ⚠ 关键提醒 数据库实例必须全局单例,否则多线程下会崩溃。 版本升级时务必写迁移脚本,或开发阶段使用 fallbackToDestructiveMigration()。

你看,整个流程就是从上往下串起来的。依赖配好 → 定义实体 → 写DAO → 集成Database → 编译自动生成实现 → 最后在业务代码里用CRUD。每一步都有Room注解驱动,你几乎不用写SQL(除了@Query里复杂的查询)。

个人经验小结:我刚开始用Room时,总想手动写数据库Helper。后来发现Room不仅自动处理线程问题,还能跟LiveData/Flow无缝结合。生产环境跑了两年,基本没出过数据层的问题。所以,尽管放心用,别自己造轮子。

补充一句:如果后续需要数据库迁移,记得把exportSchema = true开起来,然后写Migration对象。不过今天入门,先不展开。