SQLiteConstraintException: FOREIGN KEY constraint failed code 787
Asked Answered
P

1

0

I have a menu that can have multiple Sections. I have passed the menuID of a ShopMenuEntity as foreign key in ShopSectionEntity with name fk_menu_id.

Both id's have to be autoGenerated in ShopMenu and ShopSection.

ShopMenuEntity.kt

@Entity(tableName = ShopMenuEntity.TABLE_NAME)
data class ShopMenuEntity(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = COLUMN_ID)
    var menuID: Int = 0,
    var level:Int? = null,
    @ColumnInfo(name = COLUMN_PARENT_ID)
    var parentId: Int = -1,
) {
    companion object {
        const val TABLE_NAME = "shop_menu_table"
        const val COLUMN_ID = "menu_id"
        const val COLUMN_PARENT_ID = "parentId"

        fun mapHttpResponse(subMenu: NewShopMenuResponse,parentId: Int): ShopMenuEntity {
            return ShopMenuEntity(
                // below menuID should be auto incremented
                ///menuID = subMenu.id ?: -1,
                level = subMenu.level,
                parentId = parentId,
            )
        }
    }
}

ShopSectionEntity.kt

@Entity(tableName = ShopSectionEntity.TABLE_NAME,
    foreignKeys = [ForeignKey(
        entity = ShopMenuEntity::class,
        parentColumns = arrayOf(ShopMenuEntity.COLUMN_ID),
        childColumns = arrayOf(ShopSectionEntity.SHOP_MENU_ID),
        onDelete = ForeignKey.CASCADE
    )],
    indices = [Index(ShopSectionEntity.SHOP_MENU_ID)])
data class ShopSectionEntity(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = COLUMN_ID)
    var sectionID: Int= 0,
    @ColumnInfo(name = SHOP_MENU_ID)
    var shopMenuID: Int,
) {

    companion object {

        const val TABLE_NAME = "shop_section_table"
        const val COLUMN_ID = "section_id"
        const val SHOP_MENU_ID = "fk_menu_id"

        fun mapHttpResponse(section:Section,shopMenuID:Int,orderIndex:Int):ShopSectionEntity {
            return ShopSectionEntity(
                // sectionID should be auto incremented
                shopMenuID = shopMenuID,

            )
        }
    }
}

Relattion ShopMenuDB.kt

data class ShopMenuDB(
    @field:Embedded
    var shopEntity: ShopMenuEntity,
    @field:Relation(parentColumn = ShopMenuEntity.COLUMN_ID, entityColumn = ShopSectionEntity.SHOP_MENU_ID, entity = ShopSectionEntity::class)
    var sections: List<ShopSectionEntity>,
)

RoomActivity.kt

class RoomActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_room)
        CoroutineScope(Dispatchers.IO).launch{
            readDataFromRaw()?.let {
                addSubMenu(shopMenuDao(), it, -1)
            }
        }
    }

    private fun shopMenuDao() = Room.databaseBuilder(
        applicationContext,
        MatasDatabase::class.java, "matas_database"
    ).build().newShopMenuDao()

    private fun readDataFromRaw():NewShopMenuResponse?{
        val jsonText = resources.openRawResource(R.raw.respons)
            .bufferedReader().use { it.readText() }
        val moshi: Moshi = Moshi.Builder().build()
        val jsonAdapter = moshi.adapter(
            NewShopMenuResponse::class.java
        )
        val shopResponse = jsonAdapter.fromJson(jsonText)
        return shopResponse
    }

    private suspend fun addSubMenu(
        shopMenuDao: NewShopMenuDao,
        subMenu: NewShopMenuResponse,
        parentID: Int
    ) {
        val dbRootMenus = ArrayList<ShopMenuEntity>()
        val dbSections = ArrayList<ShopSectionEntity>()

        val mSubMenu = ShopMenuEntity.mapHttpResponse(subMenu, parentID)
        dbRootMenus.add(mSubMenu)
        subMenu.sections?.forEachIndexed { sectionIndex, mSection ->
            val sectionEntity = ShopSectionEntity.mapHttpResponse(mSection, mSubMenu.menuID,sectionIndex + 1)
            dbSections.add(sectionEntity)
        }
        ///shopMenuDao.insertAllLists(dbRootMenus, dbSections)
        // I tried both above and below way to save menu and sections
        shopMenuDao.insertShopMenu(dbRootMenus[0])
        shopMenuDao.insertSections(dbSections)
    }
}

NewShopMenuDao.kt

@Dao
interface NewShopMenuDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAllLists(shopMenuEntityList: List<ShopMenuEntity>, shopSectionEntities: List<ShopSectionEntity>?)
 
    @Transaction
    @Query("SELECT * FROM shop_menu_table WHERE shop_menu_table.parentId = :parentId")
    fun getShopMenu(parentId:Int): Flow<ShopMenuDB?>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertShopMenu(shopMenuEntity: ShopMenuEntity)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertSections(shopSectionEntities: List<ShopSectionEntity>?)

    @Query("SELECT * FROM ${ShopMenuEntity.TABLE_NAME}")
    fun getAll(): List<ShopMenuEntity>

    @Insert
    fun insertAll(vararg shopMenuEntity: ShopMenuEntity)

    @Query("DELETE FROM ${ShopMenuEntity.TABLE_NAME}")
    fun deleteAll()

    @Delete
    fun delete(shopMenuEntity: ShopMenuEntity)
}

I find after debug that menuId in ShopMenuEntity and sectionID in ShopSectionEntity is not auto incrementing.

I tried two different approaches to save data but was not successful.

Exception

SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY[787])
at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:1127)
at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:790)
at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:88)
at androidx.sqlite.db.framework.FrameworkSQLiteStatement.executeInsert(FrameworkSQLiteStatement.kt:42)
at androidx.room.EntityInsertionAdapter.insert(EntityInsertionAdapter.kt:85)
at matas.matas.core.data.db.dao.NewShopMenuDao_Impl$8.call(NewShopMenuDao_Impl.java:164)
at matas.matas.core.data.db.dao.NewShopMenuDao_Impl$8.call(NewShopMenuDao_Impl.java:159)
at androidx.room.CoroutinesRoom$Companion$execute$2.invokeSuspend(CoroutinesRoom.kt:65)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at androidx.room.TransactionExecutor.execute$lambda$1$lambda$0(TransactionExecutor.kt:36)
at androidx.room.TransactionExecutor.$r8$lambda$AympDHYBb78s7_N_9gRsXF0sHiw(Unknown Source:0)
at androidx.room.TransactionExecutor$$ExternalSyntheticLambda0.run(Unknown Source:4)
Premise answered 5/8, 2023 at 6:57 Comment(0)
G
2

Both id's have to be autoGenerated in ShopMenu and ShopSection.

  • They in fact don't BUT that doesn't matter (the demo below includes examples)

I have also find after debug that menuId in ShopMenuEntity and sectionID in ShopSectionEntity not auto incrementing as well.

The values are generated as the demo below shows. I believe your issue is an expectation that storing a value in the database will be reflected back to the objects.

The fix is to utilise the values returned when inserting the data.

FOREIGN KEY constraint failed code 787

This is probably due to some expectation beyond what actually can be used. e.g. as per the previous answer, you appear to be expecting objects to be updated with the respective id when the data is stored in the database.

When an alias of the rowid is generated (for autoGenerate=true the column MUST be an alias of the rowid), the value is generated by SQLite and stored. It will only be available if it is extracted from the database.

Fortunately the @Insert aonnotated functions do cater for retrieval of the value and returns it as a Long (if inserting a single object) or as an array of Longs when inserting an array/list of objects.

  • If the value was not inserted for a limited set of reasons (conflicts) then the returned value will be -1. AS Foreign Key exception is NOT one of those reasons.

When you introduce a Foreign Key, in Room via foreignKeys parameter of the @Entity annotation. You are introducing a constraint (rule) that says that the value of the child column MUST be a value that exists in the parent column of one of the rows in the parent table. That is it is designed to ensure referential integrity (that there are no orphans (children without parents)). As such breaking the rule is unacceptable and hence why an exception as opposed to a controllable/catchable type error.

As such it is IMPERATIVE that the specified value of the parent column is provided and is one that exists.

In your case the generated menuId of a ShopMenuEntity is required to enable a ShopSectionEntity to be inserted.


The following demonstrates the above


Your ShopMenuEntity, ShopSectionEntity and ShopMenuDB have been used asis bar the removal of the companion functions and are:-

@Entity(tableName = ShopMenuEntity.TABLE_NAME)
data class ShopMenuEntity(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = COLUMN_ID)
    var menuID: Int = 0,
    var level:Int? = null,
    @ColumnInfo(name = COLUMN_PARENT_ID)
    var parentId: Int = -1,
) {
    companion object {
        const val TABLE_NAME = "shop_menu_table"
        const val COLUMN_ID = "menu_id"
        const val COLUMN_PARENT_ID = "parentId"

        /*
        fun mapHttpResponse(subMenu: NewShopMenuResponse,parentId: Int): ShopMenuEntity {
            return ShopMenuEntity(
                // below menuID should be auto incremented
                ///menuID = subMenu.id ?: -1,
                level = subMenu.level,
                parentId = parentId,
            )
        }
         */
    }
}

@Entity(tableName = ShopSectionEntity.TABLE_NAME,
    foreignKeys = [ForeignKey(
        entity = ShopMenuEntity::class,
        parentColumns = arrayOf(ShopMenuEntity.COLUMN_ID),
        childColumns = arrayOf(ShopSectionEntity.SHOP_MENU_ID),
        onDelete = ForeignKey.CASCADE
    )],
    indices = [Index(ShopSectionEntity.SHOP_MENU_ID)])
data class ShopSectionEntity(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = COLUMN_ID)
    var sectionID: Int= 0,
    @ColumnInfo(name = SHOP_MENU_ID)
    var shopMenuID: Int,
) {

    companion object {

        const val TABLE_NAME = "shop_section_table"
        const val COLUMN_ID = "section_id"
        const val SHOP_MENU_ID = "fk_menu_id"

        /*
        fun mapHttpResponse(section:Section,shopMenuID:Int,orderIndex:Int):ShopSectionEntity {
            return ShopSectionEntity(
                // sectionID should be auto incremented
                shopMenuID = shopMenuID,
                )
        }
         */
    }
}

data class ShopMenuDB(
    @field:Embedded
    var shopEntity: ShopMenuEntity,
    @field:Relation(parentColumn = ShopMenuEntity.COLUMN_ID, entityColumn = ShopSectionEntity.SHOP_MENU_ID, entity = ShopSectionEntity::class)
    var sections: List<ShopSectionEntity>,
)

The NewShopMenuDao interface has been altered to:-

  1. allow the code to be run on the main thread for the demo
  2. return the menuId when a ShopMenuEntity is inserted
  3. add a query to extract All ShopMenuEntity's with the related list of ShopSectinEntity's.

As per:-

@Dao
interface NewShopMenuDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    /*suspend*/ fun insertAllLists(shopMenuEntityList: List<ShopMenuEntity>, shopSectionEntities: List<ShopSectionEntity>?)

    @Transaction
    @Query("SELECT * FROM shop_menu_table WHERE shop_menu_table.parentId = :parentId")
    fun getShopMenu(parentId:Int): /*Flow*/List<ShopMenuDB?>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    /*suspend*/ fun insertShopMenu(shopMenuEntity: ShopMenuEntity): Long /*<<<<<<<< ADDED so the menuId (generated id) is returned */

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    /*suspend*/ fun insertSections(shopSectionEntities: List<ShopSectionEntity>?)

    @Query("SELECT * FROM ${ShopMenuEntity.TABLE_NAME}")
    fun getAll(): List<ShopMenuEntity>

    @Insert
    fun insertAll(vararg shopMenuEntity: ShopMenuEntity)

    @Query("DELETE FROM ${ShopMenuEntity.TABLE_NAME}")
    fun deleteAll()

    @Delete
    fun delete(shopMenuEntity: ShopMenuEntity)

    @Transaction
    @Query("SELECT * FROM ${ShopMenuEntity.TABLE_NAME}")
    fun getAllShopMenusWithRelatedShopSections(): List<ShopMenuDB>
}

An @Database annotated abstract class has been added, as one is needed. it being:-

@Database(entities = [ShopMenuEntity::class,ShopSectionEntity::class], exportSchema = false, version = 1)
abstract class MatasDatabase: RoomDatabase() {
    abstract fun getNewShopMenuDao(): NewShopMenuDao

    companion object {
        private var instance: MatasDatabase?=null
        fun getInstance(context: Context): MatasDatabase {
            if (instance==null) {
                instance = Room.databaseBuilder(context,MatasDatabase::class.java,"matas_database.db")
                    .allowMainThreadQueries() /* for brevity of demo */
                    .build()
            }
            return instance as MatasDatabase
        }
    }
}

To actually demonstrate that your code basically works; that it does generate values and that the foreign keys can be used. The following activity code was used:-

const val TAG = "DBINFO"
class MainActivity : AppCompatActivity() {

    lateinit var db: MatasDatabase
    lateinit var dao: NewShopMenuDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = MatasDatabase.getInstance(this)
        dao = db.getNewShopMenuDao()


        /* Add a shop getting the generated menuId */
        /* menuId and parentId not given so default values (0 for menuId and -1 for parentId) */
        val firstshopmenuId = dao.insertShopMenu(ShopMenuEntity(level=100))
        /* menuId specified as 300 level and parentId default (null for level and -1 for parentId) */
        val secondShopMenuId = dao.insertShopMenu(ShopMenuEntity(300))
        /* all defaults */
        val thirdShopMenuId = dao.insertShopMenu(ShopMenuEntity())

        Log.d(TAG,"After adding the shops the menuId values are \n\t${firstshopmenuId},\n\t${secondShopMenuId}, and\n\t${thirdShopMenuId}")

        dao.insertSections(
            listOf
                (
                ShopSectionEntity(shopMenuID = firstshopmenuId.toInt()),
                ShopSectionEntity(sectionID = 500,firstshopmenuId.toInt()),
                ShopSectionEntity(shopMenuID = secondShopMenuId.toInt()),
                ShopSectionEntity(shopMenuID = firstshopmenuId.toInt()),
                ShopSectionEntity(shopMenuID = thirdShopMenuId.toInt()),
                ShopSectionEntity(shopMenuID = secondShopMenuId.toInt())
            )
        )
        try {
            dao.insertSections(listOf(ShopSectionEntity(shopMenuID = 9999)))
        } catch (e: Exception) {
            Log.d(TAG,"Ouch Something went wrong!!!! Exception was\n${e.printStackTrace()}")
        }

        for (smwss in dao.getAllShopMenusWithRelatedShopSections()) {
            val sb = StringBuilder()
            for (ss in smwss.sections) {
                sb.append("\n\tSectionID is ${ss.sectionID} referenced ShopMenu ID is ${ss.shopMenuID}")
            }
            Log.d(TAG,"SHopMenu MENUID is ${smwss.shopEntity.menuID} " +
                    "LEVEL is ${smwss.shopEntity.level} " +
                    "PARENT ID is ${smwss.shopEntity.parentId}. " +
                    "It has ${smwss.sections.size} sections. They are:- $sb")
        }
    }
}

RESULT / CONCLUSION

When run then the log first of all includes:-

2023-08-06 08:14:24.554 D/DBINFO: After adding the shops the menuId values are 
        1,
        300, and
        301
  • This shows that the first and third menuId values were generated according to the values returned from the insert.
  • It also demonstrates that if a non-zero value (300 in the example) that you can in fact specify a value and that has been used.
    • as the value was greater than 1 (the first insertion) then generated values will be greater and hence 301 for the 3rd insert.

The log then includes:-

2023-08-06 08:14:24.575 W/System.err: android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY)
2023-08-06 08:14:24.575 W/System.err:     at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
2023-08-06 08:14:24.575 W/System.err:     at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:796)
2023-08-06 08:14:24.575 W/System.err:     at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
2023-08-06 08:14:24.575 W/System.err:     at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:86)
2023-08-06 08:14:24.575 W/System.err:     at androidx.sqlite.db.framework.FrameworkSQLiteStatement.executeInsert(FrameworkSQLiteStatement.kt:42)
2023-08-06 08:14:24.576 W/System.err:     at androidx.room.EntityInsertionAdapter.insert(EntityInsertionAdapter.kt:85)
2023-08-06 08:14:24.576 W/System.err:     at a.a.so76840478kotlinroomforeignkeys.NewShopMenuDao_Impl.insertSections(NewShopMenuDao_Impl.java:138)
2023-08-06 08:14:24.576 W/System.err:     at a.a.so76840478kotlinroomforeignkeys.MainActivity.onCreate(MainActivity.kt:44)
2023-08-06 08:14:24.576 W/System.err:     at android.app.Activity.performCreate(Activity.java:7136)
2023-08-06 08:14:24.576 W/System.err:     at android.app.Activity.performCreate(Activity.java:7127)
2023-08-06 08:14:24.577 W/System.err:     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
2023-08-06 08:14:24.577 W/System.err:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2893)
2023-08-06 08:14:24.577 W/System.err:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)
2023-08-06 08:14:24.578 W/System.err:     at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
2023-08-06 08:14:24.578 W/System.err:     at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
2023-08-06 08:14:24.578 W/System.err:     at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
2023-08-06 08:14:24.579 W/System.err:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
2023-08-06 08:14:24.579 W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:106)
2023-08-06 08:14:24.580 W/System.err:     at android.os.Looper.loop(Looper.java:193)
2023-08-06 08:14:24.581 W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:6669)
2023-08-06 08:14:24.581 W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
2023-08-06 08:14:24.581 W/System.err:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
2023-08-06 08:14:24.581 W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
2023-08-06 08:14:24.582 D/DBINFO: Ouch Something went wrong!!!! Exception was

That is the purposeful FKEY violation, due to trying to reference the non-existent ShopMenuEntity that has a menuId of 9999.

However, the Log then includes, the extracted ShopMenuDB objects via the getAllShopMenusWithRelatedShopSections function as per:-

2023-08-06 08:14:24.590 D/DBINFO: SHopMenu MENUID is 1 LEVEL is 100 PARENT ID is -1. It has 3 sections. They are:- 
        SectionID is 1 referenced ShopMenu ID is 1
        SectionID is 500 referenced ShopMenu ID is 1
        SectionID is 502 referenced ShopMenu ID is 1
2023-08-06 08:14:24.590 D/DBINFO: SHopMenu MENUID is 300 LEVEL is null PARENT ID is -1. It has 2 sections. They are:- 
        SectionID is 501 referenced ShopMenu ID is 300
        SectionID is 504 referenced ShopMenu ID is 300
2023-08-06 08:14:24.590 D/DBINFO: SHopMenu MENUID is 301 LEVEL is null PARENT ID is -1. It has 1 sections. They are:- 
        SectionID is 503 referenced ShopMenu ID is 301

That is:-

  1. The ShopMenuEntity's all have the expected related ShopSectionEntity children
  2. That the SectionId of the ShopSectionEntity's have both been generated (i.e. 1, 501,502, 503 and 504) and specified (i.e. 500).
Gymnastics answered 5/8, 2023 at 22:46 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.