how to populate intermediate table in Android - android

I have two tables with a many-to-many relationship. So I created an intermediate table, but I can't find a way to populate this table correctly because I can't set a correct list from my data.
I have a list of 'courses' : each 'course' can have one or several categories.
So my table looks like this :
|idcourses|title|date|categories|
|----|----|----|----|
|700|title1|01012021|[54]|
|701|title2|01022021|[54]|
|702|title3|01032021|[48]|
|868|title4|01042021|[47, 52, 54]|
If I try a map like this :
val myMap = coursesList.map { itcategory to it.idcourses}.distinct()
I have this kind of result :
([54], 700), ([54], 701), ([48], 702), ([47, 52, 54], 868)
The whole "[47, 52, 54]" is considered as one string but I want it to be split so I can have this :
([54], 700), ([54], 701), ([48], 702), ([47], 868), ([52], 868), ([54], 868)
Does anyone know how to achieve this ??

I believe that you may be trying to do this the wrong way as it appears that your intermediate table has a column where you are expecting a list of category id's.
You cannot have a column that is a list/array it has to be a single object.
However rather than try to fix that, what would typically be used for an intermediate table is a table that primarily has a single row per mapping. That is two columns that make up a mapping. Where the two columns are a composite primary key.
other columns that have data specific to the mapping can be used.
In your case one column to map/reference/relate/associate to the course and an second column to map the course.
For example, say you have the Course Table and the Category Table per:-
#Entity
data class Course(
#PrimaryKey
val idcourses: Long? = null,
val title: String,
val date: String
)
and
#Entity
data class Category(
#PrimaryKey
val idcategories: Long? = null,
val name: String
)
Then you could have the intermediate table as :-
#Entity(primaryKeys = ["idcoursesmap","idcategoriesmap"])
data class CourseCategoryMap(
val idcoursesmap: Long,
#ColumnInfo(index = true)
val idcategoriesmap: Long
)
the index on the idcategoriesmap will likely improve the efficiency. Room would also issue a warning.
you may wish to consider defining Foreign Key constraints to enforce referential integrity. None have been included for brevity.
This is sufficient for a many-many relationship.
You would probably want to retrieve Courses with the Categories so you would probably want a POJO for this such as:-
data class CourseWithCategories(
#Embedded
val course: Course,
#Relation(
entity = Category::class,
parentColumn = "idcourses",
entityColumn = "idcategories",
associateBy = Junction(
value = CourseCategoryMap::class,
parentColumn = "idcoursesmap",
entityColumn = "idcategoriesmap"
)
)
val categories: List<Category>
)
Here's some Dao's that would or may be wanted/useful:-
abstract class AllDao {
#Insert(onConflict = IGNORE) // Insert single Category
abstract fun insert(category: Category): Long
#Insert(onConflict = IGNORE) // Insert Single Course
abstract fun insert(course: Course): Long
#Insert(onConflict = IGNORE) // Insert Single CourseCategoryMap
abstract fun insert(courseCategoryMap: CourseCategoryMap): Long
/* Inserts many course category maps */
#Insert(onConflict = IGNORE)
abstract fun insert(courseCategoryMaps: List<CourseCategoryMap>): List<Long>
#Query("SELECT * FROM course WHERE course.title=:courseTitle")
abstract fun getCourseByTitle(courseTitle: String): Course
#Query("SELECT * FROM category WHERE category.name LIKE :categoryMask")
abstract fun getCategoriesByNameMask(categoryMask: String): List<Category>
/* For retrieving courses with all the courses categories */
#Transaction
#Query("SELECT * FROM course")
abstract fun getAllCoursesWithCategories(): List<CourseWithCategories>
#Transaction
#Query("")
fun insertManyCataegoriesForACourseByIds(idcourse: Long,categories: List<Long>) {
for (categoryId: Long in categories) {
insert(CourseCategoryMap(idcourse,categoryId))
}
}
// Anoher possibility
#Transaction
#Query("")
fun insertManyCategoriesForACourse(course: Course, categories: List<Category>) {
val categoryIds = ArrayList<Long>()
for (c: Category in categories) {
categoryIds.add(c.idcategories!!)
}
insertManyCataegoriesForACourseByIds(course.idcourses!!,categoryIds)
}
}
Demonstration
To demonstrate the above, a pretty standard class annotated with #Database :-
const val DATABASE_NAME = "the_database.db"
const val DATABASE_VERSION =1
#Database(entities = [Course::class,Category::class,CourseCategoryMap::class], exportSchema = false, version = DATABASE_VERSION)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
#Volatile
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java, DATABASE_NAME)
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
And activity code to replicate what it looks like your are attempting (but twice to show 2 ways of mapping, the second using category id's that are 20 greater then the first) :-
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(Course(idcourses = 700,title = "title1", date = "01012021"))
dao.insert(Course(701,"title2","01022021"))
dao.insert(Course(702,"title3","01032021"))
dao.insert(Course(868,"title4","01042021"))
// add quite a few categories for demo
for(i in 30..300) {
dao.insert(Category(i.toLong(),"Category${i}"))
}
//example of what you are trying to do (first)
var currentCourse = dao.getCourseByTitle("title1")
dao.insertManyCataegoriesForACourseByIds(currentCourse.idcourses!!, listOf(54))
dao.insertManyCataegoriesForACourseByIds(dao.getCourseByTitle("title2").idcourses!!, listOf(54))
dao.insertManyCataegoriesForACourseByIds(dao.getCourseByTitle("title3").idcourses!!, listOf(48))
dao.insertManyCataegoriesForACourseByIds(dao.getCourseByTitle("title4").idcourses!!, listOf(47,52,54))
// second (does the same but uses categroyids 20 greater than the first)
val coursecategorymaplist = listOf<CourseCategoryMap>(
CourseCategoryMap(700,74),
CourseCategoryMap(701,74),
CourseCategoryMap(702,68),
CourseCategoryMap(868,67),
CourseCategoryMap(868,72),
CourseCategoryMap(868,74)
)
dao.insert(coursecategorymaplist)
// Extract results
for (cwc: CourseWithCategories in dao.getAllCoursesWithCategories()) {
Log.d("DBINFO","Course is ${cwc.course.title}, date is ${cwc.course.date} it has ${cwc.categories.size} categories they are:-")
for (c: Category in cwc.categories) {
Log.d("DBINFO","\tCategory is ${c.name}")
}
}
}
Results
The log includes (note double the number of categories):-
D/DBINFO: Course is title1, date is 01012021 it has 2 categories they are:-
D/DBINFO: Category is Category54
D/DBINFO: Category is Category74
D/DBINFO: Course is title2, date is 01022021 it has 2 categories they are:-
D/DBINFO: Category is Category54
D/DBINFO: Category is Category74
D/DBINFO: Course is title3, date is 01032021 it has 2 categories they are:-
D/DBINFO: Category is Category48
D/DBINFO: Category is Category68
D/DBINFO: Course is title4, date is 01042021 it has 6 categories they are:-
D/DBINFO: Category is Category47
D/DBINFO: Category is Category52
D/DBINFO: Category is Category54
D/DBINFO: Category is Category67
D/DBINFO: Category is Category72
D/DBINFO: Category is Category74
The Database
The Course Table :-
The Category Table (partial)
The CourseCategoryMap (intermediate table)

Related

Do I have a way to search objects on Room Database

I am now building an Android App with Local Database
The table structure is like following (coming from API)
#Entity
data class Person(
name: Name,
... ... ...
... ... ...
)
data class Name(
legalName: String.
common: String
)
This is sql code I have tried to person with legal name
#Query("SELECT * FROM person WHERE name.legalName = :legalName")
suspend fun getPersonByName (legalName: String): Person?
This gave me compile error as we can't search by name.legalName on Room database
In addition, we have static name list of person (only legal name) in Homepage (No ID or other reasonable fields to perform search)
DO we have proper way to search Users with legalName field?
The #Entity annotation is used by Room to determine the underlying SQLite table schema. A class so annotated is an object but the individual fields/members of the object are stored as columns in the table which are not objects.
Such columns can never be anything other than specific types being either:-
integer type values (e.g. Int, Long .... Boolean) (column type of INTEGER)
string type values (e.g. String) (column type of TEXT)
decimal/floating point type values (e.g, Float, Double) (column type REAL)
bytestream type values (e.g. ByteArray) (column type BLOB)
null (column definition must not have NOT NULL constraint)
Thus, objects are NOT stored or storable directly SQLite has no concept/understanding of objects just columns grouped into tables.
In your case the name field is a Name object and Room will require 2 Type Converters:-
One that converts the object into one of the above that can represent the object (typically a json representation of the object)
The other to convert the stored data back into the Object.
This allowing an object to be represented in a single column.
As such to query a field/member of the object you need to consider how it is represented and searched accordingly.
There will not be a name.legalName column just a name column and the representation depends upon the TypConverter as then would the search (WHERE clause).
Now consider the following based upon your code:-
#Entity
data class Person(
#PrimaryKey
var id: Long?=null,
var name: Name,
#Embedded /* Alternative */
var otherName: Name
)
data class Name(
var legalName: String,
var common: String
)
PrimaryKey added as required by Room
#Embedded as an alternative that copies the fields/members (legalName and common as fields)
Thus the name column will require TypeConverters as per a class with each of the 2 annotated twith #TypeConverter (note singular), the class where the Type Converters are defined has to be defined (see the TheDatabase class below). So :-
class TheTypeConverters {
/* Using Library as per dependency implementation 'com.google.code.gson:gson:2.10.1' */
#TypeConverter
fun convertFromNameToJSONString(name: Name): String = Gson().toJson(name)
#TypeConverter
fun convertFromJSONStringToName(jsonString: String): Name = Gson().fromJson(jsonString,Name::class.java)
}
note that there are other Gson libraries that may offer better functionality.
The entities (just the one in this case) have to be defined in the #Database annotation for the abstract class that extends RoomDatabase(). so:-
#TypeConverters(value = [TheTypeConverters::class])
#Database(entities = [Person::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getTheDAOs(): TheDAOs
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() /* For brevity convenience of the demo */
.build()
}
return instance as TheDatabase
}
}
}
The #TypeConverters annotation (plural) in addition to defining a class or classes where the TypeConverters are, also defines the scope (#Database being the most encompassing scope).
At this stage the project can be compiled (CTRL + F9) and the annotation processing will generate some code. Importantly TheDatabase_Impl in the java(generated) The name being the same as the #Database annotated class suffixed with _Impl. This includes a method createAllTables which is the SQL used when creatin the SQLite tables. The SQL for the person table is:-
CREATE TABLE IF NOT EXISTS `Person` (
`id` INTEGER,
`name` TEXT NOT NULL,
`legalName` TEXT NOT NULL,
`common` TEXT NOT NULL, PRIMARY KEY(`id`)
)
As can be seen the id column as the primary key, the name column for the converted representation of the name object and then the legal and common columns due to the name object being #Embedded via the otherName field.
Just to finish matters with the following #Dao annotated interface (allowing some data to be added):-
#Dao
interface TheDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(person: Person): Long
#Query("SELECT * FROM person")
fun getAllPersonRows(): List<Person>
}
And with MainActivity as:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getTheDAOs()
dao.insert(Person(null, name = Name("Frederick Bloggs","Fred Bloggs"), otherName = Name("Frederick ","Fred Bloggs")))
dao.insert(Person(null, name = Name("Jane Doe","Jane Doe"), otherName = Name("Jane Doe","Jane Doe")))
}
}
and the project run and then App Inspection used to view the actual database then:-
The name column contains the string {"common":"Fred Bloggs","legalName":"Frederick Bloggs"}
So the WHERE clause to locate all legal names that start with Fred could be
WHERE instr(name,',\"legalName\":\"Fred')
or
WHERE name LIKE '%,\"legalName\":\"Fred%'
it should be noted that both due to the search being within a column requires a full scan.
Of course that assumes that there is no name that has the common name ,"legalName":"Fred or as part of the common name or some other part of entire string. i.e. it can be hard to anticipate what results may be in the future.
For the alternative #Embedded Name object, the legalName and common columns are more easily searched, the equivalent search for legal names starting with Fred could be
WHERE legalname LIKE 'Fred%'
There is no potential whatsoever for Fred appearing elsewhere meeting the criteria. The search just on the single column/value nothing else. Indexing the column would very likely improve the efficiency.
Amending the #Dao annotated interface TheDAOs to be:-
#Dao
interface TheDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(person: Person): Long
#Query("SELECT * FROM person WHERE instr(name,',\"legalName\":\"Fred')")
fun getPersonsAccordingToLegalNameInNameObject(): List<Person>
#Query("SELECT * FROM person WHERE legalName LIKE 'Fred%'")
fun getPersonsAccordingToLegalName(): List<Person>
}
And MainActivity to be:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getTheDAOs()
dao.insert(Person(null, name = Name("Frederick Bloggs","Fred Bloggs"), otherName = Name("Frederick ","Fred Bloggs")))
dao.insert(Person(null, name = Name("Jane Doe","Jane Doe"), otherName = Name("Jane Doe","Jane Doe")))
logPersonList(dao.getPersonsAccordingToLegalNameInNameObject(),"RUN1")
logPersonList(dao.getPersonsAccordingToLegalName(),"RUN2")
}
private fun logPersonList(personList: List<Person>, suffix: String) {
for (p in personList) {
Log.d("DBINFO_${suffix}","Person ID is ${p.id} Name.legalName is ${p.name.legalName} Name.common is ${p.name.common} LegalName is ${p.otherName.legalName} Common is ${p.otherName.common}")
}
}
}
Then running (first time after install) the log contains:-
2023-01-14 11:26:03.738 D/DBINFO_RUN1: Person ID is 1 Name.legalName is Frederick Bloggs Name.common is Fred Bloggs LegalName is Frederick Common is Fred Bloggs
2023-01-14 11:26:03.740 D/DBINFO_RUN2: Person ID is 1 Name.legalName is Frederick Bloggs Name.common is Fred Bloggs LegalName is Frederick Common is Fred Bloggs
i.e. in this limited demo the expected results either way.
Note that Name.legalName and Name.common is not how the data is accessed, it is just text used to easily distinguish then similar values.

Android studio Room database - derived fields

I just know that when somebody is giving the answer i will kill my self for being so ... but i am struggeling with and android studio room database thing.
I have to objects A and B.
The content of object A is displayed on a RecyclerView.
All fine so fas.
Now what i also want is to display the number of objects B linked to each A without putting that number persistant in the database.
So i found that i could use the #Ignore to prevent the field in object A from being created as a field in my table A.
That i created a join to read the count of each row in table B linked to object A.
And than android studio complains that the count is nog a field in object A.
Does anybody have some example i can read.
And than android studio complains that the count is nog a field in object A.
When you #Ignore a field then you are say that Room should not consider that field as being stored and retrieved, as you have found.
You have various options.
You can have a POJO that embeds object and then has an additional variable into which the count can be placed by have the query return the POJO
Note that effectively the #Ignore'd field serves little pupose.
You might as a well have the POJO as the real A object and the #Entity annotated a sub object.
If you are only interested in the count but not the objects you can have a query that returns the single value (Long/Int), in which case the column name is irrelevant.
If you query via a relationship and are returning the parent along with the children of the parent then you can use the list's size.
Demo
The A object with the #Ignored field :-
#Entity
data class A(
#PrimaryKey
var a_id: Long?=null,
var a_name: String,
#Ignore
var a_children_count: Long=0
) {
constructor(): this(a_id=null,a_name= "")
constructor(id: Long, name: String): this(a_id = id, a_name = name,a_children_count = 0)
}
The B object, which will be a child to an A object :-
#Entity(
foreignKeys = [
ForeignKey(
entity = A::class,
parentColumns = ["a_id"],
childColumns = ["b_map_to_a"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class B(
#PrimaryKey
var b_id: Long?=null,
var b_name: String,
#ColumnInfo(index = true)
var b_map_to_a: Long?
)
POJO for getting an A object along with the list of B objects that are A's children (for the third method i.e. number of children in the list):-
data class AWithRelatedB(
#Embedded
var a: A,
#Relation(
entity = B::class,
parentColumn = "a_id",
entityColumn = "b_map_to_a"
)
var listOfB: List<B>
)
POJO for getting an A object with the count of the number of B objects :-
data class AWithNumberOfRelatedB(
#Embedded
var TheA: A,
var countOfRelatedB: Long
)
An #Dao annotated class:-
#Dao
interface TheDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(a: A): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(b: B): Long
/* Just get the number of B's in an A */
#Query("SELECT count(*) FROM A JOIN B ON a_id = b_map_to_a WHERE a_id=:aId")
fun getNumberOfBsRelatedToAnA(aId: Long): Long
/* Get an A object along with the count of the B's i.e. return a AWithNumberOfRelatedB */
#Query("SELECT a.*, (SELECT count(*) FROM B WHERE b_map_to_a = a.a_id) AS countOfRelatedB FROM a WHERE a_id=:aId")
fun getAWithTheNumberOfRelatedBs(aId: Long): AWithNumberOfRelatedB
/* Get the A with the list of B's, the size of the list is the number of B's */
#Transaction
#Query("SELECT * FROM a")
fun getEveryAWithItsBChildren(): List<AWithRelatedB>
}
Activity Code to demonstrate :-
class MainActivity : AppCompatActivity() {
lateinit var demoDb: DemoDatabase
lateinit var demoDao: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val TAG = "DBINFO"
demoDb = DemoDatabase.getInstance(this)
demoDao = demoDb.getTheDAO()
demoDao.insert(A(10,"A1"))
demoDao.insert(A(20,"A2"))
demoDao.insert(A(30,"A3"))
demoDao.insert(B(b_name = "B1 child of A1", b_map_to_a = 10))
demoDao.insert(B(b_name = "B2 child of A1", b_map_to_a = 10))
demoDao.insert(B(b_name = "B3 child of A1", b_map_to_a = 10))
demoDao.insert(B(b_name = "B4 child of A2", b_map_to_a = 20))
demoDao.insert(B(b_name = "B5 child of A2", b_map_to_a = 20))
demoDao.insert(B(b_name = "B6 child of A2", b_map_to_a = 20))
demoDao.insert(B(b_name = "B7 child of A2", b_map_to_a = 20))
Log.d(TAG,"Number of B's in A1 is ${demoDao.getNumberOfBsRelatedToAnA(10)}")
val aPlusBCount= demoDao.getAWithTheNumberOfRelatedBs(10)
Log.d(TAG,"A's Name is ${aPlusBCount.TheA.a_name} Number of B's is ${aPlusBCount.countOfRelatedB}")
for(awrc in demoDao.getEveryAWithItsBChildren()) {
Log.d(TAG,"This A's name is ${awrc.a.a_name} the number of children is ${awrc.listOfB.size} ")
}
}
}
The Result included in the Log (blank lines added to split the output into the 3) :-
2022-09-04 06:58:49.122 D/DBINFO: Number of B's in A1 is 3
2022-09-04 06:58:49.125 D/DBINFO: A's Name is A1 Number of B's is 3
2022-09-04 06:58:49.130 D/DBINFO: This A's name is A1 the number of children is 3
2022-09-04 06:58:49.130 D/DBINFO: This A's name is A2 the number of children is 4
2022-09-04 06:58:49.130 D/DBINFO: This A's name is A3 the number of children is 0
If you wanted multiple rows from getAWithTheNumberOfRelatedBs then you would change the WHERE clause accordingly (for all then no WHERE clause) and return a List<AWithNumberOfRelatedB>
e.g. (for all)
#Query("SELECT *, (SELECT count(*) FROM B WHERE b_map_to_a = a.a_id) AS countOfRelatedB FROM a")
fun (SELECT count(*) FROM B WHERE b_map_to_a = a.a_id)(): List<AWithNumberOfRelatedB>
Note that using a JOIN such as :-
#Query("SELECT *, count(*) AS countOfRelatedB FROM A JOIN B ON a.a_id=b_map_to_a GROUP BY a_id")
fun notOk(): List<AWithNumberOfRelatedB>
Then, using the data loaded as above, as A3 has no related B's A3 with a count of 0 would not be extracted as there is no join between A3 and anything. Whilst the subquery, as in (SELECT count(*) FROM B WHERE b_map_to_a = a.a_id) will return A's with no related B's.

How to query a list inside Room database in android?

This is the class that i'm saving inside room database:
#Entity
data class Person(
val name : String = "Bruno",
val age : Int = 23,
#PrimaryKey(autoGenerate = true) val id: Int = 0,
val hobbies : ArrayList<String> = arrayListOf("Basquete","Academia","Musica","Anatomia")
)
I already added type converters so it is saving successfully.
What i want is to query results by what the hobbies list has. E.g:
select * from person where hobbies in ("Basquete")
I wanted to select all person objects that has "Basquete" inside the hobbies list, but this query is returning empty. What am i doing wrong?
Their is no concept of a list in a row of a table, a column holds a single value.
Having a Type Converter will store the list of hobbies as a single value (column), as such IN will check the entire (the EXACT) value (the full list and whatever encoding is used, this dependant upon the Type Converter that converters the list to the single value).
As such it is likely that using IN, is not going to be of use.
As an example the TypeConverter may convert to something along the lines of ["Basquete","Academia","Musica"] (conversion to JSON string via com.google.code.gson dependcy)
To demonstrate using data loaded and using App Inspection then with
:-
Now consider an adaptation of your query, it being SELECT *, hobbies in ("Basquete") AS TEST1, hobbies IN ('Basquete') AS TEST2, "Basquete" IN(hobbies) AS TEST3,'Basquete' IN (hobbies) AS TEST4 ,hobbies LIKE '%Basquete%' AS TEST5 FROM person WHERE person.id = 1;
Then via App Inspection, the result is
So you could use WHERE hobbies LIKE '%Basquete%'
NOTE the enclosing single quotes, which delineates sting values, (not double quotes).
note searches with the % char as the first character will result in an in-efficient scan for the data.
This assumes that the TypeConverter is converting the List of Hobbies to a String representation of the data. If the TypeConverter converted the list to a byte-stream (e.g. ByteArray) then the above would not work.
However, if you are looking for multiple hobbies, then the complexity increases. e.g. WHERE hobbies LIKE '%Basquete%' OR hobbies LIKE '%Academia%' (to find those with either, using AND instead of OR would return only those with both).
The more correct solution where IN could be utilised would be to have a table that contains the hobbies and as the relationship would be a many-many relationship (a person could have many hobbies and people could have the same hobbies) have a third mapping table for the many-many relationship.
Example of the More Correct way
All the #Entity annotated classes and also a POJO for getting a Person with their list of hobbies:-
#Entity
data class Person(
val name : String = "Bruno",
val age : Int = 23,
#PrimaryKey(autoGenerate = true) val id: Int = 0,
//val hobbies : ArrayList<String> = arrayListOf("Basquete","Academia","Musica","Anatomia") /*<<<<<<<<<< no need */
/* Also no need for type converters */
)
/* The suggested (more correct) Hobby table */
#Entity(
indices = [
Index(value = ["hobbyName"], unique = true)
]
)
data class Hobby(
#PrimaryKey
var hobbyId: Long?=null,
var hobbyName: String /* UNIQUE Index so no duplicated hobby names */
)
/* The Mapping Table
Note also know as reference table, associative table and other names
*/
/* POJO for extracting a Person with thier list of Hobbies */
data class PersonWithHobbies(
#Embedded
var person: Person,
#Relation(
entity = Hobby::class,
parentColumn = "id",
entityColumn = "hobbyId",
associateBy = Junction(
value = PersonHobbyMap::class,
parentColumn = "personIdMap",
entityColumn = "hobbyIdMap"
)
)
var hobbies: List<Hobby>
)
/* This is the Mapping Table that maps people to hobbies */
#Entity(
primaryKeys = ["personIdMap","hobbyIdMap"],
/* Option but suggested foreign key constraint definitions to enforce and maintain referential integrity
*/
foreignKeys = [
/* For the reference to the Person */
ForeignKey(
entity = Person::class, /* The parent #Entity annotated class */
parentColumns = ["id"], /* The column in the parent that is referenced */
childColumns = ["personIdMap"], /* the column in this table that holds the reference to the parent */
onDelete = ForeignKey.CASCADE, /* will delete rows in the table if the parent is deleted */
onUpdate = ForeignKey.CASCADE /* will update the value, if the value (id) in the parent is changed */
),
/* For the reference to the Hobby */
ForeignKey(
entity = Hobby::class,
parentColumns = ["hobbyId"],
childColumns = ["hobbyIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class PersonHobbyMap(
var personIdMap: Long,
#ColumnInfo(index = true) /* more efficient to have index on the 2nd column (first is indexed as first part of the Primary key) */
var hobbyIdMap: Long
)
refer to the comments
An #Dao annotated interface with functions to insert data and also to extract persons (with and without their hobbies) if they have any of the hobbies passed (a query for using the hobby id's and another for using the hobby names)
note that the way that Room works ALL hobbies are retrieved per person (for the first 2 queries) that is extracted.
:-
#Dao
interface TheDaos {
/* Inserts */
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(person: Person): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(hobby: Hobby): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(personHobbyMap: PersonHobbyMap): Long
/* Query for retrieving the Person and their hobbies if they have hobbies according to the provided list of hobbyId's */
#Transaction
#Query("SELECT DISTINCT person.* FROM person JOIN personHobbyMap ON person.id = personHobbyMap.personIdMap JOIN hobby ON personHobbyMap.hobbyIdMap = hobby.hobbyId WHERE hobbyId IN(:hobbyIdList);")
fun getPersonsWithHobbiesIfHobbiesInListOfHobbyIds(hobbyIdList: List<Long>): List<PersonWithHobbies>
/* Query for retrieving the Person and their hobbies if they have hobbies according to the provided list of hobby names's */
#Transaction
#Query("SELECT DISTINCT person.* FROM person JOIN personHobbyMap ON person.id = personHobbyMap.personIdMap JOIN hobby ON personHobbyMap.hobbyIdMap = hobby.hobbyId WHERE hobbyName IN(:hobbyNameList);")
fun getPersonsWithHobbiesIfHobbiesInListOfHobbyNames(hobbyNameList: List<String>): List<PersonWithHobbies>
/* The equivalent of the above 2 queries BUT only gets the Person (without Hobbies) */
#Query("SELECT DISTINCT person.* FROM person JOIN personHobbyMap ON person.id = personHobbyMap.personIdMap JOIN hobby ON personHobbyMap.hobbyIdMap = hobby.hobbyId WHERE hobbyId IN(:hobbyIdList);")
fun getPersonsIfHobbiesInListOfHobbyIds(hobbyIdList: List<Long>): List<Person>
#Query("SELECT DISTINCT person.* FROM person JOIN personHobbyMap ON person.id = personHobbyMap.personIdMap JOIN hobby ON personHobbyMap.hobbyIdMap = hobby.hobbyId WHERE hobbyName IN(:hobbyNameList);")
fun getPersonsIfHobbiesInListOfHobbyNames(hobbyNameList: List<String>): List<Person>
/* NOTE
without DISTINCT or without only selecting the columns for the Person only,
if a Person has multiple matches then that person would be extracted multiple times.
*/
}
The #Database annotated class (note .allowMainThreadQueries used for brevity and convenience):-
#Database(entities = [Person::class,Hobby::class,PersonHobbyMap::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getTheDaos(): TheDaos
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
}
}
}
Finally activity code that puts it all together, adding some data and then querying the data selecting only the Person (with and then without their list of hobbies) :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: TheDaos
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getTheDaos()
val h1 = dao.insert(Hobby(hobbyName = "Basquete"))
val h2 = dao.insert(Hobby(hobbyName = "Academia"))
val h3 = dao.insert(Hobby(hobbyName = "Musica"))
val h4 = dao.insert(Hobby(hobbyName = "Anatomia"))
val h5 = dao.insert(Hobby(hobbyName = "other"))
val h6 = dao.insert(Hobby(hobbyName = "another"))
val h7 = dao.insert(Hobby(hobbyName = "yet another"))
val p1 = dao.insert(Person(name = "Bruno", age = 23))
val p2 = dao.insert(Person(name = "Sarah", age = 21))
val p3 = dao.insert(Person(name = "Tom", age = 22))
val p4 = dao.insert(Person(name = "Mary", age = 20))
val p5 = dao.insert(Person(name = "Freda", age = 19))
/* Bruno has hobbies Basquete, Academia, Musica and Anatomia */
dao.insert(PersonHobbyMap(p1,h1))
dao.insert(PersonHobbyMap(p1,h2))
dao.insert(PersonHobbyMap(p1,h3))
dao.insert(PersonHobbyMap(p1,h4))
/* Sarah has hobbies Academia, Anatomia and another */
dao.insert(PersonHobbyMap(p2,h2))
dao.insert(PersonHobbyMap(p2,h4))
dao.insert(PersonHobbyMap(p2,h6))
/* Tom has hobbies Basquete, Musica, other and yet another */
dao.insert(PersonHobbyMap(p3,h1))
dao.insert(PersonHobbyMap(p3,h3))
dao.insert(PersonHobbyMap(p3,h5))
dao.insert(PersonHobbyMap(p4,h7))
/* Mary has hobbies other, another and yet another */
dao.insert(PersonHobbyMap(p4,h5))
dao.insert(PersonHobbyMap(p4,h6))
dao.insert(PersonHobbyMap(p4,h7))
/* Freda has no Hobbies */
val sb: StringBuilder = java.lang.StringBuilder()
/* Persons and their hobbies for those that have Basquete or Academia in their list of hobbies (hobbies to include via list of hobbyId's)*/
/* i.e. Bruno (both) and Sarah (Academia) and Tom (both) */
for(pwh in dao.getPersonsWithHobbiesIfHobbiesInListOfHobbyIds(listOf(h1,h2))) {
sb.clear()
for (h in pwh.hobbies) {
sb.append("\n\t${h.hobbyName}")
}
Log.d("DBINFO_TEST1","Person is ${pwh.person.name} and has ${pwh.hobbies.size} hobbies. They are:- ${sb}")
}
/* Persons and their hobbies for those that have Basquete or Musica in their list of hobbies (hobbies to include via list of Hobby names)*/
/* i.e. Bruno (both) and Tom (Musica) */
for(pwh in dao.getPersonsWithHobbiesIfHobbiesInListOfHobbyNames(listOf("Basquete","Musica"))) {
sb.clear()
for (h in pwh.hobbies) {
sb.append("\n\t${h.hobbyName}")
}
Log.d("DBINFO_TEST2","Person is ${pwh.person.name} and has ${pwh.hobbies.size} hobbies. They are:- ${sb}")
}
}
}
Result of running the above (i.e. output to the log) :-
2022-07-28 09:35:36.954 D/DBINFO_TEST1: Person is Bruno and has 4 hobbies. They are:-
Basquete
Academia
Musica
Anatomia
2022-07-28 09:35:36.954 D/DBINFO_TEST1: Person is Tom and has 3 hobbies. They are:-
Basquete
Musica
other
2022-07-28 09:35:36.954 D/DBINFO_TEST1: Person is Sarah and has 3 hobbies. They are:-
Academia
Anatomia
another
2022-07-28 09:35:36.958 D/DBINFO_TEST2: Person is Bruno and has 4 hobbies. They are:-
Basquete
Academia
Musica
Anatomia
2022-07-28 09:35:36.959 D/DBINFO_TEST2: Person is Tom and has 3 hobbies. They are:-
Basquete
Musica
other
In addition to utilising the IN expression/clause, this recommended way of storing the data, although a little more complex, offers advantages (at least from a relational database perspective) such as:-
the reduction of bloat (the space taken up by the additional data on converting to/from JSON strings such as delimiters)
The data is normalised from a data redundancy aspect e.g. instead of storing Basquete n times it is stored just once and referenced.
if referenced by rowid (in short and integer primary key, as per the example) then SQLite accessing such data via the index is up to twice as fast.
might be unnoticeable for a small amount of data
there will likely be increased efficiency due to the reduced space usage (more data can be buffered)
storage space taken up will be less or will contain more data for the same size (SQLite stores chunks of data which may contain free space)

Inserting a List of Objects in Android Room Database with Kotlin

A Beginner in Android struggling to get around this limitation with Room Database. I'm working with two tables, Clothing, and Outfits. A user can create an Outfit by inserting the values presented to them. Then on a separate page, a user can insert an Outfit with the previous clothing they already created in the Clothing.kt. For the sake of the application, the relationship will only be one-to-many, meaning I only need to create one Outfit using many Clothing Items. Here is my code so far:
Clothing.kt
#Parcelize
#Entity(foreignKeys = [
ForeignKey(entity = Outfit::class,
parentColumns = ["id"],
childColumns = ["outfitRefFK"]
)
]
)
data class Clothing (
//Sets all attributes and primary key
#PrimaryKey(autoGenerate = true) val id: Int,
val type: String,
val color: String,
val style: String,
val description: String,
val dateAdded: Date = Date(),
val brand: String,
val theme: String,
val image: String,
#Nullable val outfitRefFK: Int
): Parcelable
Outfit.kt
#Parcelize
#Entity
data class Outfit (
#PrimaryKey(autoGenerate = true) val id: Int,
val outfitName: String,
#Ignore
val ClothingItems: List<Clothing>
):Parcelable
I've looked at a number of Android Developer Documentations, and they all mention how to Query the Outfits with the same Clothing List, but NOT how to Insert a New outfit with a List objects.
To my knowledge, SQLite cannot handle Lists. So, one approach I tried was to use a Type Converter, however, I struggled to implement this into my code, mostly because I'm new to GSON.
An example, from Google Android Docs that I have been trying to implement, is not quite making sense to me but it seems that it's possible to insert a list of objects following POJO:
Google Insert Example:
#Dao
public interface MusicDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
public fun insertSongs(varargs songs: Song)
#Insert
public fun insertBoth(song1: Song, song2: Song)
#Insert
public fun insertAlbumWithSongs(album: Album, songs: List<Song>);
}
I'm assuming my goal is to replicate this with a similar approach, creating an Outfit from List. From what I can tell, Google Docs uses 3 Tables (Music, Album, and Song), so I've been struggling with where I can modify my DB. Should I create a third Table? Has anyone come to a similar conclusion with Kotlin?
If any of you have solved this or come close, any suggestions are much appreciated.
For other sources here are my Dao's for the Tables, there not finished yet, as I couldn't figure out a way to store the Clothing Items.
Clothing.Dao
#Dao
interface ClothingDao {
//Ignores when the exact same data is put in
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addClothing(clothing: Clothing)
#Update
suspend fun updateClothing(clothing: Clothing)
#Delete
suspend fun deleteClothing(clothing: Clothing)
#Query("DELETE FROM Clothing")
suspend fun deleteAllClothing()
#Query("SELECT * FROM Clothing ORDER BY id ASC")
fun readAllData(): LiveData<List<Clothing>>
#Query("SELECT * FROM Clothing WHERE type='Top' ORDER BY id ASC")
fun selectClothingTops(): LiveData<List<Clothing>>
//Called in ListFragment Searchbar. Queries Clothing Type or Clothing Color.
#Query("SELECT * FROM Clothing WHERE type LIKE :searchQuery OR color LIKE :searchQuery")
fun searchDatabase(searchQuery: String): LiveData<List<Clothing>>
}
OutfitDao.kt
#Dao
interface OutfitDao {
// Grabs data from Outfit Table, necessary for each other Query to read
// from in the Outfit Repository class
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addOutfit(outfit: Outfit)
#Query("SELECT * FROM Outfit ORDER BY id ASC")
fun readAllData(): LiveData<List<Outfit>>
}
To my knowledge, SQLite cannot handle Lists. So, one approach I tried was to use a Type Converter, however, I struggled to implement this into my code, mostly because I'm new to GSON.
1). Add the Gson library to your project e.g. in your build.gradle (module) :-
implementation 'com.google.code.gson:gson:2.9.0'
2). Add a data class e.g ClothingList :-
data class ClothingList(
val clothingList: List<Clothing>
)
3). Amend the Outfit class to use the ClothingList as opposed to List and also remove the #Ignore annotation e.g. :-
#Entity
data class Outfit (
#PrimaryKey(autoGenerate = true) val id: Int, /* more correct to use Long */
val outfitName: String,
//#Ignore
val ClothingItems: ClothingList
)
autogenerated columns are more correctly Long's rather than Int's as in theory the stored value can be up to 64bits signed.
4). Add a new class for the TypeConverters e.g. MyTypeConverters :-
class MyTypeConverters {
#TypeConverter
fun fromDateToLong(date: Date): Long {
return date.time
}
#TypeConverter
fun fromLongToDate(date: Long): Date {
return Date(date)
}
#TypeConverter
fun fromClothingToJSON(clothinglist: ClothingList): String {
return Gson().toJson(clothinglist)
}
#TypeConverter
fun fromJSONToClothing(json: String): ClothingList {
return Gson().fromJson(json,ClothingList::class.java)
}
}
5). Amend the #Database annotated class (has the highest scope) to have the #TypeConverters annotation e.g.
#TypeConverters(value = [MyTypeConverters::class])
#Database(entities = [Clothing::class,Outfit::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
....
}
You can them have a list of clothing within an outfit. However, that is not really the ideal way, from a relational database aspect as it will introduce complexities due to the whole list of clothes being a single stored value.
Your second attempt (what appears to be) ties an item of clothing to just one outfit, So your "blue jeans" if used in a number of outfits would have to be repeated.
Suggested Solution
I'd suggest that the better solution would be for a many-many relationship, so an outfit can use any number of clothing items and a clothing item can be used by any number of outfits. Thus your "blue jeans" would be a single row.
To utilise a many-many relationship you have an intermediate table that is a cross reference between the outfit and the item of clothing. i.e. a column for the id of the outfit and a column for the id of the item of clothing. There is then no need for Type Converters or storing Lists
Working Example
Consider the following working example:-
The OutFit class
#Entity
data class Outfit(
#PrimaryKey
#ColumnInfo(name = "outfitId")
val id: Long?=null,
val outfitName: String
)
And the Clothing Class
#Entity
data class Clothing (
//Sets all attributes and primary key
#PrimaryKey/*(autoGenerate = true) inefficient not needed*/
#ColumnInfo(name = "clothingId") /* suggest to have unique column names */
val id: Long?=null, /* Long rather than Int */
val type: String,
val color: String,
val style: String,
val description: String,
val dateAdded: Date = Date(),
val brand: String,
val theme: String,
val image: String
)
The intermediate (mapping, associative, reference and other names) table for a many-many relationship
#Entity(
primaryKeys = ["outfitIdRef","clothingIdRef"],
foreignKeys = [
ForeignKey(
entity = Outfit::class,
parentColumns = ["outfitId"],
childColumns = ["outfitIdRef"],
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Clothing::class,
parentColumns = ["clothingId"],
childColumns = ["clothingIdRef"],
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)
]
)
data class OutFitClothingMappingTable (
val outfitIdRef: Long,
#ColumnInfo(index = true)
val clothingIdRef: Long
)
A POJO class OutFitWithClothingList for getting an Outfit with it's related List of clothing.
data class OutFitWithClothingList(
#Embedded
val outfit: Outfit,
#Relation(
entity = Clothing::class,
parentColumn = "outfitId",
entityColumn = "clothingId",
associateBy = Junction(
value = OutFitClothingMappingTable::class,
parentColumn = "outfitIdRef",
entityColumn = "clothingIdRef"
)
)
val clothingList: List<Clothing>
)
A POJO the opposite way around a Clothing Item with the Outfits that use it
data class ClothingWithOutFitsList(
#Embedded
val clothing: Clothing,
#Relation(
entity = Outfit::class,
parentColumn = "clothingId",
entityColumn = "outfitId",
associateBy = Junction(
value = OutFitClothingMappingTable::class,
parentColumn = "clothingIdRef",
entityColumn = "outfitIdRef"
)
)
val outfitList: List<Outfit>
)
A class with TypeConverters for the Date (stores date as a integer i.e. Long) :-
class TheTypeConverters {
#TypeConverter
fun fromDateToLong(date: Date): Long {
return date.time
}
#TypeConverter
fun fromLongToDate(date: Long): Date {
return Date(date)
}
}
A single (for brevity/convenience) #Dao annotated class Alldao including Queries to get all the Outfits with their List of clothing and also to get all the Clothing Items with the Outfits used, and of course inserts to insert into the tables.
#Dao
interface AllDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun addOutfit(outfit: Outfit): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun addClothing(clothing: Clothing): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun addOutfitClothingMap(outFitClothingMappingTable: OutFitClothingMappingTable): Long /* value not of much use other than if 1 or greater insert, if -1 not inserted */
#Query("SELECT * FROM clothing")
fun getAllClothing(): List<Clothing>
#Query("SELECT * FROM outfit")
fun getAllOutfits(): List<Outfit>
#Query("SELECT * FROM outfit")
fun getAllOutfitsWithClothingList(): List<OutFitWithClothingList>
#Query("SELECT * FROM clothing")
fun getAllClothingWithOutfitList(): List<ClothingWithOutFitsList>
}
An #Database annotated class (note for brevity and convenience uses .allowMainThreadQuesries)
#TypeConverters(value = [TheTypeConverters::class])
#Database(entities = [Outfit::class,Clothing::class,OutFitClothingMappingTable::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
#Volatile
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
}
}
}
TypeConverters defined at the database level (highest scope)
Finally activity code to demonstrate inserting Outfits, Clothing and mappings and the extraction of All Outfits with the list if clothing and All Clothing with the List of Outfits that use the item of clothing.
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 outfit1 = dao.addOutfit(Outfit(outfitName = "Outfit1"))
val outfit2 = dao.addOutfit(Outfit(outfitName = "Outfit2"))
val clothing1 = dao.addClothing(Clothing(type = "Top", color = "Red", description = "Singlet",brand = "Fred's Clothing Inc", theme = "whatever", image = "image001", style = "style1"))
val clothing2 = dao.addClothing(Clothing(type = "Bottom", color = "Blue", description = "Shorts",brand = "AC", theme = "whatever", image = "image002", style = "style2"))
val clothing3 = dao.addClothing(Clothing(type = "Bottom", color = "White", description = "Skirt",brand = "AC", theme = "whatever", image = "image003", style = "style3"))
val clothing4 = dao.addClothing(Clothing(type = "Hat", color = "Brown", description = "Hat with feather",brand = "AC", theme = "whatever", image = "image003", style = "style4"))
// etc
dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit1,clothing1))
dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit1,clothing2))
dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit2,clothing1))
dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit2,clothing3))
dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit2,clothing4))
for (owc in dao.getAllOutfitsWithClothingList()) {
Log.d("DBINFO","Outfit is ${owc.outfit.outfitName} ID is ${owc.outfit.id}, it has ${owc.clothingList.size} Items of Clothing, they are:-")
for (c in owc.clothingList) {
Log.d("DBINFO","\tClothing Item desc is ${c.description} Date is ${c.dateAdded} Brand is ${c.brand} type is ${c.type} etc")
}
}
for (cwo in dao.getAllClothingWithOutfitList()) {
Log.d("DBINFO","Clothing is ${cwo.clothing.description} color is ${cwo.clothing.color} it is used by ${cwo.outfitList.size } Outfits, they are:-")
for(o in cwo.outfitList) {
Log.d("DBINFO","\tOutfit is ${o.outfitName} it's ID is ${o.id}")
}
}
}
}
Result (output to the log)
2022-05-01 08:55:15.287 D/DBINFO: Outfit is Outfit1 ID is 1, it has 2 Items of Clothing, they are:-
2022-05-01 08:55:15.294 D/DBINFO: Clothing Item desc is Singlet Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is Fred's Clothing Inc type is Top etc
2022-05-01 08:55:15.294 D/DBINFO: Clothing Item desc is Shorts Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is AC type is Bottom etc
2022-05-01 08:55:15.294 D/DBINFO: Outfit is Outfit2 ID is 2, it has 3 Items of Clothing, they are:-
2022-05-01 08:55:15.294 D/DBINFO: Clothing Item desc is Singlet Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is Fred's Clothing Inc type is Top etc
2022-05-01 08:55:15.294 D/DBINFO: Clothing Item desc is Skirt Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is AC type is Bottom etc
2022-05-01 08:55:15.295 D/DBINFO: Clothing Item desc is Hat with feather Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is AC type is Hat etc
2022-05-01 08:55:15.298 D/DBINFO: Clothing is Singlet color is Red it is used by 2 Outfits, they are:-
2022-05-01 08:55:15.298 D/DBINFO: Outfit is Outfit1 it's ID is 1
2022-05-01 08:55:15.298 D/DBINFO: Outfit is Outfit2 it's ID is 2
2022-05-01 08:55:15.298 D/DBINFO: Clothing is Shorts color is Blue it is used by 1 Outfits, they are:-
2022-05-01 08:55:15.298 D/DBINFO: Outfit is Outfit1 it's ID is 1
2022-05-01 08:55:15.298 D/DBINFO: Clothing is Skirt color is White it is used by 1 Outfits, they are:-
2022-05-01 08:55:15.298 D/DBINFO: Outfit is Outfit2 it's ID is 2
2022-05-01 08:55:15.298 D/DBINFO: Clothing is Hat with feather color is Brown it is used by 1 Outfits, they are:-
2022-05-01 08:55:15.298 D/DBINFO: Outfit is Outfit2 it's ID is 2
Via AppInspection i.e. the data stored in the database
and the mapping table
Additional regrading #Relation
When you use #Relation, ALL the children are retrieved irrespective for the objects and they will be in whatever order suits the query optimizer. This can be frustrating/confusing if you have specified ORDER or WHERE clauses.
Here's some example queries that demonstrate
a) your query which is fine if say when creating an outfit you only want to select Tops
b) a query where you want only to find Outfits that have Tops and list all clothes (via #Relation)
-c) a query where you want to find Outfits that have tops but to then only list the clothing that are Tops (demonstrates how to get around the #Relation get all children and get only some children)
No changes other than additional #Dao functions and the activity code to demo them
So the additional #Dao functions are
#Transaction
#Query("SELECT * FROM outfit " +
" JOIN outfitclothingmappingtable ON outfit.outfitId = outfitclothingmappingtable.outfitIdRef " +
" JOIN clothing ON clothingIdRef = clothingId " +
"WHERE clothing.type LIKE :searchQuery OR color LIKE :searchQuery")
fun getOutfitsWithClothingSearchingClothing(searchQuery: String): List<OutFitWithClothingList>
/* NOTE */
/* As this uses #Relation the outfits returned will contain ALL related clothing items */
/* Things can get a little complicated though due to #Relation */
/* Say you wanted a List of the Outfits that include specific clothing and to only list those clothing items not ALL */
/* Then 2 queries and a final function that invokes the 2 queries is easiest */
/* However the first query (the actual SQL) has all the data but would need a loop to select apply the clothing to the outfits */
#Query("SELECT * FROM outfit " +
" JOIN outfitclothingmappingtable ON outfit.outfitId = outfitclothingmappingtable.outfitIdRef " +
" JOIN clothing ON clothingIdRef = clothingId " +
"WHERE clothing.type LIKE :searchQuery OR color LIKE :searchQuery")
fun getOutfitsOnlySearchingClothing(searchQuery: String): List<Outfit>
#Query("SELECT * FROM outfitclothingmappingtable JOIN clothing ON clothingIdRef = clothingId WHERE (type LIKE :searchQuery OR color LIKE :searchQuery) AND outfitIdRef=:outfitId")
fun getClothingThatMatchesSearchForAnOutfit(searchQuery: String, outfitId: Long): List<Clothing>
#Transaction
#Query("")
fun getOutfitsWithOnlyClothingsThatMatchSearch(searchQuery: String): List<OutFitWithClothingList> {
val rv = mutableListOf<OutFitWithClothingList>()
val outfits = getOutfitsOnlySearchingClothing(searchQuery)
for (o in outfits) {
rv.addAll(listOf(OutFitWithClothingList(o,getClothingThatMatchesSearchForAnOutfit(searchQuery,o.id!!))))
}
return rv
}
note that tablename.column has been used but not universally, the tablename.column is only required if the column names are ambiguous (hence why the #ColumnInfo(name = ??) was used for the id columns so they are not ambiguos.
if the column names are ambiguous and you use tablename.column name, the columns names extracted will have the same name and Room will select only the last so outfit.id would be the same value as clothing.id, again avoided by using unique column names.
So the tablename.column has only been used to show it's use.
The activity, to demonstrate, could then include :-
/* Your Query */
for (c in dao.searchDatabase("Top")) {
Log.d("SRCHINFO1","Clothing is ${c.description} ....")
}
/* #Relation Limited Search complete outfit (all clothing) that has type of Top */
for(owc in dao.getOutfitsWithClothingSearchingClothing("Top")) {
Log.d("SRCHINFO2","Outfit is ${owc.outfit.outfitName}")
for (c in owc.clothingList) {
Log.d("SRCHINFO2c","Clothing is ${c.description} ....")
}
}
/* Only the Outfits that match the search with the clothing that fits the search NOT ALL CLothing*/
for(owc in dao.getOutfitsWithOnlyClothingsThatMatchSearch("Top")) {
Log.d("SRCHINFO3","Outfit is ${owc.outfit.outfitName}")
for (c in owc.clothingList) {
Log.d("SRCHINFO3c","Clothing is ${c.description} ....")
}
}
And the output would be (first run) :-
2022-05-01 13:31:52.485 D/SRCHINFO1: Clothing is Singlet ....
2022-05-01 13:31:52.488 D/SRCHINFO2: Outfit is Outfit1
2022-05-01 13:31:52.488 D/SRCHINFO2c: Clothing is Singlet ....
2022-05-01 13:31:52.488 D/SRCHINFO2c: Clothing is Shorts ....
2022-05-01 13:31:52.489 D/SRCHINFO2: Outfit is Outfit2
2022-05-01 13:31:52.489 D/SRCHINFO2c: Clothing is Singlet ....
2022-05-01 13:31:52.489 D/SRCHINFO2c: Clothing is Skirt ....
2022-05-01 13:31:52.489 D/SRCHINFO2c: Clothing is Hat with feather ....
2022-05-01 13:31:52.494 D/SRCHINFO3: Outfit is Outfit1
2022-05-01 13:31:52.494 D/SRCHINFO3c: Clothing is Singlet ....
2022-05-01 13:31:52.494 D/SRCHINFO3: Outfit is Outfit2
2022-05-01 13:31:52.494 D/SRCHINFO3c: Clothing is Singlet ....
Your query finds Singlet
The #Relation query finds 2 Outfits that use Singlet and lists all of the clothing
The last query finds the 2 OutFits that use Singlet but only lists the Singlet not all the other clothing (as wanted)

use orderBy to get embedded relation-defined table from Android Room

For this transaction Query in Android Room :
#Transaction
#Query("SELECT * FROM TPAGroup WHERE zuid=:zuid ORDER BY `index`")
fun getGroupWithSecretsForZuid(zuid: String): List<TPAGroupWithSecrets>
and this as my data class :
data class TPAGroupWithSecrets(
#Embedded val group: TPAGroup,
#Relation(
parentColumn = "groupId",
entityColumn = "groupId"
)
var secrets: MutableList<TPASecrets>
)
I get the TPAGroup in the right order , but TPASecrets have not been ordered ! How can i get both of them in right order , ordered by their index ( which is a column common to both tables ) ?
When #Relation is used, Room gets the related objects, as you have found, without any specific order (the order will likely be by the primary key but that depends upon SQLite's query optimiser).
If you need them ordered you can either
sort the returned collection or
you can effectively override/bypass the #Relation processing that Room implements.
use a single query that orders accordingly and then builds the result from the cartesian product (see bottom for a partial example)
Here's a Working Example of 2
TPAGroup (made up)
#Entity
data class TPAGroup(
#PrimaryKey
val groupId: Long? = null,
val zuid: String,
val index: Long,
)
TPASecrets (made up)
#Entity
data class TPASecrets(
#PrimaryKey
val secretId: Long? = null,
val groupId: Long,
val index: Long
)
TPAGroupWithSecrets (uncanged)
data class TPAGroupWithSecrets(
#Embedded val group: TPAGroup,
#Relation(
parentColumn = "groupId",
entityColumn = "groupId"
)
var secrets: MutableList<TPASecrets>
)
An #Dao annotated class
#Dao
interface AllDAO {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(tpaGroup: TPAGroup): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(tpaSecrets: TPASecrets): Long
#Query("SELECT * FROM TPASecrets WHERE groupId=:groupId ORDER BY `index`;")
fun getRelatedSecrets(groupId: Long): MutableList<TPASecrets>
#Query("SELECT * FROM TPAGroup WHERE zuid=:zuid ORDER BY `index`;")
fun getGroupsForZuid(zuid: String): MutableList<TPAGroup>
#Transaction
#Query("")
fun getGroupWithSecretsForZuid(zuid: String): List<TPAGroupWithSecrets> {
val rv = ArrayList<TPAGroupWithSecrets>()
for(t in getGroupsForZuid(zuid)) {
rv.add(TPAGroupWithSecrets(t,getRelatedSecrets(t.groupId!!)))
}
// rv.sortBy { .... }
return rv
}
}
Note the #Query's and especially the last which bypasses Rooms #Relation handling (i.e. the TPAGroupWithSecrets are built outside of room)
an #Database annotated class to tie all the Room stuff together TheDatabase
#Database(entities = [TPAGroup::class,TPASecrets::class], version = 1, exportSchema = false)
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
}
}
}
set to run on the main thread for convenience and brevity
Finally putting it into action in an Activity:-
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(TPAGroup(groupId = 1,zuid = "Group1", index = 10))
dao.insert(TPAGroup(groupId = 2, zuid = "Group1", index = 9))
dao.insert(TPAGroup(groupId = 3, zuid = "Group1", index = 11))
dao.insert(TPASecrets(1000,1,5))
dao.insert(TPASecrets(1010,groupId = 1, index = 4))
dao.insert(TPASecrets(1020,1,3))
dao.insert(TPASecrets(2000,2,5))
dao.insert(TPASecrets(2010,2,6))
dao.insert(TPASecrets(2020,2,7))
dao.insert(TPASecrets(2030,2,1))
dao.insert(TPASecrets(2040,2,2))
dao.insert(TPASecrets(2050,2,3))
dao.insert(TPASecrets(3000,3,1))
dao.insert(TPASecrets(3010,3,0))
for(tgws in dao.getGroupWithSecretsForZuid("Group1")) {
Log.d("DBINFO","TPAGroup is ${tgws.group.groupId} Index is ${tgws.group.index}. It has ${tgws.secrets.size} Secrets, they are :-")
for (s in tgws.secrets) {
Log.d("DBINFO","\tSecret is ${s.secretId} Index is ${s.index}")
}
}
}
}
The result output to the log (noting that the data has been purposefully inserted to demonstrate sorting):-
2022-04-13 21:37:29.220 D/DBINFO: TPAGroup is 2 Index is 9. It has 6 Secrets, they are :-
2022-04-13 21:37:29.220 D/DBINFO: Secret is 2030 Index is 1
2022-04-13 21:37:29.220 D/DBINFO: Secret is 2040 Index is 2
2022-04-13 21:37:29.220 D/DBINFO: Secret is 2050 Index is 3
2022-04-13 21:37:29.220 D/DBINFO: Secret is 2000 Index is 5
2022-04-13 21:37:29.220 D/DBINFO: Secret is 2010 Index is 6
2022-04-13 21:37:29.220 D/DBINFO: Secret is 2020 Index is 7
2022-04-13 21:37:29.221 D/DBINFO: TPAGroup is 1 Index is 10. It has 3 Secrets, they are :-
2022-04-13 21:37:29.221 D/DBINFO: Secret is 1020 Index is 3
2022-04-13 21:37:29.221 D/DBINFO: Secret is 1010 Index is 4
2022-04-13 21:37:29.221 D/DBINFO: Secret is 1000 Index is 5
2022-04-13 21:37:29.221 D/DBINFO: TPAGroup is 3 Index is 11. It has 2 Secrets, they are :-
2022-04-13 21:37:29.221 D/DBINFO: Secret is 3010 Index is 0
2022-04-13 21:37:29.221 D/DBINFO: Secret is 3000 Index is 1
So TPAGroups are sorted according to the value if the Index (2 with index 9 is first, 3 with index 10 2nd and 3 with index 11 3rd)
You can easily see that the Secrets are ordered according to thier index rather than their primary key secrteId
Partial Example of option 3
A query such as
SELECT * FROM TPAGroup JOIN TPASecrets ON TPASecrets.groupid = TPAGroup.groupid ORDER BY TPAGroup.`index` ASC, TPASecrets.`index`;
Would produce data (using the data loaded by the working example):-
You would then need to have a POJO to receive the data. However there's an issue with duplicate columns names index and groupid so the query is more complicated requiring aliases (AS) e.g. you could use
SELECT TPAGroup.*, TPASecrets.secretId, TPASecrets.`index` AS secretIndex FROM TPAGroup JOIN TPASecrets ON TPASecrets.groupid = TPAGroup.groupid ORDER BY TPAGroup.`index` ASC, TPASecrets.`index`;
So the duplicated groupid (which would always have the same value in both) is dropped from TPASecrets and the TPASecrets column is aliased/renamed as secretsIndex. Obviously the POJO would have to cater for this.
You then have to build each TPAGroup with it's TPASecrets by looping through the results.
Not done/shown as most tend to opt for option 1 or 2 and tend to baulk at option 3. However, option 3 is probably the more efficient as there is just the single query (no need for #Transaction).

Categories

Resources