当我们尝试在我们的领域中使用多态性时,我们遇到了这样的问题,我们通过以下方式解决了它:
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
对象,您需要拥有一个具有所有密封变体的所有属性的大数据类,并使用Enum
type 来指示稍后用于域和数据之间转换的变体类型,或者如果您不使用 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
快乐编码!