Android Room Embedded Field Not Compiling - entity

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")

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.

Jackson SNAKE_CASE How to generate underscore in field names before number

I have the next peace of code
#Test
fun `simple test`() {
val objectMapper = ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.registerModule(KotlinModule())
val value = objectMapper.writeValueAsString(MyClass(myField1 = "something", myField2 = "something2"))
assertNotNull(value)
}
data class MyClass (
val myField1: String? = null,
#JsonProperty("my_field_2")
val myField2: String? = null,
)
the result of deserialization is next
{"my_field1":"something","my_field_2":"something2"}
Is it possible to configure objectMapper to automatically populate _ value, before digits in object property names, without specifying it in #JsonProperty?
Yes, this is possible using a PropertyNamingStrategy:
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
Note that you named your snake-case fields inconsistently, because there is my_field1 without a _ before the digit, and my_field_2 with a _ before the digit. The configuration above using PropertyNamingStrategies.SNAKE_CASE works fine for the first naming (like in my_field1).
If you want to use the second naming (like in my_field_2), then you would have to write your own naming strategy like this:
class MySnakeCaseStrategy : NamingBase() {
override fun translate(input: String?): String? =
if (input == null) null
else "([A-Z]+|[0-9]+)".toRegex().replace(input) { "_${it.groupValues[1]}".lowercase() }
}
That naming strategy can then be used to configure your object-mapper:
objectMapper.setPropertyNamingStrategy(MySnakeCaseStrategy())
I do not know if and how it would be possible to support both naming strategies at the same time.

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

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.

Kotlin - creating map with 3 arrays using fold not working

I have an array of customers, each customer has properties id, uuid and subCustomer and other properties that I am not interested in. I would like to do one iteration, where I would create 3 arrays where one would hold ids, other uuids and third subcustomers only.
I have tried to achieve this by using fold function like this:
customers.fold(
mapOf(
"ids" to listOf<String>(),
"uuids" to listOf<UUID>(),
"subCustomers" to listOf<String>()
))
{ acc, customer ->
acc["ids"]?.plus(customer["id"])
acc["uuids"]?.plus(customer["uuid"])
acc["subCustomers"]?.plus(customer["subCustomer"])
}
With this code I get an error in editor:
Type mismatch.
Required:
Map<String, List<{Comparable{String & UUID}> & java.io.Serializable}>>
Found:
List<Any?>?
I have tried this as well:
customers.fold(
mapOf(
"ids" to listOf<String>(),
"uuids" to listOf<UUID>(),
"subCustomers" to listOf<String>()
))
{ acc, customer ->
mapOf(
"ids" to acc["ids"]?.plus(customer["id"]),
"uuids" to acc["uuids"]?.plus(customer["uuid"]),
"subCustomers" to acc["subCustomers"]?.plus(customer["subCustomer"])
)
}
But, I get this errors:
Type mismatch.
Required:
List<{Comparable{String & UUID}> & java.io.Serializable}>
Found:
List<Any?>?
Type mismatch.
Required:
Map<String, List<{Comparable{String & UUID}> & java.io.Serializable}>>
Found:
Map<String, List<Any?>?>
Write two data classes for your data. One for your customers, and one for the three lists that you want:
data class Customer(
val id: String,
val uuid: UUID,
val subCustomer: String,
)
data class CustomerDataLists(
val ids: MutableList<String> = mutableListOf(),
val uuids: MutableList<UUID> = mutableListOf(),
val subCustomers: MutableList<String> = mutableListOf(),
)
Then, just use a simple for loop to add the data in:
val dataLists = CustomerDataLists()
for (customer in customers) {
dataLists.ids.add(customer.id)
dataLists.uuids.add(customer.uuid)
dataLists.subCustomers.add(customer.subCustomer)
}
// now dataLists is filled with customers' data
#Sweeper's answer is nice. I believe in any case it's worth using data classes instead of maps for this kind of use case.
Since you don't really have any interactions between the 3 lists in the fold, you could also build those lists independently (but it's 3 iterations of course here):
data class Customer(
val id: String,
val uuid: UUID,
val subCustomer: String,
)
data class AggregatedCustomers(
val ids: List<String>,
val uuids: List<UUID>,
val subCustomers: List<String>,
)
val customers: List<Customer> = TODO("get that list from somewhere")
val aggregated = AggregatedCustomers(
ids = customers.map { it.id }
uuids = customers.map { it.uuid }
subCustomers = customers.map { it.subCustomer }
)
This answer contiains 3 parts:
A better way to solve such problem;
Why the original code doesn't work;
Other problems need to pay attention.
1. A better way to solve such problem
Let's assume that the Consumer mentioned looks like this:
data class Customer(
val id: String,
val uuid: UUID,
val subCustomer: String,
)
It's really not necessary to use function fold in such occasion. For loop or extension function forEach is merely enough:
val customers: List<Customer> = listOf(
Customer("1", UUID.randomUUID(), "sub-1"),
Customer("2", UUID.randomUUID(), "sub-2"),
Customer("3", UUID.randomUUID(), "sub-3"),
)
val ids = mutableListOf<String>() // pay attention. use `mutableListOf` instead of `listOf()`
val uuids = mutableListOf<UUID>()
val subConsumers = mutableListOf<String>()
customers.forEach {
ids += it.id
uuids += it.uuid
subConsumers += it.subCustomer
}
2. Why the original code doesn't work
The proposed two pieces of code are in the same pattern:
customers.fold(
mapOf(
"ids" to listOf<String>(),
"uuids" to listOf<UUID>(),
"subCustomers" to listOf<String>()
)
) { acc, customer ->
// ... do something with acc and customer
}
We should first make it clear that the last statement in the fold scope is the expression to be accumulated. It's like an acc_n <combine> customer -> acc_(n+1), for each customer in customers each time, where <combine> is where we write our logic. So the first proposed piece of code doesn't work because you might not be aware that something should be returned while writing:
customers.fold(...){ acc, customer ->
acc["ids"]?.plus(customer.id)
acc["uuids"]?.plus(customer.uuid)
acc["subCustomers"]?.plus(customer.subCustomer)
}
In fact, the last statement acc["subCustomers"]?.plus(...) is an expression with type List<Any>?, kotlin regard it as your "acc_(n+1)", but you propose mapOf("ids" to ...) as acc_0, which has type Map<String, ...>: not the same type as List<Any>?. And that's why you got the first error:
Type mismatch.
Required:
Map<String, List<{Comparable{String & UUID}> & java.io.Serializable}>>
Found:
List<{Comparable{String & UUID}> & java.io.Serializable}>?
We'll talk about generic types later.
Let's move on the second piece of code. A map is proposed as the last expression in the scope of fold, which is also a map:
customers.fold(...) { acc, customer ->
mapOf(
"ids" to acc["ids"]?.plus(customer.id),
"uuids" to acc["uuids"]?.plus(customer.uuid),
"subCustomers" to acc["subCustomers"]?.plus(customer.subCustomer)
)
}
The simpliest way to eliminate error is using !! expression (not suggested!):
customers.fold(...) { acc, customer ->
mapOf(
"ids" to acc["ids"]?.plus(customer.id)!!,
"uuids" to acc["uuids"]?.plus(customer.uuid)!!,
"subCustomers" to acc["subCustomers"]?.plus(customer.subCustomer)!!
)
}
The reason is that kotlin cannot assert acc["ids"] is not null, that's why you use ?. for a null-safe method invoke. However such invoke make the return type nullable:
val cus: Customer? = Customer("1", UUID.randomUUID(), "sub-1") // cus has type Customer? : nullable
val id1: String = cus?.id // [compile error] Type mismatch. [Required: String] [Found: String?]
val id2: String? = cus?.id // OK
val id3: String = cus?.id!! // If `cus?.id` is null, throw NPE.
You've declare acc_0 (in bracket after fold) in type Map<String, List<T>> implicitly (we will talk about T later). Just know that T is not a nullable type), but a map with type Map<String, List<T>?> was found as acc_(n+1). Types mismatch and the error was shown:
Type mismatch.
Required:
List<{Comparable{String & UUID}> & java.io.Serializable}>
Found:
List<{Comparable{String & UUID}> & java.io.Serializable}>?
3. Other problem need to pay attention
An important problem is: What's the type of acc_0?
// acc_0:
mapOf(
"ids" to listOf<String>(),
"uuids" to listOf<UUID>(),
"subCustomers" to listOf<String>()
)
Of course type of each expression on the left of to is String, and List<T> is the type of each expression on the right of it. so it must be Map<String, List<T>>. What about T? Kotlin try to find the nearest ancessor of String and UUID, and find them both implements Comparable<?> and Serializable, so that's what you see in the error. That's the type of T:
Required:
List<{Comparable{String & UUID}> & java.io.Serializable}>
This may lead to some unwanted experience:
val map = mapOf(
"listA" to mutableListOf("233"),
"listB" to mutableListOf(UUID.randomUUID())
)
val listA = map["A"]!! // MutableList<out {Comparable{String & UUID}> & java.io.Serializable}!>
// generic type "collapse" into `Nothing` for no type can implement both Comparable<String> and Comparable<UUID>
listA.add(Any()) // Type mismatch. [Required: Nothing] [Found: Any]
So try not to put lists with different generic type into one map.
Another problem is, when you try to invoke acc["ids"]?.plus(customer.id), you are actually invoking such method (from kotlin _Collections.kt)
public operator fun <T> Collection<T>.plus(element: T): List<T> {
val result = ArrayList<T>(size + 1)
result.addAll(this)
result.add(element)
return result
}
A new list is created each time you invoke the method! Try use mutableListOf() in replace of listOf() for collections that you want to make changes, and use "+=" (or ?.plusAsign() as null-safe version) operator instead. This may leads to some other problem with the original code (which is too complex to explain why), but for the code in part 1: A better way to solve such problem, the += is actually invoking:
public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) {
this.add(element)
}
which just add value to list without create new ones.

get a list of parsed json elements

I parsed a json string to the following object structure using gson:
data class Base (
val expand: String,
val startAt: Long,
val maxResults: Long,
val total: Long,
val issues: List<Issue>
)
data class Issue (
val expand: String,
val id: String,
val self: String,
val key: String,
val fields: Fields
)
data class Fields (
val summary: String,
val issuetype: Issuetype,
val customfield10006: Long? = null,
val created: String,
val customfield11201: String? = null,
val status: Status,
val customfield10002: Customfield10002? = null,
val customfield10003: String? = null
)
Everything works fine and also the object model is correct, because I can access each element of the object.
However, I encountered the problem that I dont know how to get a list of all field-elements. Right now, I have only figured out how to access one item (by using an index and get()-function):
val baseObject = gson.fromJson(response, Base::class.java)
val fieldsList = baseObject.issues.get(0).fields
I actually want to have a list of all field elements and not just one. Is there a gson function allowing me to do that? I couldn't find anything about it in the gson documentation for java.
You don't have to look for some gson function when you've already created a baseObject. You just need to get from each issue it's fields and you can use a map function to achieve this, it will convert each issue to a new type so you can get issue fields there
val fieldFromAllIssues: List<Fields> = baseObject.issues.map { it.fields }
it in this context is a one issue. More explanation about it is here