The problem is: I am getting news from API and cache them in an entity(articles_table) with (Articles class), so I want to add another entity(bookmark_table) with the same (Articles) model class.
I want inside Articles model class:
First articles_table (this will represent the home cache news)
second articles_bookmark_table (this will represent user bookmarks)
is this possible? to have two tables in one class? or there is another way that does the same thing?
#Entity(tableName = "articles_table")
data class Articles(
#PrimaryKey(autoGenerate = true)
val id: Int,
val author: String,
val date: String,
val img: String,
val source: String,
val title: String,
val url: String,
val interest:String
)
is this possible? to have two tables in one class?
No, an #Entity defines a class as a table as per
Marks a class as an entity. This class will have a mapping SQLite table in the database. https://developer.android.com/reference/androidx/room/Entity
You could embed the Article Class into another #Entity class and have what is effectively a second, nearly identically structured, table.
e.g. :-
#Entity(tableName = "articles_bookmark_table", primaryKeys = ["id"])
data class BookMarkArticles(
#Embedded
val articles: Articles
)
note that when Embedding the Primary Key is not included, hence the need to define it.
I would suggest that this is a precarious way to go as it may result in confusion over ambiguous columns as the column names would be identical.
However, I'd suggest that rather than 2 basically identical tables, that you simply add an indicator (Boolean) that determines whether or not it is a bookmark row or not. e.g. :-
#Entity(tableName = "articles_table")
data class Articles(
#PrimaryKey //<<<<<<<<<< autoGenerate removed
val id: Int? = null, //<<<<<<<<<< defaults to null so if not provided auto generates id
val author: String,
val date: String,
val img: String,
val source: String,
val title: String,
val url: String,
val interest:String,
val bookmark:Boolean //<<<<<<<<<< ADDED to distinguish bookmark row or not
)
using the 2 tables with the embedded introduces a nuance when inserting/extracting, as the following demo.
The demo uses slightly modified Articles and BookMarkArticles and the 2nd Articles as ArticlesV2 to show both ways together.
So Articles is :-
#Entity(tableName = "articles_table")
data class Articles(
#PrimaryKey
val id: Int? = null,
val author: String,
val date: String,
val img: String,
val source: String,
val title: String,
val url: String,
val interest:String
)
-i.e. id is allowed to be null and thus when not provided the id will be automatically generated.
BookMarkArticles (Embeds Articles) is :-
#Entity(tableName = "articles_bookmark_table", primaryKeys = ["id"])
data class BookMarkArticles(
#Embedded
val articles: Articles
)
ArticlesV2 (the two types in a single table) is :-
#Entity(tableName = "articles_v2_table")
data class ArticlesV2(
#PrimaryKey
val id: Int? = null,
val author: String,
val date: String,
val img: String,
val source: String,
val title: String,
val url: String,
val interest:String,
val bookmark:Boolean
)
Dao's are :-
#Insert
abstract fun insert(articles: Articles): Long
#Insert
abstract fun insert(bookMarkArticles: BookMarkArticles): Long
#Insert
abstract fun insert(articlesV2: ArticlesV2): Long
#Query("SELECT * FROM articles_table")
abstract fun getAllArticles(): List<Articles>
#Query("SELECT * FROM articles_bookmark_table")
abstract fun getAllBookmarkedArticles(): List<BookMarkArticles>
#Query("SELECT * FROM articles_v2_table WHERE bookmark=:bookmarked")
abstract fun getAllArticleV2s(bookmarked: Boolean): List<ArticlesV2>
In an Activity is the following code :-
db = TheDatabase.getInstance(this)
dao = db.getDao()
dao.insert(Articles(author = "Fred",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest"))
dao.insert(Articles(author = "Bert",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest"))
dao.insert(BookMarkArticles(Articles(author = "Fred",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest")))
dao.insert(BookMarkArticles(Articles(author = "Bert",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest")))
dao.insert(ArticlesV2(author = "Fred",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest",bookmark = false))
dao.insert(ArticlesV2(author = "Bert",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest",bookmark = false))
dao.insert(ArticlesV2(author = "Fred",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest",bookmark = true))
dao.insert(ArticlesV2(author = "Bert",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest",bookmark = true))
for(a: Articles in dao.getAllArticles()) {
Log.d("ARTICLEINFO","(not bookmark) Author is ${a.author} ID is ${a.id} etc")
}
for(b: BookMarkArticles in dao.getAllBookmarkedArticles()) {
Log.d("ARTICLEINFO","(bookmark) Author is ${b.articles.author} ID is ${b.articles.id} etc")
}
for(a: ArticlesV2 in dao.getAllArticleV2s(false)) {
Log.d("ARTICLEV2INFO","(not bookmark) Author is ${a.author} ID is ${a.id} etc BOOKMARK Flag is ${a.bookmark}")
}
for(a: ArticlesV2 in dao.getAllArticleV2s(true)) {
Log.d("ARTICLEV2INFO","(bookmark) Author is ${a.author} ID is ${a.id} etc BOOKMARK Flag is ${a.bookmark}")
}
Note the nuance (nusiance) that a BookMarkArticles is constructed via an Articles object and that when extracting a BookMarkArticles that it has an embedded Articles object.
The result in the log :-
2021-09-01 16:32:09.844 D/ARTICLEINFO: (not bookmark) Author is Fred ID is 1 etc
2021-09-01 16:32:09.845 D/ARTICLEINFO: (not bookmark) Author is Bert ID is 2 etc
2021-09-01 16:32:09.847 D/ARTICLEINFO: (bookmark) Author is Fred ID is 1 etc
2021-09-01 16:32:09.847 D/ARTICLEINFO: (bookmark) Author is Bert ID is 2 etc
2021-09-01 16:32:09.849 D/ARTICLEV2INFO: (not bookmark) Author is Fred ID is 1 etc BOOKMARK Flag is false
2021-09-01 16:32:09.849 D/ARTICLEV2INFO: (not bookmark) Author is Bert ID is 2 etc BOOKMARK Flag is false
2021-09-01 16:32:09.851 D/ARTICLEV2INFO: (bookmark) Author is Fred ID is 3 etc BOOKMARK Flag is true
2021-09-01 16:32:09.851 D/ARTICLEV2INFO: (bookmark) Author is Bert ID is 4 etc BOOKMARK Flag is true
Related
I'm trying to implement caching of a JSON API response with Room.
The response I get in JSON follows this data class structure:
#Serializable
data class ApiDataResponse(
val success: Boolean,
val message: String? = null,
val albums: List<AlbumResponse> = emptyList()
)
#Serializable
data class AlbumResponse(
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicResponse>
)
#Serializable
data class PicResponse(
val picUrl: String,
val emojis: List<String>
)
Notes:
#Serializable is from kotlinx.serialization library to parse the JSON response.
These response data classes are only used inside my datasource layer, the view layer doesn't care about an ApiDataResponse and only knows a "pure" version of AlbumResponse called Album and a "pure" version of PicResponse called Pic (by "pure" I mean a data class without external library annotations).
So to implement this cache with Room I could discard the ApiDataResponse and save only the contents of AlbumResponse (and consequently PicResponse), having new data classes for Room entities following this idea:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>, // obstacle here
val pics: List<PicEntity> // obstacle here
)
// obstacle here
// #Entity
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
I already know how to save simple data in Room, with the simplest JSON I was able to do this task, the problem is that in this more complex scenario I have no idea how to achieve this goal. So I wish someone could guide me in this situation.
Maybe it's a little late, but I would still like to add some interesting information regarding MikeT's answer.
It is not necessary to create a new data class just to transform a custom object into a JSON with TypeConverter, for example:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicEntity> // can be converted directly
)
import kotlinx.serialization.Serializable
#Serializable // to be able to do the serialize with the kotlinx.serialization
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
With just these two data classes we can build the TypeConverters as follows:
import androidx.room.TypeConverter
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseConverter {
private val json = Json
#TypeConverter
fun convertStringListToString(strings: List<String>): String =
json.encodeToString(strings)
#TypeConverter
fun convertStringToStringList(string: String): List<String> =
json.decodeFromString(string)
#TypeConverter
fun convertPicEntityListToString(picsEntity: List<PicEntity>): String =
json.encodeToString(picsEntity)
#TypeConverter
fun convertStringToPicEntityList(string: String): List<PicEntity> =
json.decodeFromString(string)
}
Code to create an example dummy list:
object DummyAlbums {
fun createList(): List<AlbumEntity> = listOf(
AlbumEntity(
id = "0001",
title = "Album AB",
createdBy = "Created by AB",
enabled = true,
keywords = listOf("ab"),
pics = dummyPics(albumId = "0001", size = 0)
),
AlbumEntity(
id = "0002",
title = "Album CD",
createdBy = "Created by CD",
enabled = false,
keywords = listOf("cd", "c", "d"),
pics = dummyPics(albumId = "0002", size = 1)
),
AlbumEntity(
id = "0003",
title = "Album EF",
createdBy = "Created by EF",
enabled = true,
keywords = listOf(),
pics = dummyPics(albumId = "0003", size = 2)
)
)
private fun dummyPics(
albumId: String,
size: Int
) = List(size = size) { index ->
PicEntity(
picUrl = "url.com/$albumId/${index + 1}",
emojis = listOf(":)", "^^")
)
}
}
So we can have the following data in table:
I wanted to highlight this detail because maybe it can be important for someone to have a table with the cleanest data. And in even more specific cases, to have it clean, you can do the conversion manually using Kotlin functions, such as joinToString(), split(), etc.
I believe the issue is with columns as lists.
What you could do is add the following classes so the Lists are embedded within a class:-
data class StringList(
val stringList: List<String>
)
data class PicEntityList(
val picEntityList: List<PicEntity>
)
and then change AlbumEntity to use the above instead of the Lists, as per:-
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
//val keywords: List<String>, // obstacle here
val keywords: StringList, /// now not an obstacle
//val pics: List<PicEntity> // obstacle here
val emojis: PicEntityList// now not an obstacle
)
To be able to store the "complex" (single object) you need to convert this so some TypeConverters e.g.
class RoomTypeConverters{
#TypeConverter
fun convertStringListToJSON(stringList: StringList): String = Gson().toJson(stringList)
#TypeConverter
fun convertJSONToStringList(json: String): StringList = Gson().fromJson(json,StringList::class.java)
#TypeConverter
fun convertPicEntityListToJSON(picEntityList: PicEntityList): String = Gson().toJson(picEntityList)
#TypeConverter
fun convertJSONToPicEntityList(json: String): PicEntityList = Gson().fromJson(json,PicEntityList::class.java)
}
note this utilises the dependency com.google.code.gson
You then need to have the #TypeConverters annotation to cover the appropriate scope (at the #Database level is the most scope). Note the plural rather than singular, they are different.
To demonstrate the above works, First some functions in an interface annotated with #Dao :-
#Dao
interface AlbumDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(albumEntity: AlbumEntity): Long
#Query("SELECT * FROM albums")
fun getAllAlbums(): List<AlbumEntity>
}
Second an #Database annotated class (note the #TypeConverters annotation) :-
#TypeConverters(RoomTypeConverters::class)
#Database(entities = [AlbumEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAlbumDao(): AlbumDao
companion object {
#Volatile
private var instance: TheDatabase?=null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"album.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Third some activity code to actually do something (insert some Albums and then extract them writing the extracted data to the Log) :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AlbumDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAlbumDao()
dao.insert(AlbumEntity(
"Album001", "The First Album","Fred",false,
StringList(listOf("The","First","Album")),
PicEntityList(
listOf(
PicEntity("PE001", listOf("emoji1","emoji2","emoji3")),
PicEntity("PE002",listOf("emoji10")),
PicEntity("PE003", listOf("emoji20","emoji21"))
))
))
dao.insert(AlbumEntity(
"Album002","This is the Second Album","Mary", true,
StringList(listOf("keya","keyb","keyc","keyd","keye")),
PicEntityList(
listOf(
PicEntity("PE011", listOf("emoji30","emoji31")),
PicEntity("PE012", listOf("emoji1","emoji10","emoji20","emoji30"))
))
))
for (a in dao.getAllAlbums()) {
logAlbum(a)
}
}
fun logAlbum(albumEntity: AlbumEntity) {
val keywords = StringBuilder()
for(s in albumEntity.keywords.stringList) {
keywords.append("\n\t$s")
}
val pelog = StringBuilder()
for (pe in albumEntity.emojis.picEntityList) {
pelog.append("\n\tURL is ${pe.picUrl}")
for (emoji in pe.emojis) {
pelog.append("\n\t\tEmoji is ${emoji}")
}
}
Log.d(
"ALBUMINFO",
"Album id is ${albumEntity.id} " +
"Title is ${albumEntity.title} " +
"CreateBy ${albumEntity.createdBy} " +
"Enabled=${albumEntity.enabled}. " +
"It has ${albumEntity.keywords.stringList.size} keywords. " +
"They are $keywords\n. " +
"It has ${albumEntity.emojis.picEntityList.size} emojis. " +
"They are ${pelog}"
)
}
}
Run on the main thread for convenience and brevity
When run then the log contains:-
D/ALBUMINFO: Album id is Album001 Title is The First Album CreateBy Fred Enabled=false. It has 3 keywords. They are
The
First
Album
. It has 3 emojis. They are
URL is PE001
Emoji is emoji1
Emoji is emoji2
Emoji is emoji3
URL is PE002
Emoji is emoji10
URL is PE003
Emoji is emoji20
Emoji is emoji21
D/ALBUMINFO: Album id is Album002 Title is This is the Second Album CreateBy Mary Enabled=true. It has 5 keywords. They are
keya
keyb
keyc
keyd
keye
. It has 2 emojis. They are
URL is PE011
Emoji is emoji30
Emoji is emoji31
URL is PE012
Emoji is emoji1
Emoji is emoji10
Emoji is emoji20
Emoji is emoji30
i.e. the 2 albums have been extracted along with the appropriate embedded lists.
The Albums table itself (via App Inspection) consists of :-
An Alternative, and from a Database perspective, better approach, instead of embedding lists as a single value (String), would have the lists as related tables (with a one-many or a many-many relationship).
So I am build an app where I want to store information coming from various source into a single database. The database is as below:
#Entity(tableName = "store")
data class StoreEntity (
#PrimaryKey()
#ColumnInfo(name = "")
var current: String,
#ColumnInfo(name = "ingredientslist")
var productsIngredientList: MutableList<IngredientsEntity?>,
#ColumnInfo(name = "reviewslist")
var productsReviewsList: MutableList<ReviewsEntity?>,
#ColumnInfo(name = "listofproductsId")
var listOfId: MutableList<String>
)
and the other entities are:
#Entity(tableName = "ingredients")
data class IngredientsEntity(
#PrimaryKey()
#NonNull var id: String,
#ColumnInfo(name = "ingredient")
var ingredient: Ingredient? = null,
)
#Entity(tableName = "reviews")
data class IngredientsEntity(
#PrimaryKey()
#NonNull var id: String,
#ColumnInfo(name = "review")
var review: Review? = null,
)
I have defined the Doa as below:
#Query("UPDATE store SELECT ingredientslist SET ingredient=:shadow WHERE id = :id ")
suspend fun updateIngredients(id: String?, ingredient: Ingredient)
#Query("SELECT ingredient from ingredientslist WHERE id = :id ")
suspend fun getIngredients(id: String): Flow<Ingredient?>
suspend fun getIngredientsDistinctUntilChanged(id: String) = getIngredients(id).distinctUntilChanged()
reviews and ingredients are both coming from different sources but I would like a database where I can store :
current product displayed -> String (only one)
list of products -> list of String
list of Ingredients (List of id + ingredients)
list of Reviews (List of id + reviews)
the goal would be to be able to add/retrieve ingredients using id and same for reviews.
I am not sure if I have to access using the Doa the store database, then select ingrediendslist, then look for ingredients link to an id. or if I can just access directky ingrediendslist because store is automatically linking ingredientslist
Also is there a way to have a single PrimaryKey, which could be a single name ? the Store database will only have one single entry. See this like you can have only one store.
It's look likes to me a database into a database.
Any idea how to make it works ? I tried several Room sql command but I am not sure that it's the right way.
Maybe I need to split it into different Dbs. One for ingredients, one for reviews and one for current product id and list of products ids.
I was trying to do it in one single database to avoid having multiple DBs for only 15 products top.
Any idea or advices ?
Thanks
The database is as below:
That would be in theory, there are various issues that would result in the compilation failing. Such as trying to give a column no name as per :-
#Entity(tableName = "store")
data class StoreEntity (
#PrimaryKey()
#ColumnInfo(name = "")
var current: String,
Furthermore to store a list of Objects, although possible with the use of TypeConverters restricts or complicates matters with regard to best use of the database. Doing so also de-normalises the database and adds bloat as an object if referred to multiple times (such as an ingredient which may in theory be common to a number of stores).
Complexity arises through a value of any such object being stored with all the other values and thus would require complex SQL very likely including CASE THEN ELSE END constructs within and possibly Common Table Expressions.
As you are in the development stage I would suggest consider using the power of relationships basically have an extra 2 tables one to map/reference/associate a Store with it's ingredients, another to map a Store with it's Reviews.
Here's a working example (without products but the same technique applies for products)
So first the StoreEntity :-
#Entity(tableName = "store")
data class StoreEntity (
#PrimaryKey()
#ColumnInfo(name = "store_identifier")
var current: String
/* lists are mapped so not needed here (see StoreWithMappedReviewsAndWithMappedIngredients)
#ColumnInfo(name = "ingredientslist")
var productsIngredientList: MutableList<IngredientsEntity?>,
#ColumnInfo(name = "reviewslist")
var productsReviewsList: MutableList<ReviewsEntity?>,
#ColumnInfo(name = "listofproductsId")
var listOfId: MutableList<String>
*/
)
The Review class (made up as not included) :-
data class Review(
var reviewTitle: String,
var reviewer: String
/* etc */
)
And like your code the respective ReviewsEnntity (that Embeds (copies the member variables) the underlying Review) :-
#Entity(tableName = "reviews")
data class /*IngredientsEntity????*/ ReviewsEntity(
#PrimaryKey()
#NonNull var reviewId: String,
/* see comments for Ingredient class
#ColumnInfo(name = "review")
var review: /*Reviews? ????*/ Review = null,
*/
#Embedded
var review: Review
)
Similar for Ingredient (made up again) and IngredientsEntity :-
data class Ingredient(
var ingredientName: String,
var ingredientDescription: String
/* other member variables as required */
)
and :-
#Entity(tableName = "ingredients")
data class IngredientsEntity(
#PrimaryKey
#NonNull var ingredientId: String,
#Embedded
var ingredient: Ingredient
/* using below would require a TypeConverter the above embeds the ingredient so a column as per the ingredient class */
/*var ingredient: Ingredient? = null <<<<< null ???? why have an ingredient row with no ingredient? */
)
Now the two (3rd for products) mapping/associate/reference .... tables.
First the table that maps Stores and Reviews with the potential for many stores to have many Reviews and for Reviews to map to many Stores.
StoreReviewMappingEntity
#Entity(
tableName = "store_review_map",
primaryKeys = ["storeReviewMap_storeId","storeReviewMap_reviewId"]
/* Optional Foreign Key Constraints to enforce Referential Integrity */
)
data class StoreReviewMappingEntity(
var storeReviewMap_storeId: String,
#ColumnInfo(index = true)
var storeReviewMap_reviewId: String
)
and StoreIngredientMappingEntity for Ingredients :-
#Entity(
tableName = "store_ingredient_map",
primaryKeys = ["storeIngredientMap_storeId","storeIngredientMap_ingredientId"]
/* Optional Foreign Key Constraints to enforce Referential Integrity */
)
data class StoreIngredientMappingEntity(
var storeIngredientMap_storeId: String,
#ColumnInfo(index = true) /* index on the ingredient id so getting the ingredient is more efficient */
var storeIngredientMap_ingredientId: String
)
So instead of the 3 tables 5 tables (entities)
Now a POJO (i.e. not an Entity) StoreWithMappedReviewsAndWithMappedIngredients that allows you to get a Store with all of it's Reviews and all of it's ingredients via the mapping tables:-
data class StoreWithMappedReviewsAndWithMappedIngredients(
#Embedded
var storeEntity: StoreEntity,
#Relation(
entity = ReviewsEntity::class,
parentColumn = "store_identifier",
entityColumn = "reviewId",
associateBy = Junction(
value = StoreReviewMappingEntity::class /* the mappping table class/entity */,
parentColumn = "storeReviewMap_storeId",
entityColumn = "storeReviewMap_reviewId"
)
)
var reviewList: List<ReviewsEntity>,
#Relation(
entity = IngredientsEntity::class,
parentColumn = "store_identifier",
entityColumn = "ingredientId",
associateBy = Junction(
value = StoreIngredientMappingEntity::class,
parentColumn = "storeIngredientMap_storeId",
entityColumn = "storeIngredientMap_ingredientId"
)
)
var ingredientList: List<IngredientsEntity>
)
Now the #Dao annotated interface (one for brevity/convenience) AllDao :-
#Dao
interface AllDao {
/*
#Query("UPDATE store SELECT ingredientslist SET ingredient=:shadow WHERE id = :id ")
suspend fun updateIngredients(id: String?, ingredient: Ingredient)
#Query("SELECT ingredient from ingredientslist WHERE id = :id ")
suspend fun getIngredients(id: String): Flow<Ingredient?>
suspend fun getIngredientsDistinctUntilChanged(id: String) = getIngredients(id).distinctUntilChanged()
*/
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(ingredientsEntity: IngredientsEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(reviewsEntity: ReviewsEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(storeEntity: StoreEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(storeReviewMappingEntity: StoreReviewMappingEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(storeIngredientMappingEntity: StoreIngredientMappingEntity): Long
/* Allows removal of Review/Ingredient respectively (if only one store then no need for storeId but just-in-case) */
#Query("DELETE FROM store_review_map WHERE storeReviewMap_ReviewId=:reviewId AND storeReviewMap_StoreId=:storeId")
fun deleteReviewFromStore(storeId: String, reviewId: String): Int
#Query("DELETE FROM store_ingredient_map WHERE storeIngredientMap_IngredientId=:ingredientId AND storeIngredientMap_StoreId=:storeId")
fun deleteIngredientFromStore(storeId: String, ingredientId: String): Int
#Transaction
#Query("SELECT * FROM store")
fun getAllStoresWithReviewsAndWithIngredients(): List<StoreWithMappedReviewsAndWithMappedIngredients>
}
Note for the example .allowMainThreadQueries has been used so suspend not used just add as appropriate.
Nearly there here's an #Database annotated class :-
#Database(entities = [
StoreEntity::class,
ReviewsEntity::class,
IngredientsEntity::class,
StoreReviewMappingEntity::class,
StoreIngredientMappingEntity::class
],
version = 1,
exportSchema = false /* consider true see https://developer.android.com/training/data-storage/room/migrating-db-versions */
)
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,
"the_database.db"
)
.allowMainThreadQueries() /* for convenience/brevity of the example */
.build()
}
return instance as TheDatabase
}
}
}
Finally putting it al together in an Activity which inserts 2 Stores, 4 reviwa, 4 Ingredients and maps reviews and ingredients to only the first Store and finally extracts all of the Stores with the Reviews and Ingredients and writes the result to the log. :-
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 store1Id = "S001"
val store2Id = "S002"
val store1 = dao.insert(StoreEntity(store1Id))
val store2 = dao.insert(StoreEntity(store2Id))
val mercuryId = "I001"
val silverId = "I002"
val copperid = "I003"
val zincId = "I004"
val ing01= dao.insert( IngredientsEntity(ingredientId = mercuryId, Ingredient("Mercury","Quicksilver- Hazardous")))
val ing02 = dao.insert(IngredientsEntity(silverId, Ingredient("Silver","Au - Metal - Safe blah ...")))
val ing03 = dao.insert(IngredientsEntity( ingredient = Ingredient("Copper","Cu - Metal - Safe"),ingredientId = copperid))
val ing04 = dao.insert(IngredientsEntity(ingredientId = zincId,ingredient = Ingredient(ingredientDescription = "Zn - Metal - Safe", ingredientName = "Zinc")))
/* note ing?? values will be either 1 or greater (the rowid) or -1 if the row was not inserted (e.g. duplicate conflict ignored)*/
val r1Id = "R001"
val r2Id = "R002"
val r3Id = "R003"
val r4Id = "R004"
val r1 = dao.insert(ReviewsEntity(r1Id, Review("Review 1 - etc","Reviewer1")))
val r2 = dao.insert(ReviewsEntity(r2Id,Review("Review 2 - etc","Reviewer9")))
val r3 = dao.insert(ReviewsEntity(r3Id,Review("Review 3 - etc","Reviewer1")))
val r4 = dao.insert(ReviewsEntity(r4Id,Review("Review 4 - etc","Reviewer8")))
dao.insert(StoreReviewMappingEntity(store1Id,r2Id))
dao.insert(StoreReviewMappingEntity(store1Id,r3Id))
dao.insert(StoreReviewMappingEntity(store1Id,r1Id))
dao.insert(StoreReviewMappingEntity(store1Id,r4Id))
dao.insert(StoreIngredientMappingEntity(store1Id,zincId))
dao.insert(StoreIngredientMappingEntity(store1Id,mercuryId))
dao.insert(StoreIngredientMappingEntity(store1Id,copperid))
dao.insert(StoreIngredientMappingEntity(store1Id,silverId))
dao.insert(StoreIngredientMappingEntity(store1Id,zincId)) //<<<< due to onconflict ignore will not be inserted as duplicate
val sb: java.lang.StringBuilder = java.lang.StringBuilder().append("Extracting All Stores with Reviews and Ingredients:-")
for (swmrawmi: StoreWithMappedReviewsAndWithMappedIngredients in dao.getAllStoresWithReviewsAndWithIngredients()) {
sb.append("\nCurrentStore is ${swmrawmi.storeEntity.current} it has ${swmrawmi.reviewList.size} Reviews and ${swmrawmi.ingredientList.size} ingredients")
sb.append("\n\tThe Reviews Are:-")
for(r: ReviewsEntity in swmrawmi.reviewList) {
sb.append("\n\t\tID is ${r.reviewId}, Title is ${r.review.reviewTitle} etc")
}
sb.append("\n\tThe ingredients Are :-")
for(i: IngredientsEntity in swmrawmi.ingredientList) {
sb.append("\n\t\tID is ${i.ingredientId}, Name is ${i.ingredient.ingredientName} Description is ${i.ingredient.ingredientDescription}")
}
}
Log.d("RESULTINFO",sb.toString())
}
}
and the result from the log when run :-
D/RESULTINFO: Extracting All Stores with Reviews and Ingredients:-
CurrentStore is S001 it has 4 Reviews and 4 ingredients
The Reviews Are:-
ID is R001, Title is Review 1 - etc etc
ID is R002, Title is Review 2 - etc etc
ID is R003, Title is Review 3 - etc etc
ID is R004, Title is Review 4 - etc etc
The ingredients Are :-
ID is I001, Name is Mercury Description is Quicksilver- Hazardous
ID is I002, Name is Silver Description is Au - Metal - Safe blah ...
ID is I003, Name is Copper Description is Cu - Metal - Safe
ID is I004, Name is Zinc Description is Zn - Metal - Safe
CurrentStore is S002 it has 0 Reviews and 0 ingredients
The Reviews Are:-
The ingredients Are :-
Im investigating Android Rooms support for returning Maps from query's
I cannot get past this build error
error: The columns returned by the query does not have the fields [columnOne,columnTwo,columnThree,columnFour,columnFive,columnSix,columnSeven,columnEight] in org.my.MyDataVO even though they are annotated as non-null or primitive. Columns returned by the query: [columnA,columnB]
public abstract java.util.Map<java.lang.String, java.util.List<org.my.MyDataVO>> fetchMap(#org.jetbrains.annotations.NotNull()
My DAO Query resembles:-
#MapInfo(keyColumn = "column_a", valueColumn = "column_b")
#Query("SELECT column_a, column_b FROM my_data_table WHERE my_key_column = :myKeyColumn")
fun fetchMap(myKeyColumn: String): Map<String, List<org.my.MyDataVO>>
My DO object resembles:-
#Entity(
tableName = "my_data_table",
indices = [
Index(value = ["key_column"], unique = false),
]
)
#TypeConverters(MyDataVOListTypeConverter::class)
data class BookTableContentChildrenDO(
#ColumnInfo(name = "key_column") val keyColumn: String,
#ColumnInfo(name = "column_a") val columnA: String,
#ColumnInfo(name = "column_b") val columnB: List<org.my.MyDataVO>
) {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "my_data_table_local_id")
var myDataTableLocalId: Long = 0L
}
My Value Object MyDataVO resembles:-
#Serializable
data class MyDataVO(
#ColumnInfo(name = "column_one") val columnOne: Long,
#ColumnInfo(name = "column_two") val columnTwo: String,
#ColumnInfo(name = "column_three") val columnThree: Boolean,
#ColumnInfo(name = "column_four") val columnFour: String,
#ColumnInfo(name = "column_five") val columnFive: String,
#ColumnInfo(name = "column_six") val columnSix: String,
#ColumnInfo(name = "column_seven") val columnSeven: String,
#ColumnInfo(name = "column_eight") val columnEight: Long,
)
Is what I am attempting not possible?
Where have I made my mistake?
Why cannot room map my_data_table.column_b to a List<org.my.MyDataVO>
UPDATE
I managed to resolve the build error by adding all my TypeConverters to the #Database abstract class definition
However I am now facing a runtime error which seems very strange
I have type converters for both a single instance of org.my.MyDataVO and a List<org.my.MyDataVO>, Room is employing the Single instance TypeConverter to decode the List<org.my.MyDataVO>
as when I attempt to call my DAO fetchMap() function it fails with
java.util.concurrent.ExecutionException: kotlinx.serialization.json.internal.JsonDecodingException: Expected start of the object '{', but had 'EOF' instead
JSON input: .....xxx","columnSix":1,"columnSeven":1,"columnEight":""}]}]
Why is Room using the wrong TypeConverter?
If I do not mention the Single instance TypeConverter in my #Database abstract class I get the build error.
UPDATE 2
I can get this to work if I change the DAO fetchMap method return type to Map<String, List<Any>>
Now Room employs the correct type converter and I can fetch the map ok
UPDATE 3
Following further testing I have discovered although I do not experience any crashes or build issues my DAO is not working as I require. Although I am inserting multiple entry lists of org.my.MyDataVO, the lists returned by my fetchMap function only ever contain one entry. Why cant Room return the complete list as stored in the DB table?
Why cant Room return the complete list as stored in the DB table?
In short you cannot have a column as a List/Array directly.
so #ColumnInfo(name = "column_b") val columnB: List<org.my.MyDataVO> is not going to work.
Add a POJO such as
data class MyDataVOListHolder(
val myDataVOListHolder: List<org.my.MyDataVO>
)
and then use the POJO for the column type e.g.
#ColumnInfo(name = "column_b") val columnB: MyDataVOListHolder
Obviously you will need suitable Typeconverters to convert a myDataVOListHolder to and from a type that Room can handle (e.g. JSON String).
As an example :-
lateinit var db: TheDatabase
lateinit var dao: BookTableContentChildrenDODao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getDao()
dao.insert(
BookTableContentChildrenDO(keyColumn = "AAA", columnA = "Blah", columnB = MyDataVOListHolder(myDataVOListHolder = createMyDataVOList()))
)
dao.insert(
BookTableContentChildrenDO(keyColumn = "BBB", columnA = "Blah", columnB = MyDataVOListHolder(myDataVOListHolder = createMyDataVOList()))
)
dao.insert(
BookTableContentChildrenDO(keyColumn = "CCC", columnA = "Blah", columnB = MyDataVOListHolder(myDataVOListHolder = createMyDataVOList()))
)
for(m in dao.fetchMap("BBB")) {
Log.d("DBINFO","Key is ${m.key}")
for (myDataVo: MyDataVO in m.value.myDataVOListHolder) {
Log.d("DBINFO-EXTRA","\t C1 = ${myDataVo.columnOne} C2 = ${myDataVo.columnTwo} C3 = ${myDataVo.columnThree} etc...."
)
}
}
}
fun createMyDataVOList(): List<MyDataVO> {
var myDataVoList = ArrayList<MyDataVO>()
for (i in 1 until 5) {
myDataVoList.add(
MyDataVO((i * 3).toLong(),"col2_" +i.toString(),i % 2 == 0,"col4","col5","col6","col7",i.toLong())
)
}
return myDataVoList
}
results in the log containing :-
D/DBINFO: Key is Blah
D/DBINFO-EXTRA: C1 = 3 C2 = col2_1 C3 = false etc....
D/DBINFO-EXTRA: C1 = 6 C2 = col2_2 C3 = true etc....
D/DBINFO-EXTRA: C1 = 9 C2 = col2_3 C3 = false etc....
D/DBINFO-EXTRA: C1 = 12 C2 = col2_4 C3 = true etc....
Given that I have 3 entities, Order contains list of LineItem, each LineItem will associates with one Product by productId.
The problem that when I get data from OrderDao, it returns null for the product field, but in the lineItem field, it has data. While I can data with ProductWithLineItem.
Already tried a lot of work arounds but it does not work.
Here is my code for entities and dao
Entities
#Entity(tableName = DataConstant.ORDER_TABLE)
data class Order(
#PrimaryKey
#ColumnInfo(name = "orderId")
val id: String,
#ColumnInfo(name = "status")
var status: String
)
#Entity(tableName = DataConstant.LINE_ITEM_TABLE)
data class LineItem(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "lineItemId")
val id: Long,
#ColumnInfo(name = "productId")
val productId: String,
#ColumnInfo(name = "orderId")
val orderId: String,
#ColumnInfo(name = "quantity")
var quantity: Int,
#ColumnInfo(name = "subtotal")
var subtotal: Double
)
#Entity(tableName = DataConstant.PRODUCT_TABLE)
data class Product(
#PrimaryKey
#NonNull
#ColumnInfo(name = "productId")
val id: String,
#ColumnInfo(name = "name")
var name: String?,
#ColumnInfo(name = "description")
var description: String?,
#ColumnInfo(name = "price")
var price: Double?,
#ColumnInfo(name = "image")
var image: String?,
)
Relations POJOs
data class ProductAndLineItem(
#Embedded val lineItem: LineItem?,
#Relation(
parentColumn = "productId",
entityColumn = "productId"
)
val product: Product?
)
data class OrderWithLineItems(
#Embedded var order: Order,
#Relation(
parentColumn = "orderId",
entityColumn = "orderId",
entity = LineItem::class
)
val lineItemList: List<ProductAndLineItem>
)
Dao
#Dao
interface OrderDao {
#Transaction
#Query("SELECT * FROM `${DataConstant.ORDER_TABLE}` WHERE orderId = :id")
fun getById(id: String): Flow<OrderWithLineItems>
}
Result after running with Dao
Result after running query
Here is my code for entities and dao
You code appears to be fine, with the exception of returning a Flow, testing, using your code, but on the main thread using List (and no WHERE clause) i.e the Dao being :-
#Query("SELECT * FROM ${DataConstant.ORDER_TABLE}")
#Transaction
abstract fun getOrderWithLineItemsAndWithProduct(): List<OrderWithLineItems>
Results in :-
The data being loaded/tested using :-
db = TheDatabase.getInstance(this)
orderDao = db.getOrderDao()
orderDao.clearAll()
orderDao.insert(Product("product1","P1","desc1",10.01,"image1"))
orderDao.insert(Product("product2","P2","desc2",10.02,"image2"))
orderDao.insert(Product("product3","P3","desc3",10.03,"image3"))
orderDao.insert(Product("product4","P4","desc4",10.04,"image4"))
orderDao.insert(Product("","","",0.0,""))
val o1 = orderDao.insert(Order("Order1","initiaited"))
val o2 = orderDao.insert(Order("Order2","finalised")) // Empty aka no List Items
val o1l1 = orderDao.insert(LineItem(10,"product3","Order1",1,10.01))
val o1l2 = orderDao.insert(LineItem(20,"product4","Order1",2,20.08))
val o1l3 = orderDao.insert(LineItem(30,"","Order1",3,30.09))
val o1l4 = orderDao.insert(LineItem(40,"","x",1,10.01))
//val o1l3 = orderDao.insert(LineItem(30,"no such product id","Order1",10,0.0))
// exception whilst trying to extract if not commented out at test = ....
val TAG = "ORDERINFO"
val test = orderDao.getOrderWithLineItemsAndWithProduct()
for(owl: OrderWithLineItems in orderDao.getOrderWithLineItemsAndWithProduct()) {
Log.d(TAG,"Order is ${owl.order.id} status is ${owl.order.status}")
for(pal: ProductAndLineItem in owl.lineItemList) {
Log.d(TAG,"\tLine Item is ${pal.lineItem.id} " +
"for Order ${pal.lineItem.orderId} " +
"for ProductID ${pal.lineItem.productId} " +
"Quantity=${pal.lineItem.quantity} " +
"Product description is ${pal.product.description} Product Image is ${pal.product.image} Price is ${pal.product.price}")
}
}
As such I believe the issue might be that for some reason the Flow is detecting when the first query has completed but prior to the underlying queries.
That is when using #Relation the core objects (Order's) are extracted via the query and the core objects created then the related objects are extracted by a another query and used to build ALL the related objects as a List (unless just the one when it doesn't have to be a list). So prior to this underlying query the core object will have a null or an empty list for the underlying objects. Of course with a hierarchy of #Relations then this is replicated along/down the hierarchy.
I would suggest temporarily adding .allowMainThreadQueires to the databaseBuilder and using a List<OrderWithLineItems> or just a sole OrderWithLineItems. If using this then you get the Product(s) then the issue is with the Flow (which is what I suspect).
I have implemented a DB where I have two one-to-many relationships but it would seem that room does not allow it. Is that so?
The entities are:
#Entity(tableName = "arete_sheet")
data class EAreteSheet(
#PrimaryKey val id: Long,
#ColumnInfo(name = "sheet") val form: Sheets,
#ColumnInfo(name = "version") val version: Int,
)
#Entity(tableName = "arete_sheet_paragraph")
data class EAreteSheetParagraph(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_id") val sheet: Long,
#ColumnInfo(name = "name") val name: String
)
#Entity(tableName = "arete_sheet_form")
data class EAreteSheetForm(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_paragraph_id") val paragraph: Long,
#ColumnInfo(name = "fieldType") val fieldType: FieldType,
#ColumnInfo(name = "cell") val cell: String,
#ColumnInfo(name = "label") val label: String
)
To solve the schema I have implemented these join classes:
data class EAreteSheetWithParagraph(
#Embedded val sheet: EAreteSheet,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_id"
)
val paragraph: List<EAreteSheetParagraphWithForm>
)
data class EAreteSheetParagraphWithForm(
#Embedded val paragraph: EAreteSheetParagraph,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_paragraph_id"
)
val forms: List<EAreteSheetForm>
)
This is the DAO implementation:
#Transaction
#Query("SELECT * FROM arete_sheet")
suspend fun getSheetWithParagraphsAndForms(): List<EAreteSheetWithParagraph>
This is the mistake he gives me in the building phase:
app/build/generated/source/kapt/debug/it/ximplia/agri2000/model/db/dao/AreteSheetDAO_Impl.java:203: error: constructor EAreteSheetWithParagraph in class EAreteSheetWithParagraph cannot be applied to given types;
_item = new EAreteSheetWithParagraph();
^
required: EAreteSheet,List<EAreteSheetParagraphWithForm>
found: no arguments
reason: actual and formal argument lists differ in length
app/build/generated/source/kapt/debug/it/ximplia/agri2000/model/db/dao/AreteSheetDAO_Impl.java:204: error: sheet has private access in EAreteSheetWithParagraph
_item.sheet = _tmpSheet;
I think that Room does not allow to resolve dependencies in cascade but I would like to know if someone was successful or if I made a mistake before changing the code.
In EAreteSheetWithParagraph you are specifying a list of EAreteSheetParagraphWithForm's
as per :-
val paragraph: List<EAreteSheetParagraphWithForm>
The #Relation will try to ascertain the columns as per the EAreteSheetParagraphWithForm form class, which is not an Entity. You should change the #Relation to specify the appropriate entity (i.e. the EAreteSheetParagraph) using the entity parameter.
So EAreteSheetWithParagraph should be something like :-
data class EAreteSheetWithParagraph(
#Embedded val sheet: EAreteSheet,
#Relation(
entity = EAreteSheetParagraph::class,
parentColumn = "id",
entityColumn = "arete_sheet_id"
)
val paragraph: List<EAreteSheetParagraphWithForm>
)
However, I don't believe that you are getting that far, as the messages appear to be complaining about the sheet variable which has a type of Sheets (and that the variable is private).
Without adding the entity= parameter then the compile, if it reached that stage, would fail with :-
> Task :app:kaptDebugKotlin FAILED
error: The class must be either #Entity or #DatabaseView. - a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm
E:\AndroidStudioApps\SO68953488KotlinRoomMany1N\app\build\tmp\kapt3\stubs\debug\a\a\so68953488kotlinroommany1_n\EAreteSheetWithParagraph.java:12: error: Cannot find the child entity column `arete_sheet_id` in a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm. Options:
private final java.util.List<a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm> paragraph = null;
^
The class must be either #Entity or #DatabaseView. - a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm
I think that Room does not allow to resolve dependencies in cascade but I would like to know if someone was successful or if I made a mistake before changing the code.
As indicated above, I believe the issue is mistakes so :-
Proof of concept
Based upon your code the following shows that nested/multiple 1-n's do work:-
Using your code BUT with the following changes (to avoid issues and simplify) :-
#Entity(tableName = "arete_sheet_form")
data class EAreteSheetForm(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_paragraph_id") val paragraph: Long,
#ColumnInfo(name = "fieldType") val fieldType: /* FieldType */ String, // changed for convenience/brevity
#ColumnInfo(name = "cell") val cell: String,
#ColumnInfo(name = "label") val label: String
)
fieldType' stype changed from FieldType to String so no need for extra class and type converters.
and
#Entity(tableName = "arete_sheet")
data class EAreteSheet(
#PrimaryKey val id: Long,
#ColumnInfo(name = "sheet") val form: /*Sheets*/ String, // changed for convenience/brevity
#ColumnInfo(name = "version") val version: Int,
)
Sheets type substituted with String
EAreteSheetWithParagraph as above
EAreteSheetParagraphWithForm changed to
data class EAreteSheetParagraphWithForm(
#Embedded val paragraph: EAreteSheetParagraph,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_paragraph_id",
entity = EAreteSheetForm::class
)
val forms: List<EAreteSheetForm>
)
i.e. the entity parameter has been added according to my preference to always code the entity parameter.
AreteSheetDao used :-
#Dao
abstract class AreteSheetDAO {
#Insert
abstract fun insert(eAreteSheet: EAreteSheet): Long
#Insert
abstract fun insert(eAreteSheetParagraph: EAreteSheetParagraph): Long
#Insert
abstract fun insert(eAreteSheetForm: EAreteSheetForm): Long
#Transaction
#Query("SELECT * FROM arete_sheet")
abstract fun getSheetWithParagraphsAndForms(): List<EAreteSheetWithParagraph>
}
Code in an Activity :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AreteSheetDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getDao()
var s1 = dao.insert(EAreteSheet(10,"Sheet1",1))
var s2 = dao.insert(EAreteSheet(20,"Sheet2",1))
var p1 = dao.insert(EAreteSheetParagraph(100,s1,"Para1 (Sheet1)"))
var p2 = dao.insert(EAreteSheetParagraph(101,s1,"Para2 (Sheet1)"))
var p3 = dao.insert(EAreteSheetParagraph(201,s2,"Para3 (Sheet2)"))
var p4 = dao.insert(EAreteSheetParagraph(202,s2,"Para4 (Sheet2)"))
var f1 = dao.insert(EAreteSheetForm(1000,p1,"typex","cellx","Form1"))
var f2 = dao.insert(EAreteSheetForm(1001,p1,"typex","cellx","Form2"))
var f3 = dao.insert(EAreteSheetForm(1002,p1,"typex","cellx","Form3"))
var f4 = dao.insert(EAreteSheetForm(1010,p2,"typex","cellx","Form4"))
var f5 = dao.insert(EAreteSheetForm(1011,p2,"typex","cellx","Form5"))
var f6 = dao.insert(EAreteSheetForm(1020,p3,"typex","cellx","Form6"))
val TAG = "ARETEINFO"
for(sw: EAreteSheetWithParagraph in dao.getSheetWithParagraphsAndForms()) {
Log.d(TAG,"Sheet ID is ${sw.sheet.id} Form is ${sw.sheet.form} Version is ${sw.sheet.version}" )
for(pf: EAreteSheetParagraphWithForm in sw.paragraph) {
Log.d(TAG,"\tPara is ${pf.paragraph.name} etc")
for(f: EAreteSheetForm in pf.forms) {
Log.d(TAG,"\t\tForm is ${f.label}")
}
}
}
}
}
As can be seen, some data is loaded, 2 Sheets, each with 2 paragraphs and then 4 forms distributed unevenly (3 to para1, 2 to para2, 1 to para3, 0 to para4).
The data is then extracted (as array of EAreteSheetWithParagraph), the array traversed (traversing the underlying arrays of paragraphs and forms within paragraphs) and output to the log the Result being :-
D/ARETEINFO: Sheet ID is 10 Form is Sheet1 Version is 1
D/ARETEINFO: Para is Para1 (Sheet1) etc
D/ARETEINFO: Form is Form1
D/ARETEINFO: Form is Form2
D/ARETEINFO: Form is Form3
D/ARETEINFO: Para is Para2 (Sheet1) etc
D/ARETEINFO: Form is Form4
D/ARETEINFO: Form is Form5
D/ARETEINFO: Sheet ID is 20 Form is Sheet2 Version is 1
D/ARETEINFO: Para is Para3 (Sheet2) etc
D/ARETEINFO: Form is Form6
D/ARETEINFO: Para is Para4 (Sheet2) etc