使用 Room 时外键约束失败

Foreign Key constraint failed using Room

我以各种方式查看了这段代码。在这里搜索解决方案。我发现自动生成的主键字段不能用作父列 。但是我在 java 中使用过它,但它似乎在 Kotlin 中不起作用(我正在努力学习)。

这是我的 类:

User.class

@Entity
@Parcelize
data class User(
    @PrimaryKey(autoGenerate = true) val id: Long,
    val email: String,
    val name: String,
    val defaultRecipient: String
) : Parcelable

===

University.class

@Parcelize
@Entity(
    foreignKeys = [ForeignKey(
        entity = User::class,
        parentColumns = ["id"],
        childColumns = ["userId"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.CASCADE
    )], indices = [Index(value = ["userId"])]
)
data class University(
    val userId: Long,
    @PrimaryKey @NonNull val name: String

) : Parcelable {
    override fun equals(other: Any?): Boolean {
        if(other !is University) return false
        return (this.name == other.name) && this.userId == other.userId
    }

    override fun toString(): String {
        return this.name
    }
}

===

Semester.class

@Parcelize
@Entity(
    foreignKeys = [ForeignKey(
        entity = University::class,
        parentColumns = ["name"],
        childColumns = ["universityName"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.CASCADE
    )], indices = [Index(value = ["universityName"])]
)
data class Semester(
    val title: String,
    val universityName: String,
    @PrimaryKey(autoGenerate = true) val id: Long
) : Parcelable

===

Course.class

@Parcelize
@Entity(
    foreignKeys = [ForeignKey(
        entity = Semester::class,
        parentColumns = ["id"],
        childColumns = ["semesterId"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.CASCADE
    )], indices = [Index(value = ["semesterId"])]
)
data class Course(
    val title: String,
    val code: String,
    val semesterId: Long,
    @PrimaryKey(autoGenerate = true) val id: Long
) : Parcelable

当我尝试执行以下代码时,出现外键约束错误:

val user = userDao.getUser()
        userDao.insertUniversity(University(user.id, "Chuckles University"))
        val university = userDao.getUniversitiesByUser(user.id)[0]
        userDao.insertSemester(Semester("Fall 22", "Comsats Wah", 0))
        val semester = userDao.getSemestersByUniversity(university.name)[0]
        userDao.insertCourse(Course("Introduction to Programming", "CSC100", semester.id, 0))

I get a foreign key constraint error:

这可能是由于大学 Comsats Wah 不存在,因为通过定义 FK 你是说学期的 UniversityName 列必须是当前存在于大学 table.

行的 name 列中的值

修复

插入 Comsats Wah 大学或将 parent 大学更正为现有大学。

I discovered that autogenerated primary key field can't be used as a parent column

事实并非如此。您可以使用自动生成的主键作为 parent。根据 Room 的说法,你不能做的是在主键中有一个空值作为值(SQLite 本身确实允许一个空值,这个错误有时会被利用来指示 self-reference) .

事实上,您已经使用了它,因为用户 ID 是自动生成的,并且是大学的 parent。

问题演示

考虑以下说明您正在尝试的内容 但是 异常被捕获并在添加 Comstats Wah 后重试大学:-

    val uniname1 = "Chuckles University"
    val uniname2 = "Comsats Wah"
    val userEmail = "user@email.com"
    val userName = "user"

    db = TheDatabase.getInstance(this)
    dao = db.getAllDao()
    /* Clear Semesters and Universities */
    dao.clearSemesters()
    dao.clearUniversities()

    var userId = dao.insert(User(email = userEmail, name = userName, defaultRecipient = "blah"))
    /* If user already exists (-1 returned) then get the user by email */
    if (userId < 1) {
        userId = dao.getUserIdByEmail(userEmail)
    }

    var uniRowid = dao.insert(University(userId,uniname1))

    try {
        val university = dao.getUniversitiesByUser(userId)[0]
        dao.insertSemester(Semester("Fall 2022",uniname1,0))
        dao.insertSemester(Semester("Spring 2022",uniname1,0))
        dao.insertSemester(Semester("Fall 22", uniname2, 0)) /* FK CONFLICT due to Comsats Wah uni */
    } catch (e: SQLiteException) {
        e.printStackTrace()
    }

    try {
        dao.insert(University(userId,uniname2))
        dao.insertSemester(Semester("Fall 22", uniname2, 0)) /* Now Uni Comsats Wah exists inserts OK */
    } catch (e: SQLiteException) {
        e.printStackTrace()
    }

当运行那么日志包括:-

2022-06-04 06:30:49.966 W/System.err: android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY)
2022-06-04 06:30:49.966 W/System.err:     at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
2022-06-04 06:30:49.966 W/System.err:     at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:938)
2022-06-04 06:30:49.966 W/System.err:     at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:790)
2022-06-04 06:30:49.966 W/System.err:     at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:88)
2022-06-04 06:30:49.966 W/System.err:     at androidx.sqlite.db.framework.FrameworkSQLiteStatement.executeInsert(FrameworkSQLiteStatement.java:51)
2022-06-04 06:30:49.966 W/System.err:     at androidx.room.EntityInsertionAdapter.insertAndReturnId(EntityInsertionAdapter.kt:102)


2022-06-04 06:30:49.967 W/System.err:     at a.a.so72489240fk.AllDao_Impl.insertSemester(AllDao_Impl.java:187)
2022-06-04 06:30:49.967 W/System.err:     at a.a.so72489240fk.MainActivity.onCreate(MainActivity.kt:38)


2022-06-04 06:30:49.967 W/System.err:     at android.app.Activity.performCreate(Activity.java:7994)
2022-06-04 06:30:49.967 W/System.err:     at android.app.Activity.performCreate(Activity.java:7978)
2022-06-04 06:30:49.967 W/System.err:     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
2022-06-04 06:30:49.967 W/System.err:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
2022-06-04 06:30:49.968 W/System.err:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
2022-06-04 06:30:49.968 W/System.err:     at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
2022-06-04 06:30:49.968 W/System.err:     at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
2022-06-04 06:30:49.969 W/System.err:     at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
2022-06-04 06:30:49.969 W/System.err:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
2022-06-04 06:30:49.969 W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:106)
2022-06-04 06:30:49.969 W/System.err:     at android.os.Looper.loop(Looper.java:223)
2022-06-04 06:30:49.969 W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:7656)
2022-06-04 06:30:49.973 W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
2022-06-04 06:30:49.973 W/System.err:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
2022-06-04 06:30:49.973 W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

日志表明错误发生在 MainActivity.onCreate(MainActivity.kt:38),即第 38 行 dao.insertSemester(Semester("Fall 22", uniname2, 0)) /* FK CONFLICT due to Comsats Wah uni */。这是预料之中的。但是,第 45 行 dao.insertSemester(Semester("Fall 22", uniname2, 0)) /* Now Uni Comsats Wah exists inserts OK */

不会发生后续捕获的异常
  • 请注意,显示异常发生位置的行前后添加了空行,以便于查看。

而是通过 App Inspection 检查数据库显示:-

和:-

即*Comsats Wah 的秋季 22 学期已添加,它必须是在添加 Comstats Wah uni 之后。

额外

您的架构有一个缺陷(例如),即您需要为每个用户提供一所大学,即使它是同一所大学,因为大学拥有用户 parent。这个 compounded/complicated 作为大学名称必须是唯一的,因为名称是主键,这意味着唯一性。

更正确的模式是将没有用户的大学作为 parent,而是让大学与许多用户相关,也许一个用户可以拥有许多大学。

同样,也许学期对大学来说是共同的,甚至课程对大学和学期来说可能是共同的。

因此,您可以将用户、大学、学期和课程设置为 tables/entities,它们之间没有关系,但是 table 是 map/reference/relation/associate 构成用户的各种组件学习计划。

关系可以是 defined/set 使用 table 或 table 与 map/reference/relate/associate 的关系(同一事物的所有不同词)。这就是使用生成的 id 发挥作用的地方,它们更有效(它们无论如何都存在)。

  • SQLite 处理它所谓的 rowid 的更多 efficiently/faster

关于效率,使用 autogenerate = true 是低效的。简而言之,它包含 AUTOINCREMENT 关键字,因此使用一个名为 sqlite_sequence 的 table 来存储最高分配的 id,然后在插入新行时访问和更新这个额外的 table。

SQLite 建议不要这样做,它说

The AUTOINCREMENT keyword imposes extra CPU, memory, disk space, and disk I/O overhead and should be avoided if not strictly needed. It is usually not needed. see - https://sqlite.org/autoinc.html

SQLite 将生成一个 rowid 如果在 Room 中你改为使用:-

val id: Long?=null

所以也许可以考虑下面的演示,上面提到的问题。

所以基本的 classes 可以是(那些以 X 结尾的是 improved/more 没有 X 的原始 classes 的正确版本):-

用户

@Entity
data class User(
    @PrimaryKey/*(autoGenerate = true)*/ val id: Long?=null,
    val email: String,
    val name: String,
    val defaultRecipient: String
)

X大学

@Entity(
    indices = [Index("universityXName", unique = true)] /* University name must be Unique (if wanted to be)*/
)
data class UniversityX(
    @PrimaryKey
    val universityXId: Long?=null,
    val universityXName: String
)

第 X 学期

@Entity(
    indices = [
        Index(value = ["semesterXTitle"], unique = true)
    ]
)
data class SemesterX(
    @PrimaryKey
    val semesterXId: Long?=null,
    val semesterXTitle: String,
    val semesterXStartDate: Long,
    val semesterXEndDate: Long
)

CourseX

@Entity(
    indices = [Index("courseXTitle", unique = true)]
)
data class CourseX(
    @PrimaryKey
    val courseXId: Long?=null,
    val courseXTitle: String
)

为了迎合关系,然后添加一个 table 将用户映射到 UniversityX、SemesterX 和 CourseX

  • 这就是所谓的 mapping/associative/reference table 以及其他名称。这适用于多个 many-many 关系,这将被演示。

从上面可以看出,上面的核心 tables 没有定义外键。它们都在这个映射中 table UserUniversityXSemesterXCourseXMapping :-

@Entity(
    primaryKeys = [
        "uusc_userIdMapping","uusc_universityXIdMapping","uusc_semesterXIdMapping","uusc_courseXIdMapping"
    ],
    foreignKeys = [
        ForeignKey(User::class,["id"],["uusc_userIdMapping"], ForeignKey.CASCADE,ForeignKey.CASCADE),
        ForeignKey(UniversityX::class,["universityXId"],["uusc_universityXIdMapping"], ForeignKey.CASCADE,ForeignKey.CASCADE),
        ForeignKey(SemesterX::class,["semesterXId"],["uusc_semesterXIdMapping"], ForeignKey.CASCADE,ForeignKey.CASCADE),
        ForeignKey(CourseX::class,["courseXId"],["uusc_courseXIdMapping"],ForeignKey.CASCADE,ForeignKey.CASCADE)
    ]
)
data class UserUniversityXSemesterXCourseXMapping(
    val uusc_userIdMapping: Long,
    @ColumnInfo(index = true)
    val uusc_universityXIdMapping: Long,
    @ColumnInfo(index = true)
    val uusc_semesterXIdMapping: Long,
    @ColumnInfo(index = true)
    val uusc_courseXIdMapping: Long
)

显然,有时您可能想要检索用户、大学、学期和课程。所以一个 POJO 可以是 UserWithUniversityAndSemesterAndCourse :-

data class UserWithUniversityAndSemesterAndCourse (
    @Embedded
    val user: User,
    @Embedded
    val universityX: UniversityX,
    @Embedded
    val semesterX: SemesterX,
    @Embedded
    val courseX: CourseX
    )

要访问(insert/retrieve数据)上面的AllDaoX是:-

@Dao
interface AllDaoX {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(user: User): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(universityX: UniversityX): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(semesterX: SemesterX): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(courseX: CourseX): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(semesterXCourseXMapping: SemesterXCourseXMapping): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(userUniversityXSemesterXCourseXMapping: UserUniversityXSemesterXCourseXMapping): Long

    @RewriteQueriesToDropUnusedColumns /* easier than doing SELECT user.*, universityX.*, semesterX.*, courseX.* .... instead of SELECT * .... */
    @Query("SELECT * FROM user " +
            "JOIN userUniversityXSemesterXCourseXMapping ON user.id = uusc_userIdMapping " +
            "JOIN universityX ON userUniversityXSemesterXCourseXMapping.uusc_universityXIdMapping = universityX.universityXId " +
            "JOIN semesterX ON userUniversityXSemesterXCourseXMapping.uusc_semesterXIdMapping = semesterX.semesterXId " +
            "JOIN courseX ON UserUniversityXSemesterXCourseXMapping.uusc_courseXIdMapping = courseX.courseXId")
    fun getUserWithAllUUSCs(): List<UserWithUniversityAndSemesterAndCourse>

    @Query("DELETE FROM UserUniversityXSemesterXCourseXMapping")
    fun clearUUSCMappingTable()
    @Query("DELETE FROM SemesterXCourseXMapping")
    fun clearSCMappIngTable()
    @Query("DELETE FROM courseX")
    fun clearCourseXTable()
    @Query("DELETE FROM semesterX")
    fun clearSemesterXTable()
    @Query("DELETE FROM universityX")
    fun clearUniversityXTable()

    @Transaction
    @Query("")
    fun clearAllXTables() {
        clearUUSCMappingTable()
        clearSCMappIngTable()
        clearCourseXTable()
        clearSemesterXTable()
        clearUniversityXTable()
    }

}

大多数函数应该是 self-explanatory。但是 getUserWithAllUUSCs 的 SQL 可能需要您了解 JOINS。不妨参考https://www.sqlite.org/lang_select.html

clearAllXTables 是一个函数体作为 Dao 函数的示例(对于 Java 接口是不允许的,因此对于 java 一个抽象 class wo必须使用 ld)。

空的 @Query 是为了方便 @Transaction 的使用,它应该在函数内做所有事情是一个单一的事务,因此只写入磁盘一次而不是为每个调用的函数写入一次。

演示上述内容的使用,对于每个参加 2 门课程的 2 个用户,第一个在一所大学,第二个用户在两所大学和与第一个用户相同的课程(以演示 common/many-many 用法)。

所以考虑:-

    val daoX = db.getAllDaoX()
    daoX.clearAllXTables()

    val user2 = daoX.insert(User(null,"another#mail.com","A N Other","blah"))

    val s4id = daoX.insert(SemesterX(null,"Autumn 22",Date("01/09/2022").time,Date("31/11/2022").time))
    val s3id = daoX.insert(SemesterX(null,"Winter 22",Date("01/09/2022").time,Date("31/11/2022").time))
    val u1id = daoX.insert(UniversityX(universityXName = "Uni1"))
    val s1id = daoX.insert(SemesterX(null,"Spring 22", Date("01/03/2022").time,Date("31/05/2022").time))
    val c1id = daoX.insert(CourseX(null,"M101 - Math"))
    val c2id = daoX.insert(CourseX(null,"M110 Applied Math"))
    val c3id = daoX.insert(CourseX(null,"E100 English Language"))
    val c4id = daoX.insert(CourseX(null,"C100 Chemistry"))
    val u2Id = daoX.insert(UniversityX(universityXName = "Uni2"))
    val s2id = daoX.insert(SemesterX(null,"Summer 22",Date("01/06/2022").time,Date("31/08/2022").time))


    daoX.insert(UserUniversityXSemesterXCourseXMapping(userId,u1id,s1id,c1id))
    daoX.insert(UserUniversityXSemesterXCourseXMapping(userId,u1id,s2id,c2id))
    daoX.insert(UserUniversityXSemesterXCourseXMapping(user2,u2Id,s4id,c4id))
    daoX.insert(UserUniversityXSemesterXCourseXMapping(user2,u1id,s1id,c1id))

    for(uwuasac in daoX.getUserWithAllUUSCs()) {
        Log.d(
            "DBINFO",
            "User is ${uwuasac.user.name} " +
                    "Email is ${uwuasac.user.email} " +
                    "Uni is ${uwuasac.universityX.universityXName} " +
                    "Sem is ${uwuasac.semesterX.semesterXTitle} " +
                    "Course is ${uwuasac.courseX.courseXTitle}"
        )
    }
  • 请注意用户、大学学期、课程的插入顺序是如何无关紧要的。但是,必须在插入映射(UserUniversityXSemesterXCourseXMapping's)之前插入相应的用户 Uni 等,否则会导致 FK 约束冲突。

  • 以上不适用于无意中的重复(例如同一大学)。

当运行时,只有第一次 (上面的演示不打算运行多次作为重复处理未包括在内以尽量保持简单) 日志包括:-

D/DBINFO: User is user Email is user@email.com Uni is Uni1 Sem is Spring 22 Course is M101 - Math
D/DBINFO: User is user Email is user@email.com Uni is Uni1 Sem is Summer 22 Course is M110 Applied Math
D/DBINFO: User is A N Other Email is another#mail.com Uni is Uni2 Sem is Autumn 22 Course is C100 Chemistry
D/DBINFO: User is A N Other Email is another#mail.com Uni is Uni1 Sem is Spring 22 Course is M101 - Math

所以用户 user 在同一所大学有 2 个课程,跨 2 个学期 用户 A N Other 在其他 2 个学期的每个大学都有 2 门课程,请注意第一个用户也使用了课程 M101,但在不同的学期。

因此任何用户都可以(如果需要)在任何学期参加任何大学的任何课程。每个 Uni、Semester 和 Course 只需要存储一次。

该演示没有深入到 Uni 的特定课程或特定学期。但它展示了一个更规范化的模式。