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 会被覆盖成默认值。我曾经因为这个被坑过一次——更新用户信息时,忘了传某些字段,结果数据丢了。

注意:Room 的 @Update 会覆盖整行数据。如果你只想更新个别字段,请使用 @Query 写自定义 SQL。

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 的核心流程:

Room CRUD 核心流程 User Entity UserDao AppDatabase @Insert @Query @Update @Delete Entity 定义表结构 → DAO 定义操作 → Database 管理实例

6.8 避坑指南

最后,我把自己踩过的坑总结一下:

  • 数据库版本升级:如果你改了 Entity 结构,记得升级 version,并写 Migration。否则会 crash。
  • 不要在 DAO 里写耗时逻辑:DAO 只负责数据库操作,业务逻辑放到 Repository 或 ViewModel 里。
  • LiveData 的初始值:Room 返回的 LiveData 在第一次查询时会立即触发一次,所以 UI 初始化时就能拿到数据。
  • 测试时用内存数据库Room.inMemoryDatabaseBuilder 可以建一个不落盘的数据库,测试完自动销毁,很方便。
小技巧:如果你在调试时想看看数据库里到底有什么,可以用 Android Studio 自带的 Database Inspector。在 View > Tool Windows > App Inspection 里就能找到。

好了,这一章的内容就到这里。你跟着敲一遍,应该就能掌握 Room 的 CRUD 了。代码不多,但每一步都很关键。下次我们再聊点更高级的——比如多表查询和事务。


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