Room入门:添加依赖、定义实体、创建DAO接口、集成Database
我在带团队做第一个离线App的时候,一开始用的是裸SQLite,写完发现维护成本太高。后来切到Room,说实话,真香。咱今天就把Room入门的全流程走一遍,从依赖到跑通,一步不落。
添加Room依赖
Room其实是Google对SQLite的一层封装,但它跟Jetpack全家桶深度绑定。第一步,在build.gradle(模块级别)里加上依赖。
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就好。
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管所有表,拆开反而清晰。
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 |
你看,整个流程就是从上往下串起来的。依赖配好 → 定义实体 → 写DAO → 集成Database → 编译自动生成实现 → 最后在业务代码里用CRUD。每一步都有Room注解驱动,你几乎不用写SQL(除了@Query里复杂的查询)。
补充一句:如果后续需要数据库迁移,记得把exportSchema = true开起来,然后写Migration对象。不过今天入门,先不展开。