Values loss for copy() for Kotlin data class - kotlin

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.

Related

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.

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.

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

Why filtering out null map keys doesn't change type to not nullable in kotlin?

I have a list of objects with an optional id as String and I want to make a map out of it.
I want to have the keys of my map as non nullable: so something like this:
data class Foo(
val id: String? = null
val someStuff: String? = null,
)
val foo = listOf(Foo("id1"), Foo())
val bar = foo.filterNot { it.id == null }.associateBy { it.id }
Here bar type is Map<String?, Foo> but not Map<String, Foo>
My workaround is to add a non null asserted call: !!, but it doesn't seem clean.
Is there an easy and safe way to do this?
This looks like something that contracts could help with, but currently a contract expression can't access properties of the class in use.
As a workaround, you could define a 2nd class that has a non-null id, like so
data class Foo(
val id: String? = null,
val someStuff: String? = null
)
data class Foo2(
val id: String,
val someStuff: String? = null
)
val foo = listOf(Foo("id1"), Foo())
val bar = foo
.mapNotNull { if (it.id != null) Foo2(it.id, it.someStuff) else null }
.associateBy { it.id }
There's a six-year-old open feature request for Map.filterNotNullKeys() and a four-year old open feature request for Map.associateByNotNull().
In my opinion, the associateBy { it.id!! } would be cleanest for readability. But you could do it like this:
val bar = foo.mapNotNull { it.id?.run { it.id to it } }.toMap()
As for your actual question, that logic is way too many steps for the compiler to infer. Your last function call to associateBy sees a nullable, so it infers a nullable. For the compiler to figure this out, it would have to step back and see that the List that you call associateBy on happens to have filtered out certain objects in a way that happens to ensure that a certain nullable property won't be null within this specific list, and it's the same property that you are associating with. Now imagine it has to do this for every call to any generic function, and the various lambdas involved could potentially have multiple lines of code. Compile times would skyrocket.

Kotlin data classes JSON Deserialization

I am trying convert ApiEmployee to Employee and have written a test around it. I am confused about nulls in Kotlin as I am new to it.
ApiEmployee would be used for JSON conversion so it can have missing name field or or empty or can come as null. In that case, I don't want to add into list and safely ignore it.
I am getting Method threw 'kotlin.KotlinNullPointerException at exception. at apiEmployee.name!!.isNotBlank()
ApiEmployee
data class ApiEmployee(val image: String? = "image",
val name: String? = "name test",
val description: String? = "",
val id: String? = "")
Employee
data class Employee(val imagePath: String, val id: String)
EmployeeConverter(converts ApiEmployee to Employee)
fun apply(apiEmployees: List<ApiEmployee>): List<Employee> {
val employees = mutableListOf<Employee>()
for (apiEmployee in apiEmployees) {
if (apiEmployee.name!!.isNotBlank()){
employees.add(Employee(apiEmployee.image!!, apiEmployee.id!!)
}
}
}
EmployeeConverterTest
#Test
fun `should not add employee without name into employee list`() {
val invalidApiEmployee = ApiEmployee("image", null, "description", "id")
val convertedEmployees : List< Employee > = employeeConverter.apply(listOf( invalidApiEmployee))
assertThat(convertedEmployees.size).isEqualTo(0)
}
What you want to do is check if the name is null first and then if it is empty.
val employeeNameIsNotEmpty = apiEmployee.name?.isNotBlank() ?: false
if (employeeNameIsNotEmpty) {
// do stuff
}
The apiEmployee.name?.isNotBlank() will run and return a value only if name is not null. If name is null then the statment on the right side of ?: will return its value, which in this case should be false.
In this case however Kotlin has already put this particular example into an extension function
.isNullOrBlank()
So you could change it to:
if (!apiEmployee.name.isNullOrBlank()) {
// do stuff
}
As a side note you really don't whant to do this Employee(apiEmployee.image!!, apiEmployee.id!!).
Because image and id could still be null and crash your code with the same error.
Either pass the value for name.
ApiEmployee("image", "name", "description", "id")
(or)
Change the if condition as mentioned below (with ? operator):-
if (apiEmployee.name?.isNotBlank()){
?. performs a safe call (calls a method or accesses a property if the
receiver is non-null)
!! asserts that an expression is
non-null
The code asserts that name is not null and checking for not blank.
Probably, I think you are trying to do null and not blank check. You can use ? operator (safe call) for that. This means isNotBlank() gets executed only if the name is not null.