如何在不引入Android依赖的前提下,将现有Hibernate JPA实体扩展为带Kotlin序列化注解的Android Room实体
遇到这种跨框架的注解冲突确实头疼,既要保持核心JPA实体的纯净性(不能沾Android依赖),又要让Room和Kotlin序列化和谐共处,我给你几个可行的解决方案:
方案一:用组合代替继承(最稳妥无冲突)
继承虽然看起来省事,但JPA、Room、Kotlin序列化三者的注解规则差异太大,很容易踩坑。改用组合模式,把JPA实体作为Room实体的内部属性,完全隔离两个框架的注解:
// 原JPA实体保持不变,无任何Android依赖 @Serializable @Entity @Table( name = CustomerEntity.Constants.TABLE_NAME, indexes = [Index(name = "IX_Customer_CustomerName", columnList = "CustomerName")] ) open class CustomerEntity( @Id @Column(name = Constants.COLUMN_ID_NAME, nullable = false, length = 15) open var id: String = "", @Column(name = Constants.COLUMN_CUSTOMERNAME_NAME, length = 40) var customerName: String = "", ) { object Constants { const val TABLE_NAME: String = "Customer" const val COLUMN_ID_NAME: String = "CustomerID" const val COLUMN_CUSTOMERNAME_NAME: String = "CustomerName" } } // Android Room实体,完全独立于JPA实体 @Serializable @Entity( tableName = CustomerEntity.Constants.TABLE_NAME, indexes = [Index(name = "IX_Customer_CustomerName", columnList = CustomerEntity.Constants.COLUMN_CUSTOMERNAME_NAME)] ) data class CustomerAndroid( @PrimaryKey @ColumnInfo(name = CustomerEntity.Constants.COLUMN_ID_NAME) val id: String = "", @ColumnInfo(name = CustomerEntity.Constants.COLUMN_CUSTOMERNAME_NAME) val customerName: String = "" ) { // 提供JPA实体和Room实体的双向转换方法 fun toJpaEntity(): CustomerEntity = CustomerEntity(id, customerName) companion object { fun fromJpaEntity(entity: CustomerEntity): CustomerAndroid = CustomerAndroid(entity.id, entity.customerName) } }
这个方案的优点是完全避免了注解冲突,两个实体各司其职,原JPA实体完全不需要修改,也不会引入任何Android依赖。如果实体字段较多,可以用Kotlin扩展函数或者映射工具(比如MapStruct)来简化转换代码。
方案二:利用Room的primaryKeys注解参数+序列化字段排除
如果你坚持要用继承,可以通过Room的@Entity注解的primaryKeys参数指定主键,同时让Kotlin序列化只识别一个id字段:
首先修改原JPA实体,给id字段加上Kotlin序列化的@Transient,让序列化时忽略父类的id:
@Serializable @Entity @Table(...) open class CustomerEntity( @Id @Column(name = Constants.COLUMN_ID_NAME, nullable = false, length = 15) @kotlinx.serialization.Transient // 序列化时忽略父类的id open var id: String = "", @Column(name = Constants.COLUMN_CUSTOMERNAME_NAME, length = 40) var customerName: String = "", ) { // 常量保持不变 }
然后Room子类中定义带@PrimaryKey的id,并指定序列化名称:
@Serializable @Entity( tableName = CustomerEntity.Constants.TABLE_NAME, indexes = [Index(name = "IX_Customer_CustomerName", columnList = CustomerEntity.Constants.COLUMN_CUSTOMERNAME_NAME)] ) class CustomerAndroid : CustomerEntity() { constructor(customerID: String, customerName: String = "") : this() { this.id = customerID this.customerName = customerName } @PrimaryKey @ColumnInfo(name = CustomerEntity.Constants.COLUMN_ID_NAME) @SerialName("id") // 指定序列化名称,确保和原字段名一致 override var id: String = "" }
这个方案的前提是原JPA实体可以添加Kotlin序列化的@Transient注解(因为原实体已经用了@Serializable,所以依赖kotlinx序列化是合理的),这样序列化时只会处理子类的id字段,避免重复冲突,同时Room通过@PrimaryKey识别主键。
方案三:用Room的@Embedded嵌入JPA实体
可以把JPA实体嵌入到Room实体中,通过@Entity的primaryKeys指定主键,同时自定义序列化逻辑展平字段:
@Serializable @Entity( tableName = CustomerEntity.Constants.TABLE_NAME, primaryKeys = ["id"], indexes = [Index(name = "IX_Customer_CustomerName", columnList = "customerName")] ) class CustomerAndroid( @Embedded val jpaEntity: CustomerEntity ) { // 提供便捷访问的委托属性 var id: String get() = jpaEntity.id set(value) { jpaEntity.id = value } var customerName: String get() = jpaEntity.customerName set(value) { jpaEntity.customerName = value } // 自定义序列化逻辑,展平JPA实体的字段 @Serializer(forClass = CustomerAndroid::class) companion object : KSerializer<CustomerAndroid> { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CustomerAndroid") { element<String>("id") element<String>("customerName") } override fun serialize(encoder: Encoder, value: CustomerAndroid) { encoder.encodeStringElement(descriptor, 0, value.id) encoder.encodeStringElement(descriptor, 1, value.customerName) } override fun deserialize(decoder: Decoder): CustomerAndroid { val id = decoder.decodeStringElement(descriptor, 0) val customerName = decoder.decodeStringElement(descriptor, 1) return CustomerAndroid(CustomerEntity(id, customerName)) } } // 便捷构造函数 constructor(customerID: String, customerName: String = "") : this(CustomerEntity(customerID, customerName)) }
这个方案的好处是不用修改原JPA实体,但需要写自定义序列化器来展平字段,避免序列化时出现嵌套的jpaEntity字段,适合字段较多且不想写大量转换代码的场景。
备注:内容来源于stack exchange,提问作者moonlightcheese

