MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin Android Room数据库

2021-06-113.8k 阅读

一、Room 数据库简介

在 Android 开发中,数据持久化是一个至关重要的环节。传统的 SQLiteOpenHelper 虽然提供了基本的 SQLite 数据库操作能力,但使用起来较为繁琐,需要手动编写大量的 SQL 语句来执行数据库的创建、升级以及数据的增删改查操作。而 Room 是 Google 在 Android Jetpack 组件中推出的一个 SQLite 数据库抽象层库,它旨在让开发者更轻松、更高效地在 Android 应用中使用 SQLite 数据库,同时遵循现代 Android 开发的最佳实践。

Room 主要有以下几个优点:

  1. 强大的编译时检查:通过注解处理器,Room 能在编译期检查 SQL 语句的正确性,大大减少运行时因 SQL 错误导致的崩溃。例如,如果在定义查询方法时写错了表名或者列名,编译器会直接报错,而不是等到运行时才发现问题。
  2. 简化数据库操作:Room 提供了简洁的 API 来进行数据库操作,开发者无需编写大量重复的 SQLite 代码。比如,对于数据的插入、更新和删除操作,只需定义相应的方法并添加对应的注解,Room 会自动生成实现代码。
  3. 支持 LiveData 和 RxJava:Room 能够与 LiveData 和 RxJava 很好地集成,方便开发者实现数据的响应式编程。例如,使用 LiveData 可以让 UI 实时感知数据库数据的变化并自动更新,而 RxJava 则提供了强大的异步操作能力,使得数据库操作可以在后台线程进行,避免阻塞主线程。

二、配置项目以使用 Room

要在 Kotlin 项目中使用 Room,首先需要在项目的 build.gradle 文件中添加依赖。在 app/build.gradle 文件中,添加如下依赖:

def room_version = "2.4.3"

implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// 如果需要使用 Kotlin 扩展
implementation "androidx.room:room-ktx:$room_version"
// 如果需要使用 LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
// 如果需要使用 RxJava
implementation "androidx.room:room-rxjava2:$room_version"

添加完依赖后,同步项目,确保依赖下载成功。

三、创建 Room 数据库

  1. 定义数据库实体类: 实体类代表数据库中的表,每个实体类的实例对应表中的一行数据。在 Kotlin 中,使用 @Entity 注解来标识一个类为数据库实体。例如,我们创建一个简单的用户表对应的实体类 User
import androidx.room.Entity
import androidx.room.PrimaryKey

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

在这个 User 类中,@Entity 注解指定了表名为 users@PrimaryKey 注解标识了 id 字段为主键,并且 autoGenerate = true 表示该主键自增长。

  1. 创建数据库访问对象(DAO): DAO 负责定义数据库的操作方法,如插入、查询、更新和删除等。使用 @Dao 注解标识一个接口或抽象类为 DAO。例如:
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update

@Dao
interface UserDao {

    @Insert
    suspend fun insert(user: User)

    @Query("SELECT * FROM users")
    suspend fun getAllUsers(): List<User>

    @Update
    suspend fun update(user: User)

    @Query("DELETE FROM users WHERE id = :userId")
    suspend fun deleteById(userId: Int)
}

在上述代码中,@Insert 注解的方法用于插入数据,@Query 注解的方法用于执行自定义的 SQL 查询,@Update 注解的方法用于更新数据,@Query 注解的另一个方法用于根据 id 删除数据。注意,这里的方法都声明为 suspend,表示这是挂起函数,需要在协程中调用,以避免阻塞主线程。

  1. 创建 Room 数据库类: 使用 @Database 注解创建一个继承自 RoomDatabase 的抽象类,用于定义数据库的相关信息,如包含哪些实体类和 DAO。例如:
import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

在这个 AppDatabase 类中,@Database 注解指定了包含的实体类为 User,数据库版本为 1。同时,通过抽象方法 userDao() 暴露了 UserDao

  1. 获取数据库实例: 在应用中,通常需要一个单例的数据库实例。可以使用 Room 类的 databaseBuilder 方法来构建数据库实例。例如:
import android.content.Context
import androidx.room.Room

object DatabaseInstance {
    private lateinit var instance: AppDatabase

    fun getInstance(context: Context): AppDatabase {
        if (!::instance.isInitialized) {
            instance = Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "app_database"
            )
               .build()
        }
        return instance
    }
}

在上述代码中,通过 Room.databaseBuilder 方法创建了 AppDatabase 的实例,数据库名称为 app_database。这里使用了 Kotlin 的 lateinit 关键字来延迟初始化 instance,并通过 !::instance.isInitialized 来判断是否已经初始化,以确保单例特性。

四、数据库操作示例

  1. 插入数据: 在获取到数据库实例和对应的 DAO 后,就可以进行数据插入操作。例如,在一个视图模型(ViewModel)中:
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {

    fun insertUser(user: User) {
        CoroutineScope(Dispatchers.IO).launch {
            val dao = DatabaseInstance.getInstance(App.instance).userDao()
            dao.insert(user)
        }
    }
}

在上述代码中,insertUser 方法通过 CoroutineScope 在 IO 线程中启动一个协程,获取 UserDao 并调用 insert 方法插入用户数据。这里的 App.instance 是一个自定义的用于获取应用上下文的单例对象。

  1. 查询数据: 查询数据同样在协程中进行。例如:
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.Flow

class MainViewModel : ViewModel() {

    fun getAllUsers(): Flow<List<User>> {
        return CoroutineScope(Dispatchers.IO).async {
            val dao = DatabaseInstance.getInstance(App.instance).userDao()
            dao.getAllUsers()
        }.flow
    }
}

这里通过 CoroutineScope 在 IO 线程中异步查询所有用户数据,并返回一个 FlowFlow 是 Kotlin 中用于异步数据流的类型,适合在响应式编程中使用。在 UI 层,可以使用 collect 方法来收集 Flow 中的数据并更新 UI。

  1. 更新数据: 更新数据的操作与插入和查询类似。例如:
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {

    fun updateUser(user: User) {
        CoroutineScope(Dispatchers.IO).launch {
            val dao = DatabaseInstance.getInstance(App.instance).userDao()
            dao.update(user)
        }
    }
}

updateUser 方法中,在 IO 线程中获取 UserDao 并调用 update 方法更新用户数据。

  1. 删除数据: 删除数据也遵循相同的模式。例如:
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {

    fun deleteUserById(userId: Int) {
        CoroutineScope(Dispatchers.IO).launch {
            val dao = DatabaseInstance.getInstance(App.instance).userDao()
            dao.deleteById(userId)
        }
    }
}

deleteUserById 方法中,根据传入的 userId 在 IO 线程中调用 UserDaodeleteById 方法删除用户数据。

五、Room 与 LiveData 的集成

  1. 修改 DAO 以返回 LiveData: 为了使数据变化能够实时反映到 UI 上,可以让 DAO 方法返回 LiveData。例如,修改 UserDao 中的查询方法:
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import androidx.lifecycle.LiveData

@Dao
interface UserDao {

    @Insert
    suspend fun insert(user: User)

    @Query("SELECT * FROM users")
    fun getAllUsers(): LiveData<List<User>>

    @Update
    suspend fun update(user: User)

    @Query("DELETE FROM users WHERE id = :userId")
    suspend fun deleteById(userId: Int)
}

这里将 getAllUsers 方法的返回类型改为 LiveData<List<User>>

  1. 在视图模型中使用 LiveData: 在视图模型中,可以直接获取 LiveData 并暴露给 UI。例如:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData

class MainViewModel : ViewModel() {

    val allUsers = liveData {
        val dao = DatabaseInstance.getInstance(App.instance).userDao()
        emit(dao.getAllUsers().value?: emptyList())
    }
}

在上述代码中,使用 liveData 构建器创建了一个 LiveData,在其中获取 UserDao 并发射查询到的用户数据。如果数据为空,则发射一个空列表。

  1. 在 UI 中观察 LiveData: 在 Activity 或 Fragment 中,可以观察 LiveData 并根据数据变化更新 UI。例如,在一个 Fragment 中:
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import kotlinx.android.synthetic.main.fragment_main.*
import java.util.*

class MainFragment : Fragment() {

    private lateinit var viewModel: MainViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_main, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        viewModel.allUsers.observe(viewLifecycleOwner, Observer { users ->
            val userNames = users.map { it.name }.joinToString(", ")
            textView.text = "Users: $userNames"
        })
    }
}

onViewCreated 方法中,获取视图模型并观察 allUsers LiveData。当数据发生变化时,更新 TextView 显示用户名字列表。

六、Room 与 RxJava 的集成

  1. 修改 DAO 以返回 RxJava 类型: 如果要使用 RxJava 进行数据库操作,可以让 DAO 方法返回 RxJava 的类型,如 SingleCompletable 等。例如,修改 UserDao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import io.reactivex.Completable
import io.reactivex.Single

@Dao
interface UserDao {

    @Insert
    fun insert(user: User): Completable

    @Query("SELECT * FROM users")
    fun getAllUsers(): Single<List<User>>

    @Update
    fun update(user: User): Completable

    @Query("DELETE FROM users WHERE id = :userId")
    fun deleteById(userId: Int): Completable
}

这里将插入、更新和删除方法返回 Completable,表示操作完成但不返回数据;查询方法返回 Single,表示返回一个单一的数据对象(这里是用户列表)。

  1. 在视图模型中使用 RxJava: 在视图模型中,可以使用 RxJava 的操作符来处理数据库操作。例如:
import androidx.lifecycle.ViewModel
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers

class MainViewModel : ViewModel() {

    private val disposable = CompositeDisposable()

    fun insertUser(user: User) {
        val dao = DatabaseInstance.getInstance(App.instance).userDao()
        disposable.add(
            dao.insert(user)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe({
                    // 插入成功
                }, {
                    // 插入失败
                })
        )
    }
}

insertUser 方法中,使用 subscribeOn(Schedulers.io()) 将插入操作放在 IO 线程执行,observeOn(AndroidSchedulers.mainThread()) 将结果观察放在主线程,以便更新 UI。

  1. 管理 RxJava 的订阅: 在视图模型的 onCleared 方法中,需要取消所有的 RxJava 订阅,以避免内存泄漏。例如:
import androidx.lifecycle.ViewModel
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers

class MainViewModel : ViewModel() {

    private val disposable = CompositeDisposable()

    fun insertUser(user: User) {
        val dao = DatabaseInstance.getInstance(App.instance).userDao()
        disposable.add(
            dao.insert(user)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe({
                    // 插入成功
                }, {
                    // 插入失败
                })
        )
    }

    override fun onCleared() {
        super.onCleared()
        disposable.clear()
    }
}

七、数据库升级

  1. 增加数据库版本: 当需要对数据库进行升级时,首先要在 @Database 注解中增加版本号。例如,将 AppDatabase 的版本从 1 升级到 2
import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [User::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}
  1. 定义数据库迁移: 创建一个 Migration 对象,用于定义从旧版本到新版本的数据库迁移逻辑。例如,假设要在 users 表中添加一个新的列 email
import android.database.sqlite.SQLiteDatabase
import androidx.room.migration.Migration

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SQLiteDatabase) {
        database.execSQL("ALTER TABLE users ADD COLUMN email TEXT")
    }
}

在上述代码中,Migration 的构造函数接收旧版本号 1 和新版本号 2migrate 方法中执行了 SQL 语句来添加新列。

  1. 应用数据库迁移: 在构建数据库实例时,将 Migration 对象传递给 addMigrations 方法。例如:
import android.content.Context
import androidx.room.Room

object DatabaseInstance {
    private lateinit var instance: AppDatabase

    fun getInstance(context: Context): AppDatabase {
        if (!::instance.isInitialized) {
            instance = Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "app_database"
            )
               .addMigrations(MIGRATION_1_2)
               .build()
        }
        return instance
    }
}

这样,当应用升级且数据库版本发生变化时,Room 会自动执行迁移逻辑,确保数据库结构的正确性。

八、高级特性

  1. 关系型数据处理: Room 支持处理实体之间的关系,如一对多、多对一和多对多关系。例如,假设有一个 Order 实体和一个 Product 实体,一个订单可以包含多个产品,这是一对多关系。
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "orders")
data class Order(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val orderDate: String
)

@Entity(tableName = "products", foreignKeys = [
    androidx.room.ForeignKey(
        entity = Order::class,
        parentColumns = ["id"],
        childColumns = ["orderId"],
        onDelete = androidx.room.ForeignKey.CASCADE
    )
])
data class Product(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val name: String,
    val price: Double,
    val orderId: Int
)

Product 实体中,通过 @ForeignKey 注解定义了与 Order 实体的外键关系,onDelete = androidx.room.ForeignKey.CASCADE 表示当订单被删除时,相关的产品也会被删除。

  1. 复杂查询: 对于复杂的查询,Room 允许编写复杂的 SQL 查询语句。例如,要查询每个订单及其包含的产品列表,可以在 OrderDao 中定义如下方法:
import androidx.room.Dao
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Dao
interface OrderDao {

    @Query("""
        SELECT * FROM orders
        JOIN products ON orders.id = products.orderId
    """)
    fun getOrdersWithProducts(): Flow<List<OrderWithProducts>>
}

data class OrderWithProducts(
    val order: Order,
    val products: List<Product>
)

这里通过 JOIN 操作将 orders 表和 products 表连接起来,并返回一个包含订单及其相关产品的自定义数据类 OrderWithProducts

  1. 事务处理: Room 支持事务处理,确保多个数据库操作要么全部成功,要么全部失败。例如,在一个订单创建和相关产品插入的场景中:
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow

@Dao
interface OrderDao {

    @Insert
    suspend fun insertOrder(order: Order)

    @Insert
    suspend fun insertProducts(products: List<Product>)

    @Transaction
    suspend fun createOrderWithProducts(order: Order, products: List<Product>) {
        insertOrder(order)
        val orderId = order.id
        val productsWithOrderId = products.map { it.copy(orderId = orderId) }
        insertProducts(productsWithOrderId)
    }
}

createOrderWithProducts 方法上添加 @Transaction 注解,确保订单插入和产品插入操作在一个事务中执行。

通过以上内容,全面介绍了 Kotlin Android Room 数据库的使用,从基础的配置、创建数据库到高级的特性和集成,希望能帮助开发者在 Android 应用开发中高效地使用 Room 进行数据持久化。