6、基础CRUD实战:创建数据库、插入数据、查询全部、更新数据、删除数据
好,咱们今天来点真格的。
前面几章我们把 Room 的理论、注解、配置都过了一遍。说实话,光看不练容易忘。我个人习惯是,学一个新东西,先跑通一个最简单的 CRUD,心里就有底了。
这一章,我们就手把手把「增删改查」全走一遍。你跟着我敲,保证能跑起来。
6.1 先搭个架子:Entity + DAO + Database
老规矩,先定义一张表。我们用一个最简单的 User 实体。
@Entity(tableName = "user")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val age: Int
)
嗯,这里要注意:id 我给了默认值 0,并且 autoGenerate = true。这样插入时不用传 id,Room 会自动生成。
接下来是 DAO 接口。我建议你一口气把五个方法都写上:
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Query("SELECT * FROM user")
suspend fun getAll(): List<User>
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("DELETE FROM user")
suspend fun deleteAll()
}
你看,getAll() 和 deleteAll() 我用了 @Query,因为 Room 没有内置的「查全部」和「删全部」注解。其他三个直接用注解就行,省事。
最后是 Database 类:
@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,
"my_room.db"
).build()
INSTANCE = instance
instance
}
}
}
}
这段代码我几乎每个项目都这么写。单例 + synchronized,防止多线程同时创建数据库实例。你想想看,如果两个线程同时调 getInstance,没有同步的话,可能会建两个数据库对象,那就乱套了。
6.2 插入数据:别在主线程搞事情
Room 默认不允许在主线程做数据库操作。为什么?因为数据库读写可能耗时,卡 UI 的话用户会骂娘。
所以我们要用协程。在 ViewModel 或者 Repository 里这样写:
viewModelScope.launch {
val user = User(name = "张三", age = 25)
userDao.insert(user)
}
我在项目中遇到过一个问题:插入后想立刻拿到生成的 id。怎么办?很简单,把 insert 方法的返回值改成 Long:
@Insert
suspend fun insert(user: User): Long
返回的就是新行的 id。这个技巧很实用,比如你插入订单后,需要拿着订单 id 去插入订单明细。
6.3 查询全部:LiveData 是好朋友
查询全部数据,最常用的方式是返回 LiveData。这样当数据库变化时,UI 会自动更新。
@Query("SELECT * FROM user")
fun getAllLiveData(): LiveData<List<User>>
注意这里我没有加 suspend。因为 LiveData 本身就是异步的,Room 会在后台线程查询,然后通过 LiveData 把结果抛到主线程。
在 Activity 或 Fragment 里观察:
userDao.getAllLiveData().observe(this) { users ->
// 更新 UI
adapter.submitList(users)
}
嗯,这就是 Room 最爽的地方——数据一变,UI 自动刷新。你不用手动去调查询方法。
6.4 更新数据:按主键匹配
更新操作很简单,Room 会根据 @PrimaryKey 来匹配要更新的行。
viewModelScope.launch {
val user = User(id = 1, name = "李四", age = 30)
userDao.update(user)
}
这里有个坑:如果你只改了 name,但 age 没传,age 会被覆盖成默认值。我曾经因为这个被坑过一次——更新用户信息时,忘了传某些字段,结果数据丢了。
6.5 删除数据:同样按主键
删除和更新类似,也是按主键匹配:
viewModelScope.launch {
val user = User(id = 1, name = "李四", age = 30)
userDao.delete(user)
}
你只需要传一个包含 id 的对象就行,其他字段 Room 不会管。所以你可以这样写:
val user = User(id = 1) // 只传 id
userDao.delete(user)
当然,更直接的方式是用 @Query 按条件删除:
@Query("DELETE FROM user WHERE age > :maxAge")
suspend fun deleteByAge(maxAge: Int)
6.6 完整流程:从创建到删除
我们把上面所有操作串起来,看看完整的调用流程:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val db = AppDatabase.getInstance(application)
private val userDao = db.userDao()
val allUsers: LiveData<List<User>> = userDao.getAllLiveData()
fun insertUser(name: String, age: Int) {
viewModelScope.launch {
userDao.insert(User(name = name, age = age))
}
}
fun updateUser(user: User) {
viewModelScope.launch {
userDao.update(user)
}
}
fun deleteUser(user: User) {
viewModelScope.launch {
userDao.delete(user)
}
}
fun deleteAll() {
viewModelScope.launch {
userDao.deleteAll()
}
}
}
你看,整个 CRUD 就这么几行代码。Room 帮我们省掉了写 SQLiteOpenHelper、ContentValues、Cursor 这些繁琐的东西。
6.7 知识体系图
下面这张图帮你理清 Room CRUD 的核心流程:
6.8 避坑指南
最后,我把自己踩过的坑总结一下:
- 数据库版本升级:如果你改了 Entity 结构,记得升级 version,并写 Migration。否则会 crash。
- 不要在 DAO 里写耗时逻辑:DAO 只负责数据库操作,业务逻辑放到 Repository 或 ViewModel 里。
- LiveData 的初始值:Room 返回的 LiveData 在第一次查询时会立即触发一次,所以 UI 初始化时就能拿到数据。
- 测试时用内存数据库:
Room.inMemoryDatabaseBuilder可以建一个不落盘的数据库,测试完自动销毁,很方便。
好了,这一章的内容就到这里。你跟着敲一遍,应该就能掌握 Room 的 CRUD 了。代码不多,但每一步都很关键。下次我们再聊点更高级的——比如多表查询和事务。
公众号:蓝海资料掘金营,微信deep3321