Room Pre-packaged database has an invalid schema ERROR of Expect type=Integer Found=Boolean - kotlin

I'm a bit lost with this issue.
The Pre-packaged database has an invalid schema error has the following output:
Expected
TableInfo{name='account', columns={client_alt_phone_on_route_sheets=Column{name='client_alt_phone_on_route_sheets', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='0'}, client_titles_on_address_labels=Column{name='client_titles_on_address_labels', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=0, defaultValue='0'}, client_titles_on_invoices=Column{name='client_titles_on_invoices', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=0, defaultValue='0'}}, foreignKeys=[], indices=[]}
Found
TableInfo{name='account', columns={client_alt_phone_on_route_sheets=Column{name='client_alt_phone_on_route_sheets', type='BOOLEAN', affinity='1', notNull=true, primaryKeyPosition=0, defaultValue='FALSE'}, client_titles_on_address_labels=Column{name='client_titles_on_address_labels', type='BOOLEAN', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='FALSE'}, client_titles_on_invoices=Column{name='client_titles_on_invoices', type='BOOLEAN', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='FALSE'}}, foreignKeys=[], indices=[]}
I'm ommitting some other columns because they are not the problem and their output matches. The problem is with the columns that expected INTEGER but found BOOLEAN.
The database schema is as follows:
CREATE TABLE account
(
client_alt_phone_on_route_sheets BOOLEAN DEFAULT FALSE NOT NULL,
client_titles_on_address_labels BOOLEAN DEFAULT FALSE,
client_titles_on_invoices BOOLEAN DEFAULT FALSE,
// Omitted rows
);
Initially, I did created the account Room #Entity with the BOOLEAN columns of type Boolean:
#Entity(tableName = "account")
data class Account(
// Omitted data
// #FIXME
#ColumnInfo(name = "client_alt_phone_on_route_sheets", defaultValue = "FALSE") val routeSheetsClientAltPhone: Boolean,
// #FIXME
#ColumnInfo(name = "client_titles_on_address_labels", defaultValue = "FALSE") val clientTitlesOnAddressLabels: Boolean?,
// #FIXME
#ColumnInfo(name = "client_titles_on_invoices", defaultValue = "FALSE") val clientTitlesOnInvoices: Boolean?,
)
Then, the first time the error was thrown I did changed the Boolean type columns to Int type:
#Entity(tableName = "account")
data class Account(
// Omitted data
// #FIXME
#ColumnInfo(name = "client_alt_phone_on_route_sheets", defaultValue = "0") val routeSheetsClientAltPhone: Int,
// #FIXME
#ColumnInfo(name = "client_titles_on_address_labels", defaultValue = "0") val clientTitlesOnAddressLabels: Int?,
// #FIXME
#ColumnInfo(name = "client_titles_on_invoices", defaultValue = "0") val clientTitlesOnInvoices: Int?,
)
However, the error kept happening. I tried with this answer migrating a single column to see if it then matches in the expected/found output.
private val MIGRATION_1_2 = object : Migration(1,2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE account ADD COLUMN client_alt_phone_on_route_sheets INTEGER NOT NULL DEFAULT(0)"
)
}
}
private fun buildDatabase(context: Context) = Room.databaseBuilder(
context.applicationContext,
Database::class.java,
Constants.DATABASE_NAME
).addMigrations(MIGRATION_1_2).createFromAsset("database.db").build()
But apparently migration did not work, nor changed anything in the output because the error is still the same. Also, I'm not sure which one is the Room database and which one is the database from assets. My hunch is that the 'Found' output is the one to match from assets because of the BOOLEAN type, but that's not completely clear. Why does it seems like changing the Room #ColumnInfo value type from Boolean to Int doesn't seem to take effect? If migration above needs to be implemented for every column of type BOOLEAN on my database, what is the proper way to apply migration for multiple tables? (since I have more tables that has this BOOLEAN type, though they're not in the error)

Why does it seems like changing the Room #ColumnInfo value type from Boolean to Int doesn't seem to take effect?
Because to Room they are the same type that is any type Long/long -> Boolean that is an integer (as opposed to decimal) is given a column type of INTEGER in the create table SQL.
Room will only create column types of INTEGER, TEXT, REAL or BLOB.
SQLite however is flexible in that you can create columns with virtually any type (as long s the type doesn't break parser rules such as being a keyword). SQLite then uses a set of rules to assign a type affinity (one of the 4 listed above or the catch-all NUMERIC (which room does not support/allow)).
So from the above the issue is with the pre-packaged database. That is the pre-packaged database has a column defined with BOOLEAN room expects INTEGER and therefore you MUST change the column type from BOOLEAN to INTEGER.
You can make this change by using:-
ALTER TABLE account RENAME TO account_old;
CREATE TABLE IF NOT EXISTS account (client_alt_phone_on_route_sheets INTEGER NOT NULL DEFAULT VALUE false, .....);
INSERT INTO account SELECT * FROM account_old;
DROP TABLE IF EXISTS account_old;
Note you can obtain the CREATE TABLE SQL from room if you compile the project after creating the entities and the #Database annotated class with the entities defined in the list of entities.
After a successful compilation Room will have generated some java code in the class named the same as the #Database annotated class but suffixed with _Impl. In this class there will be a method createAllTables the SQL for the creation of the tables (indexes and views). This SQL is what room expects
You have various places where you can alter the table(s), the simplest are to:-
Make the changes (as above) in the pre-packaged database, then copy the changed file into the assets folder, or
Use the prePackagedDatabaseCallback call back see How do I use Room's prepackagedDatabaseCallback?
2. The callback is invoked after copying the file but before the validation (Expected v Found) is performed.

Related

Values loss for copy() for Kotlin data class

I have such data class:
data class BookObject(
val description: String?,
val owningCompanyData: OwningCompanyData?,
) {
var id: String? = null
var createdAt: Instant? = null
var createdBy: String? = null
var modifiedAt: Instant? = null
fun update(command: CreateOrUpdateBookObjectCommand): BookObject =
this.copy(
description = command.description,
owningCompanyData = command.owningCompanyData
)
}
When I use the update function for an object with completely filled fields, I get an object with empty id, createdAt, createdBy, modifiedAt fields (they become equal to null). But why is this happening? Why do these fields lose their values?
The kotlin documentation says:
Use the copy() function to copy an object, allowing you to alter some
of its properties while keeping the rest unchanged.
The answer actually is present in your link, located in the paragraph just before "Copying".
The compiler only uses the properties defined inside the primary constructor for the automatically generated functions.

Returning one of multiple class types from a data class

I have scenario where in I have a "Lookup Table" class that holds getters and setters for multiple class types. As I'm interfacing with a database, I need to provide a way to provide a result boolean and then the class instance which was requested.
For example. Say I have an AssetStatus, StockItemStatus and NominalCode class. I would have to write the following data class for each:
data class LookupResult(
val wasSuccessful: Boolean = false,
val resultClass: AssetStatus? = null,
)
// or
data class LookupResult(
val wasSuccessful: Boolean = false,
val resultClass: StockItemStatus? = null,
)
// or
data class LookupResult(
val wasSuccessful: Boolean = false,
val resultClass: NominalCode? = null,
)
Ideally I don't want to have to repeat myself so I was wondering if I could write one data class (or normal class?) which is able to return one of my multiple Lookup classes?
Initially I thought it would have needed to be Any but after looking into this, that might not be the best case for what I want.
So is there a way of sticking to writing once but not repeating? Or is it something I have to do?
Edit:-
All of my class types would have the following structure (note: I'm using Spring Boot as back end) :
#Entity
#Table(name = "lookup_asset_status")
data class AssetStatus(
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "asset_status_id")
val id: Long? = null,
#Column(name = "asset_status_name")
val name: String = "",
)
...
// Repeat for each entity type
...
If I understand correctly, you want something like this:
data class LookupResult<T>(
val wasSuccessful: Boolean = false,
val resultClass: T? = null,
)
Each of the classes would be written as LookupResult<AssetStatus>, LookupResult<StockItemStatus> and LookupResult<NominalCode>.
If your method needs to be able to return any of those three classes, then it should be declared to return LookupResult<*>. Note that you can only access the members of Any when you access the resultClass of a LookupResult<*>, because you don't know which exact look up result it is.

Kotlin: class instance variable value-parameter error

First post - New to Kotlin so beginner-learner. Please be gentle if my terminology is not quite up to scratch yet!
I'm attempting to call a parameter that i've declared in a secondary constructor within my main function variable but it doesnt format like the primary constructor variables and doesn't let me initialise a value that can then be called like the others.
Problem line: (it's the 'age =' bit)
var phoneTwo = MobilePhone("Apple", "iphone", "X", age = )
full syntax below:
fun main() {
var phoneTwo = MobilePhone("Apple", "iphone", "X", age = )
var phoneOne = MobilePhone("Samsung", "Galaxy", "S20",)
println("What is your hobby?: ")
phoneOne.hobby = readLine().toString()
phoneOne.stateHobby()
phoneTwo.hobby = "Plumbing"
phoneTwo.stateHobby()
phoneTwo.age = 32
println("PhoneTwo is $phoneTwo.age years old")
}
class MobilePhone(osName: String, brand: String, model: String) {
// member variables - Properties - variables within a class
var age : Int? = null
var hobby : String = "Watch Netflix"
// Initializer block
init {
println("A new mobile phone was created called $osName which is " +
"brand $brand and it's model is $model")
}
// member secondary constructor
constructor(osName: String, brand: String, model: String, age: Int):
this(osName,brand,model){
this.age = age
}
// Member function - functions within a class
fun stateHobby() {
println("Your hobby is $hobby")
}
This is about the syntax of calling a method/constructor in Kotlin, not about secondary constructors as such (which are called in exactly the same way as others).
First, let's review the syntax for calling a method (or constructor). You can just list the arguments alone (just as you do in Java, C, or many other languages), e.g.:
MobilePhone("Apple", "iphone", "X", 5)
However, Kotlin also allows you to specify the names of the parameters they're being passed to, e.g.:
MobilePhone(osName = "Apple", brand = "iphone", model = "X", age = 5)
That's more long-winded, but you may find it easier to read and safer (especially if there are multiple parameters with the same type). It also lets you put the arguments in any order, e.g.:
MobilePhone(model = "X", osName = "Apple", age = 5, brand = "iphone")
You can even mix and match the forms, as long as the unnamed arguments come first, e.g.:
MobilePhone("Apple", "iphone", age = 5, model = "X")
(This feature is only mildly useful on its own, but is very handy for a related feature: the ability to specify default values for some or all of the parameters. See that link for more.)
Hopefully this illustrates why the problem line doesn't make sense:
var phoneTwo = MobilePhone("Apple", "iphone", "X", age = )
That looks like you want to call the secondary constructor, passing the values "Apple", "iphone", and "X" to the first three parameters, and then naming another parameter but without passing it a value. This is of course a syntax error.
If you have a value to pass for the age, then just pass it, either with the parameter name:
var phoneTwo = MobilePhone("Apple", "iphone", "X", age = 5)
or without:
var phoneTwo = MobilePhone("Apple", "iphone", "X", 5)
Or if you don't have a value, then simply call the primary constructor instead:
var phoneTwo = MobilePhone("Apple", "iphone", "X")
Incidentally, this means that your class doesn't actually need a secondary constructor at all. You could simply include the optional parameter in the primary constructor, with a default value:
class MobilePhone(osName: String, brand: String, model: String, var age: Int? = null) {
Then callers can either specify the age param, or omit it (and get the null default value).
In fact, features such as multiple constructors, method overloading, and builder classes tend to be used less in Kotlin than in some other languages, because default parameters cover their main use cases.
I'm not sure what you're trying to do exactly, but hopefully this covers it! This:
var phoneTwo = MobilePhone("Apple", "iphone", "X", age = )
is trying to call your secondary constructor, and you're using a named argument for one of the parameters (age) but you're not actually passing a value for it. If you do, it'll compile
MobilePhone("Apple", "iphone", "X", age = 3)
You don't actually need to name the argument there - if you just pass an Int as the 4th parameter, it'll match your secondary constructor's signature (number of parameters, correct types for each, in the same order), so the compiler will know that's what you're calling. If you omit it, it'll match the primary constructor. You can still keep the name there for readability, if you like.
But you can actually duplicate the functionality you have there with a default parameter, which is where you supply a value to use if the call doesn't specify one:
class MobilePhone(osName: String, brand: String, model: String, val age: Int? = null) {
// member variables - Properties - variables within a class
var hobby : String = "Watch Netflix"
// Initializer block
init {
println("A new mobile phone was created called $osName which is " +
"brand $brand and it's model is $model")
}
So now, age is a parameter on the primary constructor - if you don't supply it (just calling with the first 3 items, which are required because they don't have defaults) then it defaults to null.
By making that parameter a val (or a var if you like) it becomes a class property you can reference later, instead of only being accessible during construction. So you can remove the var age property inside the class, because this is basically the same thing (and they'll clash anyway)
And now that you have a primary constructor you can call with or without the age parameter, there's no need for the secondary one anymore! This is where named parameters really come in useful - if you had defaults for each of those params, you could just supply the specific ones you're interested in, by using their names
oh also, "PhoneTwo is $phoneTwo.age years old" won't work - if you're just referencing an object, you can do $phoneTwo, but anything more complicated (like accessing one of its properties, or any method calls or more complex expressions) have to be wrapped in ${}
println("PhoneTwo is ${phoneTwo.age} years old")
println("PhoneTwo is ${phoneTwo.age * 365} days old")

how do i store image in room database

how do i store image in room database, From Json.
as i was trying to store with Byte it gives me error - java.lang.NumberFormatException: For input string: "https://upload.wikimedia.org/wikipedia/commons/4/41/Sunflower_from_Silesia2.jpg"
data Class
#Entity(tableName = "actor",indices = arrayOf(Index(value= arrayOf("id"),unique = true)))
data class ActorItem(
#PrimaryKey(autoGenerate = true)
val id_:Int,
#SerializedName("age")
#ColumnInfo(name = "age")
val age: String,
#SerializedName("id")
#ColumnInfo(name = "id")
val id: Int,
#SerializedName("image")
#ColumnInfo(name="image")
val image: Byte,
#SerializedName("name")
#ColumnInfo(name = "name")
val name: String
)
here is Json
[
{"id": 1,
"name": "Hero",
"image": "https://upload.wikimedia.org/wikipedia/commons/4/41/Sunflower_from_Silesia2.jpg",
"age": "23"}
]
Are you trying to store the path to an image in the DB, or the image itself?
If the path, then image would be of type String (not Byte). Storing a path (or name) in the DB, and not the image contents, is generally considered best practice. It requires downloading and saving the image in the filesystem outside of the DB.
If you are trying to store the image contents in the DB though, then the field type would be ByteArray (not Byte), the column type would be Blob, and you will need to download the image and write the bytes in to the DB yourself.
Related:
How insert image in room persistence library?
Simple solution is to store image file in internal directory(App-specific storage) and store the internal private path in your room database column.
Internal App storage Docs :-
https://developer.android.com/training/data-storage/app-specific
Or make base64 of file image file and store base64 string in your database column
From Json. as i was trying to store with Byte it gives me error - java.lang.NumberFormatException: For input string:
What number, between 0 and 255 does https://upload.wikimedia.org/wikipedia/commons/4/41/Sunflower_from_Silesia2.jpg resolve to 0? 1? 2? .... 255? (Rhetorical) Why? (Rhetorical)
how do i store image in room database
You VERY PROBABLY SHOULD NOT but instead store the image as a file in the Apps's storage (there would be no real difference in storage space) but probably a very noticeable improvement in response times.
The value https://upload.wikimedia.org/wikipedia/commons/4/41/Sunflower_from_Silesia2.jpg is, to many humans, obviously a link to a file that can be downloaded (an image). It does not equate to a byte or any number.
However, the file stored at that location is a series (aka stream) of bytes.
Thus in Room a ByteArray (which Room will assign a type affinity of BLOB the the column). So the column type should either be ByteArray or a type that would require a type converter where the type converter returns a ByteArray.
So instead of val image: Byte, you would probably have val image: ByteArray,.
To get the ByteArray you could (assuming permissions etc are all setup) use something like (but not restricted to):-
return URL(url).readBytes()
where url, in your case, would be the String https://upload.wikimedia.org/wikipedia/commons/4/41/Sunflower_from_Silesia2.jpg
IMPORTANT
However, at 2.7Mb
that is very likely to cause issues. Not due to SQLite limitations but due to limitations of the Android API which retrieves data from the SQLite database via a Cursor which is a buffer that is limited in size (4Mb). As such any image that is close to 4Mb may be stored but it couldn't be retrieved without complications AND highly inefficient/slow processing.
Demonstration of why NOT to store images, like the one mentioned in the question.** in the database**
As a demonstration consider the following which does store the equivalent of images (not actual images) in the image column of you table
(i.e. ByteArrays, the content unless actually displaying the image is irrelevant Room nor SQLite knows the difference between an image and any other BLOB value)
using a slightly modified version of your ActorItem class, as :-
#Entity(tableName = "actor",indices = arrayOf(Index(value= arrayOf("id"),unique = true)))
data class ActorItem(
#PrimaryKey(autoGenerate = true)
val id_:Int,
#ColumnInfo(name = "age")
val age: String,
#ColumnInfo(name = "id")
val id: Int,
#ColumnInfo(name="image")
val image: ByteArray,
#ColumnInfo(name = "name")
val name: String
) {
#androidx.room.Dao
interface Dao {
#Insert
fun insert(actorItem: ActorItem)
}
}
i.e. the important difference is a type of ByteArray as opposed to Byte for the image column
for brevity/convenience the DAO class has been included (it is sufficient just to insert some columns to demonstrate why saving the image is not a very good idea)
To accompany is an #Database class TheDatabase :-
#Database(entities = [ActorItem::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
abstract fun getActorItemDao(): ActorItem.Dao
companion object {
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 included for brevity and convenience
Finally putting the above into action via an activity is MainActivity :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: ActorItem.Dao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getActorItemDao()
val url = "https://upload.wikimedia.org/wikipedia/commons/4/41/Sunflower_from_Silesia2.jpg"
try {
for (i in 1..100) {
dao.insert(
ActorItem(
0, "The age", i,
getBitmapFromURLAsString(
url,
/* The ByteArray (bitmap) */
/* BUT for demonstration of issues associated with size issues
start at 1.08Mb size incrementing at 8kb per row
last would be 1.8Mb (100 * 8kb)
*/
i * (4096 * 2) + (1024 * 1024)),
getNameFromURLAsString(url)
)
)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun getBitmapFromURLAsString(url: String, size: Int): ByteArray {
/* Fake the byte array allowing the size of the bytearray to be specified */
val ba = ByteArray(size)
var byte: Byte = 8
for(i in 0 until (size)) {
ba[i] = byte++.mod(Byte.MAX_VALUE)
}
return ba
/* actual would be something like */
/* WARNING image Sunflower_from_Silesia2.jpg is 2.7Mb will likely cause issues */
//return URL(url).readBytes()
}
fun getNameFromURLAsString(url: String): String {
val split = url.split("/")
return split.get(split.size -1)
}
}
So the activity will try to insert 100 rows with a ByteArray in the image column (answer to how to store image in principle). For each row the size of the ByteArray is increased by 8k (the first row is 1.08Mb i.e. 1Mb and 8k in size). The name column
The above runs successfully without any trapped exceptions. And all 100 rows are inserted:-
using query to extract the length of each image column shows the size (of the last rows) :-
First warning sign that things are perhaps not that good
Running the query takes hardly any time at all. Refreshing, moving from start to end from the table view takes quite a bit of time (a minute (will be dependant upon PC/Laptop used)).
Second warning sign
Running the App takes a few seconds.
Third warning sign
Use i * (4096 * 2) + (1024 * 1024 * 2)), (i.e. start at 2Mb up to 2.8Mb), run the App and try to view via App Inspection and :-
As can be seen the Rows exist and have the expected data in them :-
Try to look at the Actor table, DatabaseInspector doesn't show the contents .
Run the query SELECT substr(image,1024 * 1024) FROM actor (i.e. 8k for the first row 1.8k for the 100th row) WAIT (for a minute or so), scroll to the last, WAIT (for a minutes or so) and :-
You should use ByteArray (on java it means byte[]). Sqlite supports saving byte arrays like this: How to store image in SQLite database
And on room database, you can just use ByteArray type for you image, and room will finish the rest of work.

Android Room Embedded Field Not Compiling

I am trying to create an embedded field. This is a simple example but I can't get this simple example to work. Eventually I need to have 3 levels of embedded items but trying to get this test case to work.
#Entity(tableName = "userItemsEntity")
#Parcelize
data class Item(
var objecttype: String?,
#PrimaryKey(autoGenerate = false)
var objectid: Int?,
var subtype: String?,
var collid: Int?,
#Embedded
var name: Name?
) : Parcelable
#Parcelize
data class Name(
var primary: Boolean? = true,
var sortindex: Int? = null,
var content: String? = null) : Parcelable
When I try and compile it it complains on the DAO that the updateItem()
SQL error or missing database (no such column: name)
DAO function
#Query("UPDATE userItemsEntity SET " +
"objecttype=:objecttype, objectid=:objectid, subtype=:subtype, collid=:collid, name=:name " +
"WHERE objectid=:objectid")
fun updateItem(
objecttype: String?,
objectid: Int,
subtype: String?,
collid: Int?,
name: Name?)
The reason is as it says there is no name column. Rather the table consists of the columns, as per the member variables of the EMBEDDED class (i.e. primary, sortindex and content).
i.e. the table create SQL is/will be :-
CREATE TABLE IF NOT EXISTS `userItemsEntity` (`objecttype` TEXT, `objectid` INTEGER, `subtype` TEXT, `collid` INTEGER, `primary` INTEGER, `sortindex` INTEGER, `content` TEXT, PRIMARY KEY(`objectid`))
Room knows to build the respective Name object from those columns when extracting rows.
So you could use :-
#Query("UPDATE userItemsEntity SET " +
"objecttype=:objecttype, objectid=:objectid, subtype=:subtype, collid=:collid, `primary`=:primary, sortindex=:sortindex, content=:content " +
"WHERE objectid=:objectid")
fun updateItem(
objecttype: String?,
objectid: Int,
subtype: String?,
collid: Int?,
primary: Boolean?,
sortindex: Int?,
content: String?
)
note that primary is an SQLite token and thus enclosed in grave accents to ensure that it is not treated as a token. Otherwise you would get :-
There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (near "primary": syntax error)
However, as you are using a WHERE clause based upon the primary key (objectid) then the update will only apply to a single row and as such you can simply use:-
#Update
fun update(item: Item): Int
obviously the function's name need not be update it could be any valid name that suits.
this has the advantage of not only being simpler but of returning the number of rows updated (would be 1 if the row exists, otherwise 0)
Impementing a name column
If you want a name column and for that name column to hold a Name object. Then, as SQLite does not have storage/column types for objects then you would not EMBED the Name class.
You would have var name: Name? with an appropriate TypeConverter that would convert the Name object into a type that SQLite caters for :-
TEXT (String),
REAL (Float, Double...),
INTEGER (Long, Int ...) or
BLOB (ByteArray)).
Typically String is used and typically GSON is used to convert from an object to a JOSN String.
SQlite does have a NUMERIC type. However, Room doesn't support it's use. I believe because the other types cover all types of data and NUMERIC is a catch-all/default.
However, using a JSON representation of an object, introduces bloat and reduces the usefulness of the converted data from an SQL aspect.
For example say you had :-
#Entity(tableName = "userOtherItemsEntity")
#Parcelize
data class OtherItem (
var objecttype: String?,
#PrimaryKey(autoGenerate = false)
var objectid: Int?,
var subtype: String?,
var collid: Int?,
var name: OtherName?) : Parcelable
#Parcelize
data class OtherName(
var primary: Boolean? = true,
var sortindex: Int? = null,
var content: String? = null) : Parcelable
Then the underlying table does have the name column. The CREATE SQL, generated by Room, would be :-
CREATE TABLE IF NOT EXISTS `userOtherItemsEntity` (`objecttype` TEXT, `objectid` INTEGER, `subtype` TEXT, `collid` INTEGER, `name` TEXT, PRIMARY KEY(`objectid`))
However, you would need TypeConverters which could be :-
#TypeConverter
fun fromOtherName(othername: OtherName ): String {
return Gson().toJson(othername)
}
#TypeConverter
fun toOtherName(json: String): OtherName {
return Gson().fromJson(json,OtherName::class.java)
}
the first using Gson to convert the object to a JSON string, e.g. when inserting data
the second converts the JSON string to an OtherName object.
using Item with Name embedded then data would be stored along the lines of :-
Whilst with the OtherItem with OtherName being converted then the data (similar data) would be along the lines of :-
in the former the 3 Name columns would take up about (1 + 1 + 12) = 16 bytes.
in the latter, The OtherName columns (discounting the word Other whenever used) would take uo some 55 bytes.
the latter may require more complex and resource expensive searches if the components of the OtherName are to be included in searches.
e.g. #Query("SELECT * FROM userItemsEntity WHERE primary") as opposed to #Query("SELECT * FROM userOtherItemsEntity WHERE instr(name,'primary\":true') > 0")