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

2024-04-07

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

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

    data class Pwm(
        val data: String
    ) : BluetoothMessageType()
}

甚至是这样,但没有必要。我发现这个给了我更多的错误,因为它不知道如何处理 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()
}

然后是实体类

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

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

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


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

Domain:

我们有一个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
}

Photo has 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
}

Data:

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

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,
    }
}

And our 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
    }
}

然后我们就有了这个复合类来联合起来PhotoEntity and PhotoSizeEntity在一起,这样我们就可以检索域模型所需的所有数据:

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

2. Dao

So our dao应该能够存储和检索这些数据。你可以有两个daos for PhotoEntity and PhotoSizeEntity为了灵活起见,我们没有使用一个,但在本例中我们将使用共享的,它看起来像这样:

@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>)
}

3. 适配器:

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

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)
}

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

fun List<PhotoSize>.toEntities(photoId: Long) = map { photoSize ->
    photoSize.toEntity(photoId)
}

fun PhotoSize.toEntity(photoId: Long) = when(this) {
    is PhotoSize.Cached -> this.toEntity(photoId)
    is PhotoSize.Empty -> this.toEntity(photoId)
    is PhotoSize.Simple -> this.toEntity(photoId)
    is PhotoSize.Stripped -> this.toEntity(photoId)
}

fun List<PhotoSizeEntity>.toModels() = map { photoSizeEntity ->
    photoSizeEntity.toModel()
}

fun PhotoSizeEntity.toModel() = when(type) {
    PhotoSizeEntity.Type.EMPTY -> this.toEmptyModel()
    PhotoSizeEntity.Type.SIMPLE -> this.toSimpleModel()
    PhotoSizeEntity.Type.CACHED -> this.toCachedModel()
    PhotoSizeEntity.Type.STRIPPED -> this.toStrippedModel()
}

private fun PhotoSizeEntity.toEmptyModel() = PhotoSize.Empty(identity.sizeType)

private fun PhotoSizeEntity.toCachedModel() = PhotoSize.Cached(
    identity.sizeType,
    locationLocal?.toModel()!!,
    width!!,
    height!!,
    bytesBase64!!
)

private fun PhotoSizeEntity.toSimpleModel() = PhotoSize.Simple(
    identity.sizeType,
    locationLocal?.toModel()!!,
    width!!,
    height!!,
    size!!
)

private fun PhotoSizeEntity.toStrippedModel() = PhotoSize.Stripped(
    identity.sizeType,
    bytesBase64!!
)

private fun PhotoSize.Cached.toEntity(photoId: Long) = PhotoSizeEntity(
    PhotoSizeEntity.Identity(photoId, type),
    PhotoSizeEntity.Type.CACHED,
    locationLocal = location.toEntity(),
    width = width,
    height = height,
    bytesBase64 = bytesBase64
)

private fun PhotoSize.Simple.toEntity(photoId: Long) = PhotoSizeEntity(
    PhotoSizeEntity.Identity(photoId, type),
    PhotoSizeEntity.Type.SIMPLE,
    locationLocal = location.toEntity(),
    width = width,
    height = height,
    size = size
)

private fun PhotoSize.Stripped.toEntity(photoId: Long) = PhotoSizeEntity(
    PhotoSizeEntity.Identity(photoId, type),
    PhotoSizeEntity.Type.STRIPPED,
    bytesBase64 = bytesBase64
)

private fun PhotoSize.Empty.toEntity(photoId: Long) = PhotoSizeEntity(
    PhotoSizeEntity.Identity(photoId, type),
    PhotoSizeEntity.Type.EMPTY
)

就是这样!

结论:

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

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

PS2:如果你喜欢 Telegram 的 TL 语言,你可以使用这个生成器来生成 Kotlin 类scheme.tl files: https://github.com/tamimattafi/mtproto https://github.com/tamimattafi/mtproto

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

快乐编码!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • 以编程方式滚动 UIScrollView

    我有一个UIScrollView其中有几个观点 当用户轻弹手指时 视图会根据手指轻弹的方向向右或向左滚动 基本上我的代码的工作方式与 iPhone 照片应用程序类似 现在 有没有一种方法可以让我以编程方式执行相同的操作 以便我最终得到一个通
  • 比较两个列表<>

    我有 gridview 控件 上面有一个复选框 当我点击 保存 按钮时 我能够找到已选中的复选框 并且到目前为止我能够做得很好 但问题是 假设用户尝试取消选中复选框 那么我将如何跟踪更改并将其保存到已选中的数据库中 有什么帮助吗 所以在这方
  • mpegts 中的 pts 和 pcr 值有限制吗?

    据我所知 PCR 存储在 mpegts 容器中的 42 位中 PTS 存储在 33 位中 So Max value for PCR is 2 42 4398046511104 Max value for PTS is 2 33 858993
  • [Firebase Messaging]:应用程序在后台时未调用后台消息处理程序方法?

    我正在开发一个使用 FCM 推送通知的应用程序 当应用程序在后台或终止并接收新通知时 我需要将此通知的数据保存在本地 SQLITE 中 而无需单击通知或再次重新打开应用程序 除非单击 否则不会在应用程序中读取通知 有什么建议么 这是我的No
  • 如何将阴影置信区间添加到具有指定值的线图中

    我有一个汇总数据的小表 其中包含四个类别的比值比 置信上限和下限 每个类别内有六个级别 我想使用 ggplot2 生成一个图表 它看起来与指定 lm 时创建的通常图表类似 它是 se 但我希望 R 仅使用表中预先指定的值 我已经成功创建了带
  • iOS 7 以编程方式按应用程序 VPN

    我正在开发一个 iOS 7 应用程序 我想知道 每应用程序 VPN 功能是否是 手动 激活的 如位置服务 您可以选择哪个应用程序可以或不可以 或者我可以设置它通过应用程序内的代码 环顾网络 我发现有关它的信息很少 苹果的公告也没有明确说明
  • 为什么某些 API 提供商需要 API 密钥?

    多个 Web 服务 API 需要您注册 API 密钥 例如 UPS Web 服务需要一个密钥 该密钥包含在对其服务的调用中 除了用户名和密码 提供商使用此密钥做什么 也许 UPS 是唯一一家同时需要 API 密钥和用户名 密码的公司 一个想
  • 匹配字符的第一个唯一实例[重复]

    这个问题在这里已经有答案了 我正在尝试匹配only字符串中字符的第一个实例 例如sdtmig 3 1 2 with XPath 替换 https www w3 org TR xpath functions func replace并将其替换
  • GWT 计时器取消不起作用

    我正在尝试编写代码来使用 GET 和 GETQuery 区分单击和双击 我明白了here http jsfiddle net KpCwN 4 所以我将它翻译成 GWT 如下所示 我的应用程序不能有全局变量 所以我用元素属性来完成该部分 im
  • Newtonsoft.Json.JsonReaderException:无法将字符串转换为日期时间:

    我正在尝试将一些数据插入本地数据库 我收到错误 Newtonsoft Json JsonReaderException 无法将字符串转换为日期时间 20 09 1982 12 00 00 路径 0 BIRTHDAY 第 1 行 位置 71
  • 将下载的 torrent 保存在内存中而不是文件 libtorrent

    使用 Rasterbar libtorrent 我不希望下载的数据放在我的硬盘上 而是放在管道或变量或软的东西上 这样我可以将其重定向到其他地方 Mysql 甚至垃圾 如果它不是我想要的 有没有如果不是使用 Libtorrent 在 C 中
  • 如何同时编译 .Net 3.5 和 4 的项目

    我需要为 Net 3 5 和 Net 4 0 编译一个项目 做到这一点的最低摩擦方式是什么 如果我从另一个程序集引用该项目 如何确定目标运行时 或者我应该直接引用二进制文件 我只需通过两个 csproj 文件来完成此操作 然后我可以轻松设置
  • 如何处理 Web 版 Twitter 数字 API

    我正在研究 Twitter 数字 api 将其集成到我的网站 该网站需要验证用户的唯一性 这里有一个link https dev twitter com twitter kit web digits 这是唯一一篇正式说明如何在网络上实现数字
  • 使用 Delphi 2010 进行远程调试时没有断点 - 所以卡在 Delphi 7 上

    去年 8 月进行初步调查后 我又重新开始使用 Delphi 2010 进行远程调试 我已确保 D2010 具有更新 4 和 5 并且远程调试器是 Embarcadero 网站上的最新版本 遵循非常有用的说明here http delphi
  • 如何删除 Perforce 中的工作区(使用 p4v)?

    我是 Perforce 的新手 创建了一些工作区作为熟悉它的练习 现在我想删除一些工作区 我只想删除工作区 以便它们不会出现在工作区视图的下拉列表中 do not想要对实际的仓库文件执行任何操作 谷歌搜索答案会产生 使工作区处于活动状态 的
  • 如何在docker中运行mongod后运行mongorestore

    我正在尝试使用 docker 设置一个 mongodb 服务器 让它从网络下载转储并用该信息填充它 我的问题是我可以让它运行并填充数据库 但完成后 它就会关闭 这就是我解决问题的方法 sudo u mongodb usr bin mongo
  • 无法将脚本文件添加到组件 html

    我在index html root 中有一个脚本文件引用 索引 html 这里不需要 sliderfunctions js 它包含一些关于 slider 的特定功能 所以我将它携带到 slider component html 文件 但正如
  • java 图像转换为矩阵

    有一个非常简单的 jpg 图像 我想将其转换为矩阵 但是使用 getRGB i j 指向像素会给出 ArrayIndexOutOfBounds 的运行时异常 以下代码对图像大小有限制吗 它只是显示整个图像中获得的第一个颜色代码 Buffer
  • 路由跟踪如何工作? [关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 这看起来几乎是神奇的 为了绘制到 Internet 上某个其他节点的整个路径 traceroute 命令执行什么操作 Traceroute 将 TTL
  • Android Room,如何保存一个实体,其中变量之一是密封类对象

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