Room: related entities - usable public constructor - android

To get a OneToMany relation with Room I create a POJO with #Embedded object and #Relation variable.
data class SubjectView(
#Embedded
var subject: Subject,
#Relation(parentColumn = "idWeb", entityColumn = "subject_id", entity = Topic::class)
var topics: List<Topic>?
)
But while compiling I have this error
error: Entities and Pojos must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type)
[...]
Tried the following constructors but they failed to match:
SubjectView(biz.eventually.atpl.data.db.Subject,java.util.List<biz.eventually.atpl.data.db.Topic>) : [subject : subject, topics : null]
Well, that constructor [subject : subject, topics : null] looks like the good one ???
However, if I change my class with no-arg constructor and an all params constructor, it does work.
class SubjectView() {
#Embedded
var subject: Subject = Subject(-1, -1, "")
#Relation(parentColumn = "idWeb", entityColumn = "subject_id", entity = Topic::class)
var topics: List<Topic>? = null
constructor(subject: Subject, topics: List<Topic>?) : this() {
this.subject = subject
this.topics = topics
}
}
I would like to know why the first (quicker) version does not compile, as it is not as the documentation shows.
Default args for all variables (as I could have seen on other post) in a constructor (data) class seems not to be mandatory though?
Thanks

There are several topics how data class generate the constructors.
Since you have a nullable Object inside your constructor, it will generate all possible constructors. That means it generates
constructor(var subject: Subject)
constructor(var subject: Subject, var topics: List<Topic>)
There are two ways to solve that. The first one is to predefine all values like and create another ignored constructor with the desired constructor.
data class SubjectView(
#Embedded
var subject: Subject,
#Relation(parentColumn = "idWeb", entityColumn = "subject_id", entity = Topic::class)
var topics: List<Topic> = ArrayList()
) {
#Ignore constructor(var subject: Subject) : this(subject, ArrayList())
}
Another way is creating a half-filled data class like
data class SubjectView(#Embedded var subject: Subject) {
#Relation var topics: List<Topic> = ArrayList()
}
Take care that the first solution is the proper solution and you need to set #Ignore to any other constructor.

Related

Saving complex Class data in Room - "Cannot figure out how to save this field into database. You can consider adding a type converter for it"

I started to learn Room and I'm facing an issue:
Given two classes, one is a Car, and the other one is an Engine iside a Car.
#Entity
class Car{
#PrimaryKey
var id = 0
var name: String? = null
var engine: Engine? = null
}
...
#Entity
class Engine{
#PrimaryKey
var id = 0
var manufacturer: String? = null
}
I also have these classes initalized to tables in my AppDatabase class.
#Database(entities = [Car::class, Engine::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
//...
}
The problem is whenever I simply want to run the project I get the following error message which points to the Car's engine field:
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
Is there no simple way for this? I'm looking for something which saves my data with the least amount of code, like Firestore which do all the work with simple annotations.
Thanks in advance.
AS a car would only have a single engine and that you have a table for the engine as well as a table for the car. Then you have a 1 to many relationship. That is a car can have an engine but the same engine can be used by many cars.
So instead of trying to embed the engine within the car you make a relationship, the car (the child) referencing the engine (the parent).
This is as simple as changing the Car to be:-
#Entity
class Car{
#PrimaryKey
var id = 0
var name: String? = null
var engine: Int? = null
}
An alternative, that would not need the relationship nor a TypeConverter would be to not have the Engine as a table but to use the #Embedded annotation prior to the engine. e.g.
#Entity
class Car{
#PrimaryKey
var id = 0
var name: String? = null
#Embedded
var engine: Engine? = null
}
...
class Engine{
#PrimaryKey
#ColumnInfo(name = "engineId")
var id = 0
var manufacturer: String? = null
}
the name of the column used to store the Engine's id changed as otherwise there would be 2 columns with the same name.
Note that with this way there is no need for the #Entity annotation as you are storing the Engine values within the Car.
This is not considered good practice as if the same engine is used by many cars then you are duplicating data and thus that it is not normalised.
The third and least desirable way from a database perspective is to store a representation of the engine object in a single column. That is to convert the object into a singular storable representation. Typically a JSON string. Thus you need code (a function) to convert from the object to the single value (JSON string) and (another function) to convert from the JSON String to the Object.
With this method not only are you not normalising the data but additionally you end up storing the bloat required to enable the object to be represented. That bloat, from a database, perspective, obfuscating the actual useful stored data to some extent.
In addition there is not a single set/standard library providing the functionality of converting objects to/from JSON, so you have to select a flavour and then include that library in the project.
Here is a class that contains Type Converters that could be used (see comment re library):-
class CarAndEngineTypeConverters{
/* Using Library as per dependency implementation 'com.google.code.gson:gson:2.10.1' */
#TypeConverter
fun convertEngineToJSONString(engine: Engine): String = Gson().toJson(engine)
#TypeConverter
fun convertJSONStringToEngine(jsonString: String): Engine = Gson().fromJson(jsonString,Engine::class.java)
}
This would suit your original classes.
Room needs to be told to use these classes (it works out when) via a #TypeConverters annotation (note the plural and not singular) this it immediately before or after the #Database annotation has the highest level of scope. The annotation itself could be #TypeConverters(value = [CarAndEngineTypeConverters::class])
To demonstrate all three together consider this over the top Car class:-
#Entity
class Car{
#PrimaryKey
var id = 0
var name: String? = null
var engine: Int? = null
#Embedded
var alternativeEngine: Engine? = null
var jsonConvertedEngine: Engine? = null
}
Over the top as the engine is stored 3 times (could be different engines)
The *Engine class
#Entity
class Engine{
#PrimaryKey
#ColumnInfo(name = "engineId")
var id = 0
var manufacturer: String? = null
}
The Type Converters as above.
With the above in place and using within an activity (noting that for brevity/convenience .allowMainThreadQueries has been used):-
db = TheDatabase.getInstance(this)
carAndEngineDAO = db.getCarAndEngineDAO()
var engine1 = Engine()
engine1.manufacturer = "Ford"
engine1.id = carAndEngineDAO.insert(engine1).toInt()
var car1 = Car()
car1.name = "Escort"
car1.engine = engine1.id /* id of the engine */
car1.alternativeEngine = engine1
car1.jsonConvertedEngine = engine1
carAndEngineDAO.insert(car1)
Using Android Studios App inspection the view the database then
The Columns id and name and obviously as expected
The engine column contains the value 0, this is the id of the respective engine in the engine table (maximum 8 bytes to store the id)
The JsonConvertedEngine column stores the JSON representation of the Engine (31 bytes)
The engineId column and manufacturer column stores the respective values (12 bytes).
The Engine Table (only needed for the relationship) is :-
You should use TypeConverters:
At first add this dependency to your project to convert Engine to Json and vice versa
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
Now you should create an Object class that convert Engine to Json. This class make Engine understandable for Room :
object CommonTypeConverters {
#TypeConverter
#JvmStatic
fun stringToEngine(value: String): Engine = fromJson(value)
#TypeConverter
#JvmStatic
fun engineToString(items: Engine?): String = toJson(items)
inline fun <reified T> toJson(value: T): String {
return if (value == null) "" else Gson().toJson(value)
}
inline fun <reified T> fromJson(value: String): T {
return Gson().fromJson(value, object : TypeToken<T>() {}.type)
}
In the end Engine is not a entity and you should add #Typeconverter annotation to your database class :
#Database(entities = [Car::class], version = 1)
#TypeConverters(CommonTypeConverters::class)
abstract class AppDatabase : RoomDatabase() {
//...
}

How to resolve "Entities and POJOs must have a usable public constructor"

I have seen this question several times on SO. however the solution doesn't seem to apply to my problem.
I have a Kotlin data-class that is used as an Entity in Room
#Entity(tableName = "training_session")
data class SessionEntity(
#PrimaryKey(autoGenerate = false) val id: Long,
#ColumnInfo(name = "current_state_marker") val currentState: Short,
#Embedded val states: List<Int>
)
It is producing
> Task :training-infrastructure:kaptDebugKotlin FAILED
error: Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type). - java.util.List
error: Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type). - java.util.List
In the same project I have a very similar entity which also has a list and that doesn't produce any errors.
Tried out the answer provided by MikeT, for me it required a small change in the way the converters were defined
data class SessionStateList (val stateList : List<Int>)
class SessionStateListConverter {
#TypeConverter
fun fromArraySessionStateList(sh: List<Int>?): String? {
return Gson().toJson(sh)
}
#TypeConverter
fun toArraySessionStateList(sh: String?): List<Int>? {
val listType: Type = object : TypeToken<ArrayList<Int?>?>() {}.type
return Gson().fromJson(sh,listType)
}
}
A quick follow-up. I had mentioned that I have another Entity that has an Embedded val something: List<Int> and I had not noticed any compiler errors.
The reason, I had not noticed any compiler errors was because the entity was not included in the #Database annotation.
You cannot have a List/Array etc as a column type. So your issue is centred on #Embedded val states: List<Int>
You could have a POJO e.g. StatesHolder :-
data class StatesHolder(
val stateList: List<Int>
)
and then have
#Entity(tableName = "training_session")
data class SessionEntity(
#PrimaryKey(autoGenerate = false) val id: Long,
#ColumnInfo(name = "current_state_marker") val currentState: Short,
val states: StatesHolder
)
Note that you cannot Embed StatesHolder as then that just inserts List. If you want to Embed then you have to Embed a wrapper that uses a StatesHolder.
You will then need TypeConverters to convert to and from a StatesHolder object to a type that can be stored. Probably a String and Probably a JSON respresentation of the StatesHold object e.g.
class Converters {
#TypeConverter
fun fromStatesHolder(sh: StatesHolder): String {
return Gson().toJson(sh)
}
#TypeConverter
fun toStatesHolder(sh: String): StatesHolder {
return Gson().fromJson(sh,StatesHolder::class.java)
}
}
You additionally need to use #TypeConverters annotation that defines the Converts::class. If coded at the #Database level the converters have full scope.
So after #Database(.....) you could have :-
#TypeConverters(Converters::class)

Exactly ONE TO ONE Relation in Room

I believe it have been asked several times but no working solution.
Room has #Relation annotation which is used for one to many relationships. That's basically if you have User and Pet models, as User can have several Pets, #Relation annotation works perfectly as return type of Pets are list (or set).
class UserAndAllPets : User() {
#Relation(parentColumn = "id", entityColumn = "userId")
var pets: List<Pet> = arrayListOf()
}
The problem is what if in my case User and Pet is one to one related. As in every user can have one pet. That means there is no point of using #Relation as it only supports list or set return types. And it's totally inefficient to use as a list, even if I use it. So I am looking for a exact one to one relation where I can get a result of
class UserAndPet {
var user: User? = null
var pet: Pet? = null
}
I have tried tried several was as well as this method (which has lots of upvotes, but it doesn't work).
Supposedly,
class UserAndPet {
#Embedded
var user: User? = null
#Embedded
var pet: Pet? = null
}
Should work, but I am getting
Not sure how to convert a Cursor to this method's return type (UserAndPet)
There is no conflict as I already use prefix for #Embedded fields.
And please, can you not redirect my to any other post on stack overflow, as I tried all but no luck.
Thanks
This feature has been added as of 2.2.0-alpha01 of room.
Ref - Room release 2.2.0-alpha01
When we considering 1 to 1 relationship below approach is also possible and for more please follow the link.
#Entity(tableName = "user")
class User(
val id: Int
// ...
) {
#Ignore /* Ignores the marked element from Room's processing logic. */
var pet: Pet? = null
}
#Entity(tableName = "pet")
class Pet(
val id: Int,
val userId: Int
)
/* Single task used for 1 to 1 read */
class UserWithPetReadTask : RxTask.CallableWithArgs() {
private var result = UserWithPetTaskResult()
override fun call(params: Array<out Any>?): Any {
App.mApp?.getDBLocked { appDb ->
/* Read user details from repo */
val user: User? = appDb?.getUserDao()?.getUserById("[userId]")
user.let {
/* Read pet associated and assign it to user*/
it?.pet = appDb?.getPetDao().getPetAssociated("[userId] or [user?.id]")
result.user = user
result.isSuccess = true
}
}
return result
}
}
class UserWithPetTaskResult {
var isSuccess = false
var user: User? = null
}

Firestore - how to exclude fields of data class objects in Kotlin

Firestore here explains, how I can use simple classes to directly use them with firestore: https://firebase.google.com/docs/firestore/manage-data/add-data
How can I mark a field as excluded?
data class Parent(var name: String? = null) {
// don't save this field directly
var questions: ArrayList<String> = ArrayList()
}
I realize this is super late, but I just stumbled upon this and thought I could provide an alternative syntax, hoping someone will find it helpful.
data class Parent(var name: String? = null) {
#get:Exclude
var questions: ArrayList<Child> = ArrayList()
}
One benefit to this is that, in my opinion, it reads a little clearer, but the main benefit is that it would allow excluding properties defined in the data class constructor as well:
data class Parent(
var name: String? = null,
#get:Exclude
var questions: ArrayList<Child> = ArrayList()
)
Since Kotlin creates implicit getters and setters for fields, you need to annotate the setter with #Exclude to tell Firestore not to use them. Kotlin's syntax for this is as follows:
data class Parent(var name: String? = null) {
// questions will not be serialized in either direction.
var questions: ArrayList<Child> = ArrayList()
#Exclude get
}

Kotlin and ObjectBox: Relations in Data Classes

How can I initialize a Data Class with a ToOne relation? For example, I have the two Data Classes below:
#Entity
data class EntityA(
#Id var id: Long,
var entityB: ToOne<EntityB>
)
#Entity
data class EntityB(
#Id var id: Long
)
Now, I want to initialize EntityA like that:
var e = EntityA(1, EntityB())
But, obviously, I can't do that because the second argument is a ToOne and not an EntityB. I tried to instantiate ToOne but its constructor wants a second argument that I don't know how to create.
Do not put the relation in the primary constructor. Then you can use a secondary constructor to call toOne.target = entity. It should look like something like this:
#Entity
data class EntityA(#Id var id: Long) {
lateinit var entityB: ToOne<EntityB>
constructor(b: EntityB) : this(0) {
entityB.target = b
}
}

Categories

Resources