1

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

DaniLiat
  • 45
  • 5

1 Answers1

1

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).
MikeT
  • 51,415
  • 16
  • 49
  • 68