将带有嵌套对象的 Json 反序列化为带有外键的 Android 房间实体的最佳方法

Best way to deserialize Json with nested objects into Android Room Entity with ForeignKeys

我有一个Clientapi。 json 响应如下所示:

 {
    "clientId": 1,
    "createdAt": null,
    "updatedAt": null,
    "monthlyPaymentAmount": null,
    "person": {
      // Omitted data here
    },
    "paymentType": {
      // Omitted data here
    },
    "deliveryInstructions": null,
    "referralName": null,
    "referralPhoneNumber": null,
    "status": 0,
    "startDate": null,
    "eventDate": null,
  }

因此,使用 Kotlin data class file from JSON 从 json 响应自动创建数据 classes,我得到了以下 Client 数据 class 我用 ForeignKeys:

变成了房间 @Entity
@Entity(
    tableName = "client",
    foreignKeys = [
        ForeignKey(
            entity = Account::class,
            parentColumns = arrayOf("account_id"),
            childColumns = arrayOf("account_id"),
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Person::class,
            parentColumns = arrayOf("person_id", "account_id"),
            childColumns = arrayOf("person_id", "account_id"),
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = PaymentType::class,
            parentColumns = arrayOf("payment_type_id", "account_id"),
            childColumns = arrayOf("payment_type_id", "account_id"),
        ),
    ],
    indices = [
        Index(value = arrayOf("client_id", "account_id"), unique = true)
    ]
)
data class Client(
    @PrimaryKey
    @ColumnInfo(name = "client_id") val clientId: Int,
    @ColumnInfo(name = "delivery_notes") val deliveryInstructions: String,
    @ColumnInfo(name = "event_date") val eventDate: Date,
    @ColumnInfo(name = "monthly_payment_amount") val monthlyPaymentAmount: Float,
    @ColumnInfo(name = "payment_type_id") val paymentType: Int,
    @ColumnInfo(name = "person_id") val person: Int,
    @ColumnInfo(name = "referral_name") val referralName: String,
    @ColumnInfo(name = "start_date") val startDate: Date,
    @ColumnInfo(name = "status") val status: Int,
    @ColumnInfo(name = "updated_at") val updatedAt: Date,
    @ColumnInfo(name = "synced_at") val syncedAt: Date,
)

还有 PaymentTypePerson 数据 class 是我省略的,但它们也是房间 @Entity 的数据。

Room 数据库需要匹配具有此 CREATE TABLE SQL 语句的以下数据库结构:

CREATE TABLE client
(
    client_id              INTEGER NOT NULL,
    account_id             INTEGER NOT NULL,
    updated_at             TEXT    NOT NULL,
    synced_at              TEXT    NOT NULL,
    person_id              INTEGER NOT NULL,
    payment_type_id        INTEGER,
    referral_name          TEXT,
    delivery_notes         TEXT,
    status                 INTEGER DEFAULT 1 NOT NULL,
    monthly_payment_amount REAL,
    start_date             TEXT,
    event_date             TEXT,
    CONSTRAINT client_fk1 FOREIGN KEY (account_id) REFERENCES account (account_id) ON DELETE CASCADE,
    CONSTRAINT client_fk2 FOREIGN KEY (person_id, account_id) REFERENCES person (person_id, account_id) ON DELETE CASCADE,
    CONSTRAINT client_fk4 FOREIGN KEY (payment_type_id, account_id) REFERENCES payment_type (payment_type_id, account_id),
    CONSTRAINT client_pk PRIMARY KEY (client_id, account_id)
);

所以,我有一个 Converters class 来将 json 响应反序列化为 Client class,如下所示:

class Converters {

    @TypeConverter
    fun clientToJson(value: Client?): String? = Gson().toJson(value)

    @TypeConverter
    fun jsonToClient(value: String): Client = Gson().fromJson(value, Client::class.java)

    @TypeConverter
    fun paymentTypeToJson(value: PaymentType?): String? = Gson().toJson(value)

    @TypeConverter
    fun jsonToPaymentType(value: String): PaymentType =
        Gson().fromJson(value, PaymentType::class.java)

    @TypeConverter
    fun objToJsonPerson(value: Person?): String? = Gson().toJson(value)

    @TypeConverter
    fun jsonToObjPerson(value: String): Person = Gson().fromJson(value, Person::class.java)

    // Omitted list of converters here
}

我不确定上面的 client 转换器是否正确地自动创建了 PaymentTypePerson 对象(大多数人认为不会)。这就是为什么我想知道将带有嵌套对象的 json 响应反序列化为带有外键的 Room 实体的正确方法是什么?

虽然我对 Foreing Keys 最困惑。当上面的转换器试图将 "person": {} 对象解析为 Int 类型的 @ColumnInfo(name = "person_id") 时会发生什么?它会知道它是一个 ForeignKey 并会自动创建一个 Person::class 吗? best/proper 反序列化嵌套对象的方法如何确保正确完成表之间的这种关系?

Will it know that it is a ForeignKey and will create a Person::class automatically?

绝对不是。

@ForeignKey 定义一个外键约束,即一个规则,规定应用约束的列必须是引用 table 中引用列中的值.引用的 (Parent) table 必须存在并相应地填充符合规则的值。

类型转换器用于将未处理的类型(不是整数类型(例如 Int Long Byte 等)、字符串、十进制数(Float、Double 等)或 ByteArray)转换为已处理的类型。

例如你的:-

@TypeConverter
fun clientToJson(value: Client?): String? = Gson().toJson(value)

将转换单个列,例如

client: Client

从客户端到 JSON 字符串并将该字符串存储在客户端列中。它不会将客户端拆分为单独的值并将它们放入单独的列中。

因此,使用您检索到的 JSON 字符串,您可以提取具有嵌入式人员和付款类型的客户 objects。

只有满足所有外键才能成功插入Client

因此您可能应该检查帐户 table 中是否存在 account_id。 插入Client前检查person_id,account_id是否存在于person_table等中,否则插入失败

如果检查无法识别行,则您必须中止或将适当的行插入 tables。

假设您的源数据引用正确。然后你应该首先提取最高级别​​ parent (我相信帐户)插入它们。然后您可以提取下一个级别(人员和付款类型)并插入它们,最后插入客户。这样外键就应该存在了。

另一种方法是关闭外键支持并加载数据,然后重新打开外键支持。但是,如果数据引用不正确,您可能会遇到外键约束冲突。

例如db.openHelper.writableDatabase.setForeignKeyConstraintsEnabled(false)

I've got with the following Client data class which I've turned into a Room @Entity with ForeignKeys:

根据 JSON,您将遇到空值问题,除非您确保将这些值更改为适当的值。

  • 例如"updatedAt": null,@ColumnInfo(name = "updated_at") val updatedAt: Date, 关联,除非 TypeConverter return 的 non-null 值将失败。

The Room database needs to match the following database structure that has this CREATE TABLE SQL statement:

它没有,例如你有:-

  • payment_type_id INTEGER,@ColumnInfo(name = "payment_type_id") val paymentType: Int,前者没有NOT NULL约束,后者有隐式NOT NULL(val paymentType: Int?没有隐式NOT空)

    • 重复多列
  • status INTEGER DEFAULT 1 NOT NULL,@ColumnInfo(name = "status") val status: Int, 后者没有默认值,在 @ColumnInfo 注释中使用 defaultValue = "1" 将应用它。

    • 但是,您不能使用方便的 @Insert 注释函数,因为它总是会提供一个值。要应用默认值,您必须使用 @Query("INSERT INTO (csv_of_the_columns_that_are_not_to_have_a_default_value_applied) VALUES ....
  • CONSTRAINT client_pk PRIMARY KEY (client_id, account_id)@PrimaryKey @ColumnInfo(name = "client_id") val clientId: Int,。只有客户端 ID 是主键。你有 indices = [Index(value = arrayOf("client_id", "account_id"), unique = true)]。但是,您应该 primaryKeys = arrayOf("client_id", "account_id")

额外

仔细看了看。我相信您的问题不在于类型转换器,也不在于目前的外键,而在于试图将方钉装入圆孔的尝试。

如果不深入研究试图从 JSON 的角度忽略字段,我认为核心问题的解决方案是您不能只让客户适应嵌入的人员和付款 objects您要存储的客户端。

因此,首先考虑将实体重命名为 ClientTable 的替代方案 class :-

@Entity(
    tableName = "client",
    foreignKeys = [
        ForeignKey(
            entity = Account::class,
            parentColumns = arrayOf("account_id"),
            childColumns = arrayOf("account_id"),
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Person::class,
            parentColumns = arrayOf("person_id", "account_id"),
            childColumns = arrayOf("person_id", "account_id"),
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = PaymentType::class,
            parentColumns = arrayOf("payment_type_id", "account_id"),
            childColumns = arrayOf("payment_type_id", "account_id"),
        ),
    ],
    indices = [
        Index(value = arrayOf("client_id", "account_id"), unique = true)
    ]
)
data class ClientTable(
    @PrimaryKey
    @ColumnInfo(name = "client_id") val clientId: Int,
    @ColumnInfo(name = "delivery_notes") val deliveryInstructions: String,
    @ColumnInfo(name = "event_date") val eventDate: Date,
    @ColumnInfo(name = "monthly_payment_amount") val monthlyPaymentAmount: Float,
    @ColumnInfo(name = "payment_type_id") val paymentTypeid: Int,
    @ColumnInfo(name = "person_id") val personid: Int,
    @ColumnInfo(name = "referral_name") val referralName: String,
    @ColumnInfo(name = "start_date") val startDate: Date,
    @ColumnInfo(name = "status") val status: Int,
    @ColumnInfo(name = "updated_at") val updatedAt: Date,
    @ColumnInfo(name = "synced_at") val syncedAt: Date,

    /* Not required but may be useful BUT will not be columns in the table */
    @Ignore
    val person: Person,
    @Ignore
    val paymentType: PaymentType
)

唯一的变化是额外的两个但 @Ignore 注释 vals,用于 Person 和 PaymentType。 @Ignore 导致它们不作为列包含在 table 中。它们只是用于演示(从数据库中提取数据时,您可能会遇到它们为 null 的问题)。

请注意,为了测试 PaymentType 是:-

@Entity
data class PaymentType(
    @PrimaryKey
    val paymentTypeId: Long? = null,
    val paymentTypeName: String
)

并且是:-

@Entity
data class Person(
    @PrimaryKey
    val personId: Long,
    val personName: String
)
  • 所以 // Omitted data here 不会引起问题。

而不是你的 JSON 下面的 JSON 已经被使用(但是它是动态构建的):-

{"clientId":1,"deliveryInstructions":"x","eventDate":"Jan 21, 2022 10:57:59 AM","monthlyPaymentAmount":111.11,"paymentType":{"paymentTypeId":20,"paymentTypeName":"Credit Card"},"person":{"personId":10,"personName":"Bert"},"referralName":"Fred","startDate":"Jan 21, 2022 10:57:59 AM","status":1,"syncedAt":"Jan 21, 2022 10:57:59 AM","updatedAt":"Jan 21, 2022 10:57:59 AM"}

添加了一个简单的非类型转换器 json 提取器来模仿 API:-

class JsonApiExample {
    
     fun testExtractJsonFromString(json: String): Client {
          return Gson().fromJson(json,Client::class.java)
     }
}

现在转到另一个挂钩,带有嵌入式 Person/PaymentType 且不包含不属于 JSON 字段的 personId 和 paymentTypeId 的客户:-

data class Client(
    val clientId: Int,
    val deliveryInstructions: String,
    val eventDate: Date,
    val monthlyPaymentAmount: Float,
    val referralName: String,
    val startDate: Date,
    val status: Int,
    val updatedAt: Date,
    val syncedAt: Date,
    val person: Person,
    val paymentType: PaymentType
) {
    fun getClientAsClientTable(): ClientTable {
        return ClientTable(
            this.clientId,
            this.deliveryInstructions,
            this.eventDate,
            this.monthlyPaymentAmount,
            this.paymentType.paymentTypeId!!.toInt(),
            this.person.personId.toInt(),
            this.referralName,
            this.startDate,
            this.status,
            this.updatedAt,
            this.syncedAt,
            this.person,
            this.paymentType
        )
    }
}

如您所见,重要的一点是 getClientAsClientTable 函数 。这将生成 return 一个 ClientTable Object 并有效地使方钉变圆以适合。

所以测试它,至于创建一个可以插入的 ClientTable(外键允许,由于在 Clientclass) 考虑:-

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /* Create a Client ready to be converted to JSON */
        val clientx = Client(
            clientId = 1,
            deliveryInstructions = "x",
            eventDate = Date(),
            monthlyPaymentAmount = 111.11F, referralName = "Fred", startDate = Date(), status = 1, updatedAt = Date(), syncedAt = Date(),
        Person(10,"Bert"), paymentType = PaymentType(20,"Credit Card"))

        /* Convert the Client to JSON mimicing the API and write it to the log to allow inspection */
        val jsonClientX = Gson().toJson(clientx)
        Log.d("TESTIT",jsonClientX)

        /* Extract the Client from the JSON */
        val clientxrevisited = JsonApiExample().testExtractJsonFromString(jsonClientX)
        /* Extract the ClientTable from the Client */
        val ClientTable = clientxrevisited.getClientAsClientTable()
        /* Allow a Break point to be placed so the results can be inspected*/
        if (1 == 1) {
            Log.d("TESTIT","Testing")
        }
    }
}

当 运行 使用 BreakP整数:-

  • 刻度是列
  • 突出显示的是嵌入的 objects,您可以从中插入或忽略相应的 table。
    • 请注意,字段可能有所不同,只是选择了 id 和 name 只是为了演示。

因此 RECAP 两个 class;一个用于 JSON extract/import,另一个用于 Entity/Table,并且可以将一个变成另一个。

上一个回答的后续演示

这是基于一些稍作修改的实体插入单个客户端的演示。

帐号 :-

@Entity
data class Account(
    @PrimaryKey
    val account_id: Long? = null,
    val accountName: String   
)

支付类型

@Entity( primaryKeys = ["payment_type_id","account_id"])
data class PaymentType(
    @ColumnInfo(name = "payment_type_id")
    val paymentTypeId: Long,
    @ColumnInfo(name = "account_id")
    val accountId: Long,
    val paymentTypeName: String
)
  • 添加了 accountId(account_id 列)以适应 ClientTable 中的外键约束(根据问题)
  • 复合主键

(同样)

@Entity( primaryKeys = ["person_id","account_id"])
data class Person(
    @ColumnInfo(name = "person_id")
    val personId: Long,
    @ColumnInfo(name = "account_id")
    val accountId: Long,
    val personName: String
)

客户如建议

data class Client(
    val clientId: Long,
    val deliveryInstructions: String,
    val eventDate: Date,
    val monthlyPaymentAmount: Float,
    val referralName: String,
    val startDate: Date,
    val status: Long,
    val updatedAt: Date,
    val syncedAt: Date,
    val person: Person,
    val paymentType: PaymentType
) {
    fun getClientAsClientTable(): ClientTable {
        return ClientTable(
            this.clientId,
            this.deliveryInstructions,
            this.eventDate,
            this.monthlyPaymentAmount,
            this.paymentType.paymentTypeId,
            this.person.personId,
            this.referralName,
            this.startDate,
            this.status,
            this.updatedAt,
            this.syncedAt
        )
    }
}
  • 理想情况下,id 应该是 Long 而不是 Int,因为它们有可能溢出 Int。这么长的都用过了

ClientTable正式(客户端):-

@Entity(
    tableName = "client",
    foreignKeys = [
        ForeignKey(
            entity = Account::class,
            parentColumns = arrayOf("account_id"),
            childColumns = arrayOf("account_id"),
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Person::class,
            parentColumns = arrayOf("person_id", "account_id"),
            childColumns = arrayOf("person_id", "account_id"),
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = PaymentType::class,
            parentColumns = arrayOf("payment_type_id", "account_id"),
            childColumns = arrayOf("payment_type_id", "account_id"),
        ),
    ],
    indices = [
        Index(value = arrayOf("client_id", "account_id"), unique = true)
    ]
)
data class ClientTable(
    @PrimaryKey
    @ColumnInfo(name = "client_id") val clientId: Long,
    @ColumnInfo(name = "delivery_notes") val deliveryInstructions: String,
    @ColumnInfo(name = "event_date") val eventDate: Date,
    @ColumnInfo(name = "monthly_payment_amount") val monthlyPaymentAmount: Float,
    @ColumnInfo(name = "payment_type_id") val paymentTypeid: Long,
    @ColumnInfo(name = "person_id") val personid: Long,
    @ColumnInfo(name = "referral_name") val referralName: String,
    @ColumnInfo(name = "start_date") val startDate: Date,
    @ColumnInfo(name = "status") val status: Long,
    @ColumnInfo(name = "updated_at") val updatedAt: Date,
    @ColumnInfo(name = "synced_at") val syncedAt: Date,
    @ColumnInfo(name = "account_id") var accountId: Long = 1 //????? ADDED
)
  • 注意添加accountId

转换器

class Converters {

    @TypeConverter
    fun dateToLong(date: Date): Long {
        return date.time / 1000 // divided by 1000 to strip milliseconds as easier to handle dates
    }

    @TypeConverter
    fun dateFromLong(dateAsLong: Long): Date {
        return Date(dateAsLong * 1000) // reapply milliseconds
    }
}

AllDao(因为它暗示所有这些都在一起):-

@Dao
abstract class AllDao {
    @Insert(onConflict = IGNORE)
    abstract fun insert(account: Account): Long
    @Insert(onConflict = IGNORE)
    abstract fun insert(paymentType: PaymentType): Long
    @Insert(onConflict = IGNORE)
    abstract fun insert(person: Person): Long
    @Insert(onConflict = IGNORE)
    abstract fun insert(clientTable: ClientTable): Long

    @Query("SELECT count(*) >= 1 FROM account WHERE account_id=:accountId")
    abstract fun doesAccountExistByAccountId(accountId: Long): Boolean
    @Query("SELECT count(*) >= 1 FROM paymenttype WHERE account_id=:accountId AND payment_type_id=:paymentTypeId")
    abstract fun doesPaymentTypeExistByAccountIdPaymentTypeId(accountId: Long, paymentTypeId: Long): Boolean
    @Query("SELECT count(*) >= 1 FROM person WHERE account_id=:accountId AND person_id=:personId")
    abstract fun doesPersonExistByAccountIdPersonId(accountId: Long, personId: Long): Boolean

    @Query("")
    @Transaction
    fun insertFromAPIJson(json: String): Long {
        var rv: Long = -1
        val client = Gson().fromJson(json,Client::class.java)
        val clientTable = client.getClientAsClientTable()
        insert(Account(client.person.accountId,"NO NAME"))
        val accountExits = doesAccountExistByAccountId(client.person.accountId)
        insert(PaymentType(client.paymentType.paymentTypeId,client.paymentType.accountId,client.paymentType.paymentTypeName))
        val paymentTypeExists = doesPaymentTypeExistByAccountIdPaymentTypeId(client.paymentType.accountId,client.paymentType.paymentTypeId)
        insert(Person(client.person.personId, client.person.accountId, client.person.personName))
        val personExists = doesPersonExistByAccountIdPersonId(client.person.accountId,client.person.personId)
        if (accountExits && paymentTypeExists && personExists) {
            clientTable.accountId = client.person.accountId
            rv = insert(clientTable)
        }
        return rv
    }
}
  • 显然注意insertFromAPIJson函数
  • 还要注意抽象 class 而不是接口
  • 记下临时帐户名(您必须确定如何命名)

TheDatabase 抽象 class 用 @Database 注释,包括基本的 getInstance 函数 :-

@TypeConverters(Converters::class)
@Database(entities = [Account::class,ClientTable::class,PaymentType::class,Person::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getAllDao(): AllDao

    companion object {
        private var instance: TheDatabase? = null
        fun getInstance(context: Context): TheDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as TheDatabase
        }
    }
}

最后从 JSON 添加单个客户端(再次构建客户端并提取 JSON 以模仿 API)。 主要活动 :-

class MainActivity : AppCompatActivity() {
    lateinit var db: TheDatabase
    lateinit var dao: AllDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /* Create a Client ready to be converted to JSON */
        val clientx = Client(
            clientId = 1,
            deliveryInstructions = "x",
            eventDate = Date(),
            monthlyPaymentAmount = 111.11F, referralName = "Fred", startDate = Date(), status = 1, updatedAt = Date(), syncedAt = Date(),
            Person(10,1,"Bert"), paymentType = PaymentType(20,1,"Credit Card"))

        db = TheDatabase.getInstance(this)
        dao = db.getAllDao()
        dao.insertFromAPIJson(Gson().toJson(clientx))

    }
}

结果

使用应用检查:-

已添加帐户 :-

已添加 PaymentType :-

已添加:-

客户端 :-

运行 再

正如预期的那样,由于 onConflict IGNORE,数据保持不变