How to add to previously pre-populated Room database? - android

Previously, I pre-populated some data and added it to the Room db. But I have some new data that I need to add. As a way to add these, I can do it by requesting the dao directly, but the application will do this every time it is opened. This will have brought an unnecessary burden to the application. So is there a better way than the code below? I'm asking if there is a better way than this.
private fun addNewWord(){
val newWord1 = Word(1, "John", "Weight")
val newWord2 = Word(2, "Alex", "So"
wordViewModel.addWord(newWord1, newWord2)
}
I found a solution like the code I wrote above, but I don't think it's correct enough. I'm looking for a better way. Is this a best-practice?

The are numerous better (as far as This will have brought an unnecessary burden to the application goes), assuming that the unnecessary burden is the overhead associated with the attempt to insert records whenever the App is run (more correctly whenever the database is opened).
The simplest solution, but one that is not really better (it may even be what you have tried) would be to IGNORE duplicates (if you aren't already ignoring them). This involves using INSERT OR IGNORE .... where a column or columns or a combination of columns has a UNQIUE index.
All Room tables (other than for FTS (Full Text Search)) MUST have a Primary Key. A Primary Key is implicitly UNIQUE. So if INSERT OR IGNORE .... is used then the UNIQUE CONFLICT is ignored (the row is not inserted and the conflict which would result in an exception is ignored).
To specify INSERT OR IGNORE for a convenience #Insert then you can specify the onConflict value of the #Insert annotation. e.g.
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(word: Word): Long
the convenience #Insert returns the rowid of the inserted row or -1 if the row was not inserted due to it being ignored, this could be checked to see if a row was inserted.
a normally hidden column that exists for ALL Room tables (as it exists for any SQLite table that is not a virtual table or a table that is defined as an WITHOUT ROWID table).
You can also specify unique indexes on a column or a combination of columns via the indicies parameter of the #Entity annotation e.g.:-
#Entity(
indices = [
/* A composite UNIQUE index on word1 combined with word2 */
Index(value = ["word1","word2"], unique = true)
/* example
* if a row exists that has A as word1 and B as word2 then (index value can be considered as AB):-
* inserting a new row with B and A would be inserted (index value BA)
* inserting a new row with B and B (or A and A) would be inserted (index value BB or AA)
* inserting a row with B and A would result in a UNIQUE CONFLICT
* inserting a row with BA and nothing (aka null) would be inserted (assuming NOT NULL was not coded or implied for column word2)
*/
/*
An Index on a single column (word1 so the value in the word1 column cannot be duplicated)
NOTE the above would not apply if this index was also included
In this case then word1 =A and word2 = A above (third insert (or A and A)) would be considered a duplicate
*/
, Index(value = ["word1"], unique = true)
])
data class Word(
#PrimaryKey
var id: Long?=null,
var word1: String,
var word2: String
)
However, this simple solution would still run and try to insert the new data whenever the App is run.
A better solution without the "unnecessary burden"
If the goal is to only apply new data once then there would need to be a method to see if the data has already been applied, perhaps via a Migration (aka a new version).
The Migration would only run once as the user_version, which is part of the databases file header is checked/updated by Room.
The migration would also be run if the App is installed after the new database version has been specified.
Working Demo
Perhaps consider the following Migration based Working Demo based upon what you data appears to be:-
The Room database code:-
#Entity(
indices = [
/* A composite UNIQUE index on word1 combined with word2 */
Index(value = ["word1","word2"], unique = true)
/* example
* if a row exists that has A as word1 and B as word2 then (index value can be considered as AB):-
* inserting a new row with B and A would be inserted (index value BA)
* inserting a new row with B and B (or A and A) would be inserted (index value BB or AA)
* inserting a row with B and A would result in a UNIQUE CONFLICT
* inserting a row with BA and nothing (aka null) would be inserted (assuming NOT NULL was not coded or implied for column word2)
*/
])
data class Word(
#PrimaryKey
var id: Long?=null,
var word1: String,
var word2: String
)
#Dao
interface TheDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(word: Word): Long
#Query("SELECT * FROM word")
fun getAllWords(): List<Word>
}
const val DATABASE_NAME = "the_database.db"
const val ASSET_NAME = DATABASE_NAME
#Database(entities = [Word::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, DATABASE_NAME)
.createFromAsset(ASSET_NAME,pdc)
.allowMainThreadQueries() /* For convenience of the demo */
.addCallback(cb)
.addMigrations(mig1to2)
.build()
}
return instance as TheDatabase
}
private val mig1to2 = object: Migration(1,2) {
override fun migrate(database: SupportSQLiteDatabase) {
Log.d("MIG1-2", "Migration is running")
val cv = ContentValues()
cv.put("word1", "NEWWORD W1=W")
cv.put("word2", "NEWWORD W2=W")
database.insert("word", OnConflictStrategy.IGNORE, cv)
cv.clear()
cv.put("word1", "NEWWORD W1=X")
cv.put("word2", "NEWWORD W2=X")
database.insert("word", OnConflictStrategy.IGNORE, cv)
cv.clear()
cv.put("word1", "NEWWORD W1=Y")
cv.put("word2", "NEWWORD W2=Y")
database.insert("word", OnConflictStrategy.IGNORE, cv)
cv.clear()
cv.put("word1", "NEWWORD W1=Z")
cv.put("word2", "NEWWORD W2=Z")
database.insert("word", OnConflictStrategy.IGNORE, cv)
}
}
val cb = object: RoomDatabase.Callback() {
val TAG = "DBCALLBACK"
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
Log.d(TAG,"onCreate called")
}
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
Log.d(TAG,"onOpen called")
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
super.onDestructiveMigration(db)
Log.d(TAG,"onDestructiveMigration called")
}
}
val pdc = object: PrepackagedDatabaseCallback(){
val TAG = "PPDOPEN"
override fun onOpenPrepackagedDatabase(db: SupportSQLiteDatabase) {
super.onOpenPrepackagedDatabase(db)
Log.d(TAG,"Prepackaged Database has been copied and opened")
}
}
}
}
note that the database version is 1
the callbacks are included to show when and what is called.
the file the_database.db is in the assets folder and has 3 rows as per:-
The activity code used is:-
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()
for(w in dao.getAllWords()) {
Log.d("DBINFO","Word ID is ${w.id} WORD1 is ${w.word1} WORD2 is ${w.word2}")
}
}
}
i.e. It simply accesses the database, extracts all the rows and writes the data to the log.
When run as a new install at database version 1 then the output to the log is:-
2023-01-02 15:26:47.055 D/PPDOPEN: Prepackaged Database has been copied and opened
2023-01-02 15:26:47.119 D/DBCALLBACK: onOpen called
2023-01-02 15:26:47.124 D/DBINFO: Word ID is 1 WORD1 is ORIGINALWORD1=W1_A WORD2 is ORIGINALWORD2=W2_A
2023-01-02 15:26:47.124 D/DBINFO: Word ID is 2 WORD1 is ORIGINALWORD1=W1_B WORD2 is ORIGINALWORD2=W2_B
2023-01-02 15:26:47.124 D/DBINFO: Word ID is 3 WORD1 is ORIGINALWORD1=W1_C WORD2 is ORIGINALWORD2=W2_C
If run again, still at version 1 then the output is:-
2023-01-02 15:28:27.976 D/DBCALLBACK: onOpen called
2023-01-02 15:28:27.981 D/DBINFO: Word ID is 1 WORD1 is ORIGINALWORD1=W1_A WORD2 is ORIGINALWORD2=W2_A
2023-01-02 15:28:27.981 D/DBINFO: Word ID is 2 WORD1 is ORIGINALWORD1=W1_B WORD2 is ORIGINALWORD2=W2_B
2023-01-02 15:28:27.981 D/DBINFO: Word ID is 3 WORD1 is ORIGINALWORD1=W1_C WORD2 is ORIGINALWORD2=W2_C
i.e. the copy of the prepackaged database wasn't invoked as the database existed and the output is otherwise the same.
If the database version is changed to 2 (no schema changes) and the App is rerun then the output is:-
2023-01-02 15:31:32.464 D/MIG1-2: Migration is running
2023-01-02 15:31:32.529 D/DBCALLBACK: onOpen called
2023-01-02 15:31:32.536 D/DBINFO: Word ID is 1 WORD1 is ORIGINALWORD1=W1_A WORD2 is ORIGINALWORD2=W2_A
2023-01-02 15:31:32.536 D/DBINFO: Word ID is 2 WORD1 is ORIGINALWORD1=W1_B WORD2 is ORIGINALWORD2=W2_B
2023-01-02 15:31:32.536 D/DBINFO: Word ID is 3 WORD1 is ORIGINALWORD1=W1_C WORD2 is ORIGINALWORD2=W2_C
2023-01-02 15:31:32.536 D/DBINFO: Word ID is 4 WORD1 is NEWWORD W1=W WORD2 is NEWWORD W2=W
2023-01-02 15:31:32.536 D/DBINFO: Word ID is 5 WORD1 is NEWWORD W1=X WORD2 is NEWWORD W2=X
2023-01-02 15:31:32.536 D/DBINFO: Word ID is 6 WORD1 is NEWWORD W1=Y WORD2 is NEWWORD W2=Y
2023-01-02 15:31:32.536 D/DBINFO: Word ID is 7 WORD1 is NEWWORD W1=Z WORD2 is NEWWORD W2=Z
i.e. the Migration was invoked and the new data introduced as per the code in the Migration.
If the App is rerun (still at version 2) then :-
2023-01-02 15:34:21.336 D/DBCALLBACK: onOpen called
2023-01-02 15:34:21.342 D/DBINFO: Word ID is 1 WORD1 is ORIGINALWORD1=W1_A WORD2 is ORIGINALWORD2=W2_A
2023-01-02 15:34:21.342 D/DBINFO: Word ID is 2 WORD1 is ORIGINALWORD1=W1_B WORD2 is ORIGINALWORD2=W2_B
2023-01-02 15:34:21.342 D/DBINFO: Word ID is 3 WORD1 is ORIGINALWORD1=W1_C WORD2 is ORIGINALWORD2=W2_C
2023-01-02 15:34:21.342 D/DBINFO: Word ID is 4 WORD1 is NEWWORD W1=W WORD2 is NEWWORD W2=W
2023-01-02 15:34:21.342 D/DBINFO: Word ID is 5 WORD1 is NEWWORD W1=X WORD2 is NEWWORD W2=X
2023-01-02 15:34:21.342 D/DBINFO: Word ID is 6 WORD1 is NEWWORD W1=Y WORD2 is NEWWORD W2=Y
2023-01-02 15:34:21.342 D/DBINFO: Word ID is 7 WORD1 is NEWWORD W1=Z WORD2 is NEWWORD W2=Z
i.e. the Migration isn't invoked and all the data remains.
If the App is uninstalled and then installed/run (as per a new install) then:-
2023-01-02 15:37:25.096 D/PPDOPEN: Prepackaged Database has been copied and opened
2023-01-02 15:37:25.113 D/MIG1-2: Migration is running
2023-01-02 15:37:25.169 D/DBCALLBACK: onOpen called
2023-01-02 15:37:25.175 D/DBINFO: Word ID is 1 WORD1 is ORIGINALWORD1=W1_A WORD2 is ORIGINALWORD2=W2_A
2023-01-02 15:37:25.175 D/DBINFO: Word ID is 2 WORD1 is ORIGINALWORD1=W1_B WORD2 is ORIGINALWORD2=W2_B
2023-01-02 15:37:25.175 D/DBINFO: Word ID is 3 WORD1 is ORIGINALWORD1=W1_C WORD2 is ORIGINALWORD2=W2_C
2023-01-02 15:37:25.175 D/DBINFO: Word ID is 4 WORD1 is NEWWORD W1=W WORD2 is NEWWORD W2=W
2023-01-02 15:37:25.175 D/DBINFO: Word ID is 5 WORD1 is NEWWORD W1=X WORD2 is NEWWORD W2=X
2023-01-02 15:37:25.175 D/DBINFO: Word ID is 6 WORD1 is NEWWORD W1=Y WORD2 is NEWWORD W2=Y
2023-01-02 15:37:25.176 D/DBINFO: Word ID is 7 WORD1 is NEWWORD W1=Z WORD2 is NEWWORD W2=Z
i.e. the prepackaged database has been copied and the migration was invoked thus introducing the new data (which is NOT in the prepackaged database).
Other ways
Another way could be to utilise the version number in the prepackaged database. This would involve accessing the prepackaged via the asset manager extracting the version number (4 bytes at offset 60) from the header (first 100 bytes of the file), comparing it with the version number in the actual database if the former is higher then new data exists. So both databases could be opened the rows copied.
Room uses the version number (user_version), so another take could be to instead use the application id (4 bytes offset 68).
Both of these would require setting the values in the pre-packaged database and updating them along with a new distribution/APK.
Another option could be to a have a core database/file accessible via the internet with a method of detecting a change to the data.
Other ways could be to introduce detection via extra columns and perhaps even an extra table. However, the less the burden, the likelihood the greater the the complexity of the solution.

Related

Inserting a List of Objects in Android Room Database with Kotlin

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

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

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

How to sort child data using relation in Android ROOM database which uses (One to many relationship)

Store Entity
data class Store(
val storeId: Int,
val name: String,
val storeRank:Int
)
Product Entity
data class Product(
val productId: Int,
val name: String
)
Reference Entity
data class Reff(
val storeId: Int,
val productId: Int,
val productRankInStore:Int
)
Relation
data class StoreAndProduct(
#Embedded
val store: Store,
#Relation(
entity = Product::class,
parentColumn = "storeId",
entityColumn = "productId",
associateBy = Junction(
parentColumn = "storeId",
entityColumn = "productId",
value = Reff::class
)
)
val product: List<Product>
)
Here I need to sort Products using the key productRankInStore. I have already implemented the relation and which is working fine. But I couldn't find any other way to sort the products using productRankInStore
NB: Same product have different rank in different store ( productRankInStore )
If you have an abstract class rather than an interface for the #Dao annotated class(es) then you could effectively override how Room handles #Relation using a function that does the 2 stages, with the latter sorted accordingly.
You do this by having 2 #Query 's :-
the primary (Store(s)) and
secondary query (the products sorted by rank)
You then combine them into a function e.g. :-
#Dao
abstract class AllDAO {
#Query("SELECT * FROM store")
abstract fun getAllStores(): List<Store>
#Query("SELECT product.* FROM reff JOIN product ON product.productId = reff.productId WHERE reff.storeId=:storeId ORDER BY productRankInStore DESC")
abstract fun getStoreProductsSortedByRank(storeId: Int): List<Product>
#Query("")
#Transaction
fun getStoreAndProductsSortedByProductRank(): List<StoreAndProduct> {
val rv = arrayListOf<StoreAndProduct>()
for (store in getAllStores() /* obviously change initial query if desired */) {
rv.add(StoreAndProduct(store,getStoreProductsSortedByRank(store.storeId)))
}
return rv
}
}
You can then use:-
dao.getStoreAndProductsSortedByProductRank()
e.g. if you have data as :-
and
and Rank 1 is the top then the following
for(s in dao.getStoreAndProductsSortedByProductRank()) {
Log.d("DBINFO","Store is ${s.store.name}")
for (p in s.product) {
Log.d("DBINFO","\tProduct is ${p.name}")
}
}
will output :-
2022-03-26 06:43:15.753 D/DBINFO: Store is Store1
2022-03-26 06:43:15.753 D/DBINFO: Product is ProductA
2022-03-26 06:43:15.753 D/DBINFO: Product is ProductB
2022-03-26 06:43:15.753 D/DBINFO: Product is ProductC
2022-03-26 06:43:15.753 D/DBINFO: Store is Store2
2022-03-26 06:43:15.753 D/DBINFO: Product is ProductC
2022-03-26 06:43:15.753 D/DBINFO: Product is ProductA
2022-03-26 06:43:15.753 D/DBINFO: Product is ProductB
2022-03-26 06:43:15.753 D/DBINFO: Store is Store3
2022-03-26 06:43:15.753 D/DBINFO: Product is ProductC
2022-03-26 06:43:15.753 D/DBINFO: Product is ProductB
2022-03-26 06:43:15.754 D/DBINFO: Product is ProductA
NOTE the rankInStore will not be available (as per your StoreAndProduct).
If you need the rankInStore available then you would need to do something like have and use a ProductAndRank POJO.

Is there a way to map an entity to a temporary table in Android Room?

I need to create a temporary table to insert hierarchical data in an Android Kotlin app database that uses Room. Is there a way to link an entity class to a temporary table ? Thanks in advance for any hint.
From testing it does not appear that TEMPORARY tables are supported/usable within Room but that instead you would have to use psuedo temporary tables (non-temporary that would be cleared not necessarily defined as #Entity annotated classes).
Consider the following demonstration that :-
shows the use of TEMPORARY table outside of Room using the android SQLiteDatabase class.
shows the use, within room of a pseudo temporary table, culminating in a query that links to an #Entity annotated table.
shows how the TEMP table does not appear to work resulting in a table not found, even though data is apparently successfully inserted.
First two #Entity annotated classes Parent and Child :-
#Entity
data class Parent(
#PrimaryKey
val parentId: Long? = null,
val parentName: String
)
and :-
#Entity
data class Child(
#PrimaryKey
val childId: Long? = null,
val parentLink: Long = -1,
val childName: String
)
A single #Dao annotated abstract class AllDao :-
#Dao
abstract class AllDao {
#Insert(onConflict = IGNORE)
abstract fun insert(parent: Parent): Long
#Insert(onConflict = IGNORE)
abstract fun insert(child: Child): Long
#SkipQueryVerification /* ignore the unable to resolve parentAndChild table */
#Query("SELECT parent.* FROM parentAndChild JOIN main.parent ON parentAndChild.parentId = parent.parentId")
abstract fun getAllFromParentAndChild(): List<Parent>
}
Note the query that retrieves data from the non-room parentAndChild table and for simplicity extracts a List via a link (JOIN) to the Parent table.
an #Database annotated class TheDatabase :-
const val DATABASE_NAME = "the_database.db"
#Database(entities = [Parent::class, Child::class], version = 1)
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
}
}
}
Finally an activity where the testing/demonstration is undertaken:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
lateinit var supdb: SupportSQLiteDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
testNonRoomTempTable() // Example of using a TEMPORARY table with SQLiteDatabase rather than Room
/* Now with Room */
db = TheDatabase.getInstance(this)
supdb = db.openHelper.writableDatabase
dao = db.getAllDao()
/* Instantiate some Parent and Child Objects
* Parent being an #Entity annotated class
* Child being an #Entity annotated class
* */
val p1 = Parent(10,"Parent10")
val p2 = Parent(20,"Parent20")
val c1 = Child(1000,p1.parentId!!,"Child1")
val c2 = Child(2000,p2.parentId!!,"Child2")
/* Insert the objects into the respective Room tables */
dao.insert(p1)
dao.insert(p2)
dao.insert(c1)
dao.insert(c2)
/* Create and populate a pseudo temp table (unknown to room aka not via an #Entity annotated class)*/
createTemptTable()
insertTempTableRow(p1.parentId,p1.parentName, c1.childId!!,c1.childName)
insertTempTableRow(p2.parentId,p2.parentName,c2.childId!!,c2.childName)
logTempTable() /* write the pseudo date to the log i.e. confirm existence */
/* Use a link to an #Entity annotated class to get some data */
for (p: Parent in dao.getAllFromParentAndChild()) {
Log.d("DBINFO","Parent is ${p.parentName}")
}
/* Replicate BUT using a TEMPORARY TABLE rather than a pseudo temp table */
createTemptTable(true) // true results in TEMP keyword i.e. a TEMPORARY table
insertTempTableRow(p1.parentId,p1.parentName, c1.childId,c1.childName)
insertTempTableRow(p2.parentId,p2.parentName,c2.childId,c2.childName)
logTempTable() //<<<<< fails table not found
/* Use a link to an #Entity annotated class to get some data */
for (p: Parent in dao.getAllFromParentAndChild()) {
Log.d("DBINFO","Parent is ${p.parentName}")
}
}
fun createTemptTable(useTemptable: Boolean = false) {
var createTableSQL = "CREATE TABLE IF NOT EXISTS parentAndChild (parentId INTEGER, parentName TEXT, childId INTEGER, childName TEXT)"
if (useTemptable) {
createTableSQL = "CREATE TEMP TABLE IF NOT EXISTS parentAndChild (parentId INTEGER, parentName TEXT, childId INTEGER, childName TEXT)"
}
supdb.execSQL("DROP TABLE IF EXISTS parentAndChild")
supdb.execSQL(createTableSQL)
/*
var csr = supdb.query("SELECT * FROM temp.sqlite_master")
DatabaseUtils.dumpCursor(csr)
csr = supdb.query("SELECT * FROM sqlite_master")
DatabaseUtils.dumpCursor(csr)
csr.close()
*/
}
fun insertTempTableRow(parentId: Long, parentName: String, childId: Long, childName: String) {
val cv = ContentValues()
cv.put("parentId",parentId)
cv.put("parentName",parentName)
cv.put("childId",childId)
cv.put("childName",childName)
Log.d("DBINFO","insert result is " +
supdb.insert("parentAndChild",androidx.room.OnConflictStrategy.ABORT,cv))
}
#SuppressLint("Range")
fun logTempTable() {
Log.d("DBINFO","Logging ParentAndChild Table :-")
val csr = supdb.query("SELECT * FROM parentAndChild")
while (csr.moveToNext()) {
Log.d("DBINFO",
"Parent is ${csr.getString(csr.getColumnIndex("parentId"))} Child is ${csr.getString(csr.getColumnIndex("childId"))}")
}
Log.d("DBINFO","Completed Logging ParentAndChild Table")
}
fun testNonRoomTempTable() {
val altdb = SQLiteDatabase.openOrCreateDatabase(this.getDatabasePath("alt_database.db"),null)
altdb.execSQL("CREATE TEMP TABLE IF NOT EXISTS parentAndChild (parentId INTEGER, parentName TEXT, childId INTEGER, childName TEXT)")
val cv: ContentValues = ContentValues()
cv.put("parentId",10)
cv.put("parentName","Parent1")
cv.put("childId",1000)
cv.put("childName","Child1")
altdb.insert("parentAndChild",null,cv)
var csr = altdb.query("parentAndChild",null,null,null,null,null,null)
DatabaseUtils.dumpCursor(csr)
csr.close()
}
}
Results when run
The testNonRoomTempTable() runs as expected and produces the following dump of the Cursor into the log:-
2022-02-20 09:22:33.031 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor#2caa87e
2022-02-20 09:22:33.032 I/System.out: 0 {
2022-02-20 09:22:33.032 I/System.out: parentId=10
2022-02-20 09:22:33.032 I/System.out: parentName=Parent1
2022-02-20 09:22:33.032 I/System.out: childId=1000
2022-02-20 09:22:33.032 I/System.out: childName=Child1
2022-02-20 09:22:33.032 I/System.out: }
2022-02-20 09:22:33.032 I/System.out: <<<<<
i.e. the TEMPORARY table is accessible when not using Room
Then when using a table that is cleared (DROPPED) and created and thus effectively TEMPORARY then the log includes :-
2022-02-20 09:22:33.085 D/DBINFO: insert result is 1
2022-02-20 09:22:33.085 D/DBINFO: insert result is 2
2022-02-20 09:22:33.085 D/DBINFO: Logging ParentAndChild Table :-
2022-02-20 09:22:33.091 D/DBINFO: Parent is 10 Child is 1000
2022-02-20 09:22:33.091 D/DBINFO: Parent is 20 Child is 2000
2022-02-20 09:22:33.091 D/DBINFO: Completed Logging ParentAndChild Table
2022-02-20 09:22:33.093 D/DBINFO: Parent is Parent10
2022-02-20 09:22:33.094 D/DBINFO: Parent is Parent20
i.e. the pseudo temporary table has hen created, populated and accessed. The last tow lines being via a link to the Parent table with Parent objects extracted.
The when trying to use an actual TEMP table :-
2022-02-20 09:22:33.095 D/DBINFO: insert result is 1
2022-02-20 09:22:33.096 D/DBINFO: insert result is 2
2022-02-20 09:22:33.096 D/DBINFO: Logging ParentAndChild Table :-
2022-02-20 09:22:33.098 E/SQLiteLog: (1) no such table: parentAndChild
i.e. even though rows have been inserted, when the attempt to access the table is made, a table not found exception results.

how to populate intermediate table in Android

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

Categories

Resources