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).
Related
How to create Entity and data classes by ROOM in android?
I have JSON structure:
data class ListResponse(val item: ListItem)
data class ListItem(
#SerializedName("id")
val id: List<CheckUnCheckItem>
)
data class CheckUnCheckItem(
#SerializedName("check")
val check: CheckItem,
#SerializedName("unCheck")
val UnCheck: UnCheckItem
)
data class CheckItem(
#SerializedName("url")
val url: String,
#SerializedName("text")
val text: String,
#SerializedName("color")
val color: String
)
data class UnCheckItem(
#SerializedName("url")
val urlUnCheck: String,
#SerializedName("text")
val textUnCheck: String,
#SerializedName("color")
val colorUnCheck: String
)
But How can I create such ROOM Entity?
Do I need to use #TypeConverter?
#Entity(tableName = TABLE_NAME)
data class ListEntity(
#PrimaryKey #SerializedName("id")
val id: CheckUnCheckItem,
#SerializedName("check")
val check: CheckItem,
#SerializedName("unCheck")
val unCheck: UnCheckItem,
#SerializedName("url")
val url: String,
#SerializedName("text")
val text: String,
#SerializedName("size")
val size: String
){
companion object{
const val TABLE_NAME = "db_table"
}
class RoomTypeConverters{
#TypeConverter
fun convertCheckItemListToJSONString(checkList: CheckItem): String = Gson().toJson(checkList)
#TypeConverter
fun convertJSONStringToCheckItemList(jsonString: String): CheckItem = Gson().fromJson(jsonString,CheckItem::class.java)
}
}
is my data and entity classes are correct?
Do I need class witch extends RoomDatabase?
Or better I need to separate db and create for check and uncheck another db?
Or better I need to separate db and create for check and uncheck another db?
As database implies it is able to store data not just one but many. As such a single database is all that would be required. SQLite is a relational database and is designed to store related data. Related data is typically stored in multiple tables. So again a single database will very likely be sufficient.
Do I need to use #TypeConverter?
You never actually need Type Converters. However, for any Object, other than those directly handled (e.g. String, Int, Long, Double, Float, ByteArray) then you either need to break these down into such handled objects or have a types converter that will convert the object to and from such an object.
For example, based upon your #Entity annotated ListEntity class then:-
field id would need a TypeConverter as the type CheckUnCheckItem is not an object type that can be directly handled by Room. So you would need two TypeConverters that could convert from a CheckUncheckItem to and from a type that can be handled by Room.
fields check and uncheck would need two TypeConverters (and it looks as though the Type Converters you have coded will handle the conversion).
fields url,text and size, as they are all String types do not need Type Converters as Room handles strings.
Room has to know about the Type Converters. So you need an #TypeConverters annotation. It's placement defines the scope. Using the annotation to preced the #Database annotation has the most far reaching scope.
Do I need class witch extends RoomDatabase?
Yes. However it has to be an abstract class and should have an abstract function to retrieve an instance of each #Dao annotated interface (or abstract class, in which case the functions have to be abstract, there is no need for an abstract class with Kotlin as functions in an interface can have bodies)).
This class should be annotated with the #Database annotation, the entities parameter of the annotation should include the list of classes for each each table (#Entity annotated class). e.g.
#TypeConverters(value = [ListEntity.RoomTypeConverters::class])
#Database(entities = [ListEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase(){
}
However, using the above along with your classes results in a build error as per:-
ListEntity.java:11: error: Cannot figure out how to save this field into database. You can consider adding a type converter for it. private final a.a.so74708202kotlinroomentitydesign.CheckUnCheckItem id = null;
as explained CheckUnCheckItem cannot be handled by Room.
Amending the RoomTypeConverters class to be:-
class RoomTypeConverters{
#TypeConverter
fun convertItemListToJSONString(invoiceList: Item): String = Gson().toJson(invoiceList)
#TypeConverter
fun convertJSONStringToItemList(jsonString: String): Item = Gson().fromJson(jsonString,Item::class.java)
#TypeConverter
fun convertCheckUnCheckItemToJSONString(cuc: CheckUnCheckItem): String = Gson().toJson(cuc)
#TypeConverter
fun convertJSONStringToCheckUnCheckItem(jsonString: String): CheckUnCheckItem = Gson().fromJson(jsonString,CheckUnCheckItem::class.java)
}
Resolves the build issue and in theory you have a potentially usable database.
However, you obviously need code to access the database. As such you would very likely want to have. as previously mentioned, an #Dao annotated interface e.g
#Dao
interface TheDAOs {
#Insert
fun insert(listEntity: ListEntity): Long
#Query("SELECT * FROM ${TABLE_NAME}")
fun getAll(): List<ListEntity>
}
This will suffice to allow rows to be inserted into the database and for all the rows to be extracted from the database into a List<ListEntity).
From the built database you need to get an instance of the TheDAOs and thus the #Database annotated class could then be
:-
#TypeConverters(value = [ListEntity.RoomTypeConverters::class])
#Database(entities = [ListEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase(){
abstract fun getTheDAOsInstance(): TheDAOs
}
To demonstrate actual use of the above then consider the following code in an activity:-
class MainActivity : AppCompatActivity() {
lateinit var roomDBInstance: TheDatabase
lateinit var theDAOs: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
roomDBInstance = Room.databaseBuilder(this,TheDatabase::class.java,"The_database_name.db")
.allowMainThreadQueries() /* NOTE ADDED FOR CONVENIENCE AND BREVITY */
.build()
/* Note the database itself does not yet exist, it's creation is delayed until an attempt is made to access it. So:- */
theDAOs = roomDBInstance.getTheDAOsInstance() /* Still the database is not created/accessed */
showData(theDAOs.getAll()) /* No data has been added BUT the database will now exist */
theDAOs.insert(
ListEntity(
id = CheckUnCheckItem(
check = Item (
url ="URL001",
text = "TEXT001",
color = "RED"
),
unCheck = Item(
url ="URL002",
text = "TEXT002",
color = "BLUE"
)
),
check = Item(url = "URL003", text ="TEXT003", color ="WHITE"),
unCheck = Item(url = "URL004", text = "TEXT004", color = "BLACK"),
url = "URL005", text = "TEXT005", size = "BIG"
)
)
showData(theDAOs.getAll())
}
fun showData(listEntities: List<ListEntity>) {
for (li in listEntities) {
Log.d(
"DBINFO",
"id is $li.id.check.url${li.id.check.text}.... " +
"\n\tcheck is ${li.check.url} .... " +
"\n\tuncheck is ${li.unCheck.url} ...." +
"\n\turl is ${li.url} text is ${li.text} size is ${li.size}"
)
}
}
}
The output to the log being:-
D/DBINFO: id is ListEntity(id=CheckUnCheckItem(check=Item(url=URL001, text=TEXT001, color=RED), unCheck=Item(url=URL002, text=TEXT002, color=BLUE)), check=Item(url=URL003, text=TEXT003, color=WHITE), unCheck=Item(url=URL004, text=TEXT004, color=BLACK), url=URL005, text=TEXT005, size=BIG).id.check.urlTEXT001....
check is URL003 ....
uncheck is URL004 ....
url is URL005 text is TEXT005 size is BIG
The Database via App Inspection being"-
So finally
is my data and entity classes are correct?
From a database aspect yes, they work after a few amendments. However, I suspect that your classes are probably not what you intended.
An Alternative Approach
If this were to be approached from a database perspective and normalised and without bloat and without the need for type converters then consider the following:-
The embedded Item's (uncheck and check) are basically repetition, so could probably be a table (related to the db_table). Hence 2 tables. One for the ListEntity (Alternative) and another for the Items (AlternativeItem) so the 2 #Entity annotated classes could be:-
/* Alternative Approach */
#Entity(
/* Foreign Keys NOT REQUIRED, they enforce Referential Integrity */
foreignKeys = [
ForeignKey(
entity = AlternativeItem::class,
parentColumns = ["alternativeItemId"],
childColumns = ["unCheckIdMap"]
/* OPTIONAL within a Foreign Key, they help automatically maintain Referential Integrity*/,
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = AlternativeItem::class,
parentColumns = ["alternativeItemId"],
childColumns = ["checkIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Alternative(
#PrimaryKey
val id: Long?=null,
#ColumnInfo(index = true)
val unCheckIdMap: Long, /* map to the id of the related Item (AlternativeItem) for the uncheck */
#ColumnInfo(index = true)
val checkIdMap: Long, /* map to the id of the related Item (AlternativeItem) for the uncheck */
val url: String,
val text: String,
val size: String
)
#Entity
data class AlternativeItem(
#PrimaryKey
val alternativeItemId: Long?=null,
val alternativeItemUrl: String,
val alternativeItemText: String,
val alternativeItemColor: String
)
As you would typically want the Alternative along with it's related AlternativeItems then a POJO that caters for the togetherness :-
data class AlternativeWithUncheckAndCheck(
#Embedded
val alternative: Alternative,
#Relation(entity = AlternativeItem::class, parentColumn = "unCheckIdMap", entityColumn = "alternativeItemId")
val unCheck: AlternativeItem,
#Relation(entity = AlternativeItem::class, parentColumn = "checkIdMap", entityColumn = "alternativeItemId")
val check: AlternativeItem
)
There would be a need for some extra functions in the #Dao annotated interface, so :-
#Insert
fun insert(alternative: Alternative): Long
#Insert
fun insert(alternativeItem: AlternativeItem): Long
#Transaction
#Query("")
fun insertAlternativeAndUncheckAndCheck(alternative: Alternative, uncheck: AlternativeItem, check: AlternativeItem): Long {
var uncheckId = insert(uncheck)
var checkId = insert(check)
return insert(Alternative(null,url = alternative.url, text = alternative.text, size = alternative.size, unCheckIdMap = uncheckId, checkIdMap = checkId ))
}
#Transaction
#Query("SELECT * FROM alternative")
fun getAllAlternativesWithRelatedUnCheckAndCheck(): List<AlternativeWithUncheckAndCheck>
note that the insertAlternativeAndUncheckAndCheck does what it says (note that it is overly simple and could need some enhancements to expand upon the principle)
To demonstrate this, all that is then required is to add the new entities to the entities parameter and to then add some code to the activity.
The amended #Database annotation:-
#Database(entities = [ListEntity::class, /* for the alternative approach */ Alternative::class, AlternativeItem::class], exportSchema = false, version = 1)
The activity code (that caters for both approaches in a similar/equivalanet way of storing and retrieving the data) :-
class MainActivity : AppCompatActivity() {
lateinit var roomDBInstance: TheDatabase
lateinit var theDAOs: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
roomDBInstance = Room.databaseBuilder(this,TheDatabase::class.java,"The_database_name.db")
.allowMainThreadQueries() /* NOTE ADDED FOR CONVENIENCE AND BREVITY */
.build()
/* Note the database itself does not yet exist, it's creation is delayed until an attempt is made to access it. So:- */
theDAOs = roomDBInstance.getTheDAOsInstance() /* Still the database is not created/accessed */
showData(theDAOs.getAll()) /* No data has been added BUT the database will now exist */
theDAOs.insert(
ListEntity(
id = CheckUnCheckItem(
check = Item (
url ="URL001",
text = "TEXT001",
color = "RED"
),
unCheck = Item(
url ="URL002",
text = "TEXT002",
color = "BLUE"
)
),
check = Item(url = "URL003", text ="TEXT003", color ="WHITE"),
unCheck = Item(url = "URL004", text = "TEXT004", color = "BLACK"),
url = "URL005", text = "TEXT005", size = "BIG"
)
)
showData(theDAOs.getAll())
/* Alternative equivalent */
theDAOs.insertAlternativeAndUncheckAndCheck(
Alternative(url = "URL005", size = "BIG", text = "TEXT005", checkIdMap = -1, unCheckIdMap = -1),
check = AlternativeItem(alternativeItemUrl = "URL001", alternativeItemText = "TEXT001", alternativeItemColor = "RED"),
uncheck = AlternativeItem(alternativeItemUrl = "URL002", alternativeItemText = "TEXT002", alternativeItemColor = "BLUE" )
)
showAlternativeData(theDAOs.getAllAlternativesWithRelatedUnCheckAndCheck())
}
fun showData(listEntities: List<ListEntity>) {
for (li in listEntities) {
Log.d(
"DBINFO",
"id is $li.id.check.url${li.id.check.text}.... " +
"\n\tcheck is ${li.check.url} .... " +
"\n\tuncheck is ${li.unCheck.url} ...." +
"\n\turl is ${li.url} text is ${li.text} size is ${li.size}"
)
}
}
fun showAlternativeData(listAlternatives: List<AlternativeWithUncheckAndCheck>) {
for (la in listAlternatives) {
Log.d("DBALTINFO",
"id is ${la.alternative.id} URL is ${la.alternative.url} TEXT is ${la.alternative.text} SIZE is ${la.alternative.size} " +
"\n\t UNCHECK id is ${la.unCheck.alternativeItemId} url is ${la.unCheck.alternativeItemUrl} text is ${la.unCheck.alternativeItemText} color is ${la.unCheck.alternativeItemColor}" +
"\n\t CHECK id is ${la.check.alternativeItemId} url is ${la.check.alternativeItemUrl} text is ${la.check.alternativeItemText} color is ${la.check.alternativeItemColor}")
}
}
}
Note that the Alternative code is probably more along the lines of what you probably want according to the interpretation of the shown JSON.
When run then the result is now:-
D/DBINFO: id is ListEntity(id=CheckUnCheckItem(check=Item(url=URL001, text=TEXT001, color=RED), unCheck=Item(url=URL002, text=TEXT002, color=BLUE)), check=Item(url=URL003, text=TEXT003, color=WHITE), unCheck=Item(url=URL004, text=TEXT004, color=BLACK), url=URL005, text=TEXT005, size=BIG).id.check.urlTEXT001....
check is URL003 ....
uncheck is URL004 ....
url is URL005 text is TEXT005 size is BIG
D/DBALTINFO: id is 1 URL is URL005 TEXT is TEXT005 SIZE is BIG
UNCHECK id is 1 url is URL002 text is TEXT002 color is BLUE
CHECK id is 2 url is URL001 text is TEXT001 color is RED
it is suspected that BLACK/WHITE or RED/BLUE is superfluous in your interpretationof the JSON to data classes (and hence excluded in the alternative).
The database, via App Inspection (in regards to the alternative approach) is:-
and :-
i.e. only the actual data is store the BLOAT (field/type descriptions, separators, enclosing data) is not stored thus
the database will hold more data in less space.
the handling of the data will thus be more efficient (e.g. a buffer can hold more actual data instead of BLOAT).
querying the data such as for example searching for all BLUE's is directly a search for that, whilst with converted data you may have issues distinguishing between BLOAT and actual data
However, the negative, is that more code and thought is required.
Note this answer is intended to deal with the basic principles and is most certainly not fully comprehensive.
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)
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)
When saving a list of objects in my room database using a Dao
#Insert()
fun saveCharmRankMaterialCosts(materialCosts: List<CharmRankCraftingCost>) : List<Long>
And this is used from my repository class to save results from an API call:
val charmRankCosts = CharmRankCraftingCost.fromJsonCraftingCost(
charmRankId.toInt(),
jsonCharmRank.crafting
)
// save crafting/upgrade costs for the rank
val results = charmDao.saveCharmRankMaterialCosts(charmRankCosts)
Log.d("CharmRepository", "Saved charm material costs: ${results.toString()}");
assert(!results.contains(-1))
When running this code, insert ID's are returned and the assertion is never triggered (i.e. no inserts fail).
But when I inspect the data base on the device, most of the supposedly inserted IDs are missing from the table. I'm very confused as to what is going on here. I've debugged this issue for many hours and have been unsuccessful in getting this to work. Is there something obvious I'm missing?
The issue seems to have been related to foreign key constraints. I had a CharmRank data class with multiple related data objects. See below:
/**
* Copyright Paul, 2020
* Part of the MHW Database project.
*
* Licensed under the MIT License
*/
#Entity(tableName = "charm_ranks")
data class CharmRank(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "charm_rank_id")
var id: Int = 0,
#ColumnInfo(name = "charm_id")
var charmId : Int,
#ColumnInfo(name = "charm_rank_level")
var level: Int = 0, // 3
#ColumnInfo(name = "charm_rank_rarity")
var rarity: Int = 0, // 6
#ColumnInfo(name = "charm_rank_name")
var name: String = "",
#ColumnInfo(name = "craftable")
var craftable: Boolean
)
Each charm rank has associated skills and items to craft said rank. These objects are simply relational objects in that they hold the ID of the CharmRank and a SkillRank in the case of the skills object, or the ID of the CharmRank and the ID of the Item object.
data class CharmRankSkill(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "charm_rank_skill_id")
var id: Int,
var charmRankId : Int,
var skillRankId: Int
)
data class CharmRankCraftingCost(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "charm_rank_crafting_cost_id")
var id: Int,
#ColumnInfo(name = "charm_rank_id")
var charmRankId: Int,
#ColumnInfo(name = "charm_rank_crafting_cost_item_quantity")
val quantity: Int,
val itemId: Int
)
Originally in CharmRankCraftingCost, I had a foreign key constraint on the Item object and the CharmRank object. Below is the foreign key constraint on the Item object:
ForeignKey(
entity = Item::class,
parentColumns = ["item_id"],
childColumns = ["itemId"],
onDelete = ForeignKey.CASCADE
)
The Item data object has IDs provided by the remote data source, so when I insert items into it's respective table, the conflict resolution is set to Replace. During the process of saving the relational items to the data base for the CharmRanks, I also have to save the Item objects prior to saving CharmRankCraftingCosts. It seems that what was happening is that when the Item objects are inserted, sometimes the items would get replaced, which would trigger the cascade action of the foreign key resulting in the CharmRankCraftingCosts items I just saved for the CharmRank to be deleted due to the cascading effect.
Removing the foreign key constraint on the Item table solved my issue.
As I understood from the comments, you make a delete before inserts. The problem is that it happens that the insert gets completed before the delete since you do them in separate threads. What you need is to do both in one transaction. Create a method in the the DAO class with #Transaction annotation (Make sure your dao is an abstract class so you can implement the body of this method):
#Dao
public abstract class YourDao{
#Insert(onConflict = OnConflictStrategy.IGNORE)
public abstract List<Long> insertData(List<Data> list);
#Query("DELETE FROM your_table")
public abstract void deleteData();
#Transaction
public void insertAndDeleteInTransaction(List<Data> list) {
// Anything inside this method runs in a single transaction.
deleteData();
insertData(list);
}
}
Read this for Kotlin Version of the code.