Android Room,如何保存一个实体,其中变量之一是密封类对象

Sty*_*kis 7 android kotlin android-sqlite android-room sealed-class

我想在我的 Room 数据库中保存一个对象,其中一个变量可以是一种类型或另一种类型。我认为密封类是有意义的,所以我采取了这种方法:

sealed class BluetoothMessageType() {
    data class Dbm(
        val data: String
    ) : BluetoothMessageType()

    data class Pwm(
        val data: String
    ) : BluetoothMessageType()
}
Run Code Online (Sandbox Code Playgroud)

甚至是这样,但没有必要。我发现这个给了我更多的错误,因为它不知道如何处理 open val,所以如果我找到第一个版本的解决方案,无论如何我都会很高兴。

sealed class BluetoothMessageType(
    open val data: String
) {
    data class Dbm(
        override val data: String
    ) : BluetoothMessageType()

    data class Pwm(
        override val data: String
    ) : BluetoothMessageType()
}
Run Code Online (Sandbox Code Playgroud)

然后是实体类

@Entity(tableName = MESSAGES_TABLE_NAME)
data class DatabaseBluetoothMessage(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0L,
    val time: Long = Instant().millis,
    val data: BluetoothMessageType
)
Run Code Online (Sandbox Code Playgroud)

我已经创建了一个 TypeConverter 来将其与字符串进行转换,所以我认为这不是问题。

首先,这可能吗?我认为这应该以与抽象类类似的方式运行,但我也没有找到一个可行的解决方案。如果不可能,当我想保存一些可能是一种或另一种类型的数据(如果不是密封类)时,我应该采取什么样的方法?

Tam*_*afi 5

当我们尝试在我们的领域中使用多态性时,我们遇到了这样的问题,我们通过以下方式解决了它:

领域:

我们有一个Photo如下所示的模型:

sealed interface Photo {
    val id: Long

    data class Empty(
        override val id: Long
    ) : Photo

    data class Simple(
        override val id: Long,
        val hasStickers: Boolean,
        val accessHash: Long,
        val fileReferenceBase64: String,
        val date: Int,
        val sizes: List<PhotoSize>,
        val dcId: Int
    ) : Photo
}
Run Code Online (Sandbox Code Playgroud)

Photo里面有PhotoSize,看起来像这样:

sealed interface PhotoSize {
    val type: String

    data class Empty(
        override val type: String
    ) : PhotoSize

    data class Simple(
        override val type: String,
        val location: FileLocation,
        val width: Int,
        val height: Int,
        val size: Int,
    ) : PhotoSize

    data class Cached(
        override val type: String,
        val location: FileLocation,
        val width: Int,
        val height: Int,
        val bytesBase64: String,
    ) : PhotoSize

    data class Stripped(
        override val type: String,
        val bytesBase64: String,
    ) : PhotoSize
}
Run Code Online (Sandbox Code Playgroud)

数据:

为了实现这一目标,我们的数据模块还有很多工作要做。我将把这个过程分解为三个部分,以使其看起来更容易:

1. 实体:

所以,一般使用Room和SQL,很难保存这样的对象,所以我们不得不想出这个想法。我们的PhotoEntity(我们域中的本地版本)Photo如下所示:

@Entity
data class PhotoEntity(
    // Shared columns
    @PrimaryKey
    val id: Long,
    val type: Type,

    // Simple Columns
    val hasStickers: Boolean? = null,
    val accessHash: Long? = null,
    val fileReferenceBase64: String? = null,
    val date: Int? = null,
    val dcId: Int? = null
) {
    enum class Type {
        EMPTY,
        SIMPLE,
    }
}
Run Code Online (Sandbox Code Playgroud)

我们的PhotoSizeEntity样子是这样的:

@Entity
data class PhotoSizeEntity(
    // Shared columns
    @PrimaryKey
    @Embedded
    val identity: Identity,
    val type: Type,

    // Simple columns
    @Embedded
    val locationLocal: LocalFileLocation? = null,
    val width: Int? = null,
    val height: Int? = null,
    val size: Int? = null,

    // Cached and Stripped columns
    val bytesBase64: String? = null,
) {
    data class Identity(
        val photoId: Long,
        val sizeType: String
    )

    enum class Type {
        EMPTY,
        SIMPLE,
        CACHED,
        STRIPPED
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我们将这个复合类联合起来PhotoEntityPhotoSizeEntity这样我们就可以检索域模型所需的所有数据:

data class PhotoCompound(
    @Embedded
    val photo: PhotoEntity,
    @Relation(entity = PhotoSizeEntity::class, parentColumn = "id", entityColumn = "photoId")
    val sizes: List<PhotoSizeEntity>? = null,
)
Run Code Online (Sandbox Code Playgroud)

2. 道

所以我们dao应该能够存储和检索这些数据。为了灵活性起见,您可以使用两个daos代替一个,但PhotoEntityPhotoSizeEntity本例中我们将使用一个共享的,如下所示:

data class PhotoCompound(
    @Embedded
    val photo: PhotoEntity,
    @Relation(entity = PhotoSizeEntity::class, parentColumn = "id", entityColumn = "photoId")
    val sizes: List<PhotoSizeEntity>? = null,
)
Run Code Online (Sandbox Code Playgroud)

3. 适配器:

解决了将数据保存到SQL数据库的问题后,我们现在需要解决域实体和本地实体之间的转换问题。我们的Photo转换器又名适配器如下所示:

@Dao
interface IPhotoDao {

    @Transaction
    @Query("SELECT * FROM PhotoEntity WHERE id = :id")
    suspend fun getPhotoCompound(id: Long): PhotoCompound

    @Transaction
    suspend fun insertOrUpdateCompound(compound: PhotoCompound) {
        compound.sizes?.let { sizes ->
            insertOrUpdate(sizes)
        }

        insertOrUpdate(compound.photo)
    }

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertOrUpdate(entity: PhotoEntity)
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertOrUpdate(entities: List<PhotoSizeEntity>)
}
Run Code Online (Sandbox Code Playgroud)

对于PhotoSize,它看起来像这样:

fun Photo.toCompound() = when(this) {
    is Photo.Empty -> this.toCompound()
    is Photo.Simple -> this.toCompound()
}

fun PhotoCompound.toModel() = when (photo.type) {
    PhotoEntity.Type.EMPTY -> Photo.Empty(photo.id)
    PhotoEntity.Type.SIMPLE -> this.toSimpleModel()
}

private fun PhotoCompound.toSimpleModel() = photo.run {
    Photo.Simple(
        id,
        hasStickers!!,
        accessHash!!,
        fileReferenceBase64!!,
        date!!,
        sizes?.toModels()!!,
        dcId!!
    )
}

private fun Photo.Empty.toCompound(): PhotoCompound {
    val photo = PhotoEntity(
        id,
        PhotoEntity.Type.EMPTY
    )

    return PhotoCompound(photo)
}

private fun Photo.Simple.toCompound(): PhotoCompound {
    val photo = PhotoEntity(
        id,
        PhotoEntity.Type.SIMPLE,
        hasStickers = hasStickers,
        accessHash = accessHash,
        fileReferenceBase64 = fileReferenceBase64,
        date = date,
        dcId = dcId,
    )

    val sizeEntities = sizes.toEntities(id)
    return PhotoCompound(photo, sizeEntities)
}
Run Code Online (Sandbox Code Playgroud)

就是这样!

结论:

要将密封类保存到 Room 或 SQL,无论是作为Entity,还是作为Embedded对象,您需要拥有一个包含所有密封变体的所有属性的大数据类,并使用一种Enum类型来指示稍后使用的变体类型域和数据之间的转换,或者如果您不使用清洁架构,则在代码中进行指示。坚硬,但坚固且灵活。我希望Room有一些注释可以生成此类代码以摆脱样板代码。

PS:这个类取自Telegram的方案,它们还解决了与服务器通信时的多态性问题。在这里查看他们的 TL 语言: https: //core.telegram.org/mtproto/TL

scheme.tlPS2:如果你喜欢 Telegram 的 TL 语言,你可以使用这个生成器从文件生成 Kotlin 类:https: //github.com/tamimattafi/mtproto

编辑:您可以使用此代码生成库自动为复合类生成 Dao,以使其更容易插入,从而删除大量样板以正确映射事物。链接: https: //github.com/tamimattafi/android-room-compound

快乐编码!