Search code examples
android-roomandroid-room-relation

Parameter specified as non-null is null exception with Room Relations


I've defined a model that represents a meeting, with a menu and a workout plan. The menu has a list of courses, each of which has a list of meals, and the workout plan has a list of exercises.

[
  {
    "menu": {
      "courses": [
        {
          "meals": [
            {
              ...
            }
          ],
        }
      ],
    },
    "workoutPlan": {
      "exercises": [
        {
          ...
        },
      ]
    },
  }
]

in that way:

PopulatedMeeting.kt

data class PopulatedMeeting(
    @Embedded val meeting: MeetingEntity,
    @Relation(
        parentColumn = "menuId",
        entityColumn = "id",
        entity = MenuEntity::class
    )
    val menu: PopulatedMenu,
    @Relation(
        parentColumn = "workoutPlanId",
        entityColumn = "id",
        entity = WorkoutPlanEntity::class
    )
    val workoutPlan: PopulatedWorkoutPlan
)

PopulatedMenu.kt

data class PopulatedMenu(
    @Embedded
    val menu: MenuEntity,
    @Relation(
        parentColumn = "id",
        entityColumn = "id",
        associateBy = Junction(
            value = MenuCourseCrossRef::class,
            parentColumn = "menu_id",
            entityColumn = "course_id"
        ),
        entity = CourseEntity::class
    )
    val courses: List<PopulatedCourse>
)

When I run the app, I'm getting this execption: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter menu


Solution

  • The reason is most likely that you have a Meeting that does not reference a Menu.

    Consider the following data which results in:-

     Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter menu
        at a.a.so74866469kotlinroomrelations.PopulatedMeeting.<init>(Unknown Source:7)
        at a.a.so74866469kotlinroomrelations.AllDao_Impl.getAllPopulatedMeetings(AllDao_Impl.java:382)
        at a.a.so74866469kotlinroomrelations.MainActivity.onCreate(MainActivity.kt:34)
    
    • based upon what can easily be ascertained from your code.

    The database, via App inspection has:-

    1. The MeetingEntity table populated with:-

      1. enter image description here
      • Note the menuid value of 100
    2. The MenuEntity table populated with:-

      1. enter image description here
      • i.e. there is no row with an id of 100

    Hence the menu will be null when retrieving a PopulatedMeeting.


    The following activity code was used to create the above:-


    class MainActivity : AppCompatActivity() {
    
        lateinit var db: TheDatabase
        lateinit var dao: AllDao
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            db = TheDatabase.getInstance(this)
            dao = db.getALlDao()
            val pm = dao.getAllPopulatedMeetings()
    
            val c1 = dao.insert(CourseEntity(courseName = "C1"))
            val c2 = dao.insert(CourseEntity(courseName = "C2"))
            val c3 = dao.insert(CourseEntity(courseName = "C3"))
            val w1 = dao.insert(WorkoutPlanEntity(workoutPlanName = "W1"))
            val w2 = dao.insert(WorkoutPlanEntity(workoutPlanName = "W2"))
            val w3 = dao.insert(WorkoutPlanEntity(workoutPlanName = "W3"))
            val m1 = dao.insert(MenuEntity( workoutPlanId = w1, menuName = "M1"))
            val m2 = dao.insert(MenuEntity(workoutPlanId = w2, menuName = "M2"))
            val m3 = dao.insert(MenuEntity(workoutPlanId = w3, menuName = "M3"))
            dao.insert(MenuCourseCrossRef(menu_id = m1, course_id = c1))
            dao.insert(MenuCourseCrossRef(menu_id = m1, course_id = c2))
            dao.insert(MenuCourseCrossRef(menu_id = m2, course_id = c2))
            dao.insert(MenuCourseCrossRef(menu_id = m2, course_id = c3))
            dao.insert(MenuCourseCrossRef(menu_id = m3, course_id = c3))
            val meet1 = dao.insert(MeetingEntity(menuId =  m1, meetingName = "MEET1"))
            val meet2 = dao.insert(MeetingEntity(menuId = m2, meetingName = "MEET2"))
            logPopulatedMeetings(dao.getAllPopulatedMeetings(),"STG1")
    
            val meet3 = dao.insert(MeetingEntity(menuId = 100, meetingName = "MEET3"))
            logPopulatedMeetings(dao.getAllPopulatedMeetings(),"STG2")
    
        }
    
    
        fun logPopulatedMeetings(populatedMeetingsList: List<PopulatedMeeting>, suffix: String) {
            val TAG = "DBINFO_$suffix"
            val sb = StringBuilder()
            for (pm in populatedMeetingsList) {
                sb.clear()
                for (c in pm.menu.courses) {
                    sb.append("\n\t${c.courseName}")
                }
                Log.d(TAG,"Meeting is ${pm.meeting.meetingName} Menu is ${pm.menu.menu.menuName} it has ${pm.menu.courses.size} courses. They are:-$sb")
            }
        }
    }
    

    The log when running the above includes:-

    2022-12-21 10:37:37.520 D/DBINFO_STG1: Meeting is MEET1 Menu is M1 it has 2 courses. They are:-
            C1
            C2
    2022-12-21 10:37:37.520 D/DBINFO_STG1: Meeting is MEET2 Menu is M2 it has 2 courses. They are:-
            C2
            C3
    2022-12-21 10:37:37.530 D/AndroidRuntime: Shutting down VM
    2022-12-21 10:37:37.534 E/AndroidRuntime: FATAL EXCEPTION: main
        Process: a.a.so74866469kotlinroomrelations, PID: 19356
        java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so74866469kotlinroomrelations/a.a.so74866469kotlinroomrelations.MainActivity}: java.lang.NullPointerException: Parameter specified as non-null
    

    i.e. the PopulatedMeetings with a valid reference to a Menu are fine and utilise your PopulatedMeeting and PopulatedMenu (albeit it that the related Workoutplan was excluded for convenience/brevity).

    You may wish to consider enforcing Referential Integrity (e.g. so that the menu_id cannot be a value that does not reference an actual menu).

    To enforce referential integrity you can setup Foreign Keys e.g. if the following were coded:-

    @Entity(
        foreignKeys = [
            ForeignKey(
                MenuEntity::class,
                parentColumns = ["id"],
                childColumns = ["menuId"],
                onDelete = ForeignKey.CASCADE,
                onUpdate = ForeignKey.CASCADE
            )
        ]
    )
    data class MeetingEntity(
        @PrimaryKey
        val id: Long?=null,
        val menuId: Long,
        val meetingName: String
    )
    

    Then the code above would instead fail with the following in the log (and more importantly when trying to insert the errant reference):-

    2022-12-21 10:48:08.427 D/DBINFO_STG1: Meeting is MEET1 Menu is M1 it has 2 courses. They are:-
            C1
            C2
    2022-12-21 10:48:08.427 D/DBINFO_STG1: Meeting is MEET2 Menu is M2 it has 2 courses. They are:-
            C2
            C3
    2022-12-21 10:48:08.430 D/AndroidRuntime: Shutting down VM
    2022-12-21 10:48:08.433 E/AndroidRuntime: FATAL EXCEPTION: main
        Process: a.a.so74866469kotlinroomrelations, PID: 19822
        java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so74866469kotlinroomrelations/a.a.so74866469kotlinroomrelations.MainActivity}: android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY)
            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
            at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
            at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
            at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
            at android.os.Handler.dispatchMessage(Handler.java:106)
            at android.os.Looper.loop(Looper.java:223)
            at android.app.ActivityThread.main(ActivityThread.java:7656)
            at java.lang.reflect.Method.invoke(Native Method)
            at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
         Caused by: android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY)
            at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
            at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:938)
            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.insertAndReturnId(EntityInsertionAdapter.kt:102)
            at a.a.so74866469kotlinroomrelations.AllDao_Impl.insert(AllDao_Impl.java:139)
            at a.a.so74866469kotlinroomrelations.MainActivity.onCreate(MainActivity.kt:38)
    
    • Note that you would have to have code that handles the Foreign Key conflict rather than just failing. e.g.

    :-

    fun insertIgnoringFKConflict(meetingEntity: MeetingEntity): Long {
        var rv = -1L;
        try {
            rv = insert(meetingEntity)
        } catch (e: SQLiteConstraintException) {
            rv = -1
        }
        finally {
            return rv
        }
    }
    

    In which case replacing the insert with insertIgnoringFKConflict for the 3 Meetings results in no failure and the log including:-

    2022-12-21 10:59:12.898 D/DBINFO_STG1: Meeting is MEET1 Menu is M1 it has 2 courses. They are:-
            C1
            C2
    2022-12-21 10:59:12.898 D/DBINFO_STG1: Meeting is MEET2 Menu is M2 it has 2 courses. They are:-
            C2
            C3
            
            
            
            
    2022-12-21 10:59:12.904 D/DBINFO_STG2: Meeting is MEET1 Menu is M1 it has 2 courses. They are:-
            C1
            C2
    2022-12-21 10:59:12.904 D/DBINFO_STG2: Meeting is MEET2 Menu is M2 it has 2 courses. They are:-
            C2
            C3
    
    • i.e. the errant 3rd meeting did not get inserted and processing contibued allowing the 2nd output of all of the PopulatedMeetings (STG2).