【问题标题】:What is the difference between @ForeignKey and @Relation annotations in Room database?Room 数据库中的@ForeignKey 和@Relation 注释有什么区别?
【发布时间】:2020-02-19 10:16:16
【问题描述】:

我无法理解这些注释之间的区别。 在我的用例中,我想在表之间创建一对多的关系。 并找到了两种选择:一种使用@ForeignKey,另一种使用@Relation

我还发现,如果我更新该行(例如使用 OnCoflictStrategy.Replace),我将丢失该行的外键,这是真的吗?

【问题讨论】:

    标签: android android-room android-architecture-components


    【解决方案1】:

    虽然这两个概念都用于为您的 Room 数据库带来结构,但它们的用例不同在于:

    • @ForeignKey 用于在插入/修改您的数据时强制执行关系结构
    • @Relation 用于在检索/查看您的数据时强制执行关系结构。

    为了更好地理解ForeignKeys 的需求,请考虑以下示例:

    @Entity
    data class Artist(
        @PrimaryKey val artistId: Long,
        val name: String
    )
    
    @Entity
    data class Album(
        @PrimaryKey val albumId: Long,
        val title: String,
        val artistId: Long
    )
    
    

    使用此数据库的应用程序有权假设 Album 表中的每一行在 Artist 表中存在相应的行。不幸的是,如果用户使用外部工具编辑数据库或应用程序中存在错误,则可能会在 Album 表中插入与 Artist 中的任何行不对应的行 表。或者,可能会从 Artist 表中删除行,从而在 Album 表中留下与 Artist 中任何剩余行不对应的孤立行.这可能会导致应用程序或应用程序稍后出现故障,或者至少使应用程序的编码变得更加困难。

    一种解决方案是在数据库模式中添加 SQL 外键约束,以强制 ArtistAlbum 表之间的关系。

    @Entity
    data class Artist(
        @PrimaryKey val id: Long,
        val name: String
    )
    
    @Entity(
        foreignKeys = [ForeignKey(
            entity = Artist::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("artistId"),
            onUpdate = ForeignKey.CASCADE,
            onDelete = ForeignKey.CASCADE
        )]
    )
    data class Album(
        @PrimaryKey val albumId: Long,
        val title: String,
        val artistId: Long
    )
    
    

    现在,每当您插入新专辑时,SQL 都会检查是否存在具有该给定 ID 的艺术家,然后您才能继续进行交易。此外,如果您更新艺术家的信息或将其从 Artist 表中删除,SQL 会检查该艺术家的任何专辑并更新/删除它们。这就是ForeignKey.CASCADE 的魔力!

    但这不会自动让它们在查询期间一起返回,所以输入@Relation

    // Our data classes from before
    @Entity
    data class Artist(
        @PrimaryKey val id: Long,
        val name: String
    )
    
    @Entity(
        foreignKeys = [ForeignKey(
            entity = Artist::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("artistId"),
            onUpdate = ForeignKey.CASCADE,
            onDelete = ForeignKey.CASCADE
        )]
    )
    data class Album(
        @PrimaryKey val albumId: Long,
        val title: String,
        val artistId: Long
    )
    
    // Now embedded for efficient querying
    data class ArtistAndAlbums(
        @Embedded val artist: Artist,
        @Relation(
             parentColumn = "id",
             entityColumn = "artistId"
        )
        val album: List<Album> // <-- This is a one-to-many relationship, since each artist has many albums, hence returning a List here
    )
    
    

    现在您可以通过以下方式轻松获取艺术家及其专辑的列表:

    @Transaction 
    @Query("SELECT * FROM Artist")
    fun getArtistsAndAlbums(): List<ArtistAndAlbums>
    

    之前您必须编写较长的样板 SQL 查询来连接和返回它们。

    注意:@Transaction 注解是让 SQLite 一次性而不是单独执行两个搜索查询(一个在 Artist 表中查找,一个在 Album 表中查找)所必需的。

    来源:

    Android 开发者文档节选:

    有时,您希望在数据库逻辑中将实体或数据对象表示为一个有凝聚力的整体,即使该对象包含多个字段。在这些情况下,您可以使用 @Embedded 注释来表示您希望在表中分解为其子字段的对象。然后,您可以像查询其他单个列一样QUERY嵌入字段。

    外键允许您指定跨实体的约束,以便 SQLite 在您修改数据库时确保关系有效。

    SQLite 的ForeignKey documentation

    【讨论】:

    • 如果我需要获取所有艺术家但有些没有专辑,会发生什么?像左连接这样的东西?我需要这样的数据结构:data class AllArtists(val id: Int, val name: String, val albums: List?) 是否可行?
    • @user3738208 没错!只需通过附加一个问号使val album: List&lt;Album&gt; 可以为空,如果没有找到给定艺术家的专辑,Room 将用空填充它。
    【解决方案2】:

    @ForeignKey 定义了一个约束(又名规则),要求子列存在于父列中。如果尝试破坏该规则,则会发生冲突(可以通过 onDelete/onUpdate 定义以各种方式处理)。

    @Relationship 用于定义在父对象中返回多个子对象(可能是外键子对象)的关系。

    在这一切之下,@Relation 会自动(有效地)加入表格并生成子对象的数量。虽然 @ForeignKey 只会影响架构(onDelete/onUpdate 处理除外),但不会导致各个表被连接。

    也许考虑以下几点:-

    服务实体

    @Entity(
        tableName = "services"
    )
    class Services {
    
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = "services_id")
        var id: Long = 0
        var service_date: String = ""
        var user_mobile_no: String = ""
    
    }
    

    ServiceDetail 实体:-

    @Entity(
        tableName = "service_detail",
        foreignKeys = [
            ForeignKey(
                entity = Services::class,
                parentColumns = ["services_id"],
                childColumns = ["services_id"],onDelete = ForeignKey.SET_DEFAULT
            )
        ]
    )
    class ServiceDetail {
    
        @PrimaryKey
        var id: Long? = null;
        var services_id: Long = 0;
        @ColumnInfo(defaultValue = "1")
        var service_type_id: Long = 0;
    
        constructor()
    
        @Ignore
        constructor(services_id: Long, service_type_id: Long) {
            this.services_id = services_id
            this.service_type_id = service_type_id
        }
    }
    
    • 这就是说,为了添加 ServiceDetail,services_id 列的值必须是 services 表的 services_id 列中存在的值,否则会发生冲突。此外,如果从 services 表中删除了一行,则 service_detail 表中引用该行的所有行也将被删除(否则该行无法从 services 表中删除)。

    现在考虑这个普通类 (POJO),它不是实体(也称为表):-

    class ServiceWithDetail {
    
        @Embedded
        var services: Services? = null
    
        @Relation(entity = ServiceDetail::class,parentColumn = "services_id",entityColumn = "services_id")
        var serviceDetail: List<ServiceDetail>? = null
    }
    

    这大致是说当您请求一个 ServiceWithDetail 对象然后获取一个服务对象以及相关 service_detail 对象的列表时

    你会有一个道,例如:-

    @Query("SELECT * FROM services")
    fun getAllServices() :List<ServiceWithDetail>
    

    因此它将从服务表中获取所有服务以及相关服务(即 services_detail 中的 services_id 与正在处理的当前服务行的 services_id 相同)。

    onConflictStrategy

    REPLACE 执行以下操作:-

    当 UNIQUE 或 PRIMARY KEY 约束违反发生时,REPLACE 算法删除导致约束的预先存在的行 在插入或更新当前行和 命令继续正常执行。

    如果一个 NOT NULL 约束 发生冲突时,REPLACE 冲突解决方法替换 NULL 具有该列默认值的值,或者如果该列没有 默认值,则使用 ABORT 算法。如果一个 CHECK 约束 或发生外键约束冲突,REPLACE 冲突 分辨率算法的工作原理类似于 ABORT。

    当 REPLACE 冲突解决策略删除行以 满足约束,删除触发器当且仅当递归时触发 触发器已启用。

    更新钩子不会为被删除的行调用 更换冲突解决策略。 REPLACE 也不会增加 更改计数器。本段中定义的异常行为 在未来的版本中可能会发生变化。REPLACE

    因此,您所经历的行为的可能性。但是,这取决于更新在做什么。如果 ForeignKey(s) 的值不同,那么它们应该假设没有外键冲突,将外键值替换为新的有效值。如果外键值不变,则替换行将具有相同的外键。

    【讨论】:

    • 如果我猜对了,ServiceWithDetail 是一对五的关系,它返回一个列表。一对一的关系呢?有没有办法只返回一个对象而不是一个列表?
    • @AlitonOliveira 这是一个较晚的回复,但为了以后的用户着想,这里列出了答案:developer.android.com/training/data-storage/room/…
    猜你喜欢
    • 2018-01-14
    • 1970-01-01
    • 2011-08-17
    • 2022-01-21
    • 2012-11-10
    • 2019-01-25
    • 1970-01-01
    • 2021-10-02
    相关资源
    最近更新 更多