Kotmin Room crossRef Entity with keys having the same name - kotlin

I'm creating a CRUD factory. Basically all my entites will inherit from a BaseEntity with id as a primary key
I'm trying to understand how to create a cross ref table for a M2M relationship.
Here is a simplifyied example, without inheritance. ArticlesEntity have many MagasinsEntity and MagasinsEntity many ArticlesEntity. The Entity ArticlesMagasinsCrossRef is the junction
But both ArticlesEntity and MagasinsEntity have id as the primaryKey.
#Entity(
tableName = "articles"
)
data class ArticlesEntity(
#PrimaryKey(autoGenerate = false) val id: UUID = UUID.randomUUID(),
val title: String,
)
#Entity(tableName = "magasins")
data class MagasinsEntity(
#PrimaryKey(autoGenerate = false) val id: UUID = UUID.randomUUID(),
val nomMagasin: String
)
#Entity(
tableName = "articles_magasins"
)
data class ArticlesMagasinsCrossRefEntity(
val id: UUID, // how is it possible here to have the id of Articles ?
val id: UUID // how is it possible here to have the id of Magasins ?
)
Edit
I tried of course to change the name of the columns:
data class ArticlesMagasinsCrossRefEntity(
val articleRd: UUID
val magasinId: UUID
)
but the build failed for the relation data class :for example
data class RelMagasinWithArticles(
#Embedded val magasin: MagasinsEntity,
#Relation(
parentColumn = "magasinId",
entityColumn = "id",
associateBy = Junction(ArticlesMagasinsCrossRefEntity::class)
)
val articles: List<ArticleEntity>
)

You need to use the Junction's parentColumn and entityColumn parameters e.g.
data class RelMagasinWithArticles(
#Embedded val magasin: MagasinsEntity,
#Relation(
parentColumn = "id", /* The column in the #Embedded table (articles) */
entityColumn = "id", /* The column in the Related table (magasins) */
associateBy = Junction(
ArticlesMagasinsCrossRefEntity::class,
parentColumn = "magasinId", /* The column in the junction table that maps to the #Embedded table */
entityColumn = "articleRd" /* The column in the junction table that maps to the #Relation Table */
)
)
val articles: List<ArticlesEntity>
)
Note
You will also need to define a primary key for the ArticlesMagasinsCrossRefEntity class e.g. :-
#Entity(
tableName = "articles_magasins",
/*<<<<< all Room table MUST have a primary key */
/* as primary key on a single column would be restrictive use a composite
primary key
*/
primaryKeys = ["articleRd","magasinId"]
)
data class ArticlesMagasinsCrossRefEntity(
/* Cannot have identical member names - i.e. only 1 could be id
val id: UUID, // how is it possible here to have the id of Articles ?
val id: UUID // how is it possible here to have the id of Magasins ?
*/
val articleRd: UUID,
/* Room will issue warning if the is no index on the 2nd column */
#ColumnInfo(index = true)
val magasinId: UUID
)
note that you can use the #ColumnInfo name parameter to specify column names.
Demonstration
So using you code with the suggested code overriding your code and with the following #Dao interface:-
#Dao
interface AllDao {
#Insert
fun insert(articlesEntity: ArticlesEntity)
#Insert
fun insert(magasinsEntity: MagasinsEntity)
#Insert
fun insert(articlesMagasinsCrossRefEntity: ArticlesMagasinsCrossRefEntity)
#Query("SELECT * FROM magasins")
#Transaction
fun getMWA(): List<RelMagasinWithArticles>
}
a suitable #Database annotated class and code in the activity:-
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 a1uuid = UUID.randomUUID()
val a2uuid = UUID.randomUUID()
val a3uuid = UUID.randomUUID()
dao.insert(ArticlesEntity(a1uuid, "Article1"))
dao.insert(ArticlesEntity(a2uuid,"Article2"))
dao.insert(ArticlesEntity(a3uuid,"Article3"))
val m1uuid = UUID.randomUUID()
val m2uuid = UUID.randomUUID()
val m3uuid = UUID.randomUUID()
dao.insert(MagasinsEntity(m1uuid,"Magasin1"))
dao.insert(MagasinsEntity(m2uuid,"Magasin2"))
dao.insert(MagasinsEntity(m3uuid,"Magasin3"))
dao.insert(ArticlesMagasinsCrossRefEntity(a1uuid,m2uuid))
dao.insert(ArticlesMagasinsCrossRefEntity(a1uuid,m3uuid))
dao.insert(ArticlesMagasinsCrossRefEntity(a2uuid,m1uuid))
dao.insert(ArticlesMagasinsCrossRefEntity(a3uuid,m1uuid))
dao.insert(ArticlesMagasinsCrossRefEntity(a3uuid,m2uuid))
dao.insert(ArticlesMagasinsCrossRefEntity(a3uuid,m3uuid))
val sb = StringBuilder()
for(mwa in dao.getMWA()) {
sb.append("\nMagasin is ${mwa.magasin.nomMagasin}. ID is ${mwa.magasin.id} it has ${mwa.articles.size} articles. They are:-" )
for (article in mwa.articles) {
sb.append("\n\tArticle is ${article.title} ID is ${article.id}")
}
}
Log.d("DBINFO",sb.toString())
}
The output to the log is:-
D/DBINFO: Magasin is Magasin1. ID is 0f3384ee-6232-423e-b2f1-a12ebdac6487 it has 2 articles. They are:-
Article is Article2 ID is 2729d017-de05-41d2-8de3-8351dfca0a6b
Article is Article3 ID is 42057ea7-bc03-409f-b2b8-2dc3fa5def19
Magasin is Magasin2. ID is ba649833-a8ce-4cf2-a1b8-bcab8f7a7d0a it has 2 articles. They are:-
Article is Article1 ID is 8763421d-b86d-4725-8e6b-65570958ebdc
Article is Article3 ID is 42057ea7-bc03-409f-b2b8-2dc3fa5def19
Magasin is Magasin3. ID is eed6f0a5-0825-4cda-9eb4-c4e973a49738 it has 2 articles. They are:-
Article is Article1 ID is 8763421d-b86d-4725-8e6b-65570958ebdc
Article is Article3 ID is 42057ea7-bc03-409f-b2b8-2dc3fa5def19

Related

JPA Specification joins with Kotlin

I have three JPA entities that look a bit like this:
#Entity
#Table(name = "users")
data class UserEntity(
#Id
val id: UUID,
var name: String,
#OneToMany(mappedBy = "user")
val roles: MutableList<UserRoleAssignmentEntity>,
#OneToMany(mappedBy = "user")
val qualifications: MutableList<UserQualificationAssignmentEntity>,
)
data class UserQualificationAssignmentEntity(
#Id
val id: UUID,
#ManyToOne
#JoinColumn(name = "user_id")
val user: UserEntity,
#Enumerated(value = EnumType.STRING)
val qualification: UserQualification
)
#Entity
#Table(name = "user_role_assignments")
data class UserRoleAssignmentEntity(
#Id
val id: UUID,
#ManyToOne
#JoinColumn(name = "user_id")
val user: UserEntity,
#Enumerated(value = EnumType.STRING)
val role: UserRole
)
I have created a JPA Specification that optionally takes a List of UserRoles and / or UserQualifications and builds up some criteria:
class UserFilterSpecification(private val qualifications: List<UserQualification>?, private val roles: List<UserRole>?) : Specification<UserEntity> {
override fun toPredicate(
root: Root<UserEntity>,
query: CriteriaQuery<*>,
criteriaBuilder: CriteriaBuilder
): Predicate? {
val predicates = mutableListOf<Predicate>()
if (qualifications?.isNotEmpty() == true) {
predicates.add(
criteriaBuilder.and(
root.join<UserEntity, MutableList<UserQualificationAssignmentEntity>>("qualifications").`in`(qualifications)
)
)
}
if (roles?.isNotEmpty() == true) {
predicates.add(
criteriaBuilder.and(
root.join<UserEntity, MutableList<UserRoleAssignmentEntity>>("roles").`in`(roles)
)
)
}
return criteriaBuilder.and(*predicates.toTypedArray())
}
}
However, when I try to use this:
userRepository.findAll(UserFilterSpecification(qualifications, roles))
I get the error:
Unable to locate Attribute with the the given name [roles] on this ManagedType [uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserEntity]
for roles and:
Unable to locate Attribute with the the given name [qualifications] on this ManagedType [uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserEntity]
I'm assuming this is because I'm not doing the join correctly i.e:
root.join<UserEntity, MutableList<UserQualificationAssignmentEntity>>("qualifications").`in`(qualifications)
root.join<UserEntity, MutableList<UserQualificationAssignmentEntity>>("roles").`in`(roles)
I've tried a couple of permutations (role, roles.role etc), but nothing seems to stick. What am I missing?

How to manipulate a specific set of data from Live or Flow Data Kotlin MVVM?

I have three Room entities. An entity Person includes id and name, entity Holiday includes id, personId, onHoliday and Sickness entity includes id, personId and onSickness.
Also, I have a POJO Entity called ScreenPOJO that includes viewTypeId and personName.
My goal is to get the person's name (from Person) and viewTypeId that I can observe in Activity.
The viewTypeId is an Integer that depends on the onHoliday - true/false and onSickness true/false. So let's say when the onHoliday is false and onSickness is false the viewTypeId = 1, when onHoliday is true then viewTypeId = 2 and when onSickness is true then viewTypeId = 2.
In this example, it is achievable by creating a query and returning the result by using the POJO Entity.
Sometimes the query can be too complex and for that reason, I would like to somehow merge all three Live/Flow data together using the person_id. I read that I can use MediatorLiveData, however, I do not have experience yet to put all the data together and return only the result that I need (person name and viewTypeId).
Person Entity:
#Entity(tableName = "person_table")
data class Person(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id") val id: Int,
#SerializedName("name")
#ColumnInfo(name = "name") val name: String,
)
Sickness Entity:
#Entity(tableName = "sickness_table")
data class Sickness(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id") val id: Int,
#SerializedName("person_id")
#ColumnInfo(name = "person_id") val personId: Int,
#SerializedName("on_sickness")
#ColumnInfo(name = "on_sickness") val onSickness: Boolean
)
Holiday Entity:
#Entity(tableName = "holiday_table")
data class Holiday(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id") val id: Int,
#SerializedName("person_id")
#ColumnInfo(name = "person_id") val personId: Int,
#SerializedName("on_holiday")
#ColumnInfo(name = "on_holiday") val onHoliday: Boolean
)
ScreenPOJO Entity:
data class ScreenPOJO(
val viewTypeId: Int,
val personName: String
)
ViewModel, the data are originally as Flow :
val allPersons: LiveData<List<Person>> = repository.allPersons.asLiveData()
val allHolidays: LiveData<List<Holiday>> = repository.allHolidays.asLiveData()
val allSickness: LiveData<List<Sickness>> = repository.allSickness.asLiveData()
ViewModel, example of insert Person:
private val _insertPersonStatus = MutableLiveData<ViewModelStatus>()
val insertPersonStatus: LiveData<ViewModelStatus> = _insertPersonStatus
fun insertPerson(person: Person) = viewModelScope.launch {
try {
val insertedRowId = repository.insertPerson(person)
if (insertedRowId > -1) {
_insertPersonStatus.postValue(ViewModelStatus.SUCCESS(insertedRowId,ViewModelStatus.SUCCESS.Type.VM_INSERT))
} else {
_insertPersonStatus.value = ViewModelStatus.FAIL("Fail",ViewModelStatus.FAIL.Type.VM_INSERT)
}
} catch (ex: Exception) {
_insertPersonStatus.value = ViewModelStatus.EXCEPTION(ex.localizedMessage.toString(),ViewModelStatus.EXCEPTION.Type.VM_INSERT)
}
}
I think it is better to modify the data layer in a way, which will allow you to write less code in the business-logic and view layers, therefore the code will be simpler and cleaner which is always a good sign.
Therefore, instead of merging LiveData you should think about redesigning Person data class.
Since I don't see much sense in storing holidays and sickness separately from persons, I assume you should somehow store holiday and sickness related data in the person object.
I suggest something like this (I will omit room annotations for simplicity):
data class Person(
val id: Long,
val name: String,
val onHoliday: Boolean,
val onSickness: Boolean
)
In case when the person has multiple holidays or sickness days, you can do something like this (which is kinda common practice nowadays):
data class Person(
val id: Long,
val name: String,
val holidays: List<Holiday>,
val sicknessDays: List<Sickness>
)
I am not sure that it is easy to insert a collection in Room, but there are workarounds here. Maybe you will also need #Embedded annotation to keep the POJO's inside of the POJO

Best way to deserialize Json with nested objects into Android Room Entity with ForeignKeys

I have a Client api. The json response looks like this:
{
"clientId": 1,
"createdAt": null,
"updatedAt": null,
"monthlyPaymentAmount": null,
"person": {
// Omitted data here
},
"paymentType": {
// Omitted data here
},
"deliveryInstructions": null,
"referralName": null,
"referralPhoneNumber": null,
"status": 0,
"startDate": null,
"eventDate": null,
}
So, using the Kotlin data class file from JSON to automatically create data classes from the json response, I've got with the following Client data class which I've turned into a Room #Entity with ForeignKeys:
#Entity(
tableName = "client",
foreignKeys = [
ForeignKey(
entity = Account::class,
parentColumns = arrayOf("account_id"),
childColumns = arrayOf("account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Person::class,
parentColumns = arrayOf("person_id", "account_id"),
childColumns = arrayOf("person_id", "account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = PaymentType::class,
parentColumns = arrayOf("payment_type_id", "account_id"),
childColumns = arrayOf("payment_type_id", "account_id"),
),
],
indices = [
Index(value = arrayOf("client_id", "account_id"), unique = true)
]
)
data class Client(
#PrimaryKey
#ColumnInfo(name = "client_id") val clientId: Int,
#ColumnInfo(name = "delivery_notes") val deliveryInstructions: String,
#ColumnInfo(name = "event_date") val eventDate: Date,
#ColumnInfo(name = "monthly_payment_amount") val monthlyPaymentAmount: Float,
#ColumnInfo(name = "payment_type_id") val paymentType: Int,
#ColumnInfo(name = "person_id") val person: Int,
#ColumnInfo(name = "referral_name") val referralName: String,
#ColumnInfo(name = "start_date") val startDate: Date,
#ColumnInfo(name = "status") val status: Int,
#ColumnInfo(name = "updated_at") val updatedAt: Date,
#ColumnInfo(name = "synced_at") val syncedAt: Date,
)
There's also PaymentType and Person data classes which I'm omitting, but they do are Room #Entity's as well.
The Room database needs to match the following database structure that has this CREATE TABLE SQL statement:
CREATE TABLE client
(
client_id INTEGER NOT NULL,
account_id INTEGER NOT NULL,
updated_at TEXT NOT NULL,
synced_at TEXT NOT NULL,
person_id INTEGER NOT NULL,
payment_type_id INTEGER,
referral_name TEXT,
delivery_notes TEXT,
status INTEGER DEFAULT 1 NOT NULL,
monthly_payment_amount REAL,
start_date TEXT,
event_date TEXT,
CONSTRAINT client_fk1 FOREIGN KEY (account_id) REFERENCES account (account_id) ON DELETE CASCADE,
CONSTRAINT client_fk2 FOREIGN KEY (person_id, account_id) REFERENCES person (person_id, account_id) ON DELETE CASCADE,
CONSTRAINT client_fk4 FOREIGN KEY (payment_type_id, account_id) REFERENCES payment_type (payment_type_id, account_id),
CONSTRAINT client_pk PRIMARY KEY (client_id, account_id)
);
So, I've a Converters class to deserialize the json response into Client class as follows:
class Converters {
#TypeConverter
fun clientToJson(value: Client?): String? = Gson().toJson(value)
#TypeConverter
fun jsonToClient(value: String): Client = Gson().fromJson(value, Client::class.java)
#TypeConverter
fun paymentTypeToJson(value: PaymentType?): String? = Gson().toJson(value)
#TypeConverter
fun jsonToPaymentType(value: String): PaymentType =
Gson().fromJson(value, PaymentType::class.java)
#TypeConverter
fun objToJsonPerson(value: Person?): String? = Gson().toJson(value)
#TypeConverter
fun jsonToObjPerson(value: String): Person = Gson().fromJson(value, Person::class.java)
// Omitted list of converters here
}
I'm hesitant if the client converter above does correctly creates PaymentType and Person objects automatically (mostly convinced that no). That's why I would like to know what's the proper way to deserialize a json response with nested objects into Room entities with Foreign Keys?
I'm most confused with Foreing Keys though. What will happen when the converter above tries to parse the "person": {} object into the #ColumnInfo(name = "person_id") which is of type Int? Will it know that it is a ForeignKey and will create a Person::class automatically? How's the best/proper way to deserialize nested objects ensuring this relation between the tables is properly done?
Demonstration following on from previous answer
This a demonstration of inserting a single client based upon some slightly modified entities.
Account :-
#Entity
data class Account(
#PrimaryKey
val account_id: Long? = null,
val accountName: String
)
PaymentType
#Entity( primaryKeys = ["payment_type_id","account_id"])
data class PaymentType(
#ColumnInfo(name = "payment_type_id")
val paymentTypeId: Long,
#ColumnInfo(name = "account_id")
val accountId: Long,
val paymentTypeName: String
)
added accountId (account_id column) to suit Foreign Key constraints in the ClientTable (as per question)
composite primary key
Person (likewise)
#Entity( primaryKeys = ["person_id","account_id"])
data class Person(
#ColumnInfo(name = "person_id")
val personId: Long,
#ColumnInfo(name = "account_id")
val accountId: Long,
val personName: String
)
Client as suggested
data class Client(
val clientId: Long,
val deliveryInstructions: String,
val eventDate: Date,
val monthlyPaymentAmount: Float,
val referralName: String,
val startDate: Date,
val status: Long,
val updatedAt: Date,
val syncedAt: Date,
val person: Person,
val paymentType: PaymentType
) {
fun getClientAsClientTable(): ClientTable {
return ClientTable(
this.clientId,
this.deliveryInstructions,
this.eventDate,
this.monthlyPaymentAmount,
this.paymentType.paymentTypeId,
this.person.personId,
this.referralName,
this.startDate,
this.status,
this.updatedAt,
this.syncedAt
)
}
}
ideally id's should be Long rather than Int as they have the potential to overflow an Int. So Long's have been used.
ClientTable formally (Client) :-
#Entity(
tableName = "client",
foreignKeys = [
ForeignKey(
entity = Account::class,
parentColumns = arrayOf("account_id"),
childColumns = arrayOf("account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Person::class,
parentColumns = arrayOf("person_id", "account_id"),
childColumns = arrayOf("person_id", "account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = PaymentType::class,
parentColumns = arrayOf("payment_type_id", "account_id"),
childColumns = arrayOf("payment_type_id", "account_id"),
),
],
indices = [
Index(value = arrayOf("client_id", "account_id"), unique = true)
]
)
data class ClientTable(
#PrimaryKey
#ColumnInfo(name = "client_id") val clientId: Long,
#ColumnInfo(name = "delivery_notes") val deliveryInstructions: String,
#ColumnInfo(name = "event_date") val eventDate: Date,
#ColumnInfo(name = "monthly_payment_amount") val monthlyPaymentAmount: Float,
#ColumnInfo(name = "payment_type_id") val paymentTypeid: Long,
#ColumnInfo(name = "person_id") val personid: Long,
#ColumnInfo(name = "referral_name") val referralName: String,
#ColumnInfo(name = "start_date") val startDate: Date,
#ColumnInfo(name = "status") val status: Long,
#ColumnInfo(name = "updated_at") val updatedAt: Date,
#ColumnInfo(name = "synced_at") val syncedAt: Date,
#ColumnInfo(name = "account_id") var accountId: Long = 1 //????? ADDED
)
NOTE the addition of the accountId
Converters
class Converters {
#TypeConverter
fun dateToLong(date: Date): Long {
return date.time / 1000 // divided by 1000 to strip milliseconds as easier to handle dates
}
#TypeConverter
fun dateFromLong(dateAsLong: Long): Date {
return Date(dateAsLong * 1000) // reapply milliseconds
}
}
AllDao (as it implies all of them together) :-
#Dao
abstract class AllDao {
#Insert(onConflict = IGNORE)
abstract fun insert(account: Account): Long
#Insert(onConflict = IGNORE)
abstract fun insert(paymentType: PaymentType): Long
#Insert(onConflict = IGNORE)
abstract fun insert(person: Person): Long
#Insert(onConflict = IGNORE)
abstract fun insert(clientTable: ClientTable): Long
#Query("SELECT count(*) >= 1 FROM account WHERE account_id=:accountId")
abstract fun doesAccountExistByAccountId(accountId: Long): Boolean
#Query("SELECT count(*) >= 1 FROM paymenttype WHERE account_id=:accountId AND payment_type_id=:paymentTypeId")
abstract fun doesPaymentTypeExistByAccountIdPaymentTypeId(accountId: Long, paymentTypeId: Long): Boolean
#Query("SELECT count(*) >= 1 FROM person WHERE account_id=:accountId AND person_id=:personId")
abstract fun doesPersonExistByAccountIdPersonId(accountId: Long, personId: Long): Boolean
#Query("")
#Transaction
fun insertFromAPIJson(json: String): Long {
var rv: Long = -1
val client = Gson().fromJson(json,Client::class.java)
val clientTable = client.getClientAsClientTable()
insert(Account(client.person.accountId,"NO NAME"))
val accountExits = doesAccountExistByAccountId(client.person.accountId)
insert(PaymentType(client.paymentType.paymentTypeId,client.paymentType.accountId,client.paymentType.paymentTypeName))
val paymentTypeExists = doesPaymentTypeExistByAccountIdPaymentTypeId(client.paymentType.accountId,client.paymentType.paymentTypeId)
insert(Person(client.person.personId, client.person.accountId, client.person.personName))
val personExists = doesPersonExistByAccountIdPersonId(client.person.accountId,client.person.personId)
if (accountExits && paymentTypeExists && personExists) {
clientTable.accountId = client.person.accountId
rv = insert(clientTable)
}
return rv
}
}
Obviously note the insertFromAPIJson function
Also note abstract class rather than an interface
Note the improvised account name (something you will have to determine how to name)
TheDatabase the abstract class annotated with #Database including a basic getInstance function :-
#TypeConverters(Converters::class)
#Database(entities = [Account::class,ClientTable::class,PaymentType::class,Person::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
}
}
}
Finally adding the single client from the JSON (again the client is built and the JSON extracted to mimic the API). MainActivity :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/* Create a Client ready to be converted to JSON */
val clientx = Client(
clientId = 1,
deliveryInstructions = "x",
eventDate = Date(),
monthlyPaymentAmount = 111.11F, referralName = "Fred", startDate = Date(), status = 1, updatedAt = Date(), syncedAt = Date(),
Person(10,1,"Bert"), paymentType = PaymentType(20,1,"Credit Card"))
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
dao.insertFromAPIJson(Gson().toJson(clientx))
}
}
The Result
Using App Inspection :-
The Account has been added :-
The PaymentType has been added :-
The Person has been added :-
and the Client :-
Run again
As expected the data remains the same due to onConflict IGNORE
Will it know that it is a ForeignKey and will create a Person::class automatically?
Absolutely not.
#ForeignKey defines a ForeignKey constraint i.e. a rule that says that the column(s) to which the constraint is applied must be a value in the referenced column(s) in the referenced table. The referenced (Parent) tables have to exist and be populated accordingly with values that match the rule.
Type converters are used to convert an unhandled type (not a type this is integer (e.g. Int Long Byte etc), String, decimal number (Float, Double etc) or ByteArray) into a handled type.
As an example your :-
#TypeConverter
fun clientToJson(value: Client?): String? = Gson().toJson(value)
would convert a single column such as
client: Client
from a Client To a JSON String and store that string in the client column. It is not going to split the client into individual values and place them into individual columns.
So with your retrieved JSON String you extract Client objects with embedded Person and Payment Type.
You can only successfully insert the Client if all the Foreign Keys can be met.
So you should probably check that the account_id exists in the account table.
The check that the person_id,account_id exists in the person_table and so on before inserting the Client otherwise the insert will fail.
If the checks fail to identify the rows then you either have to abort or insert the appropriate rows into the tables.
Assuming that your source data is referentially correct. Then you should first extract the highest level parent (Account I believe) inserting them. You can then extract the next level (Person and Payment Type) and insert them and then finally insert the Client. This way the Foreign Keys should exist.
Al alternative would be to turn off Foreign key support and load the data and then turn on Foreign Key support back on. However, if the data is not referentially correct you may encounter Foreign Key constraint conflicts.
e.g. db.openHelper.writableDatabase.setForeignKeyConstraintsEnabled(false)
I've got with the following Client data class which I've turned into a Room #Entity with ForeignKeys:
based upon the JSON you will have issues with the null values unless you ensure that the values are changed to appropriate values.
e.g. "updatedAt": null, associated with #ColumnInfo(name = "updated_at") val updatedAt: Date, unless the TypeConverter returns a non-null value will fail.
The Room database needs to match the following database structure that has this CREATE TABLE SQL statement:
It does not e.g. you have:-
payment_type_id INTEGER, but #ColumnInfo(name = "payment_type_id") val paymentType: Int, The former does not have the NOT NULL constraint, the latter has an implicit NOT NULL (val paymentType: Int? does not have the implicit NOT NULL)
repeated for a number of columns
status INTEGER DEFAULT 1 NOT NULL, but #ColumnInfo(name = "status") val status: Int, the latter does not have the default value using defaultValue = "1" in the #ColumnInfo annotation would apply it.
However, you cannot use the convenience #Insert annotated function as it will ALWAYS supply a value. To have the default value apply you would have to use #Query("INSERT INTO (csv_of_the_columns_that_are_not_to_have_a_default_value_applied) VALUES ....
CONSTRAINT client_pk PRIMARY KEY (client_id, account_id) but #PrimaryKey #ColumnInfo(name = "client_id") val clientId: Int,. Only the client ID is the Primary Key. You do have indices = [Index(value = arrayOf("client_id", "account_id"), unique = true)]. However, you should instead have primaryKeys = arrayOf("client_id", "account_id")
Additional
Having had a closer look. I believe that your issue is not with type converters nor at present with Foreign keys but with what is an attempt to fit the square peg into the round hole.
Without delving into trying to ignore fields from a JSON perspective a solution to what I believe is the core issue is that the you cannot just fit the Client with Person and Payment objects Embedded into the Client that you want to store.
So first consider this alternative class for the Entity renamed ClientTable :-
#Entity(
tableName = "client",
foreignKeys = [
ForeignKey(
entity = Account::class,
parentColumns = arrayOf("account_id"),
childColumns = arrayOf("account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Person::class,
parentColumns = arrayOf("person_id", "account_id"),
childColumns = arrayOf("person_id", "account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = PaymentType::class,
parentColumns = arrayOf("payment_type_id", "account_id"),
childColumns = arrayOf("payment_type_id", "account_id"),
),
],
indices = [
Index(value = arrayOf("client_id", "account_id"), unique = true)
]
)
data class ClientTable(
#PrimaryKey
#ColumnInfo(name = "client_id") val clientId: Int,
#ColumnInfo(name = "delivery_notes") val deliveryInstructions: String,
#ColumnInfo(name = "event_date") val eventDate: Date,
#ColumnInfo(name = "monthly_payment_amount") val monthlyPaymentAmount: Float,
#ColumnInfo(name = "payment_type_id") val paymentTypeid: Int,
#ColumnInfo(name = "person_id") val personid: Int,
#ColumnInfo(name = "referral_name") val referralName: String,
#ColumnInfo(name = "start_date") val startDate: Date,
#ColumnInfo(name = "status") val status: Int,
#ColumnInfo(name = "updated_at") val updatedAt: Date,
#ColumnInfo(name = "synced_at") val syncedAt: Date,
/* Not required but may be useful BUT will not be columns in the table */
#Ignore
val person: Person,
#Ignore
val paymentType: PaymentType
)
The only changes are the two additional BUT #Ignore annotated vals, for the Person and for the PaymentType. The #Ignore results in them not being included as a column in the table. They are there just for demonstration (you might have problems with them being null when extracting the data from the database).
Note that for testing the PaymentType is :-
#Entity
data class PaymentType(
#PrimaryKey
val paymentTypeId: Long? = null,
val paymentTypeName: String
)
and the Person is :-
#Entity
data class Person(
#PrimaryKey
val personId: Long,
val personName: String
)
so the // Omitted data here does not cause issues.
Instead of your JSON the following JSON has been used (however it is built on the fly) :-
{"clientId":1,"deliveryInstructions":"x","eventDate":"Jan 21, 2022 10:57:59 AM","monthlyPaymentAmount":111.11,"paymentType":{"paymentTypeId":20,"paymentTypeName":"Credit Card"},"person":{"personId":10,"personName":"Bert"},"referralName":"Fred","startDate":"Jan 21, 2022 10:57:59 AM","status":1,"syncedAt":"Jan 21, 2022 10:57:59 AM","updatedAt":"Jan 21, 2022 10:57:59 AM"}
A simple not Type Converter json extractor to mimic the API has been added:-
class JsonApiExample {
fun testExtractJsonFromString(json: String): Client {
return Gson().fromJson(json,Client::class.java)
}
}
Now to the other peg The Client with the embedded Person/PaymentType and WITHOUT the personId and paymentTypeId that are not fields in the JSON:-
data class Client(
val clientId: Int,
val deliveryInstructions: String,
val eventDate: Date,
val monthlyPaymentAmount: Float,
val referralName: String,
val startDate: Date,
val status: Int,
val updatedAt: Date,
val syncedAt: Date,
val person: Person,
val paymentType: PaymentType
) {
fun getClientAsClientTable(): ClientTable {
return ClientTable(
this.clientId,
this.deliveryInstructions,
this.eventDate,
this.monthlyPaymentAmount,
this.paymentType.paymentTypeId!!.toInt(),
this.person.personId.toInt(),
this.referralName,
this.startDate,
this.status,
this.updatedAt,
this.syncedAt,
this.person,
this.paymentType
)
}
}
As you can see the important bit is the getClientAsClientTable funtion. This will generate and return a ClientTable Object and effectively make the square peg round to fit.
So testing it, as far as creating a ClientTable that could be inserted (Foreign Keys permitting, which would not be the case due to there being no account_id column in the ClientTable nora field in the Client class) consider :-
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/* Create a Client ready to be converted to JSON */
val clientx = Client(
clientId = 1,
deliveryInstructions = "x",
eventDate = Date(),
monthlyPaymentAmount = 111.11F, referralName = "Fred", startDate = Date(), status = 1, updatedAt = Date(), syncedAt = Date(),
Person(10,"Bert"), paymentType = PaymentType(20,"Credit Card"))
/* Convert the Client to JSON mimicing the API and write it to the log to allow inspection */
val jsonClientX = Gson().toJson(clientx)
Log.d("TESTIT",jsonClientX)
/* Extract the Client from the JSON */
val clientxrevisited = JsonApiExample().testExtractJsonFromString(jsonClientX)
/* Extract the ClientTable from the Client */
val ClientTable = clientxrevisited.getClientAsClientTable()
/* Allow a Break point to be placed so the results can be inspected*/
if (1 == 1) {
Log.d("TESTIT","Testing")
}
}
}
When run with a BreakPoint :-
The ticks are the columns
The highlighted are the embedded objects from which you would be able to insert or ignore into the respective tables.
note that the fields may well be different just id and name were chosen just to demonstrate.
So to RECAP two classes; one for the JSON extract/import, and the other for the Entity/Table with a means of turning one into the other.
A single class may be possible if you can ascertain how to ignore/map JSON fields e.g. perhaps this How to add and ignore a field for json response

Structuring DB tables and #Relation to create a questionaire from db entities? (kotlin, Room)

Iam seeking a little guidence before diving into a new project.
My aim is to produce a form/questionaire within an app that pulls Question data, based on a question's Group and its Category. Where each Category contains many Groups, and each Group contains many Questions.
To support the UI and nested recycler views, the intent was to provide the viewmodel with a single object that contains nested lists. i.e An object with a list of Categories, containing a list of Groups, containing a list of Questions.
In terms of setting up room, entities and DAOs, and their relationships, my understanding is that that the best way to achieve this is to:
Create a Questions entity (conatining text,options..etc.)
Create a reference table for the relationship between Questions and Groups (many-to-many)
Create a parent/child table for the relationship between Groups and Categories (one-to-many)
Following this a set of relation based data classes should be use to marry up each pair.
GroupWithQuestions data class (using #Relation to list questions in each group using the reference table)
CategoryWithGroupsWithQuestions data class (using #Relation to list groups in each category using parent/child table)
QuestionaireWithCategoriesWith...Questions data class (containing a list of CategoryWithGroupsWithQuestions)
This is complicated, relationships need to be followed through multiple tables, and thus will be hard to update and time consuming to resolve errors. I feel like Im over thinking the approach (or am missing something).
Is there a simpler/smarter way?
(Is the single object approach part of the problem?)
Thanks in advance for your suggestions and comments.
Create a Questions entity (conatining text,options..etc.)
Create a reference table for the relationship between Questions and Groups (many-to-many)
Create a parent/child table for the relationship between Groups and Categories (one-to-many)
A one-many simply needs a column in the child for the parent.
This is complicated, relationships need to be followed through multiple tables, and thus will be hard to update and time consuming to resolve errors. I feel like Im over thinking the approach (or am missing something).
This isn't really that complicated: I believe the following is pretty close to what you appear to be asking for:-
Entities
Category :-
#Entity(
indices = [
Index(value = ["categoryName"],unique = true) /* Assume that a category name should be unique */
]
)
data class Category(
#PrimaryKey
val categoryId: Long? = null,
#ColumnInfo
val categoryName: String
)
Group :-
#Entity(
foreignKeys = [
ForeignKey(
entity = Category::class,
parentColumns = ["categoryId"],
childColumns = ["categoryIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Group(
#PrimaryKey
val groupId: Long? = null,
#ColumnInfo(index = true)
val categoryIdMap: Long,
val groupName: String
)
Foriegn Key constraints are not necessary but they help to enforce referential integrity.
onDelete and onUpdate aren't necessary but can be helpful
Question
#Entity(
)
data class Question(
#PrimaryKey
val questionId: Long? = null,
#ColumnInfo(index = true)
val questionText: String,
val questionOption: Int
)
QuestiongroupMap (could be GroupQuestionMap) :-
#Entity(
primaryKeys = ["questionIdMap","groupIdMap"],
foreignKeys = [
ForeignKey(
entity = Question::class,
parentColumns = ["questionId"],
childColumns = ["questionIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE),
ForeignKey(
entity = Group::class,
parentColumns = ["groupId"],
childColumns = ["groupIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class QuestionGroupMap(
val questionIdMap: Long,
#ColumnInfo(index = true)
val groupIdMap: Long
)
POJO's
GroupWithQuestions
data class GroupWithQuestions(
#Embedded
val group: Group,
#Relation(
entity = Question::class,
entityColumn = "questionId",
parentColumn = "groupId",
associateBy = Junction(
QuestionGroupMap::class,
parentColumn = "groupIdMap",
entityColumn = "questionIdMap"
)
)
val questionList: List<Question>
)
via the QuestiongroupMap and hence the association and Junction
CategoryWithGroupsWithQuestions
data class CategoryWithGroupWithQuestions(
#Embedded
val category: Category,
#Relation(entity = Group::class,entityColumn = "categoryIdMap",parentColumn = "categoryId")
val groupWithQuestionsList: List<GroupWithQuestions>
)
NOTE even though you are getting a List of GroupWithQuestions, it is the Group entity that is specified.
Some extras that may be of use :-
data class CategoryWithGroup(
#Embedded
val category: Category,
#Relation(entity = Group::class,entityColumn = "categoryIdMap",parentColumn = "categoryId")
val group: Group
)
data class GroupWithCategory(
#Embedded
val group: Group,
#Relation(entity = Category::class,entityColumn = "categoryId",parentColumn = "categoryIdMap")
val category: Category
)
Dao's
AllDao (i.e. all in one place for brevity/convenience) :-
#Dao
abstract class AllDao {
#Insert
abstract fun insert(category: Category): Long
#Insert
abstract fun insert(group: Group): Long
#Insert
abstract fun insert(question: Question): Long
#Insert
abstract fun insert(questionGroupMap: QuestionGroupMap): Long
#Transaction
#Query("SELECT * FROM `group`")
abstract fun getAllGroupsWithCategory(): List<GroupWithCategory>
#Transaction
#Query("SELECT * FROM category")
abstract fun getAllCategoriesWithGroups(): List<CategoryWithGroup>
#Transaction
#Query("SELECT * FROM `group`")
abstract fun getAllGroupsWithQuestions(): List<GroupWithQuestions>
#Transaction
#Query("SELECT * FROM category")
abstract fun getAllCategoriesWithGroupsWithQuestions(): List<CategoryWithGroupWithQuestions>
}
an #Database class TheDatabase :-
#Database(entities = [Category::class,Group::class,Question::class,QuestionGroupMap::class],exportSchema = false,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,
"thedatabase.db"
)
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
allowMainThreadQueries for brevity/convenience
Finally putting the above into action in an activity, result in a list of CategoreiesWithgroupsWithQuestions being extracted and output 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 TAG = "DBINFO"
val cat1 = dao.insert(Category(categoryName = "Cat1"))
val cat2 = dao.insert(Category(categoryName = "Cat2"))
val cat3 = dao.insert(Category(categoryName = "Cat3"))
val grp1 = dao.insert(Group(groupName = "Grp1",categoryIdMap = cat1))
val grp11 = dao.insert(Group(groupName = "Grp11",categoryIdMap = cat1))
val grp111 = dao.insert(Group(groupName = "Grp111",categoryIdMap = cat1))
val grp1111 = dao.insert(Group(groupName = "Grp1111",categoryIdMap = cat1))
val grp2 = dao.insert(Group(groupName = "Grp2",categoryIdMap = cat2))
val grp22 = dao.insert(Group(groupName = "Grp22",categoryIdMap = cat2))
val grp3 = dao.insert(Group(groupName = "Grp3",categoryIdMap = cat3))
val q1 = dao.insert(Question(questionText = "Q1 ....", questionOption = 11110000))
val q2 = dao.insert(Question(questionText = "Q2....", questionOption = 11010101))
val q3 = dao.insert(Question(questionText = "Q3....", questionOption = 10000001))
val q4 = dao.insert(Question(questionText = "Q4....",questionOption = 11000001))
val q5 = dao.insert(Question(questionText = "Q5....",questionOption = 11100011))
dao.insert(QuestionGroupMap(q1,grp1))
dao.insert(QuestionGroupMap(q1,grp2))
dao.insert(QuestionGroupMap(q1,grp3))
dao.insert(QuestionGroupMap(q2,grp2))
dao.insert(QuestionGroupMap(q2,grp22))
dao.insert(QuestionGroupMap(q3,grp3))
dao.insert(QuestionGroupMap(q4,grp11))
dao.insert(QuestionGroupMap(q4,grp111))
dao.insert(QuestionGroupMap(q4,grp1111))
dao.insert(QuestionGroupMap(q5,grp22))
/* extract the data via the geAllCategoriesWithGroupsWithQuestions query*/
for (cwgwq: CategoryWithGroupWithQuestions in dao.getAllCategoriesWithGroupsWithQuestions()) {
Log.d(TAG,"Category is ${cwgwq.category.categoryName} ID is ${cwgwq.category.categoryId}, it has ${cwgwq.groupWithQuestionsList.size} groups, which are:-")
for(gwq: GroupWithQuestions in cwgwq.groupWithQuestionsList) {
Log.d(TAG,"\tGroup is ${gwq.group.groupName} ID is ${gwq.group.groupId}, it has ${gwq.questionList.size} questions, which are:-")
for(q: Question in gwq.questionList) {
Log.d(TAG,"\t\tQuestion is ${q.questionText} options are ${q.questionOption} ID is ${q.questionId}")
}
}
}
}
}
result :-
D/DBINFO: Category is Cat1 ID is 1, it has 4 groups, which are:-
D/DBINFO: Group is Grp1 ID is 1, it has 1 questions, which are:-
D/DBINFO: Question is Q1 .... options are 11110000 ID is 1
D/DBINFO: Group is Grp11 ID is 2, it has 1 questions, which are:-
D/DBINFO: Question is Q4.... options are 11000001 ID is 4
D/DBINFO: Group is Grp111 ID is 3, it has 1 questions, which are:-
D/DBINFO: Question is Q4.... options are 11000001 ID is 4
D/DBINFO: Group is Grp1111 ID is 4, it has 1 questions, which are:-
D/DBINFO: Question is Q4.... options are 11000001 ID is 4
D/DBINFO: Category is Cat2 ID is 2, it has 2 groups, which are:-
D/DBINFO: Group is Grp2 ID is 5, it has 2 questions, which are:-
D/DBINFO: Question is Q1 .... options are 11110000 ID is 1
D/DBINFO: Question is Q2.... options are 11010101 ID is 2
D/DBINFO: Group is Grp22 ID is 6, it has 2 questions, which are:-
D/DBINFO: Question is Q2.... options are 11010101 ID is 2
D/DBINFO: Question is Q5.... options are 11100011 ID is 5
D/DBINFO: Category is Cat3 ID is 3, it has 1 groups, which are:-
D/DBINFO: Group is Grp3 ID is 7, it has 2 questions, which are:-
D/DBINFO: Question is Q1 .... options are 11110000 ID is 1
D/DBINFO: Question is Q3.... options are 10000001 ID is 3

Dynamically rename columns in room table using kotlin

I have an application which loads a csv file with 4 fixed columns and 3 optional. I create tables in a room with 7 columns, where the last 3 have a name (atr1, atr2, atr3) and take a value of the String type. Is there a way to use Kotlin to rename these columns depending on the value in the csv file? Or is it possible to create 4 columns in the room and then add more depending on the columns in the csv file?
Entity
#Entity(tableName = "IND1")
data class IND1(
#PrimaryKey #ColumnInfo(name = "ID") val id: Int,
#ColumnInfo(name = "nr") val number: Int,
#ColumnInfo(name = "name") val name: String,
#ColumnInfo(name = "date") val date: LocalDate,
#ColumnInfo(name = "atr1") val atr1: String?,
#ColumnInfo(name = "atr2") val atr2: String?,
#ColumnInfo(name = "atr3") val atr3: String?
)
Database
#Database(entities = arrayOf(IND1::class, STT4::class, UNT1::class, USR1::class, WHS1::class), version = 2, exportSchema = true)
#TypeConverters(DateTimeConverter::class )
abstract class LocalRoomDatabase : RoomDatabase() {
abstract fun roomDao(): RoomDao
}
Module in dagger
#Module
object DatabaseModule {
#Singleton
#Provides
fun provideDatabase(applicationContext: MobileApplication): LocalRoomDatabase =
Room.databaseBuilder(applicationContext, LocalRoomDatabase::class.java, "local_database")
.addTypeConverter(DateTimeConverter())
.fallbackToDestructiveMigration()
.build()
#Singleton
#Provides
fun provideRoomDao(database: LocalRoomDatabase) = database.roomDao()
}
Is there a way to use Kotlin to rename these columns depending on the value in the csv file?
Not with Room as you would have issues with the Entities and the underlying generated code.
Or is it possible to create 4 columns in the room and then add more depending on the columns in the csv file?
Again no. As again basically Room creates and expects the tables to conform to the Entities.
However, a solution would be to have a table (Entity) for the items in the list/csv. Each related to the respective row in the IND1 table.
For example IND1 could become :-
#Entity(tableName = "IND1")
data class IND1(
#PrimaryKey #ColumnInfo(name = "ID") val id: Long, /* should really be Long */
#ColumnInfo(name = "nr") val number: Int,
#ColumnInfo(name = "name") val name: String,
#ColumnInfo(name = "date") val date: String /* Change for convenience */,
/* MOVED
#ColumnInfo(name = "atr1") val atr1: String?,
#ColumnInfo(name = "atr2") val atr2: String?,
#ColumnInfo(name = "atr3") val atr3: String?
*/
)
i.e. the attributes will be stored in a related table with a 1-many relationship (1 IND1 can have many attributes (0+)).
the ID column, as it uniquely identifies the IND1 row is a (the best) candidate for the relationship.
So the other table/entity could be Attribute :-
#Entity (
/* Foreign Key is optional but recommended to enforce
referential integrity
*/
foreignKeys = [
ForeignKey(
entity = IND1::class,
parentColumns = ["ID"],
childColumns = ["IND1_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Attribute(
#PrimaryKey #ColumnInfo(name = "AttributeId") val id: Long,
#ColumnInfo(name = "IND1_id", index = true) val parent_IND1: Long,
#ColumnInfo(name = "attribute") val atr: String
)
the IND1_id column is used for the relationship i.e. it is the ID of the owning (parent) IND1.
To combine into a single object you could have a POJO (i.e. not an actual table and therefore not an entity) such as IND1WithAttributes :-
data class IND1WithAttributes(
#Embedded
val ind1: IND1,
#Relation(entity = Attribute::class,parentColumn = "ID",entityColumn = "IND1_id")
val attributes: List<Attribute>
)
You would then retrieve the combined IND1 and it's attributes using a query such as
#Transaction
#Query("SELECT * FROM ind1")
fun getAllIND1sWithAttributes(): List<IND1WithAttributes>
As an example of using the above, the following was used (not using Dagger) :-
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()
/* Add some data */
val item1Id = dao.insert(IND1(1,100,"Item1","2021-06-25"))
dao.insert(Attribute(1,item1Id,"First Attribute in List"))
dao.insert(Attribute(2,item1Id,"Second Attribute"))
dao.insert(Attribute(3,item1Id,"Third Attribute"))
/* extract the data and output to the log */
for(iwa: IND1WithAttributes in dao.getAllIND1sWithAttributes()) {
Log.d("IND1INFO","IND1 Name = ${iwa.ind1.name} number = ${iwa.ind1.number} date = ${iwa.ind1.date} id = ${iwa.ind1.id}")
for(a: Attribute in iwa.attributes) {
Log.d("IND1INFO","\tAttribute = ${a.atr} id = ${a.id} id of parent IND1 = ${a.parent_IND1}")
}
}
}
}
Running the above and the Log shows :-
2021-06-25 23:31:38.064 D/IND1INFO: IND1 Name = Item1 number = 100 date = 2021-06-25 id = 1
2021-06-25 23:31:38.064 D/IND1INFO: Attribute = First Attribute in List id = 1 id of parent IND1 = 1
2021-06-25 23:31:38.064 D/IND1INFO: Attribute = Second Attribute id = 2 id of parent IND1 = 1
2021-06-25 23:31:38.064 D/IND1INFO: Attribute = Third Attribute id = 3 id of parent IND1 = 1