Room简单实操

news2024/11/17 1:35:40

1. Room介绍,直接Copy官网介绍:

Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。具体来说,Room 具有以下优势:

  • 提供针对 SQL 查询的编译时验证。
  • 提供方便注解,可最大限度减少重复和容易出错的样板代码。
  • 简化了数据库迁移路径。

总结就是简化了操作数据库的难度,相对于直接操作Sqlite Api。

2. Room主要组件,包含如下三个部分,内容直接Copy官网:

  • 数据库类(Room Database),用于保存数据库并作为应用持久性数据底层连接的主要访问点 -- 数据库类为应用提供与该数据库关联的 DAO 的实例。
  • 数据实体(Entities),用于表示应用的数据库中的表。
  • 数据访问对象 (Data Access Objects),提供在数据库中查询、更新、插入和删除数据的方法。

如果链接访问不了,直接将前缀修改为中文官网的域名(https://developer.android.google.cn/)

Room架构示意图

引入依赖库,参考Android版本依赖Version catalog

// Room
implementation(libs.room.runtime)
annotationProcessor(libs.room.compiler)
// To use Kotlin Symbol Processing (KSP)
ksp(libs.room.compiler)
// optional - Kotlin Extensions and Coroutines support for Room
implementation(libs.room.ktx)
// 测试数据库
testImplementation(libs.room.testing)

一、单表

1.1 数据实体

每个 Room 实体定义为带有 @Entity 注解的类。Room 实体包含数据库中相应表中的每一列的字段,包括构成主键的一个或多个列。

/**
 * 用户表对象.
 *
 * @param id
 * 如果您在定义实体类时将一个字段标记为 @PrimaryKey(autoGenerate = true),
 * 并且在插入记录时不为该字段赋值,Room 将会自动生成一个唯一的值并填充到该字段中。
 */
@Entity
class User(
    /**
     * 主键字段不能为空,并且必须具有唯一性,以确保每条记录都可以唯一标识.
     *
     * autoGenerate = true之后我们将主键默认设置为空或者0,
     * room也会自动自动生成一个唯一的值并填充到该字段中,
     * 但是不建议设置为空,建议默认设置为0。
     */
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,
    val age: Int = 0,
    @GenderType
    val gender: Int = GenderType.MALE
) {
    override fun toString(): String {
        return "User(id=$id, name='$name', age=$age, gender=$gender)"
    }
}

注意

1. 要保留某个字段,Room 必须拥有该字段的访问权限。

2. 可以通过将某个字段设为公开或为其提供 getter 和 setter 方法,确保 Room 能够访问该字段。

1.1.1 自定义表明和列名

  1. 默认情况下,Room 将类名称用作数据库表名称。如果希望表具有不同的名称,请设置 @Entity 注解的 tableName 属性。
  2. Room 默认使用字段名称作为数据库中的列名称。如果您希望列具有不同的名称,请将 @ColumnInfo 注解添加到该字段并设置 name 属性。
@Entity(tableName = "users")
class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    @ColumnInfo(name = "uaerName")
    val name: String,
    val age: Int = 0,
    @GenderType
    val gender: Int = GenderType.MALE
)

注意:SQLite 中的表和列名称不区分大小写。

1.1.2 主键介绍

在 Room 数据库中,主键通常定义为长整型(long)是因为长整型提供了更大的范围,可以容纳更多的唯一标识符。这种做法是出于以下几个考虑:

  1. 唯一标识符范围:长整型可以表示更大范围的整数值,最大可达到 2^63 - 1,相比于整型(int)的最大值 2^31 - 1,长整型提供了更大的唯一标识符范围,可以满足大多数情况下的需求。

  2. 避免溢出:使用长整型可以避免标识符溢出的问题。长整型的范围比较大,即使在较长时间内也不太可能出现溢出的情况。

  3. 与数据库兼容性:在许多数据库系统中,主键通常被定义为长整型或者类似的数据类型(如 BIGINT),这样可以确保 Room 数据库与其他数据库系统的兼容性。


@PrimaryKey(autoGenerate = true)
val id: Long = 0

1. 如果您在定义实体类时将一个字段标记为 @PrimaryKey(autoGenerate = true),
并且在插入记录时不为该字段赋值,Room 将会自动生成一个唯一的值并填充到该字段中。

2. 主键字段不能为空,并且必须具有唯一性,以确保每条记录都可以唯一标识.

3. autoGenerate = true之后我们将主键默认设置为空或者0,room也会自动自动生成一个唯一的值并填充到该字段中,但是不建议设置为空,建议默认设置为0。


1.1.3 忽略字段 

默认Room 会为实体中定义的每个字段创建一个列。 如果某个实体中有不想保留的字段,则可以使用 @Ignore 为这些字段添加注解

@Entity
class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,
    val age: Int = 0,
    @GenderType
    val gender: Int = GenderType.MALE,
    @Ignore val headBm: Bitmap?
)

1.2 数据访问对象 (DAO)

对象创建须知

1. 介绍

将每个 DAO 定义为一个接口或一个抽象类。通常应使用接口。

无论哪种形式,都必须始终使用 @Dao 为创建 DAO 对象添加注解。DAO 不具有属性,但它们定义了一个或多个方法,可用于与应用数据库中的数据进行交互。

两种类型的 DAO 方法可以定义数据库交互:

  1. 在不编写任何 SQL 代码的情况下插入、更新和删除数据库中的行的便捷方法。
  2. 编写自己的 SQL 查询以与数据库进行交互的查询方法。

2. 总结:

1. 对象必须是接口或者抽象类

2. 对象必须使用@DAO注解进行修饰

3. 增、删、改不需要使用任何SQL语句,直接使用@Insert、@Delete、@Update注解方法即可

4. 查询方法直接编写SQL语句就能使用,例如:@Query("SELECT *FROM user")

1.2.1 插入

使用 @Insert 注解,您可以定义将其参数插入数据库中的相应表的方法,可以同时插入一个或多个对象。

1. 插入单个对象,参数必须是具体的@Entity数据库表,例如例子中的User表
  • @Insert 注解的方法每个参数都必须是一个带有 @Entity 注解的 Room 数据实体类实例。
  • 如果 @Insert 方法接收到单个参数,则可返回一个 long 值,这是插入项的新 rowId
2. 插入多个对象,参数必须是具体的@Entity数据库表集合或者数组
  • 数据实体类实例的集合,而且每个参数都指向一个数据库。调用 @Insert 方法时,Room 会将每个传递的实体实例插入到相应的数据库表中。
  • 如果参数是数组或集合,则可改为返回由 long 值组成的数组或集合,并且每个值都作为其中一个插入项的 rowId

想了解如何返回 rowId 值,请参阅 @Insert 注解的参考文档以及 rowid 表的 SQLite 文档。

/**
 * 插入多个用户数据(碰到冲突就替换为最新的).
 *
 * @param users 用户数据(可变对象)
 * @return 插入数据ID集合
 */
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg users: User): List<Long>

/**
 * 插入用户集合数据.
 *
 * @param users 用户数据集合
 * @return 插入数据ID集合
 */
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(users: List<User>): List<Long>

OnConflictStrategy常量介绍:

  • NONE:没有冲突处理策略,事务将会回滚。
  • REPLACE:替换旧数据并继续事务。
  • ROLLBACK:事务回滚。已弃用,请使用 ABORT
  • ABORT:事务回滚。
  • FAIL:事务失败。已弃用,请使用 ABORT
  • IGNORE:忽略冲突。

1.2.2 更新

更新方法直接用@Update修饰,ROOM支持更新单个对象(单行)、集合对象(多行)、数组对象(查看源码:EntityDeletionOrUpdateAdapter)。与 @Insert 方法一样,@Update 方法接受数据实体实例作为参数。

/**
 * 更新用户数据.
 *
 * @param users 用户数据(可变对象)
 * @return 更新数据ID集合
 */
@Update
fun update(vararg users: User)

Room 使用主键 (就是User表中的id)将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。

@Update 方法可以选择性地返回 int 值,指示成功更新的行数。

1.2.3 删除

删除方法直接用@Delete修饰,ROOM支持删除单个对象、集合对象、数组对象(查看源码:EntityDeletionOrUpdateAdapter)

/**
 * 删除多个用户数据.
 *
 * @param users 用户数据(可变对象)
 */
@Delete
fun delete(vararg users: User)

/**
 * 删除用户集合.
 *
 * @param users 用户集合
 */
@Delete
fun delete(users: List<User>)

从源码中我们可以看到,ROOM中删除和更新调用的是相同的方法。

Room 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。

@Delete 方法可以选择性地返回 int 值,指示成功删除的行数。

1.2.4 查询

Room的查询主要是写Sql语句,只要是对Sql语句熟悉的,写起来就是得心应手。

如下常用的查询方法,使用如下注解在方法上:@Query("查询的SQL")

1. 查询数据库中所有对象
/**
 * 查询所有用户数据.
 *
 * @return 用户数据集合
 */
@Query("SELECT *FROM user")
fun getAll(): List<User>
2. 查询数据库表列的子集

在数据库查询的时候,我们偶尔不需要查询全部的列,例如此例中的用户表我们可能只需要查询用户ID、用户名

借助 Room可以从任何查询返回简单对象,前提是将一组结果列映射到需要查询的简单的对象。

举例:

1. 创建需要的列的对象

/**
 * 偶尔我们只是需要查询用户数据的列子集。
 * 借助 Room,可以从任何查询返回简单对象,前提是可以将一组结果列映射到返回的对象。
 */
data class UserTuple(
    val id: Long,
    val name: String
) 

2. 查询指定子集

/**
 * 查询用户数据的列子集.
 *
 * @return 返回用户数据列子集
 */
@Query("SELECT id, name FROM user")
fun getSimpleAll(): List<UserTuple>
3. 将简单参数传递给查询,使用指定参数进行查询
/**
 * 通过用户ID查询用户数据.
 *
 * @param id 用户ID
 * @return 用户数据
 */
@Query("SELECT *FROM user WHERE id = :id")
fun getById(id: Int): User

/**
 * 通过用户名查询用户数据(没啥意义,用户名可以重名,查询的是最近的用户).
 *
 * @param name 用户名
 * @return 用户数据
 */
@Query("SELECT *FROM user WHERE name LIKE :name LIMIT 1")
fun findByName(name: String): User

/**
 * 通过用户名查询用户数据集合.
 *
 * @param name 用户名
 * @return 用户数据集合
 */
@Query("SELECT *FROM user WHERE name LIKE :name ORDER BY id DESC")
fun findAllByName(name: String): List<User>

/**
 * 查询某一年龄段用户数据.
 *
 * @param minAge 最小年龄
 * @param maxAge 最大年龄
 * @return 用户数据集合
 */
@Query("SELECT *FROM user WHERE age BETWEEN :minAge AND :maxAge")
fun getAllBetweenAges(minAge: Int, maxAge: Int): List<User>
4. 将一组参数传递给查询,查询指定组查询条件的数据
/**
 * 通过用户ID查询用户数据.
 *
 * @param userIds 用户ID集合
 * @return 用户数据集合
 */
@Query("SELECT * FROM user WHERE id IN (:userIds)")
fun getAllByIds(userIds: IntArray): List<User>

1.2.5 完整DAO对象

增、删、改、查完整的DAO对象代码:

/**
 * 用户表数据库操作对象.
 */
@Dao
interface UserDao {

    /**
     * 插入多个用户数据(碰到冲突就替换为最新的).
     *
     * @param users 用户数据(可变对象)
     * @return 插入数据ID集合
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(vararg users: User): List<Long>

    /**
     * 插入用户集合数据.
     *
     * @param users 用户数据集合
     * @return 插入数据ID集合
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(users: List<User>): List<Long>

    /**
     * 删除多个用户数据.
     *
     * @param users 用户数据(可变对象)
     */
    @Delete
    fun delete(vararg users: User)

    /**
     * 删除用户集合.
     *
     * @param users 用户集合
     */
    @Delete
    fun delete(users: List<User>)

    /**
     * 更新用户数据.
     *
     * @param users 用户数据(可变对象)
     * @return 更新数据ID集合
     */
    @Update
    fun update(vararg users: User)

    /**
     * 查询所有用户数据.
     *
     * @return 用户数据集合
     */
    @Query("SELECT *FROM user")
    fun getAll(): List<User>

    /**
     * 查询用户数据的列子集.
     *
     * @return 返回用户数据列子集
     */
    @Query("SELECT id, name FROM user")
    fun getSimpleAll(): List<UserTuple>

    /**
     * 通过用户ID查询用户数据.
     *
     * @param id 用户ID
     * @return 用户数据
     */
    @Query("SELECT *FROM user WHERE id = :id")
    fun getById(id: Int): User

    /**
     * 通过用户ID查询用户数据.
     *
     * @param userIds 用户ID集合
     * @return 用户数据集合
     */
    @Query("SELECT * FROM user WHERE id IN (:userIds)")
    fun getAllByIds(userIds: IntArray): List<User>

    /**
     * 通过用户名查询用户数据(没啥意义,用户名可以重名,查询的是最近的用户).
     *
     * @param name 用户名
     * @return 用户数据
     */
    @Query("SELECT *FROM user WHERE name LIKE :name LIMIT 1")
    fun findByName(name: String): User

    /**
     * 通过用户名查询用户数据集合.
     *
     * @param name 用户名
     * @return 用户数据集合
     */
    @Query("SELECT *FROM user WHERE name LIKE :name ORDER BY id DESC")
    fun findAllByName(name: String): List<User>

    /**
     * 查询某一年龄段用户数据.
     *
     * @param minAge 最小年龄
     * @param maxAge 最大年龄
     * @return 用户数据集合
     */
    @Query("SELECT *FROM user WHERE age BETWEEN :minAge AND :maxAge")
    fun getAllBetweenAges(minAge: Int, maxAge: Int): List<User>

}

1.3 数据库对象

数据库类必须满足以下条件:

  • 必须带有 @Database 注解,该注解包含列出所有与数据库关联的数据实体的 entities 数组 -- 可以定义N多表,只需要放在entities = [User::class, Work::class]
  • 必须是一个抽象类,用于扩展 RoomDatabase。
  • 对于与数据库关联的每个 DAO 类,数据库类必须定义一个具有零参数的抽象方法,并返回 DAO 类的实例。
/**
 * 数据库.
 */
@Database(entities = [User::class], version = 1)
abstract class TestDataBase : RoomDatabase() {
    /**
     * 用户表操作类.
     */
    abstract fun userDao(): UserDao
}

注意

1. 如果您的应用在单个进程中运行,在实例化 TestDataBase 对象时应遵循单例设计模式。每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例。

2. 如果应用在多个进程中运行,请在数据库构建器调用中包含 enableMultiInstanceInvalidation()。这样,如果您在每个进程中都有一个 TestDataBase 实例,可以在一个进程中使共享数据库文件失效,并且这种失效会自动传播到其他进程中 TestDataBase 的实例。

1.4 构建数据库实例,进行数据库操作

@RunWith(AndroidJUnit4::class)
class RoomTest {
    companion object {
        private const val TAG = "RoomTest"
    }

    private lateinit var mDb: TestDataBase
    private lateinit var mUserDao: UserDao
    private lateinit var mWorkDao: WorkDao
    private lateinit var mUserWorkDao: UserWorkDao

    @Before
    fun createDb() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        mDb = Room.databaseBuilder(
            context,
            TestDataBase::class.java,
            "pp-test-database"
        ).build()
        mUserDao = mDb.userDao()
        mWorkDao = mDb.workDao()
        mUserWorkDao = mDb.userWorkDao()
    }

    @After
    fun closeDb() {
        mDb.close()
    }

    @Test
    fun testInsert() {
        mUserDao.insertAll(
            User(name = "刘亦菲", age = 20, gender = GenderType.FEMALE),
            User(name = "唐嫣", age = 30, gender = GenderType.FEMALE)
        ).run {
            assertEquals(2, this.size)
            mUserDao.getAll().run {
                for ((index, value ) in this.withIndex()) {
                    // 更新
                    // mUserDao.update(User(id = value.id, name = "修改$index", age = value.age, gender = value.gender))
                    // 插入数据
                    mWorkDao.insertAll(Work(introduce = "打工人$index", userId = value.id))
                }

                // 删除
                // mUserDao.delete(this[0])
            }



            mUserWorkDao.getAllUserWork().run {
                for ((index, value) in this.withIndex()) {
                    "index=$index, value=$value".logI(TAG)
                }
            }
        }
    }
}

Room操作单表已经介绍完成,按照上述操作即可成功使用room操作Sqlite数据库

二、多表

2.1 定义对象之间的关系

由于 SQLite 是关系型数据库,因此您可以定义各个实体之间的关系。虽然大多数对象关系映射库都允许实体对象相互引用,但 Room 明确禁止这样做。

原因:了解 Room 为何不允许对象引用

Room 中,可以通过两种方式定义和查询实体之间的关系:

  • 使用具有嵌入式对象的中间数据类
  • 使用具有多重映射返回值类型的关系型查询方法。

2.1.1 中间数据类

创建Work工作类对象

@Entity(
    foreignKeys = [ForeignKey(
        entity = User::class,
        parentColumns = arrayOf("id"),
        childColumns = arrayOf("userId"),
        onDelete = ForeignKey.CASCADE
    )]
)
data class Work(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val introduce: String,
    /**
     * 外键,对应用户表的id.
     */
    val userId: Long
) {
    override fun toString(): String {
        return "Work(id=$id, introduce='$introduce', userId=$userId)"
    }
}

在 Room 中,@ForeignKey 注解用于定义外键约束,这些约束用于确保数据的完整性和一致性。使用外键约束可以建立表与表之间的关联关系,从而实现数据的引用完整性。

外键约束的主要作用包括:

  1. 维护关联关系:通过外键约束,您可以在一个表中引用另一个表中的数据。这样可以建立表与表之间的关联关系,使得数据之间的关系更加清晰明确。

  2. 确保引用完整性:外键约束可以确保在引用另一个表中的数据时,被引用的数据确实存在。这样可以避免出现引用无效数据的情况,保证了数据的完整性。

  3. 实现级联操作:通过外键约束,可以实现级联操作,例如在删除或更新父表中的数据时,自动删除或更新子表中相关联的数据。这样可以保持数据之间的一致性。

1. 定义一对一关系

两个实体之间的一对一关系是指:父实体的每个实例恰好对应于子实体的 1 个实例,反之亦然。

例子:

  • 一个用户只有一份工作,每个工作只能有一个用户。因此User尸体和Work实体之间就是一对一的关系。
  • 如需定义一对一关系,为两个实体分别创建一个类。其中一个实体必须包含一个变量,且该变量是对另一个实体的主键的引用。

上述例子中User和Work实体,work表的userId对应的就是User表的id,重新copy一份如下

@Entity
class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,
    val age: Int = 0,
    @GenderType
    val gender: Int = GenderType.MALE
) 


data class Work(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val introduce: String,
    /**
     * 外键,对应用户表的id.
     */
    val userId: Long
)

  1. 需要创建一个新的数据类,其中每个实例都包含父实体的一个实例和与之对应的子实体实例。
  2. 使用 @Embedded 注释表示要分解为表格中的子字段的对象,父实体。将 @Relation 注释添加到子实体的实例,同时将 parentColumn 设置为父实体主键列的名称,并将 entityColumn 设置为引用父实体主键的子实体列的名称。
/**
 * 用户和对应工作数据.
 *
 * 一对一的关系
 */
data class UserWork(
    @Embedded
    val user: User,
    @Relation(
        parentColumn = "id",
        entityColumn = "userId"
    )
    val work: Work
) {
    override fun toString(): String {
        return "UserWork(user=$user, work=$work)"
    }
}

向 DAO 类添加一个方法,用于返回将父实体与子实体配对的数据类的所有实例。该方法需要 Room 运行两次查询,因此应向该方法添加 @Transaction 注释,以确保整个操作以原子方式执行。

/**
 * 用户和对应工作表数据库操作对象.
 */
@Dao
interface UserWorkDao {
    /**
     * 查询用户和用户工作数据.
     *
     * @return 用户和用户工作数据集合
     */
    @Transaction
    @Query("SELECT *FROM user")
    fun getAllUserWork(): List<UserWork>

}
2. 定义一对多关系

父实体的每个实例对应于子实体的零个或多个实例,但子实体的每个实例只能恰好对应于父实体的一个实例

  • 定义一对多关系,需要为这两个实体分别创建一个类。与一对一关系一样,子实体必须包含一个变量,且该变量是对父实体的主键的引用。
  • 将 @Relation 注释添加到子实体的实例,同时将 parentColumn 设置为父实体主键列的名称,并将 entityColumn 设置为引用父实体主键的子实体列的名称。
/**
 * 用户和对应工作数据.
 *
 * 一对多的关系
 */
class UserWorkList(
    @Embedded
    val user: User,
    @Relation(
        parentColumn = "id",
        entityColumn = "userId"
    )
    val workList: MutableList<Work>
) {
    override fun toString(): String {
        return "UserWorkList(user=$user, workList=$workList)"
    }
}

向 DAO 类添加一个方法,用于返回将父实体与子实体配对的数据类的所有实例。该方法需要 Room 运行两次查询,因此应向该方法添加 @Transaction 注释,以确保整个操作以原子方式执行。

/**
 * 用户和对应工作表数据库操作对象.
 */
@Dao
interface UserWorkDao {
    

    /**
     * 查询用户和用户工作数据.
     *
     * @return 用户和用户工作数据集合
     */
    @Transaction
    @Query("SELECT *FROM user")
    fun getUserWithWorkList(): List<UserWorkList>
}
3. 定义多对多关系

两个实体之间的多对多关系是指:父实体的每个实例对应于子实体的零个或多个实例,反之亦然。

例子:

  1. 每个用户表都可以包含多个计划,每个计划都可以包含在多个用户表中。因此,User 实体和 Work 实体之间存在多对多关系。
  2. 如需定义多对多关系,需要为两个实体分别创建一个类。
  3. 多对多关系与其他关系类型均不同的一点在于,子实体中通常不存在对父实体的引用。因此,需要创建第三个类来表示两个实体之间的关联实体(即交叉引用表)。交叉引用表中必须包含表中表示的多对多关系中每个实体的主键列。
@Entity
class User(
    /**
     * 主键字段不能为空,并且必须具有唯一性,以确保每条记录都可以唯一标识.
     *
     * autoGenerate = true之后我们将主键默认设置为空或者0,
     * room也会自动自动生成一个唯一的值并填充到该字段中,
     * 但是不建议设置为空,建议默认设置为0。
     */
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,
    val age: Int = 0,
    @GenderType
    val gender: Int = GenderType.MALE
)

/**
 * 计划表.
 */
@Entity
data class Plan(
    @PrimaryKey(autoGenerate = true)
    val planId: Long = 0,
    val planName: String,
    val planTime: Long
)

/**
 * 用户和对应计划数据.
 *
 * 多对多的关系的交叉引用表
 */
@Entity(primaryKeys = ["id", "planId"])
data class UserPlanCrossRef(
    val id: Long,
    val planId: Long
)

您想如何查询这些相关实体:

  1. 如果想查询用户表和每个用户表所含计划的列表,则应创建一个新的数据类,其中包含单个 User 对象,以及该用户表所包含的所有 Plan 对象的列表。
  2. 如果想查询计划表和每个计划所在用户表的列表,则应创建一个新的数据类,其中包含单个 Plan 对象,以及包含该计划的所有 User 对象的列表。

在这两种情况下,都可以通过以下方法在实体之间建立关系:在上述每个类中的 @Relation 注释中使用 associateBy 属性来确定提供 User 实体与 Plan 实体之间关系的交叉引用实体。

/**
 * 用户表对应的计划列表.
 */
data class UserWithPlans(
    @Embedded val user: User,
    @Relation(
        parentColumn = "id",
        entityColumn = "planId",
        associateBy = Junction(UserPlanCrossRef::class)
    )
    val plans: MutableList<Plan>
)


/**
 * 计划表对应的用户列表.
 */
data class PlanWithUsers(
    @Embedded val plan: Plan,
    @Relation(
        parentColumn = "planId",
        entityColumn = "id",
        associateBy = Junction(UserPlanCrossRef::class)
    )
    val users: MutableList<User>
)

向 DAO 类添加一个方法,用于提供您的应用所需的查询功能。

  • getUserWithPlans:该方法会查询数据库并返回查询到的所有 UserWithPlans 对象。
  • getPlanWithUsers:该方法会查询数据库并返回查询到的所有 PlanWithUsers 对象。

这两个方法都需要 Room 运行两次查询,因此应为这两个方法添加 @Transaction 注释,以确保整个操作以原子方式执行。

/**
 * 用户和对应计划表数据库操作对象.
 */
@Dao
interface UserPlanDao {

    /**
     * 查询用户和对应计划数据.
     *
     * @return 用户和对应计划数据
     */
    @Transaction
    @Query("SELECT *FROM user")
    fun getUserWithPlans(): List<UserWithPlans>

    /**
     * 查询计划和对应用户数据.
     *
     * @return 查询计划和对应用户数据
     */
    @Transaction
    @Query("SELECT *FROM `plan`")
    fun getPlanWithUsers(): List<PlanWithUsers>
}

注意:如果 @Relation 注释不适用于你的特定用例,可能需要在 SQL 查询中使用 JOIN 关键字来手动定义适当的关系。如需详细了解如何手动查询多个表,请参阅使用 Room DAO 访问数据。

4. 定义嵌套关系

嵌套关系请直接参考官方文档,一般不建议使用

定义嵌套关系

2.1.2 多重映射返回值类型

注意:Room 在 2.4 及更高版本中仅支持多重映射返回值类型。

在多重映射返回值类型方法中,无需定义任何其他数据类,而是根据所需的映射结构为您的方法定义多重映射返回值类型,并直接在 SQL 查询中定义实体之间的关系。

以下查询方法会返回 User 和 Work 实例的映射,用于表示指定用户和用户参与的所有工作

@Query("SELECT *FROM user JOIN work ON user.id = work.userId")
fun getUserWithWorks(): Map<User, List<Work>>

2.1.3 完整的数据库对象,需要将涉及到的数据表定义在

@Database(entities = [User::class, Work::class, Plan::class, UserPlanCrossRef::class], version = 1)

/**
 * 数据库.
 */
@Database(entities = [User::class, Work::class, Plan::class, UserPlanCrossRef::class], version = 1)
abstract class TestDataBase : RoomDatabase() {
    /**
     * 用户表操作类.
     */
    abstract fun userDao(): UserDao

    /**
     * 工作表操作类.
     */
    abstract fun workDao(): WorkDao

    /**
     * 计划表操作类.
     */
    abstract fun planDao(): PlanDao

    /**
     * 用户表和工作表操作类.
     */
    abstract fun userWorkDao(): UserWorkDao

    /**
     * 用户表和计划表操作类.
     */
    abstract fun userPlanDao(): UserPlanDao
}

示例代码:

PractiseProject

参考

1. Room使用官方文档(https://developer.android.com/?hl=zh-cn)

2. Room使用中文官方文档(Android 开发者  |  Android Developers)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1643103.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入理解分布式事务⑧ ---->MySQL 事务的实现原理 之 MySQL 事务流程(MySQL 事务执行流程 和 恢复流程)详解

目录 MySQL 事务的实现原理 之 MySQL 事务流程&#xff08;MySQL 事务执行流程 和 恢复流程&#xff09;详解MySQL 事务流程1、MySQL 事务执行流程1-1&#xff1a;MySQL 事务执行流程如图&#xff1a; 2、MySQL 事务恢复流程2-1&#xff1a;事务恢复流程如下图&#xff1a; MyS…

基于点灯Blinker的ESP8266远程网络遥控LED

本文介绍基于ESP8266模块实现的远程点灯操作&#xff0c;手机侧APP选用的是点灯-Blinker&#xff0c;完整资料及软件见文末链接 一、ESP8266模块简介 ESP8266是智能家居等物联网场景下常用的数传模块&#xff0c;具有强大的功能&#xff0c;通过串口转WIFI的方式可实现远距离…

区块链扩容:水平扩展 vs.垂直扩展

1. 引言 随着Rollups 的兴起&#xff0c;区块链扩容一直集中在模块化&#xff08;modular&#xff09;vs. 整体式&#xff08;monolithic&#xff09;之争。 如今&#xff0c;模块化与整体式这种一分为二的心理模型&#xff0c;已不适合于当前的扩容场景。本文&#xff0c;将展…

【C语言回顾】字符函数、字符串函数,内存函数

前言1. 字符函数1.1 字符分类函数1.2 字符转换函数1.2.1 tolower&#xff08;将大写字母转化为小写字母&#xff09;1.2.2 toupper&#xff08;将小写字母转化为大写字母&#xff09; 2. 字符串函数2.1 求字符串长度函数 strlen2.2 字符串输入函数 gets()&fgets()2.2.1 get…

虚拟机网络实现桥接模式

虚拟机网络实现桥接模式 虚拟化软件&#xff1a;VMware 17 Linux&#xff1a;rocky8_9 主机&#xff1a;Win10 文章目录 虚拟机网络实现桥接模式1. 桥接模式介绍2. 查看Win本机的网络信息&#xff08;以笔记本电脑以WiFi联网为例&#x…

vue快速入门(五十五)插槽基本用法

注释很详细&#xff0c;直接上代码 上一篇 新增内容 当传输内容只有一种时的基础写法 源码 App.vue <template><div id"app"><h1>被淡化的背景内容</h1><my-dialog><!-- 插槽内容:文字以及dom结构都可以传 --><span>你确…

【LLM 论文】背诵增强 LLM:Recitation-Augmented LM

论文&#xff1a;Recitation-Augmented Language Models ⭐⭐⭐ ICLR 2023, Google Research, arXiv:2210.01296 Code&#xff1a;github.com/Edward-Sun/RECITE 文章目录 论文速读 论文速读 论文的整体思路还是挺简单的&#xff0c;就是让 LLM 面对一个 question&#xff0c;…

蓝桥杯-路径之谜

题目描述 小明冒充X星球的骑士&#xff0c;进入了一个奇怪的城堡。城堡里面什么都没有&#xff0c;只有方形石头铺成的地面。 假设城堡的地面时n*n个方格。如下图所示。 按习俗&#xff0c;骑士要从西北角走到东南角。可以横向或者纵向移动&#xff0c;但是不能斜着走&#x…

详解SDRAM基本原理以及FPGA实现读写控制(一)

文章目录 一、SDRAM简介二、SDRAM存取结构以及原理2.1 BANK以及存储单元结构2.2 功能框图2.3 SDRAM速度等级以及容量计算 三、SDRAM操作命令3.1 禁止命令&#xff1a; 4b1xxx3.2 空操作命令&#xff1a;4b01113.3 激活命令&#xff1a;4b00113.4 读命令&#xff1a;4b01013.5 写…

使用docker-compose编排Lnmp(dockerfile) 完成Wordpress

目录 一、 Docker-Compose 1.1Docker-Compose介绍 1.2环境准备 1.2.1准备容器目录及相关文件 1.2.2关闭防火墙关闭防护 1.2.3下载centos:7镜像 1.3Docker-Compose 编排nginx 1.3.1切换工作目录 1.3.2编写 Dockerfile 文件 1.3.3修改nginx.conf配置文件 1.4Docker-Co…

GDPU Java 天码行空10

&#xff08;一&#xff09;实验目的 1、掌握JAVA中文件、IO类及其构造方法&#xff1b; 2、重点掌握文件类型所具有的文件操作方法&#xff1b; 3、重点掌握IO中类所具有的IO操作方法&#xff1b; 4、熟悉递归调用的思想及应用&#xff1b; 5、掌握IO中读写常用方法。 &…

鸿蒙UI复用

鸿蒙UI复用 简介BuilderBuilder的使用方式一Builder的使用方式二Builder的使用方式三 Component使用Component复用UI 简介 在页面开发过程中&#xff0c;会遇到有UI相似的结构&#xff0c;如果每个UI都单独声明一份&#xff0c;会产生大量冗余代码&#xff0c;不利于阅读。遇到…

CSS浮动(如果想知道CSS有关浮动的知识点,那么只看这一篇就足够了!)

前言&#xff1a;在学习CSS排版的时候&#xff0c;浮动是我们必须要知道的知识点&#xff0c;浮动在设计之初是为了实现文字环绕效果的&#xff0c;但是后来被人们发现浮动在CSS排版中有着很好的实用价值&#xff0c;所以浮动便成为了CSS排版的利器之一。 ✨✨✨这里是秋刀鱼不…

论文辅助笔记:Tempo 之 model.py

0 导入库 import math from dataclasses import dataclass, asdictimport torch import torch.nn as nnfrom src.modules.transformer import Block from src.modules.prompt import Prompt from src.modules.utils import (FlattenHead,PoolingHead,RevIN, )1TEMPOConfig 1.…

LabVIEW鸡蛋品质智能分级系统

LabVIEW鸡蛋品质智能分级系统 随着现代农业技术的飞速发展&#xff0c;精确、高效的农产品质量控制已成为行业的重要需求。其中&#xff0c;鸡蛋作为日常膳食中不可或缺的重要组成部分&#xff0c;其品质直接关系到消费者的健康与满意度。本文设计并实现了一套基于LabVIEW的鸡…

docker私有仓库的registry

简介 Docker私有仓库的Registry是一个服务&#xff0c;主要用于存储、管理和分发Docker镜像。具体来说&#xff0c;Registry的功能包括&#xff1a; 存储镜像&#xff1a;Registry提供一个集中的地方来存储Docker镜像&#xff0c;包括镜像的层次结构和元数据。 版本控制&…

node应用部署运行案例

生产环境: 系统&#xff1a;linux centos 7.9 node版本&#xff1a;v16.14.0 npm版本:8.3.1 node应用程序结构 [rootRainYun-Q7c3pCXM wiki]# dir assets config.yml data LICENSE node_modules nohup.out output.log package.json server wiki.log [rootRainYun-Q7c…

使用MATLAB/Simulink点亮STM32开发板LED灯

使用MATLAB/Simulink点亮STM32开发板LED灯-笔记 一、STM32CubeMX新建工程二、Simulink 新建工程三、MDK导入生成的代码 一、STM32CubeMX新建工程 1. 打开 STM32CubeMX 软件&#xff0c;点击“新建工程”&#xff0c;选择中对应的型号 2. RCC 设置&#xff0c;选择 HSE(外部高…

单链表式并查集

如果用暴力算法的话&#xff0c;那么会直接超时&#xff0c;我们要学会用并查集去记录下一个空闲的位置 #include<bits/stdc.h> using namespace std;const int N 100005;int n; int fa[N]; int a[N];int find(int x) {if (fa[x] x) {return x;}fa[x] find(fa[x]);re…

ChatGPT DALL-E绘图,制作各种表情包,实现穿衣风格的自由切换

DALL-E绘图功能探索&#xff1a; 1、保持人物形象一致&#xff0c;适配更多的表情、动作 2、改变穿衣风格 3、小女孩的不同年龄段展示 4、不同社交平台的个性头像创作 如果不会写代码&#xff0c;可以问GPT。使用地址&#xff1a;我的GPT4 视频&#xff0c;B站会发&#…