我想从一个具有另一个对象列表的对象构建一个实体. 在运行项目时,我收到以下错误: "无法确定如何将此字段保存到数据库中.您可以考虑为其添加类型转换器." 有什么办法来解决这个问题吗?

我的实体:

@Parcelize
@Entity
data class Maintenance(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    var description: String,
    var listParts: List<Parts>
): Parcelable
@Parcelize
@Entity
data class Parts (
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    var serialNumber: Long,
    var description: String,
    var model: String,
) : Parcelable

我try 将列表类型更改为可变列表,但不起作用.

推荐答案

SQLite和Room只能直接存储特定类型的值:

  1. 空值(NULL),
  2. 整型值(整型),
  3. 十进制类型的值(实数),
  4. 字符串类型值(TEXT)和
  5. 字节流类型值(BLOB)

其他值需要转换为上述值之一.显然,NULL是没有用的,因为该信息不能用于重构底层值(您的List).INTEGER值需要一些复杂的方法来重建基础值(List),小数也是如此.因此,您需要将该值转换为List的TEXTual或ByteStream表示,以便存储List.

Hence the suggestion that a TypeConverter is used.

通常使用JSON表示,这是底层对象(部件列表)的字符串表示.

However,因为Parts本身就是一个表,所以不将存储在该表中的数据复制到另一个表中,而是利用作为relational数据库的SQLite的relationship功能似乎是明智的.

典型的relationship是通过在另一个表中存储唯一标识相关行的单个值来反映的(您已经定义了这样的值,即id字段,因为它必须是作为主键的唯一值).

单个Maintenance对象极有可能与另一个Maintenance具有相同的相关部分,因此单个部分可能与多个Maintenance相关.这是一种多对多类型的关系.

通常,多对多关系是通过使用另一个表来提供的,该表存储两个值,即对每个相关行(维护和相关部分)的引用.这样的表有许多术语,如关联表、参考表、映射表等.

通常,主键是两个值的组合(Room需要定义主键).

因为访问可能是通过部件(例如,获取使用特定部件的所有维护)或通过维护(例如,获取维护使用的所有部件,即显然是您想要的).因此,根据第二个值创建索引可能会更有效(如果不存在这样的索引,Room将发出警告).

因此,建议您修改Maintenance类以不包括List字段,而是为关系表引入一个新的带@Entity注释的类.

所以你可以:-

@Entity
data class Maintenance(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    var description: String,
    /*var listParts: List<Parts> defunct id using a many-many relationship*/
)

@Entity
data class Parts (
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    var serialNumber: Long,
    var description: String,
    var model: String,
)
@Entity(
    primaryKeys = ["maintenanceId","partsId"]
)
data class MaintenancePartsRelationship(
    val maintenanceId: Int,
    @ColumnInfo(index = true)
    val partsId: Int
)
  • 为简洁起见,省略了包裹内容

要对其部件进行维护,您可以使用POJO,例如:-

data class MaintenanceWithParts(
    @Embedded
    val maintenance: Maintenance,
    @Relation(
        entity = Parts::class, parentColumn = "id", entityColumn = "id",
        associateBy = Junction(MaintenancePartsRelationship::class,"maintenanceId","partsId")
    )
    val partsList: List<Parts>
)

如果您曾经想通过使用部件的维护来获取部件,那么您可以拥有如下POJO:

data class PartsWithMaintenances(
    @Embedded
    val parts: Parts,
    @Relation(
        entity = Maintenance::class, parentColumn = "id", entityColumn = "id",
        associateBy = Junction(MaintenancePartsRelationship::class, parentColumn = "partsId", entityColumn = "maintenanceId")
    )
    val maintenanceList: List<Maintenance>
)
  • 使用这些POJO,AssociateBy的Junction定义了关系/引用/映射/associatave表和引用父项/子项的列.
    • 基本上它们是相互颠倒的

Working Demo

为了演示上述内容,随后引入了以下附加代码:

@Dao
interface AllDAOs {
    @Insert( onConflict = OnConflictStrategy.IGNORE)
    fun insert(maintenance: Maintenance): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(parts: Parts): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(maintenancePartsRelationship: MaintenancePartsRelationship): Long
    @Query("SELECT * FROM maintenance")
    fun getALlMaintenancesWithTheirParts(): List<MaintenanceWithParts>
    @Query("SELECT * FROM parts")
    fun getAllPartsWithTheirMaintenances(): List<PartsWithMaintenances>
}

@Database(entities = [Maintenance::class,Parts::class,MaintenancePartsRelationship::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getAllDAOs(): AllDAOs
    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
        }
    }
}

最后是一些活动代码:

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

        val p1Id = dao.insert(Parts(id= 0, serialNumber = 100000000001, description = "Parts 1", model = "P1Model"))
        val p2Id = dao.insert(Parts(id= 0, serialNumber = 100000000002, description = "Parts 2", model = "P2Model"))
        val p3Id = dao.insert(Parts(id= 0, serialNumber = 100000000003, description = "Parts 3", model = "P3Model"))

        val m1Id = dao.insert(Maintenance(id = 0, description = "M1"))
        val m2Id = dao.insert(Maintenance(id = 0, description = "M2"))
        val m3Id = dao.insert(Maintenance(id = 0, description = "M3"))
        val p4Id = dao.insert(Parts(id= 0, serialNumber = 100000000004, description = "Parts 4", model = "P4Model"))
        val m4Id = dao.insert(Maintenance(id = 0, description = "M4"))

        dao.insert(MaintenancePartsRelationship(maintenanceId = m1Id.toInt(), partsId = p1Id.toInt()))
        dao.insert(MaintenancePartsRelationship(maintenanceId = m1Id.toInt(), partsId = p3Id.toInt()))


        dao.insert(MaintenancePartsRelationship(maintenanceId = m2Id.toInt(), partsId = p2Id.toInt()))
        dao.insert(MaintenancePartsRelationship(maintenanceId = m2Id.toInt(), partsId = p4Id.toInt()))

        dao.insert(MaintenancePartsRelationship(maintenanceId = m3Id.toInt(), partsId = p4Id.toInt()))
        dao.insert(MaintenancePartsRelationship(maintenanceId = m3Id.toInt(), partsId = p1Id.toInt()))
        dao.insert(MaintenancePartsRelationship(maintenanceId = m3Id.toInt(), partsId = p3Id.toInt()))
        dao.insert(MaintenancePartsRelationship(maintenanceId = m3Id.toInt(), partsId = p2Id.toInt()))

        dao.insert(MaintenancePartsRelationship(maintenanceId = m3Id.toInt(), partsId = p2Id.toInt()))

        for (mwp in dao.getALlMaintenancesWithTheirParts()) {
            val sb = StringBuilder()
            for (p in mwp.partsList) {
                sb.append("\n\tParts ID is ${p.id} Desc is ${p.description} Model is ${p.model} Serial is ${p.serialNumber}")
            }
            Log.d("DBINFO","Maintenance ID is ${mwp.maintenance.id} Desc is ${mwp.maintenance.description}. It has ${mwp.partsList.size} Parts. They are:-$sb")
        }

        for (pwm in dao.getAllPartsWithTheirMaintenances()) {
            val sb = StringBuilder()
            for (m in pwm.maintenanceList) {
                sb.append("\n\tMaintenance ID is ${m.id} Desc is ${m.description}")
            }
            Log.d("DBINFO","Parts ID is ${pwm.parts.id} Desc is ${pwm.parts.description} Model is ${pwm.parts.model} Serial is ${pwm.parts.serialNumber}. It has ${pwm.maintenanceList.size} Maintenances. They are:-$sb")
        }
    }
}
  • 为简洁起见,请在主线程上运行注释
  • the above first gets an instance of the Database 然后 the DAO from the database.
  • 然后添加多个部件和维护(顺序无关紧要,因为在此阶段它们彼此独立).
  • 然后添加MaintenanceWithParts行,形成各种关系
    • M1有第1部分和第3部分
    • M2分为第2部分和第4部分
    • M3全部4个部分
    • M4没有部件
    • 相反,所有部件都由两次维护使用
  • 最后,提取所有维护及其部件并将其写入日志(log).同样,所有部件及其维护都写入日志(log).

Result

运行时,日志(log)包括:-

2023-12-30 06:34:58.747 D/DBINFO: Maintenance ID is 1 Desc is M1. It has 2 Parts. They are:-
        Parts ID is 1 Desc is Parts 1 Model is P1Model Serial is 100000000001
        Parts ID is 3 Desc is Parts 3 Model is P3Model Serial is 100000000003
2023-12-30 06:34:58.747 D/DBINFO: Maintenance ID is 2 Desc is M2. It has 2 Parts. They are:-
        Parts ID is 2 Desc is Parts 2 Model is P2Model Serial is 100000000002
        Parts ID is 4 Desc is Parts 4 Model is P4Model Serial is 100000000004
2023-12-30 06:34:58.747 D/DBINFO: Maintenance ID is 3 Desc is M3. It has 4 Parts. They are:-
        Parts ID is 1 Desc is Parts 1 Model is P1Model Serial is 100000000001
        Parts ID is 2 Desc is Parts 2 Model is P2Model Serial is 100000000002
        Parts ID is 3 Desc is Parts 3 Model is P3Model Serial is 100000000003
        Parts ID is 4 Desc is Parts 4 Model is P4Model Serial is 100000000004
2023-12-30 06:34:58.747 D/DBINFO: Maintenance ID is 4 Desc is M4. It has 0 Parts. They are:-


2023-12-30 06:34:58.750 D/DBINFO: Parts ID is 1 Desc is Parts 1 Model is P1Model Serial is 100000000001. It has 2 Maintenances. They are:-
        Maintenance ID is 1 Desc is M1
        Maintenance ID is 3 Desc is M3
2023-12-30 06:34:58.750 D/DBINFO: Parts ID is 2 Desc is Parts 2 Model is P2Model Serial is 100000000002. It has 2 Maintenances. They are:-
        Maintenance ID is 2 Desc is M2
        Maintenance ID is 3 Desc is M3
2023-12-30 06:34:58.750 D/DBINFO: Parts ID is 3 Desc is Parts 3 Model is P3Model Serial is 100000000003. It has 2 Maintenances. They are:-
        Maintenance ID is 1 Desc is M1
        Maintenance ID is 3 Desc is M3
2023-12-30 06:34:58.750 D/DBINFO: Parts ID is 4 Desc is Parts 4 Model is P4Model Serial is 100000000004. It has 2 Maintenances. They are:-
        Maintenance ID is 2 Desc is M2
        Maintenance ID is 3 Desc is M3

Additional (Referential Integrity)

尽管上述方法奏效,但并没有强制执行所谓的参照整体法.例如,您可以包括以下内容:

 dao.insert(MaintenancePartsRelationship(9999,6666))

这将导致引用表中的行引用不存在的维护和不存在的部件,这可能会导致问题,但至少是浪费.这些引用并不具有它们与任何东西相关的完整性.

通常情况下,您会希望不允许这样做.SQLite通过外键约束的方式满足了不允许这种浪费的需要.Room通过允许指定外键来满足这一要求.

您可能希望考虑通过包含ForeignKey定义来修改上述(MaintenancePartsRelationship表)以不允许此类差异.

因此,您可以考虑使用:

@Entity(
    primaryKeys = ["maintenanceId","partsId"],
    foreignKeys = [
        ForeignKey(
            entity = Maintenance::class,
            parentColumns = ["id"],
            childColumns = ["maintenanceId"],
            /* Optional but helps maintain referential Integrity */
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Parts::class,
            parentColumns = ["id"],
            childColumns = ["partsId"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)

这就是说:

  1. maintenanceIdMUSTMaintenance表中的现有id值,并且
  2. partsId值,MUSTParts表中现有的id值.

使用CASCADE时,onDeleteonUpdate会自动维护参照完整性,因为:

  1. 对于onDelete,如果父项被删除,则引用该父项的子项将自动删除.
  2. 对于onUpdate,如果更改了参考值(id),则会对子对象进行该更改.
    1. 这通常是比较罕见的情况.

因此,onDeleteonUpdate的可选方面.

  • 一个问题是,如果引用完整性受到损害,则会发生异常,这将引入复杂的编码来处理.例如,如果try 上面的插入,则异常将如下所示

:-

2023-12-30 07:21:15.449 2921-2921/a.a.so77733410kotlinroomstoringlist E/AndroidRuntime: FATAL EXCEPTION: main
Process: a.a.so77733410kotlinroomstoringlist, PID: 2921
java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so77733410kotlinroomstoringlist/a.a.so77733410kotlinroomstoringlist.MainActivity}: android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY)
    
  • 然而,处理上面的内容可能会优先于遇到可能更模糊的异常,因为引用了不存在的父对象.

Using Type Converters

Consider the following that demonstrates using Type Converters:-

@Entity
data class OriginalMaintenance(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    var description: String,
    var listParts: PartsList
)

data class PartsList(
    var partsList: List<Parts>
)

class TheTypeConverters {
    @TypeConverter
    fun convertFromPartsListToJSOnString(lp: PartsList): String = Gson().toJson(lp)
    @TypeConverter
    fun convertFromJSONStringToPartsList(js: String): PartsList = Gson().fromJson(js,PartsList::class.java)
}
  • 原始维护以反映原始维护(但也保留建议)
  • 添加了PartsList以适应com.google.code.gson库(添加到项目中).
  • TypeConverters用于在存储数据时将部件列表转换为JSON字符串,并在检索数据时将存储的字符串转换为部件列表.

To inform Room where the TypeConverters can be found then the following is added (in this case at the @Database level (fullest scope)):-

@TypeConverters(TheTypeConverters::class)

The entities parameter of the @Database annotation is changed to include the new OriginalMaintenance class:-

@Database(entities = [Maintenance::class,Parts::class,MaintenancePartsRelationship::class,OriginalMaintenance::class], version = 1, exportSchema = false)

The AllDAOs interface has the following added:-

@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(originalMaintenance: OriginalMaintenance): Long
@Query("SELECT * FROM originalmaintenance")
fun getAllOriginalMaintenance(): List<OriginalMaintenance>

Finally the activity code has the following added:-

    val pl1 = PartsList(
        listOf(
            Parts(0,100000000001, description = "Parts 1", model = "P1Model"),
            Parts(0, serialNumber = 100000000003, description = "Parts 3", model = "P3Model")
        )
    )
    val pl2 = PartsList(
        listOf(
            Parts(id=0, serialNumber = 100000000002, description = "Parts 2", model = "P2Model"),
            Parts(id=0, serialNumber = 100000000004, description = "Parts 4", model = "P4Model")
        )
    )
    val pl3 = PartsList(
        listOf(
            Parts(0,100000000001, description = "Parts 1", model = "P1Model"),
            Parts(id=0, serialNumber = 100000000002, description = "Parts 2", model = "P2Model"),
            Parts(0, serialNumber = 100000000003, description = "Parts 3", model = "P3Model"),
            Parts(id=0, serialNumber = 100000000004, description = "Parts 4", model = "P4Model")
        )
    )

    dao.insert(OriginalMaintenance(0,"OM1",pl1))
    dao.insert(OriginalMaintenance(0,"OM2",pl2))
    dao.insert( OriginalMaintenance(0,"OM3",pl3))
    dao.insert(OriginalMaintenance(0,"OM4", PartsList(listOf())))

    for (om in dao.getAllOriginalMaintenance()) {
        val sb = StringBuilder()
        for (p in om.listParts.partsList) {
            sb.append("\n\tParts ID is ${p.id} Desc is ${p.description} Model is ${p.model} Serial is ${p.serialNumber}")
        }
        Log.d("DBINFO","Maintenance ID is ${om.id} Desc is ${om.description}. It has ${om.listParts.partsList.size} Parts. They are:-$sb")
    }
  • This code will add the equivalent of the initial demo 然后 extract the data and output it to the log, as per

:-

2023-12-30 08:22:11.060 D/DBINFO: Maintenance ID is 1 Desc is OM1. It has 2 Parts. They are:-
        Parts ID is 0 Desc is Parts 1 Model is P1Model Serial is 100000000001
        Parts ID is 0 Desc is Parts 3 Model is P3Model Serial is 100000000003
2023-12-30 08:22:11.060 D/DBINFO: Maintenance ID is 2 Desc is OM2. It has 2 Parts. They are:-
        Parts ID is 0 Desc is Parts 2 Model is P2Model Serial is 100000000002
        Parts ID is 0 Desc is Parts 4 Model is P4Model Serial is 100000000004
2023-12-30 08:22:11.061 D/DBINFO: Maintenance ID is 3 Desc is OM3. It has 4 Parts. They are:-
        Parts ID is 0 Desc is Parts 1 Model is P1Model Serial is 100000000001
        Parts ID is 0 Desc is Parts 2 Model is P2Model Serial is 100000000002
        Parts ID is 0 Desc is Parts 3 Model is P3Model Serial is 100000000003
        Parts ID is 0 Desc is Parts 4 Model is P4Model Serial is 100000000004
2023-12-30 08:22:11.061 D/DBINFO: Maintenance ID is 4 Desc is OM4. It has 0 Parts. They are:-

一切正常.However基础数据讲述了一个不同的故事.

首先,添加OM时,部件列表可能包含也可能不包含部件表中存在的部件.每个这样的列表都以任何形式存储,无论它是否存在于零件表中.

然后使用应用判断来查看数据库(其中包含两种技术的数据).

比较维护表和原始维护表.

enter image description here

然后

enter image description here

  • 如容易看到的,后者在partsList列中包含更多的数据,并且该数据被标识符和分隔符inflating ,并且如果一个部分被使用不止一次,则该部分被重复.

The Parts table effectively stores exactly the same data but is, in comparison:-

enter image description here

  • 当然,如果使用原始的,如果添加了部件,这些数据就会存在,尽管那时它几乎没有什么用处,很可能会导致混乱.

Of course using the relational aspect and the many-many table has to be considered this table:-

enter image description here

  • 很明显,使用关系会更节省空间.即使没有考虑到数字占用的存储空间要小得多.如果考虑这一因素,那么由整数值组成的引用表将比等价的JSON字符串消耗更少的存储空间.(Integer将存储在最多8个字节中,字符串值将在每个字符中消耗最少1个字节).

  • 最终也有可能遇到其他问题.例如

    • 您将如何做等同于确定使用部件的维护(如关系所示的简单)
    • 您将如何更改部件的值?(只需使用关系更新部件表中的部件)

Kotlin相关问答推荐

编译后的JavaFX应用程序立即以静默方式崩溃

如何创建一个空的kotlin工作?

找不到有效的 Docker 环境

使用调度程序运行异步 Kotlin 代码

Kotlin:使用另一个列表和字母顺序对列表进行排序的有效方法

在子类中覆盖 kotlin 运算符扩展函数

将 SharedPreferences 中的值公开为流

Kotlin 中私有集的完整语法 struct 是什么?

Kotlin 中 lambda 表达式中的默认参数

Kotlin 语言是如何用 Kotlin 编写的?

Kotlin boxed Int 不一样

Android Kotlin StringRes 数量String

Kotlin的web-framework框架

无法在Kotlin中使用argb color int值?

Kotlin协程处理错误和实现

编译错误:-Xcoroutines has no effect: coroutines are enabled anyway in 1.3 and beyond

如果作为 RxJava Observable 提供,Kotlin 密封类子类需要强制转换为基类

使用 java lambda 调用 kotlin 函数时,Kotlin 无法访问 kotlin.jvm.functions.Function1

Kotlin中默认导入哪些包/函数?

Android studio,构建kotlin时出现奇怪错误:生成错误代码