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)
Related
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.
I make a Room database and have the Entity Data classes CastDb and CastDbModel.
When I build the project I get the following error. But the thing is that CastDb class doesn't suppose to have an id field. Any Idea how to fit it?
Cannot find the child entity column `id` in com.mvvm.data.db.entities.CastDb. Options: name, profile_path, character
private com.mvvm.data.db.convertermodels.Cast cast;
#Entity(tableName = "cast_model")
data class CastDbModel(
#PrimaryKey(autoGenerate = false)
var id : Int,
#ColumnInfo (name = "cast")
var cast : Cast
)
{
constructor() : this(0, Cast(CastDb()))
}
#Entity(tableName = "cast")
data class CastDb(
#PrimaryKey(autoGenerate = false)
var name: String,
var profile_path: String,
var character: String)
{
constructor() : this("", "", "")
}
Also, I have a converter model class Cast
data class Cast(
#Embedded
var cast: CastDb
)
Converter class:
class CastConverter {
#TypeConverter
fun fromCast(value: Cast): String {
return Gson().toJson(value)
}
#TypeConverter
fun toCast(value: String): Cast {
return Gson().fromJson(value, Cast::class.java)
}
}
The code you have supplied is fine as far as compiling is concerned. I suspect that your issue with code elsewhere that utilises the CastDB, perhaps (considering it mentions child column) an attempt to form a relationship where the ClassDBModel class has been inferred but that the Cast class has been provided (and hence why the id column is missing).
So the error is either in some other code or possibly that you have issues with the dependencies. As such you need to locate the other code or check the Room dependencies including version (are you using the same version?, are you using the latest stable version (2.4.3 or perhaps 2.5.0-alpha02)?).
However, I think you are getting mixed up with relationships and what data should be stored where.
With the code above you have two database tables cast_model and cast where the cast_model contains(embeds) a cast. So you would appear to be trying to save the same data twice (if not then as it stands the cast table is redundant)
Using your code plus the following in an #Dao annotated interface :-
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(castDbModel: CastDbModel): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(castDb: CastDb): Long
#Query("SELECT * FROM cast")
fun getAllCast(): List<Cast>
#Query("SELECT * FROM cast_model")
fun getAllFromCastModel(): List<CastDbModel>
And then using the code in an activity (note run on the main thread for brevity and convenience) :-
castdb = CastDatabase.getInstance(this) /* Get an instance of the database */
castDao = castdb.getCastDao() /* get the Dao instance */
/* Prepare some Cast objects (with embedded CastDb objects) not store in the database as yet */
val castdb_et = CastDb(name = "Elizabeth Taylor","etaylor.text","Kate")
val castdb_rb = CastDb("Richard Burton","rburton.txt","Petruchio")
val castdb_cc = CastDb("Cyril Cusack","ccusack.txt","Grumio")
/* Store the Cast objects in the cast table */
castDao.insert(castdb_et)
castDao.insert(castdb_rb)
castDao.insert(castdb_cc)
/* Create and store some CastDbModels in the cast_model table */
castDao.insert(CastDbModel(100,Cast(castdb_et))) /* already in the cast table */
/* following cast is not in the cast table */
castDao.insert(CastDbModel(200,Cast( CastDb("Natasha Pyne","npyne.txt","Bianca")))) /* not in the cast table */
castDao.insert(CastDbModel(300,Cast(castdb_cc))) /* already in the cast table */
/* Get all of the cast's stored in the database and output them to the log */
for(c in castDao.getAllCast()) {
Log.d("CASTINFO","Name is ${c.cast.name} Profile is ${c.cast.profile_path} Character is ${c.cast.character}")
}
/* Get all the castDBModels stored in the database and output them to the log */
for (cm in castDao.getAllFromCastModel()) {
Log.d("CASTMODELINFO","CastModel ID is ${cm.id} Name is ${cm.cast.cast.name} Character is ${cm.cast.cast.character} ")
}
The log contains (as expected):-
D/CASTINFO: Name is Elizabeth Taylor Profile is etaylor.text Character is Kate
D/CASTINFO: Name is Richard Burton Profile is rburton.txt Character is Petruchio
D/CASTINFO: Name is Cyril Cusack Profile is ccusack.txt Character is Grumio
D/CASTMODELINFO: CastModel ID is 100 Name is Elizabeth Taylor Character is Kate
D/CASTMODELINFO: CastModel ID is 200 Name is Natasha Pyne Character is Bianca
D/CASTMODELINFO: CastModel ID is 300 Name is Cyril Cusack Character is Grumio
The actual data stored in the database (via App Inspection) :-
In the cast table :-
In the cast_model table :-
So you have a situation where you are either storing the same data twice and wasting storage (an probably later complicating matters such as updating data) or one of the tables is redundant.
If you are trying to relate one table to the other then there is no need e.g. why would you say, for example get Elizabeth Taylor from the cast to then get "Elizabeth Taylor" from the cast_model table, other than the id column (which has no use beyond uniquely identifying the row) the exact same data exists in both tables.
I suspect that what you want is a table with say Elizabeth Taylor's details and then perhaps a play/film where she and others, such as Richard Burton, Cyril Cusack and Natasha Pyne were characters in the play/film.
In which case you would probably want:-
a table for the actors/artists themselves
a table for the play/films
a table for the cast of the film (that links the character with the artist to the film).
often due to the object orientated approach, this is seen as a list objects that contain the character and the artist, which as objects cannot be directly stored, so converted to a JSON representation of the entire list and held in (in this case) the play/film table.
As such I believe that there is a good chance that you need to review the underlying design.
As an example (assuming films with cast (characters)) then perhaps the following may help you.
First the 3 tables i.e. the #Entity annotated classes for Films, Artists and Characters/Cast where a row in the character table has a reference (relationship) to the film and also to the artist, as well as some data (profile of the character). So similar to what you appear to be wanting:-
#Entity
data class Artist(
#PrimaryKey
var artistId: Long?=null,
var artistName: String,
var artistDOB: String
)
#Entity
data class Film(
#PrimaryKey
var filmId: Long?=null,
var filmName: String,
var filmYear: Int
)
#Entity(
primaryKeys = ["artistIdReference","filmIdReference"],
/*Optional but enforces referential integrity*/
foreignKeys = [
ForeignKey(
Artist::class,
parentColumns = ["artistId"],
childColumns = ["artistIdReference"],
/* Optional but helps to maintain referential integrity */
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
Film::class,
parentColumns = ["filmId"],
childColumns = ["filmIdReference"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Character(
var characterName: String,
var characterProfile: String,
var artistIdReference: Long,
#ColumnInfo(index = true)
var filmIdReference: Long
)
Then some POJO's for combining the related data:-
data class CharacterWithArtist(
#Embedded
var character: Character,
#Relation(
entity = Artist::class,
parentColumn = "artistIdReference",
entityColumn = "artistId"
)
var artist: Artist
)
data class FilmWithCharacters(
#Embedded
var film: Film,
#Relation(
entity = Character::class,
parentColumn = "filmId",
entityColumn = "filmIdReference"
)
var characterWithArtistList: List<CharacterWithArtist>
)
Then an #Dao annotated interface, for function to insert data and also one to extract Films with the cast including the artists.
A pretty standard #Database (other than for convenience and brevity running on the main thread is allowed) :-
#Database(entities = [Artist::class,Film::class,Character::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
}
}
}
With all the above, perhaps note that no Type Converters are required, also not some of the comments (such as in regard to the Foreign Keys).
Finally putting it all together in an activity that adds two Films, 4 artists, and then for the first film adds the cast/characters for the film. After inserting the data, the data is extracted and output to the log handling the related data accordingly to provide The Film and the cast with the actor:-
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 etaylorId = dao.insert(Artist(artistName = "Elizabeth Taylor", artistDOB = "1932-02-27"))
val rburtonId = dao.insert(Artist(artistName = "Richard Burton", artistDOB = "1925-11-10"))
val ccusackId = dao.insert(Artist(artistName = "Cyril Cusack", artistDOB = "1910-11-26"))
val npyneId = dao.insert(Artist(artistName = "Natasha Pyne", artistDOB = "1946-07-09"))
val tots = dao.insert(Film(filmName = "Taming of the Shrew", filmYear = 1967))
val sw = dao.insert(Film(filmName = "Star Wars", filmYear = 1997))
dao.insert(Character(characterName = "Kate","Katherina (Kate) Minola is a fictional character in William Shakespeare's play The Taming of the Shrew.",etaylorId,tots))
dao.insert(Character(characterName = "Petruchio","Tamer of Kate",rburtonId,tots))
dao.insert(Character(characterName = "Grumio", characterProfile = "blah",ccusackId,tots))
dao.insert(Character(characterName = "Bianca","Kate's sister",npyneId,tots))
for(fwc in dao.getAllFilmsWithCast()) {
val sb = StringBuilder()
for (cwa in fwc.characterWithArtistList) {
sb.append("\n\t Character is ${cwa.character.characterName} played by ${cwa.artist.artistName} \n\t\t${cwa.character.characterProfile}")
}
Log.d(
"FILMINFO",
"Film is ${fwc.film.filmName} (id is ${fwc.film.filmId}) made in ${fwc.film.filmYear}. Cast of ${fwc.characterWithArtistList.size}. They are $sb")
}
}
}
The Result
The log includes:-
D/FILMINFO: Film is Taming of the Shrew (id is 1) made in 1967. Cast of 4. They are
Character is Kate played by Elizabeth Taylor
Katherina (Kate) Minola is a fictional character in William Shakespeare's play The Taming of the Shrew.
Character is Petruchio played by Richard Burton
Tamer of Kate
Character is Grumio played by Cyril Cusack
blah
Character is Bianca played by Natasha Pyne
Kate's sister
D/FILMINFO: Film is Star Wars (id is 2) made in 1997. Cast of 0. They are
The store objects version
As previously mentioned, often you will see objects stored instead of utilising the relationship aspect and the objects, as they cannot be stored directly as JSON representations of the object or a List/Array of objects.
Adapting the above example, it might be that the castlist would be stored as part of the Film as a list of characters and the artists playing the characters.
So AltFilm (so that both Film and AltFilm can co-exist) could be derived from :-
data class CastList(
var castList: List<CharacterWithArtist>
)
#TypeConverters(RoomTypeConverters::class)
#Entity
data class AltFilm(
#PrimaryKey
var filmId: Long?=null,
var filmName: String,
var filmYear: Int,
var castList: CastList
)
class RoomTypeConverters {
#TypeConverter
fun convertFromCastListToJSONString(castList: CastList): String = Gson().toJson(castList)
#TypeConverter
fun convertFromJSONStringToCastList(jsonString: String): CastList = Gson().fromJson(jsonString,CastList::class.java)
}
CastList being a single object that consists of a List of CharacterWithArtist (i.e. Artist and Character combined (to utilise the existing classes (which even though annotated with #Entity for use with the first methodology, is ignored i.e. for this usage case they can be considered to not be annotated with #Entity)))
The two TypeConverters do as their function names say.
To facilitate use of the above, then two additional functions in the #Dao annotated class AllDao :-
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(altFilm: AltFilm): Long
#Query("SELECT * FROM altFilm")
fun getAllFromAltFilm(): List<AltFilm>
Now to compare the two methodologies (utilisation of Relationships v storing objects as JSON) then the activity code can be changed to be (will add the same Films "Taming of the Shrew " and "Taming of the Shrew 2" using both methods):-
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 etaylorId = dao.insert(Artist(artistName = "Elizabeth Taylor", artistDOB = "1932-02-27"))
val rburtonId = dao.insert(Artist(artistName = "Richard Burton", artistDOB = "1925-11-10"))
val ccusackId = dao.insert(Artist(artistName = "Cyril Cusack", artistDOB = "1910-11-26"))
val npyneId = dao.insert(Artist(artistName = "Natasha Pyne", artistDOB = "1946-07-09"))
val tots = dao.insert(Film(filmName = "Taming of the Shrew", filmYear = 1967))
val sw = dao.insert(Film(filmName = "Star Wars", filmYear = 1997))
val tots2 = dao.insert(Film(filmName = "Taming of the Shrew 2", filmYear = 1967))
dao.insert(Character(characterName = "Kate","Katherina (Kate) Minola is a fictional character in William Shakespeare's play The Taming of the Shrew.",etaylorId,tots))
dao.insert(Character(characterName = "Petruchio","Tamer of Kate",rburtonId,tots))
dao.insert(Character(characterName = "Grumio", characterProfile = "blah",ccusackId,tots))
dao.insert(Character(characterName = "Bianca","Kate's sister",npyneId,tots))
/* Add a second film with the same cast (for comparison) of the 2 methodologies */
dao.insert(Character(characterName = "Kate","Katherina (Kate) Minola is a fictional character in William Shakespeare's play The Taming of the Shrew.",etaylorId,tots2))
dao.insert(Character(characterName = "Petruchio","Tamer of Kate",rburtonId,tots2))
dao.insert(Character(characterName = "Grumio", characterProfile = "blah",ccusackId,tots2))
dao.insert(Character(characterName = "Bianca","Kate's sister",npyneId,tots2))
/*Sort of replicate the above but with some changes to the actual data so it is easy to distinguish when comparing */
/* Ignoring the Start Wars Film that has not cast 2 Taming of the Shrew films with JSON representation of the cast */
dao.insert(
AltFilm(
filmName = "Taming of the Shrew",
filmYear = 1967,
castList = CastList(
listOf(
CharacterWithArtist(
Character("Kate","The Shrew",etaylorId,tots),
Artist(artistName = "Liz Taylor", artistDOB = "01-01-1900")
),
CharacterWithArtist(
Character("Petruchio","Shrew Tamer",rburtonId,tots),
Artist(artistName = "Dicky Burton", artistDOB = "02-02-1901")
)
)
)
)
)
dao.insert(
AltFilm(
filmName = "Taming of the Shrew 2",
filmYear = 1967,
castList = CastList(
listOf(
CharacterWithArtist(
Character("Kate","The Shrew",etaylorId,tots),
Artist(artistName = "Liz Taylor", artistDOB = "01-01-1900")
),
CharacterWithArtist(
Character("Petruchio","Shrew Tamer",rburtonId,tots),
Artist(artistName = "Dicky Burton", artistDOB = "02-02-1901")
)
)
)
)
)
/* Extract an output the 2 films via the relationships (Film, Artist and Character tables) */
for(fwc in dao.getAllFilmsWithCast()) {
val sb = StringBuilder()
for (cwa in fwc.characterWithArtistList) {
sb.append("\n\tCharacter is ${cwa.character.characterName} played by ${cwa.artist.artistName} \n\t\t${cwa.character.characterProfile}")
}
Log.d(
"FILMINFO",
"Film is ${fwc.film.filmName} (id is ${fwc.film.filmId}) made in ${fwc.film.filmYear}. Cast of ${fwc.characterWithArtistList.size}. They are $sb")
}
/* Do the same for the 2 films via the embedded JSON representation of the cast */
for (af in dao.getAllFromAltFilm()) {
val sb = StringBuilder()
for(cwa in af.castList.castList) {
sb.append("\n\tCharacter is ${cwa.character.characterName} played by ${cwa.artist.artistName} \n\t\t${cwa.character.characterProfile}")
}
Log.d(
"ALTFILM",
"Film(alt) is ${af.filmName} (id is ${af.filmId}) made in ${af.filmYear}. Cast of ${af.castList.castList.size}. They are $sb"
)
}
}
}
The output showing that basically the same data (albeit with a intended changes, such as fewer characters and different names and DOBs) can be stored/retrieved either way :-
D/FILMINFO: Film is Taming of the Shrew (id is 1) made in 1967. Cast of 4. They are
Character is Kate played by Elizabeth Taylor
Katherina (Kate) Minola is a fictional character in William Shakespeare's play The Taming of the Shrew.
Character is Petruchio played by Richard Burton
Tamer of Kate
Character is Grumio played by Cyril Cusack
blah
Character is Bianca played by Natasha Pyne
Kate's sister
D/FILMINFO: Film is Star Wars (id is 2) made in 1997. Cast of 0. They are
D/FILMINFO: Film is Taming of the Shrew 2 (id is 3) made in 1967. Cast of 4. They are
Character is Kate played by Elizabeth Taylor
Katherina (Kate) Minola is a fictional character in William Shakespeare's play The Taming of the Shrew.
Character is Petruchio played by Richard Burton
Tamer of Kate
Character is Grumio played by Cyril Cusack
blah
Character is Bianca played by Natasha Pyne
Kate's sister
D/ALTFILM: Film(alt) is Taming of the Shrew (id is 1) made in 1967. Cast of 2. They are
Character is Kate played by Liz Taylor
The Shrew
Character is Petruchio played by Dicky Burton
Shrew Tamer
D/ALTFILM: Film(alt) is Taming of the Shrew 2 (id is 2) made in 1967. Cast of 2. They are
Character is Kate played by Liz Taylor
The Shrew
Character is Petruchio played by Dicky Burton
Shrew Tamer
However, comparing the tables (via App Inspection), shows quite a marked difference
Film Table v AltFilm Table
versus
As can be seen in addition to the actual data the JSON representation of the CastList objects contains a great deal of bloat to enable it to be converted back into a CatList object and this bloat will be repeated throughout not only the column but from row to row.
That in comparison to storing just the core data just once per Artist (see previous image of the Artist table).
I'm making an Workout log app.
One Workout has multiple sets.
I want to store this in a one-to-many relationship in Room.
In conclusion, I succeeded in saving, but I'm not sure what one class does.
All of the other example sample code uses this class, so I made one myself, but it doesn't tell me what it means.
WorkoutWithSets
data class WorkoutWithSets(
#Embedded val workout: Workout,
#Relation (
parentColumn = "workoutId",
entityColumn = "parentWorkoutId"
)
val sets: List<WorkoutSetInfo>
)
The following two entity classes seem to be sufficient to express a one-to-many relationship. (Stored in Room)
Workout
#Entity
data class Workout(
#PrimaryKey(autoGenerate = true)
var workoutId: Long = 0,
var title: String = "",
var memo: String = "",
)
It seems that the following two entity classes are sufficient enough to store a one-to-many relationship.. (stored in Room)
WorkoutSetInfo
#Entity(
foreignKeys = [
ForeignKey(
entity = Workout::class,
parentColumns = arrayOf("workoutId"),
childColumns = arrayOf("parentWorkoutId"),
onDelete = ForeignKey.CASCADE
)
]
)
data class WorkoutSetInfo(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
val set: Int,
var weight: String = "",
var reps: String = "",
var unit: WorkoutUnit = WorkoutUnit.kg,
val parentWorkoutId: Long = 0
)
Even if the WorkoutWithSet class does not exist, the Workout and WorkoutSetInfo classes are stored in Room.
What does WorkoutWithSets class mean? (or where should I use it?)
What does WorkoutWithSets class mean?
It is a class that can be used to retrieve a Workout along with all the related WorkoutSetInfos via a simple #Query that just retrieves the parent Workouts.
What Room does is add an additional query that retrieves the children (WorkoutSetInfo's) for each Workout.
The result being a list of WorkOutWithSets each element (a Workout) containing/including a list of all the related WorkoutSetInfo's.
You would use this when you want to process a Workout (or many Workouts) along with the related WorkoutSetInfo's (aka the child WorkoutSetInfo's for the parent Workout).
What Room does is consider the type (objects) to be returned.
So if you had
#Query("SELECT * FROM workout")
fun getJustWorkouts(): List<Workout>
then the function would return just a list of Workout objects.
But if you had
#Query("SELECT * FROM workout")
fun getWorkoutsWithSets(): List<WorkoutWithSets>
then the function would return a list of WorkoutWithSets and thus the parent Workouts with the child WorkoutSetInfo's.
What Room does is build and execute an underlying query, for eack Workout extracted, along the lines of "SELECT * FROM workoutInfoSet WHERE workout.workoutId = parentWorkoutId" and hence why it suggests the use of the #Transaction annotation (the build will include a warning if #Transaction is not coded).
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)
I have question how to insert relation in room. So, I have Product and ProductsList entities. ProductsList could have a lot of Products, but products shouldn't know anything about lists where are they contained.
#Entity(tableName = "products")
data class Product(
#PrimaryKey(autoGenerate = true)
val productId: Long,
val productName: String
)
#Entity
data class ProductList(
#PrimaryKey(autoGenerate = true)
val productListId: Long,
val listName: String
)
I created ProductsListWithProducts class:
data class ProductListWithProducts(
#Embedded
val productList: ProductList,
#Relation(
parentColumn = "productListId",
entityColumn = "productId",
entity = Product::class
)
val productsId: List<Product>
)
but I don't understand how to insert data in Database. For example I already added Products in its table and after it want to create new ProductList. I have checked other answers and found that for it just using Dao to insert it something like:
#Dao
abstract class ProductListDao {
#Transaction
fun insert(productList: ProductList, products: List<Product>) {
insert(productList)
for (product in products) {
insert(product)
}
}
But I don't see how adding relation between this tables, because I don't want to foreign keys in product entity (because in this case I need to create many-to-many relation). I thought about additional entity
#Entity(primaryKeys = ["productId", "productListId"])
data class ProductListProducts(
val productId: Long,
val productListId: Long
)
but it's using also to define many-to-many relation.
Can I add this relation without creating many-to-many relation, just one-to-many?
Yes have ProductListProducts however you may wish to consider using :-
#Entity(
primaryKeys = ["productId", "productListId"]
,indices = [
Index(value = ["productListId"]) /* Index else Room warns */
]
/* Foreign Keys are optional BUT enforce referential integrity */
, foreignKeys = [
ForeignKey(
entity = Product::class,
parentColumns = ["productId"],
childColumns = ["productId"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = ProductList::class,
parentColumns = ["productListId"],
childColumns = ["productListId"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class ProductListProducts(
val productId: Long,
val productListId: Long
)
you may wish to refer to https://sqlite.org/foreignkeys.html
This is an associative table (reference table, mapping table and many other terms). So you use a Relationship that utilises the association for the Junction between the ProductList and Product. Therefore your ProductListWithProducts POJO becomes :-
data class ProductListWithProducts (
#Embedded
val productList: ProductList,
#Relation(
entity = Product::class,
parentColumn = "productListId",
entityColumn = "productId",
associateBy = Junction(
ProductListProducts::class,
parentColumn = "productListId",
entityColumn = "productId"
)
)
val product: List<Product>
)
Demonstration using the above classes (and your classes where the id's have been altered to be Long=0)
With a Dao class like :-
#Dao
abstract class AllDao {
#Insert
abstract fun insert(product: Product): Long
#Insert
abstract fun insert(productList: ProductList): Long
#Insert
abstract fun insert(productListProducts: ProductListProducts): Long
#Transaction
#Query("SELECT * FROM ProductList")
abstract fun getProductListWithProducts(): List<ProductListWithProducts>
}
Then the following (run on the main thread for brevity/convenience) :-
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
var p1 = dao.insert(Product( productName = "Product1"))
var p2 = dao.insert(Product(productName = "Product2"))
var pl1 = dao.insert(ProductList(listName = "List1"))
var pl2 = dao.insert(ProductList(listName = "List2"))
dao.insert(ProductListProducts(p1,pl1))
dao.insert(ProductListProducts(p1,pl2))
dao.insert(ProductListProducts(p2,pl1))
dao.insert(ProductListProducts(dao.insert(Product(productName = "Product3")),dao.insert(
ProductList(listName = "List3")))
)
for(plwp: ProductListWithProducts in dao.getProductListWithProducts()) {
Log.d(TAG,"ProductList is ${plwp.productList.listName} ID is ${plwp.productList.productListId}")
for(p: Product in plwp.product) {
Log.d(TAG,"\t Product is ${p.productName} ID is ${p.productId}")
}
}
results in the log containing :-
D/DBINFO: ProductList is List1 ID is 1
D/DBINFO: Product is Product1 ID is 1
D/DBINFO: Product is Product2 ID is 2
D/DBINFO: ProductList is List2 ID is 2
D/DBINFO: Product is Product1 ID is 1
D/DBINFO: ProductList is List3 ID is 3
D/DBINFO: Product is Product3 ID is 3
Can I add this relation without creating many-to-many relation, just one-to-many?
The above (i.e. the associative table) will handle 1 to many but if you don't want the the extra table then you would have to include the identifier of the parent in the child. You could enforce 1-many, in the associative table, by making the column for the 1 unique.
As for Mass type insertions you say
For example I already added Products in its table and after it want to create new ProductList
Then you could perhaps go about by having the following additional #Dao's :-
#Insert
abstract fun insert(productListList: List<ProductList>): LongArray
#Insert
abstract fun insertManyProductListProducts(productListProductsList: List<ProductListProducts>): LongArray
/* This can be used to get a specific product or products according to a pattern */
/* e.g. */
/* if productPatterName is product1 then an exact match */
/* if prod% then all that start with prod */
/* if %prod all that end in prod */
/* if %prod% then all that have prod anywhere */
#Query("SELECT productId FROM products WHERE productName LIKE :productNamePattern")
abstract fun getProductIdByName(productNamePattern: String): LongArray
And then have code such as :-
/* Adding many new ProductLists to existing Products */
/* 1 add the new ProductLists */
/* Noting that the insert returns an array of the productListId's inserted */
val insertedProductLists = dao.insert(
listOf(
ProductList(listName = "ListX1"),
ProductList(listName = "ListX2")
)
)
/* 2. Determine the Product(s) that will be related to the new list of ProductLists */
val productIdList = dao.getProductIdByName("Product%") /* All products */
/* 3. Prepare the List of ProductListProducts for mass insertion */
val plplist: ArrayList<ProductListProducts> = ArrayList()
for(pid: Long in productIdList) {
for(plid: Long in insertedProductLists) {
plplist.add(ProductListProducts(pid,plid))
}
}
/* 4. add the relationships */
dao.insertManyProductListProducts(plplist)
If you wanted 1 existing product e.g. product1 then you would use dao.getProductIdByName("Product1"), of course you can also increase/reduce the productLists in the Array to suit.
This would result in :-
D/DBINFO: ProductList is List1 ID is 1
D/DBINFO: Product is Product1 ID is 1
D/DBINFO: Product is Product2 ID is 2
D/DBINFO: ProductList is List2 ID is 2
D/DBINFO: Product is Product1 ID is 1
D/DBINFO: ProductList is List3 ID is 3
D/DBINFO: Product is Product3 ID is 3
D/DBINFO: ProductList is ListX1 ID is 4
D/DBINFO: Product is Product1 ID is 1
D/DBINFO: Product is Product2 ID is 2
D/DBINFO: Product is Product3 ID is 3
D/DBINFO: ProductList is ListX2 ID is 5
D/DBINFO: Product is Product1 ID is 1
D/DBINFO: Product is Product2 ID is 2
D/DBINFO: Product is Product3 ID is 3