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

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

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:-
The MeetingEntity table populated with:-
Note the menuid value of 100
The MenuEntity table populated with:-
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).

Related

Kotlin MVVM, How to get the latest value from Entity in ViewModel?

I have created an app where I try to insert a record with the latest order number increased by one.
The main function is triggered from Activity, however, the whole process is in my ViewModel.
Issue no 1, After I insert a new record the order by number is not updated.
Issue no 2, When I insert first record the order by number is null, for that reason I am checking for null and setting the value to 0.
My goal here is to get the latest order_by number from Entity in my ViewModel, increased by 1 and add that new number to my new record using fun addTestData(..).
Entity:
#Entity(tableName = "word_table")
data class Word(
#ColumnInfo(name = "id") val id: Int,
#ColumnInfo(name = "word") val word: String,
#ColumnInfo(name = "order_by") val orderBy: Int
Dao:
#Query("SELECT order_by FROM word_table ORDER BY order_by DESC LIMIT 1")
suspend fun getHighestOrderId(): Int
Repository:
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun getHighestOrderId(): Int {
return wordDao.getHighestOrderId()
}
ViewModel:
private var _highestOrderId = MutableLiveData<Int>()
val highestOrderId: LiveData<Int> = _highestOrderId
fun getHighestOrderId() = viewModelScope.launch {
val highestOrderId = repository.getHighestOrderId()
_highestOrderId.postValue(highestOrderId)
}
fun addTestData(text: String) {
for (i in 0..1500) {
getHighestOrderId()
var highestNo = 0
val highestOrderId = highestOrderId.value
if (highestOrderId == null) {
highestNo = 0
} else {
highestNo = highestOrderId
}
val addNumber = highestNo + 1
val word2 = Word(0, text + "_" + addNumber,addNumber)
insertWord(word2)
}
}
Activity:
wordViewModel.addTestData(text)

Get data from 4 tables in Android Room

I'm trying to get data from the database using Room, I want to get the data in the format {registration_number, List, List} but I'm getting an error:
"Cannot find the parent entity column area_name in ... and my intermediate class"
and in fact I hide that maybe I am taking the wrong approach, please guide me, because I am new in this area
to extract the data I use an intermediate class
my class is:
data class LastConfiscats(
#ColumnInfo(name = "registration_number")
var slaugh_num: String,
// #ColumnInfo(name = "area_name",
#Relation(entity = Area::class, parentColumn = "area_name", entityColumn = "name")
var areaName: List<String>,
// #ColumnInfo(name = "confiscation_name")
#Relation(entity = Confiscation::class, parentColumn = "confiscation_name", entityColumn = "name")
var confiscationName: List<String>
and DAO method to select data:
#Query("SELECT registration_number, area.[name] AS area_name, confiscations.[name] AS confiscation_name " +
"FROM car_body, car_body_confiscations" +
"INNER JOIN area ON car_body_confiscations.area_id == area.id " +
"INNER JOIN confiscations ON car_body_confiscations.confiscation_id == confiscations.id " +
"WHERE car_body.id == car_body_confiscations.car_body_id ORDER BY car_body.id DESC LIMIT :row_count")
fun getLastConfiscats(row_count: Int): LiveData<List<LastConfiscats>>
The linkage scheme between the tables that I am trying to implement is as follows:
There are examples on the internet how to make a relationship between 2 tables but I need to create a relationship between 4 tables.
Please help me to get the data in the right way
UPDATE :
My Area entity is:
#Entity(tableName = "area")
data class Area( #PrimaryKey(autoGenerate = true) var id: Int?, var name: String? )
but in my Confiscation entity I also have "name" column:
#Entity(tableName = "confiscations")
data class Confiscation( #PrimaryKey(autoGenerate = true) var id: Int?, var name: String? )
The actual message you are getting is because when you use #Relation the parent MUST exist and be annotated with #Embedded.
The parent and entity columns MUST be columns in the respective classes.
As an example the following will enable you to get a List of Confiscations, with the related CarBody and the respective Areas (note colum names based upon the screen shots):-
data class LastConfiscats(
#Embedded
var carBodyConfiscations: Car_Body_Confiscations,
#Relation(entity = CarBody::class, parentColumn = "car_body_id", entityColumn = "id")
var carBody: CarBody,
#Relation(entity = Area::class, parentColumn = "areaId", entityColumn = "id")
var area: List<Area>
)
You could use the above with a query such as:-
#Query("SELECT * FROM Car_Body_Confiscations")
fun getCardBodyJoinedWithStuff(): List<LastConfiscats>
No JOINS needed. That is because Room builds the underlying SQL. First is basically the copy of the supplied query. After retrieving the Car_Body_Confiscations it then uses queries based upon the field names/#ColumnInfo and runs queries for each Car_Body_Connfiscation.
For each #Relationship it populates the respective fields (1 carBody and the List of Areas) using queries that it builds. Here's and example of part of the code, for the above from the java(generated) for the query above :-
Main (parent query)
#Override
public List<LastConfiscats> getCardBodyJoinedWithStuff() {
final String _sql = "SELECT * FROM Car_Body_Confiscations";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
....
Later Nn (getting the CarBody(s) there will only be 1)
StringBuilder _stringBuilder = StringUtil.newStringBuilder();
_stringBuilder.append("SELECT `id`,`registrationNumber`,`datetime`,`userId`,`testId` FROM `CarBody` WHERE `id` IN (");
final int _inputSize = _map.size();
Even Later On (Areas)
StringBuilder _stringBuilder = StringUtil.newStringBuilder();
_stringBuilder.append("SELECT `id`,`name` FROM `Area` WHERE `id` IN (");
Now if you want to code your own JOINS etc and alias columns then you will have to consider a few things.
The receiving class MUST be able to be built from the result set and thus column names MUST match the fields in the POJO (unless using #Prefix annotation).
You also need to be aware that the result set will be the cartesian product, thus in the case of doing the above, bypassing how Room does it, the for each combination/permutation of confiscation/carbody/area you get a row (unless grouped/excluded by where clause). So if you have 1 confiscation joined to 1 car but with 10 areas then you would get 10 rows all with the same confiscation and carbody.
You may wish to consider having a look at Room #Relation annotation with a One To Many relationship. Which explains this a little more and includes an example of using a JOINs
Additional - User and TestLists
You may well want to include the CarBody's User and the Test_Lists so you have a result with all of the related data.
This needs to be looked at from a hierarchical perspective. That is the confiscation has a direct link/reference/map to the CarBody but underneath that are the links/references/mappings to the User from the CarBody and to the Test_Lists.
So to incorporate this you need a POJO for a CarBody with it's User and it's Test_Lists. So, for example:-
data class CarBodyWithUserAndWithTestList(
#Embedded
var carBody: CarBody,
#Relation(
entity = Users::class,
parentColumn = "userId",
entityColumn = "id"
)
var users: Users,
#Relation(
entity = Test_List::class,
parentColumn = "testId",
entityColumn = "id"
)
var testList: List<Test_List>
)
With this you can then amend the LastConfiscats to include a CarBodyWithUserAndWithTestList instead of just a CarBody e.g.:
data class LastConfiscats(
#Embedded
var carBodyConfiscations: Car_Body_Confiscations,
#Relation(entity = CarBody::class, parentColumn = "car_body_id", entityColumn = "id")
//var carBody: CarBody, /* REMOVED */
var carBodyWithUserAndWithTestList: CarBodyWithUserAndWithTestList, /* ADDED */
#Relation(entity = Area::class, parentColumn = "areaId", entityColumn = "id")
var area: List<Area>
)
Note that the #Relation has the CarBody class as the entity. That is because the CarBody is the class that needs to be inspected in order for Room to ascertain the columns used for the links/references/,mappings.
*Working Example/Demo
Here's the entire code for a Working example that inserts some data into all the tables and then extracts the data using the getCardBodyJoinedWithStuff query, it then writes the data to the Log.
the code includes ForeignKey constraints which enforces and helps to maintain referential integrity.
for id's Long rather than Int has been used as Long properly reflects the potential size of the field/value.
autoGenerate = true has not been used as this is inefficient and not needed see https://sqlite.org/autoinc.html, which includes as the very first statement 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. (autoGenerate = true results in AUTOINCREMENT)
So all the classes/interfaces :-
#Entity(
foreignKeys = [
ForeignKey(
Users::class,
parentColumns = ["id"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
Test_List::class,
parentColumns = ["id"],
childColumns = ["testId"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class CarBody(
#PrimaryKey
var id: Long?=null,
var registrationNumber: Int,
var datetime: String,
#ColumnInfo(index = true)
var userId: Long,
#ColumnInfo(index = true)
var testId: Long
)
#Entity
data class Users(
#PrimaryKey
var id:Long?=null,
var name: String,
var lastName: String,
var email: String,
var password: String
)
#Entity
data class Test_List(
#PrimaryKey
var id: Long?=null,
var date: String,
var is_saved: Boolean
)
#Entity(
foreignKeys = [
ForeignKey(
entity = CarBody::class,
parentColumns = ["id"],
childColumns = ["car_body_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = Confiscation::class,
parentColumns = ["id"],
childColumns = ["confiscation_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = Area::class,
parentColumns = ["id"],
childColumns = ["areaId"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Car_Body_Confiscations(
#PrimaryKey
var id: Long?=null,
#ColumnInfo(index = true)
var car_body_id: Long,
#ColumnInfo(index = true)
var confiscation_id: Long,
#ColumnInfo(index = true)
var areaId: Long
)
#Entity
data class Area(
#PrimaryKey
var id: Long?=null,
var name: String
)
#Entity
data class Confiscation(
#PrimaryKey
var id: Long?=null,
var name: String
)
#Dao
interface AllDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(area: Area): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(carBodyConfiscations: Car_Body_Confiscations): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(carBody: CarBody): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(confiscation: Confiscation): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(users: Users): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(testList: Test_List): Long
#Transaction
#Query("SELECT * FROM Car_Body_Confiscations")
fun getCardBodyJoinedWithStuff(): List<LastConfiscats>
}
#Database(entities = [
Area::class,
Car_Body_Confiscations::class,
CarBody::class,
Confiscation::class,
Users::class,
Test_List::class
],
exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
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
}
}
}
data class LastConfiscats(
#Embedded
var carBodyConfiscations: Car_Body_Confiscations,
#Relation(entity = Confiscation::class, parentColumn = "confiscation_id", entityColumn = "id")
var confiscation: Confiscation,
#Relation(entity = CarBody::class, parentColumn = "car_body_id", entityColumn = "id")
//var carBody: CarBody, /* REMOVED */
var carBodyWithUserAndWithTestList: CarBodyWithUserAndWithTestList, /* ADDED */
#Relation(entity = Area::class, parentColumn = "areaId", entityColumn = "id")
var area: List<Area>
)
data class CarBodyWithUserAndWithTestList(
#Embedded
var carBody: CarBody,
#Relation(
entity = Users::class,
parentColumn = "userId",
entityColumn = "id"
)
var users: Users,
#Relation(
entity = Test_List::class,
parentColumn = "testId",
entityColumn = "id"
)
var testList: List<Test_List>
)
The following activity code (note that main thread used for brevity and convenience):-
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()
dao.insert(Users(100,"Fred","Bloggs","fredBloggs#mail.com","password"))
dao.insert(Users(200,"Jane","Doe","janeDoe#email.org","password"))
/* example where id is autogenerated */
val marySmithId = dao.insert(Users(name = "Mary", lastName = "Smith", email = "marysmith#mailit.co.uk", password = "1234567890"))
dao.insert(Test_List(1,"2022-01-01",false))
dao.insert(Test_List(2,"2022-02-02",true))
dao.insert(CarBody(1000,1234,"2022-01-01",100 /* Fred Bloggs*/,2 ))
dao.insert(CarBody(2000,4321,"2021-12-05",100,1))
dao.insert(CarBody(3000,1111,"2021-09-10",200,2))
dao.insert(Area(100,"Area100"))
dao.insert(Area(200,"Area200"))
dao.insert(Area(300,"Area300"))
dao.insert(Area(400,"Area400"))
dao.insert(Confiscation(901,"C1"))
dao.insert(Confiscation(902,"C2"))
dao.insert(Confiscation(903,"C3"))
dao.insert(Confiscation(904,"C4"))
dao.insert(Car_Body_Confiscations(500,1000,901,100))
dao.insert(Car_Body_Confiscations(510,2000,904,400))
dao.insert(Car_Body_Confiscations(520,3000,902,300))
/* Extract the data and output to the Log */
for(cbc in dao.getCardBodyJoinedWithStuff()) {
val areaList = StringBuilder()
for (a in cbc.area) {
areaList.append("\n\t\tArea is ${a.name} ID is ${a.id}")
}
val testList = StringBuilder()
testList.append("\n\t\tThere are ${cbc.carBodyWithUserAndWithTestList.testList.size} TestLists, they are:")
for (t in cbc.carBodyWithUserAndWithTestList.testList) {
testList.append("\n\t\t\t${t.date} Save is ${t.is_saved} ID is ${t.id}")
}
Log.d(
"DBINFO",
"CBC ID =${cbc.carBodyConfiscations.id}" +
"\n\tConfiscation Name is ${cbc.confiscation.name}" +
"\n\tAreas (there is/are ${cbc.area.size}) they are $areaList}" +
"\n\tCarBody Reg is ${cbc.carBodyWithUserAndWithTestList.carBody.registrationNumber} " +
"Date is ${cbc.carBodyWithUserAndWithTestList.carBody.datetime}" +
"\n\t\tUser is ${cbc.carBodyWithUserAndWithTestList.users.name}" +
",${cbc.carBodyWithUserAndWithTestList.users.lastName} " +
"email is ${cbc.carBodyWithUserAndWithTestList.users.email}" +
"$testList"
)
}
}
}
Result
The Log after running:-
D/DBINFO: CBC ID =500
Confiscation Name is C1
Areas (there is/are 1) they are
Area is Area100 ID is 100}
CarBody Reg is 1234 Date is 2022-01-01
User is Fred,Bloggs email is fredBloggs#mail.com
There are 1 TestLists, they are:
2022-02-02 Save is true ID is 2
D/DBINFO: CBC ID =510
Confiscation Name is C4
Areas (there is/are 1) they are
Area is Area400 ID is 400}
CarBody Reg is 4321 Date is 2021-12-05
User is Fred,Bloggs email is fredBloggs#mail.com
There are 1 TestLists, they are:
2022-01-01 Save is false ID is 1
D/DBINFO: CBC ID =520
Confiscation Name is C2
Areas (there is/are 1) they are
Area is Area300 ID is 300}
CarBody Reg is 1111 Date is 2021-09-10
User is Jane,Doe email is janeDoe#email.org
There are 1 TestLists, they are:
2022-02-02 Save is true ID is 2
Re the Comment
I actually have a Cartesian product, I had to process it somehow, although I do not know how yet.
You may find that the above is fine and processes the product pretty easily.
Where Room's relationship handling can become restrictive is if you want to selectively retrieve related data. The way Room handles #Relation means that it retrieves ALL children irrespective of any JOINS and WHERE clauses. They are only effective if they affect the result of the topmost parent.
In your case, where you don't actually cater for lists (such as multiple users per carbody) then Room should suffice.
The original Query - revisited
Changing your query a little to (largely to suit the previous classes ) to:-
#Query("SELECT " +
"registrationNumber, " +
"area.[name] AS area_name, " +
"confiscation.[name] AS confiscation_name " +
"FROM carbody, car_body_confiscations " +
"INNER JOIN area ON car_body_confiscations.areaId == area.id " +
"INNER JOIN confiscation ON car_body_confiscations.confiscation_id == confiscation.id " +
"WHERE carbody.id == car_body_confiscations.car_body_id " +
"ORDER BY carbody.id DESC " +
"LIMIT :row_count"
)
fun getLastConfiscats(row_count: Int): /*LiveData<*/List<MyQueryPOJO>/*>*/
see the following re MyQueryPOJO
And adding a suitable class (no #Embeddeds or #Relations needed, so Room doesn't get confused with column names) :-
data class MyQueryPOJO(
/* The output columns of the query */
var registrationNumber: Int,
#ColumnInfo(name = "area_name")
var not_the_area_name: String,
var confiscation_name: String
)
note how the not_the_area_name field has the #ColumnInfo annotation to tell it to use the area_name output column
In the activity, using:-
for (mqo in dao.getLastConfiscats(10)) {
Log.d("DBINFO","Reg = ${mqo.registrationNumber} Confiscation = ${mqo.confiscation_name} Area Name = ${mqo.not_the_area_name}")
}
Results in (with the same data) :-
D/DBINFO: Reg = 1111 Confiscation = C2 Area Name = Area300
D/DBINFO: Reg = 4321 Confiscation = C4 Area Name = Area400
D/DBINFO: Reg = 1234 Confiscation = C1 Area Name = Area100
as the relationships all all basically 1-1 (the references are back to front for a 1-many) the cartesian product is fine as there will not be any duplicates.

how to save a complex object in a Room database?

I'm trying to save Lists and Books data in a local database using Room.
where each list contains books.
but have a problem implementing this.
Well, I used type converters & it worked but the google docs suggest that that's not an ideal way of saving complex objects and suggest using Relationships
So I tried to define a relationship between the two objects but I couldn't get it to work.
can someone please help out.
#Entity(tableName = "lists_table")
data class Lists(
#PrimaryKey
#ColumnInfo(name = "display_name")
#Json(name = "display_name") val displayName: String,
val books: List<Books>
)
#Entity(tableName = "books_table")
data class Books(
#PrimaryKey
#ColumnInfo(name = "author") val author: String,
#ColumnInfo(name = "book_image") #Json(name = "book_image") val bookImage: String,
#ColumnInfo(name = "width") #Json(name = "book_image_width") val imageWidth: Int,
#ColumnInfo(name = "height") #Json(name = "book_image_height") val imageHeight: Int,
#ColumnInfo(name = "contributor") val contributor: String,
#ColumnInfo(name = "description") val description: String,
#ColumnInfo(name = "publisher") val publisher: String,
#ColumnInfo(name = "rank") val rank: Int,
#ColumnInfo(name = "rank_last_week") #Json(name = "rank_last_week") val rankLastWeek: Int,
#ColumnInfo(name = "title") val title: String,
#ColumnInfo(name = "weeks_on_list") #Json(name = "weeks_on_list") val weeksOnList: Int,
)
Assuming that a Book can exist in many lists, not just one list (a many-many relationship) then you have an table (know by many names such as a reference table, mapping table, associative table ....). Such a table has a map to one of the objects (list or book) an a map to the other (book or list).
To map an object then the map must uniquely identify the mapped object. As PRIMARY KEYS must be UNIQUE then in your case the display_name column is the primary key for a Lists object and the author column is the primary key for a book object.
You may wish to reconsider, as the implication is that every book must have an author unique to the book, so an author could not have multiple books.
So the mapping table would consist of these two columns (display_name and author). The display_name together with the author would be the primary key.
You cannot use the #PrimaryKey annotation to specify a composite primary key, so you use the primaryKeys parameter of the #Entity annotation, which takes a value that is an array of Strings (the respective column names) .
A consideration, is whether or not to enforce the relationships actually being true relationships. ForeignKey constraints (rules) can not only enforce referential integrity they can also help maintain the integrity by specifying that children or updated/deleted in line with the parent.
So you could have:-
#Entity(
primaryKeys = ["display_name_map","bookId_map"],
/* Optional but suggested */
foreignKeys = [
ForeignKey(
entity = Lists::class,
parentColumns = ["display_name"],
childColumns = ["display_name_map"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = Books::class,
parentColumns = ["bookId"],
childColumns = ["bookId_map"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class ListsBookMap(
val display_name_map: String,
#ColumnInfo(index = true) /* have an index associated with the table according to bookId */
val bookId_map: Long
)
Note that bookId is the mapped column, thus an author can author many books
i.e. Books has changed to:-
#PrimaryKey
val bookId: Long?=null, /* ADDED as the primary key so that an author can have many books */
#ColumnInfo(name = "author") val author: String,
To facilitate retrieving the parent (Lists) with it's related children (Books) then you utilise a POJO with the parent annotated with #Embedded and the list of children annotated with the #Relation annotation that includes the associateBy parameter to specify the mapping table and the columns therein, i.e. the Junction.
So this could be:-
data class ListWithBooks(
#Embedded
val list: Lists,
#Relation(
entity = Books::class,
parentColumn = "display_name",
entityColumn = "bookId",
associateBy = Junction(
ListsBookMap::class,
parentColumn = "display_name_map",
entityColumn = "bookId_map"
)
)
val booksList: List<Books>
)
You would ned to be able to insert data, including the mappings so you could have an #Dao annotated interface such as :-
#Dao
interface ListAndBookDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(books: Books): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(lists: Lists): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(listsBookMap: ListsBookMap): Long
#Transaction
#Query("SELECT * FROM lists_table")
fun getAllListsWithListOfBooks(): List<ListsWithBooks>
}
Note the #Query this will retrieve Lists with the books for the current Lists.
Demo
Using the above, and a suitable #Database annotated abstract class, and the following in an activity (note run on the main thread for convenience and brevity):-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: ListAndBookDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getListAndBookDao()
val b1Id = dao.insert(Books(null,"A1","B1IMAGE",10,12,"C","Desc","P",5,4,"The Book 1",10))
val b2Id = dao.insert(Books(null,"A2","B2IMAGE",10,12,"C","Desc","P",5,4,"The Book 2",10))
val b3Id = dao.insert(Books(null,"A3","B3IMAGE",10,12,"C","Desc","P",5,4,"The Book 3",10))
val b4Id = dao.insert(Books(null,"A1","B4IMAGE",10,12,"C","Desc","P",5,4,"The Book 4",10))
val b5Id = dao.insert(Books(null,"A4","B5IMAGE",10,12,"C","Desc","P",5,4,"The Book 5",10))
val list1Name = "BL001"
val list2Name = "BL002"
val list3Name = "BL003"
val list4Name = "BL004"
val list5Name ="BL005"
dao.insert(Lists(list1Name))
dao.insert(Lists(list2Name))
dao.insert(Lists(list3Name))
dao.insert(Lists(list4Name))
dao.insert(Lists(list5Name))
/* Add books to the lists (i.e. the mappings)*/
dao.insert(ListsBookMap(list1Name,b1Id))
dao.insert(ListsBookMap(list1Name,b3Id))
dao.insert(ListsBookMap(list1Name,b5Id))
dao.insert(ListsBookMap(list2Name,b2Id))
dao.insert(ListsBookMap(list2Name,b4Id))
dao.insert(ListsBookMap(list3Name,b1Id))
dao.insert(ListsBookMap(list3Name,b2Id))
dao.insert(ListsBookMap(list3Name,b3Id))
dao.insert(ListsBookMap(list3Name,b4Id))
dao.insert(ListsBookMap(list3Name,b5Id))
dao.insert(ListsBookMap(list4Name,b3Id))
val sb = StringBuilder()
for (lwb in dao.getAllListsWithListOfBooks()) {
sb.clear()
for (b in lwb.booksList) {
sb.append("\n\tBook is ${b.title}, Author is ${b.author} Image is ${b.bookImage} ....")
}
Log.d("DBINFO","List is ${lwb.list.displayName} it has ${lwb.booksList.size} books. They are:-$sb")
}
}
}
When run (only designed to run once) then the output to the log includes:-
D/DBINFO: List is BL001 it has 3 books. They are:-
Book is The Book 1, Author is A1 Image is B1IMAGE ....
Book is The Book 3, Author is A3 Image is B3IMAGE ....
Book is The Book 5, Author is A4 Image is B5IMAGE ....
D/DBINFO: List is BL002 it has 2 books. They are:-
Book is The Book 2, Author is A2 Image is B2IMAGE ....
Book is The Book 4, Author is A1 Image is B4IMAGE ....
D/DBINFO: List is BL003 it has 5 books. They are:-
Book is The Book 1, Author is A1 Image is B1IMAGE ....
Book is The Book 2, Author is A2 Image is B2IMAGE ....
Book is The Book 3, Author is A3 Image is B3IMAGE ....
Book is The Book 4, Author is A1 Image is B4IMAGE ....
Book is The Book 5, Author is A4 Image is B5IMAGE ....
D/DBINFO: List is BL004 it has 1 books. They are:-
Book is The Book 3, Author is A3 Image is B3IMAGE ....
D/DBINFO: List is BL005 it has 0 books. They are:-
i.e. as expected

Kotlin entered value not searching database

We have worked on this code to error trap a value entered in a Edit Text field
When the value is entered correctly we are informed that the entered value does not match
BUT if we select the value from a recycler view list and populate the Edit Text field with the value the search tells us we have a match
Here is the code for the search in the DBHelper
fun getOneName(id: Int): Contact? {
val db = this.writableDatabase
val selectQuery = "SELECT * FROM $TABLE_NAME WHERE $colId = ?"
db.rawQuery(selectQuery, arrayOf(id.toString())).use { // .use requires API 16
if (it.moveToFirst()) {
val result = Contact(id = 0,name ="")
result.id = it.getInt(it.getColumnIndex(colId))
result.name = it.getString(it.getColumnIndex(colName))
return result
}
}
return null
}
We used this for the Model Class our first time using data class as just plain class
data class Contact (
var id: Int,
var name: String
)
And here is the button click that manages the search
btnGetID.setOnClickListener {
if(etPerson.text.toString().trim().isNullOrEmpty()){
message("Enter Contact Name")
return#setOnClickListener
}
var numeric = true
var string = etPerson.text.toString().trim()
numeric = string.matches(".*\\d+.*".toRegex())
if(numeric){
message("No NUMBERS")
return#setOnClickListener
}
val dbManager = DBHelper(this)
var name = etPerson.text.toString()
//val contact = dbManager.getOneName(name)
val contact = dbManager.getOneName(id.toInt())
if(contact?.name.equals(name)){
println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! contact ID= "+contact)
etPerson.setText("The contact name is $name the ID is "+contact?.id.toString())
}else{
etPerson.setText("Name NOT = to $name and the ID is "+contact?.id.toString())
}
}
We know the name Sally is in the DB if we type Sally in the else statement shows Name NOT = bla
If we select Sally from the Recyclerview List the first statement shows The contact name bla bla
Kotlin 1.2.71 API 27
Our question is why is the hand typed name failing if it mataches?
HERE IS THE CORRECT CODE FOR THE DBHelper
fun getOneName(name: String): Contact? {
val db = this.writableDatabase
val selectQuery = "SELECT * FROM $TABLE_NAME WHERE $colName = ?"
db.rawQuery(selectQuery, arrayOf(name)).use { // .use requires API 16
if (it.moveToFirst()) {
val result = Contact(id = 0,name ="")
result.id = it.getInt(it.getColumnIndex(colId))
result.name = it.getString(it.getColumnIndex(colName))
return result
}
}
return null
}

LINQ - How to read ID which is Autonumber after submitting the data

I am working on LINQ. and in one transaction I have to update three different tables.
e.g A(A1,A2,A3) B(B1,B2) AB(A1,B1)
here B1 is autonumber in my database. I want to submit all the changes together. so in one data context I wrote
using (BBBDataContext DC= new BBBDataContext())
{
A tba = new A()
{
A1 = this.A1,
A2 = this.A2,
A3 = this.A3,
};
DC.A.InsertOnSubmit(tba);
B tbb= new B()
{
B2 = this.B2,
};
DC.B.InsertOnSubmit(tbb);
// NOW i WONT B1(WHICH IS AUTONUMBER) FROM B SO THAT I USE IT IN MY NEXT TABLE.
AB tbab = new AB()
{
A1 = this.A1,
B1 = ??????,
};
DC.AB.InsertOnSubmit(tbab);
//FINALLY I WILL WRITE SUBMIT CHANGES SO MY ALL TABLES GET CHANGES SAME TIME
DC.SubmitChanges();
}
Que: what should I write # the place of ?????. for B1 in AB table??
Short answer: B
Bit longer answer:
When you create the object and add it you won't get the ID before you commit. So what you do is reference the object and not the ID property of the object.
class A {
public int Id { get;set;}
public ICollection <B> Bees { get;set;}
}
class B {
public int Id { get;set;}
public int InstanceOfAId { get;set;}
public A InstanceOfA { get;set;}
}
var a = new A();
var b = new B();
b.InstanceOfA = a;
Depending on your model you will define a relationship. In code first you could do it this way:
EntityTypeConfiguration<B> cfgB = new EntityTypeConfiguration<B>();
cfgB.HasRequired(p => p.InstanceOfA).WithMany(p => p.Bees)
.HasForeignKey(p => p.InstanceOfAId);
In EDMX you can do the same in the designer.
You will use InstanceOfA to assign to when inserting, after that InstanceOfAId will always hold the Id value. When retrieving data InstanceOfA will only be filled when requested.
You can use it like this:
var x = DC.B.Include(p=>p.A);
or
var x = DC.B.Select(p=> new { Id = p.Id, A_Id = p.A.Id});
or
var x = DC.A.Select(p=> new { Id = p.Id, BeeCount = p.Bees.Count()});

Resources