I am rewriting my old Sqlite Android app that was in Java to be a Jetpack Compose app in Kotlin that uses a Room database.
I've got about half of the app done but now I am seeing a strange behavior where my DAO query is not returning the data it should be, and the cause seems to be because the correct constructor, defined in my data model class, is not being called.
I am pretty sure this constructor WAS being called back before, before I added a new table to the database. I'm not 100% on this but I think so.
Anyway, here's some relevant code:
Database:
Data Model (I've added an #Ignore property, firearmImageUrl, for this imageFile column from the firearm_image table so it's part of the Firearm object. Maybe not the best way to do this, for joining tables? But this is a small simple app that like 5 people worldwide might use, more likely just me):
#Entity(tableName = "firearm")
class Firearm {
#ColumnInfo(name = "_id")
#PrimaryKey(autoGenerate = true)
var id = 0
var name: String = ""
var notes: String? = null
#Ignore
var shotCount = 0
#Ignore
var firearmImageUrl: String = ""
#Ignore
constructor() {
}
#Ignore
constructor(
name: String,
notes: String?
) {
this.name = name
this.notes = notes
}
#Ignore
constructor(
name: String,
notes: String?,
shotCount: Int
) {
this.name = name
this.notes = notes
this.shotCount = shotCount
}
#Ignore
constructor(
id: Int,
name: String,
notes: String?,
shotCount: Int
) {
this.id = id
this.name = name
this.notes = notes
this.shotCount = shotCount
}
// THIS IS THE CONSTRUCTOR THAT I **WANT** TO BE CALLED AND IS NOT. THIS USED TO HAVE AN
// #IGNORE TAG ON IT BUT REMOVING IT DID NOTHING
constructor(
id: Int,
name: String,
notes: String?,
shotCount: Int,
firearmImageUrl: String
) {
this.id = id
this.name = name
this.notes = notes
this.shotCount = shotCount
this.firearmImageUrl = firearmImageUrl
}
// THIS IS THE CONSTRUCTOR THAT IS BEING CALLED BY THE BELOW DAO METHOD, EVEN THOUGH
// ITS PARAMETERS DO NOT MATCH WHAT'S BEING RETURNED BY THAT QUERY
constructor(
id: Int,
name: String,
notes: String?,
) {
this.id = id
this.name = name
this.notes = notes
}
}
DAO (I removed the suspend keyword just so this thing would hit a debug breakpoint; also this query absolutely works, I copy-pasted it into the Database Inspector and ran it against the db and it returns the proper data with firearmImageUrl populated with a path):
#Query(
"SELECT f._id, " +
"f.name, " +
"f.notes, " +
"CASE WHEN SUM(s.roundsFired) IS NULL THEN 0 " +
"ELSE SUM(s.roundsFired) " +
"END shotCount, " +
"fi.imageFile firearmImageUrl " +
"FROM firearm f " +
"LEFT JOIN shot_track s ON f._id = s.firearmId " +
"LEFT JOIN firearm_image fi ON f._id = fi.firearmId " +
"WHERE f._id = :firearmId " +
"GROUP BY f._id " +
"ORDER BY f.name"
)
fun getFirearm(firearmId: Int): Firearm?
Repo:
override fun getFirearm(firearmId: Int): Firearm? {
return dao.getFirearm(firearmId)
}
Use Case (I'm dumb and decided to do this Clean Architecture but it's way overkill; this is just an intermediate class and calls the Repo method):
data class FirearmUseCases(
/**
* Gets the valid Firearms in the application.
*/
val getFirearms: GetFirearms,
/**
* Gets the specified Firearm.
*/
val getFirearm: GetFirearm
)
class GetFirearm(private val repository: FirearmRepository) {
operator fun invoke(firearmId: Int): Firearm? {
return repository.getFirearm(firearmId)
}
}
ViewModel:
init {
savedStateHandle.get<Int>("firearmId")?.let { firearmId ->
if (firearmId > 0) {
viewModelScope.launch {
firearmUseCases.getFirearm(firearmId)?.also { firearm ->
_currentFirearmId.value = firearm.id
// and so on... point is, the object is retrieved in this block
}
}
}
}
}
What's happening is the DAO is calling the constructor that I've commented above, and not the constructor that has the parameters that match what the query is returning. Not sure why. That constructor did have an #Ignore tag on it before tonight but I just tried removing it and there was no difference; constructor with only 3 parameters is still being called.
Thanks for any help, this Room stuff is nuts. I should've just stuck with Sqlite lmao. It's such a simple app, the old version was super fast and worked fine. Silly me wanting to learn contemporary design though.
I believe that your issue is based upon shotCount being #Ignored (which you obviously want). Thus, even though you have it in the output, Room ignores the column and thus doesn't use the constructor you wish.
I would suggest that the resolution is quite simple albeit perhaps a little weird and that is to have Firearm not annotated with #Entity and just a POJO (with no Room annotation) and then have a separate #Entity annotated class specifically for the table.
You could obviously add constructors/functions, as/if required to the Firearm class to handle FirearmTable's
e.g.
#Entity(tableName = "firearm")
data class FireArmTable(
#ColumnInfo(name = BaseColumns._ID)
#PrimaryKey
var id: Long?=null,
var name: String,
var notes: String? = null
)
using BaseColumns._ID would change the ID column name should it ever change.
using Long=null? without autogenerate = true will generate an id (if no value is supplied) but is more efficient see https://sqlite.org/autoinc.html (especially the very first sentence)
the above are just suggestions, they are not required
and :-
class Firearm() : Parcelable {
#ColumnInfo(name = "_id")
#PrimaryKey(autoGenerate = true)
var id = 0
var name: String = ""
var notes: String? = null
//#Ignore
var shotCount = 0
//#Ignore
var firearmImageUrl: String = ""
....
Using the above and using (tested with .allowMainThreadQueries) then the following:-
db = TheDatabase.getInstance(this)
dao = db.getFirearmDao()
val f1id = dao.insert(FireArmTable( name = "F1", notes = "Awesome"))
val f2id = dao.insert(FireArmTable(name = "F2", notes = "OK"))
dao.insert(Firearm_Image(firearmId = f1id, imageFile = "F1IMAGE"))
dao.insert(Shot_track(firearmId = f1id, roundsFired = 10))
dao.insert(Shot_track(firearmId = f1id, roundsFired = 20))
dao.insert(Shot_track(firearmId = f1id, roundsFired = 30))
dao.insert(Firearm_Image(firearmId = f2id, imageFile = "F2IMAGE"))
dao.insert(Shot_track(firearmId = f2id, roundsFired = 5))
dao.insert(Shot_track(firearmId = f2id, roundsFired = 15))
logFirearm(dao.getFirearm(f1id.toInt()))
val f1 = dao.getFirearm(f1id.toInt())
val f2 = dao.getFirearm(f2id.toInt())
logFirearm(f2)
}
fun logFirearm(firearm: Firearm?) {
Log.d("FIREARMINFO","Firearm: ${firearm!!.name} Notes are: ${firearm.notes} ImageURL: ${firearm.firearmImageUrl} ShotCount: ${firearm.shotCount}")
}
Where getFirearm is your Query copied and pasted, shows the following in the log:-
D/FIREARMINFO: Firearm: F1 Notes are: Awesome ImageURL: F1IMAGE ShotCount: 60
D/FIREARMINFO: Firearm: F2 Notes are: OK ImageURL: F2IMAGE ShotCount: 20
i.e. Shotcounts as expected.
Related
I fetch data from API and I would like to reuse it in data class like this (ItemStateRoom is data with String, Int etc.):
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
#ColumnInfo(name = "state")
#JsonAdapter(ItemStateAdapter::class)
var state: ItemStateRoom?
{
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
}
When I set state to null of course I have null, but program compiles well.
If it is like at the top error occurs:
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).
I also tried to do like this:
#Ignore
#JsonAdapter(ItemStateAdapter::class)
#ColumnInfo(name = "state")
var state: ItemStateRoom?,
Then I have errors:
error: Cannot figure out how to read this field from a cursor.
.ItemStateRoom state;
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
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).
EDIT1:
Description: I make realm to room migration, realmobjects are downloaded as well and they are not null, but some of objects(data classes) I have changed to Room i.e. ItemStateRoom or ItemSubStatus. For sure it is not null, I download it from backend in postman it gives value for sure.
#TypeConverters(Converters::class)
#Entity(tableName = "itemsubstatus")
data class ItemSubStatusRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
#ColumnInfo(name = "title") val title: String = "",
#ColumnInfo(name = "description") val description: String? = null
)
TypeConverters:
class Converters {
private val gson = GsonBuilder().serializeNulls().create()
#TypeConverter
fun typeSubStatusToString(type: ItemSubStatusRoom?): String =
gson.toJson(type)
#TypeConverter
fun typeItemSubStatusFromString(value: String): ItemSubStatusRoom? =
gson.fromJson(value, ItemSubStatusRoom::class.java) as ItemSubStatusRoom?
}
My response data is simple:
class ItemsDataResponse (
#SerializedName("items")
val items: ArrayList<ItemRoom>,
val total: Int
)
in my ItemData it is also simple
#TypeConverters(Converters::class)
#Entity(tableName = "item")
data class ItemRoom #JvmOverloads constructor(#PrimaryKey(autoGenerate = true) var id: Int = 0,
[...]
#ColumnInfo(name = "sub_status")
var subStatus: ItemSubStatusRoom?,
``
When it is saying there is no "public constructor", it is saying that it does not know how to construct the ItemRoom object that does not include the state field/member.
So with state #Ignore'd you would need to have a constructor that doesn't expect the state as a parameter.
with a Data Class the definition within the parenthesises is the constructor as such.
e.g.
constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
in which case Room would always construct an ItemRoom where state is null.
However, I suspect that the above is not what you want. Rather that you want the state to be saved in the database. As such, as it is not a type that can be stored directly and hence the need for a TypeConverter (actually 2)
only String, integer (Int, Long, Byte, Boolean etc types), decimal (Float, Double etc types) or byte streams (ByteArray) can be stored directly
Then if you want the state field to be saved in the database, then you will need 2 TypeConverters.
One which will convert from the ItemStateRoom to one of the types that can be stored (so it is passed an ItemStateRoom and returns one of the types that can be stored directly).
The other will convert from the type stored to an ItemStateRoom object.
So assuming that you want to store the state you could have, something like :-
#TypeConverters(value = [Converters::class])
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
//#Ignore /*<<<<< if used a constructor is needed that doesn't require the state, as the state is not available */
/* if not used then state has to be converted to a type that room can store */
#ColumnInfo(name = "state")
#JsonAdapter(ItemStateAdapter::class)
var state: ItemStateRoom?
)
{
//constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
}
data class ItemStateRoom(
/* made up ItemStateRoom */
var roomNumber: Int,
var roomType: String
)
class ItemStateAdapter() {
/* whatever .... */
}
class Converters {
#TypeConverter
fun convertItemStateRoomToJSONString(itemStateRoom: ItemStateRoom): String = Gson().toJson(itemStateRoom)
#TypeConverter
fun convertFromJSONStringToItemStateRoom(jsonString: String): ItemStateRoom = Gson().fromJson(jsonString,ItemStateRoom::class.java)
}
note the commented out #Ignore and constructor (for if #Ignore'ing the state field).
how you implement, if at all, the #JsonAdapter, would be as you have it.
note the #TypeConverters (which you would omit if #Ignoreing the state field)
It might be preferable to include the #TypeConverters at the #Database level, where it has full scope. (see https://developer.android.com/reference/androidx/room/TypeConverters)
Example
Here's an example of storing the state (ItemStateRoom) that uses the code above, with a pretty standard #Database annotated class and an #Dao annotated interface :-
#Dao
interface ItemRoomDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(itemRoom: ItemRoom): Long
#Query("SELECT * FROM item")
fun getAllItems(): List<ItemRoom>
}
Two items are inserted and then extracted and written to the log using the following activity code :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: ItemRoomDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getItemRoomDao()
dao.insert(ItemRoom(type = "Bedroom", template = "The Template", title = "Bedroom1", state = ItemStateRoom(101,"King size - Shower - Bath - Kitchenette")))
dao.insert(ItemRoom(type = "Bedroom", template = "The Template", title = "Bedroom2", state = ItemStateRoom(102,"Queen size - Shower - Bath")))
for(i in dao.getAllItems()) {
Log.d("DBINFO","Item ID is ${i.id} Title is ${i.title} Room Number is ${i.state!!.roomNumber} Room Type is ${i.state!!.roomType}")
}
}
}
The result in the log:-
D/DBINFO: Item ID is 1 Title is Bedroom1 Room Number is 101 Room Type is King size - Shower - Bath - Kitchenette
D/DBINFO: Item ID is 2 Title is Bedroom2 Room Number is 102 Room Type is Queen size - Shower - Bath
The database via App Inspection :-
as can be seen the ItemStateRoom has been converted to (and from according to the output in the log) a JSON String.
Ignore the weatherforecast table, that was from another answer that was used for providing this answer.
Alternative Approach
Instead of converting the ItemState to a JSON representation (which is unwieldly from a database perspective) consider the potential advantages of instead embedding the ItemState. The difference in this approach is that the fields of the ItemState are each saved as individual columns.
e.g.
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
//#Ignore /*<<<<< if used a constructor is needed that doesn't require the state, as the state is not available */
/* if not used then state has to be converted to a type that room can store */
//#ColumnInfo(name = "state")
//#JsonAdapter(ItemStateAdapter::class)
//var state: ItemStateRoom?
#Embedded
var itemStateRoom: ItemStateRoom
)
{
//constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
/*
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
*/
}
Thus to update, at least from the database aspect, you are freed from trying to manipulate a representation of the data. You can update the actual data directly.
Using the above, with the example code (changed to use itemState instead of state) then the database looks like:-
You say
But to reassign values dynamically I have to use #JsonAdapter.
The JsonAdapter is NOT going to magically update the data in the database, to update the data you will have to use a function in an interface (or an abstract class) that is annotated with #Dao which will either be annotated with #Update (convenience) or #Query (with appropriate UPDATE SQL).
Im investigating Android Rooms support for returning Maps from query's
I cannot get past this build error
error: The columns returned by the query does not have the fields [columnOne,columnTwo,columnThree,columnFour,columnFive,columnSix,columnSeven,columnEight] in org.my.MyDataVO even though they are annotated as non-null or primitive. Columns returned by the query: [columnA,columnB]
public abstract java.util.Map<java.lang.String, java.util.List<org.my.MyDataVO>> fetchMap(#org.jetbrains.annotations.NotNull()
My DAO Query resembles:-
#MapInfo(keyColumn = "column_a", valueColumn = "column_b")
#Query("SELECT column_a, column_b FROM my_data_table WHERE my_key_column = :myKeyColumn")
fun fetchMap(myKeyColumn: String): Map<String, List<org.my.MyDataVO>>
My DO object resembles:-
#Entity(
tableName = "my_data_table",
indices = [
Index(value = ["key_column"], unique = false),
]
)
#TypeConverters(MyDataVOListTypeConverter::class)
data class BookTableContentChildrenDO(
#ColumnInfo(name = "key_column") val keyColumn: String,
#ColumnInfo(name = "column_a") val columnA: String,
#ColumnInfo(name = "column_b") val columnB: List<org.my.MyDataVO>
) {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "my_data_table_local_id")
var myDataTableLocalId: Long = 0L
}
My Value Object MyDataVO resembles:-
#Serializable
data class MyDataVO(
#ColumnInfo(name = "column_one") val columnOne: Long,
#ColumnInfo(name = "column_two") val columnTwo: String,
#ColumnInfo(name = "column_three") val columnThree: Boolean,
#ColumnInfo(name = "column_four") val columnFour: String,
#ColumnInfo(name = "column_five") val columnFive: String,
#ColumnInfo(name = "column_six") val columnSix: String,
#ColumnInfo(name = "column_seven") val columnSeven: String,
#ColumnInfo(name = "column_eight") val columnEight: Long,
)
Is what I am attempting not possible?
Where have I made my mistake?
Why cannot room map my_data_table.column_b to a List<org.my.MyDataVO>
UPDATE
I managed to resolve the build error by adding all my TypeConverters to the #Database abstract class definition
However I am now facing a runtime error which seems very strange
I have type converters for both a single instance of org.my.MyDataVO and a List<org.my.MyDataVO>, Room is employing the Single instance TypeConverter to decode the List<org.my.MyDataVO>
as when I attempt to call my DAO fetchMap() function it fails with
java.util.concurrent.ExecutionException: kotlinx.serialization.json.internal.JsonDecodingException: Expected start of the object '{', but had 'EOF' instead
JSON input: .....xxx","columnSix":1,"columnSeven":1,"columnEight":""}]}]
Why is Room using the wrong TypeConverter?
If I do not mention the Single instance TypeConverter in my #Database abstract class I get the build error.
UPDATE 2
I can get this to work if I change the DAO fetchMap method return type to Map<String, List<Any>>
Now Room employs the correct type converter and I can fetch the map ok
UPDATE 3
Following further testing I have discovered although I do not experience any crashes or build issues my DAO is not working as I require. Although I am inserting multiple entry lists of org.my.MyDataVO, the lists returned by my fetchMap function only ever contain one entry. Why cant Room return the complete list as stored in the DB table?
Why cant Room return the complete list as stored in the DB table?
In short you cannot have a column as a List/Array directly.
so #ColumnInfo(name = "column_b") val columnB: List<org.my.MyDataVO> is not going to work.
Add a POJO such as
data class MyDataVOListHolder(
val myDataVOListHolder: List<org.my.MyDataVO>
)
and then use the POJO for the column type e.g.
#ColumnInfo(name = "column_b") val columnB: MyDataVOListHolder
Obviously you will need suitable Typeconverters to convert a myDataVOListHolder to and from a type that Room can handle (e.g. JSON String).
As an example :-
lateinit var db: TheDatabase
lateinit var dao: BookTableContentChildrenDODao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getDao()
dao.insert(
BookTableContentChildrenDO(keyColumn = "AAA", columnA = "Blah", columnB = MyDataVOListHolder(myDataVOListHolder = createMyDataVOList()))
)
dao.insert(
BookTableContentChildrenDO(keyColumn = "BBB", columnA = "Blah", columnB = MyDataVOListHolder(myDataVOListHolder = createMyDataVOList()))
)
dao.insert(
BookTableContentChildrenDO(keyColumn = "CCC", columnA = "Blah", columnB = MyDataVOListHolder(myDataVOListHolder = createMyDataVOList()))
)
for(m in dao.fetchMap("BBB")) {
Log.d("DBINFO","Key is ${m.key}")
for (myDataVo: MyDataVO in m.value.myDataVOListHolder) {
Log.d("DBINFO-EXTRA","\t C1 = ${myDataVo.columnOne} C2 = ${myDataVo.columnTwo} C3 = ${myDataVo.columnThree} etc...."
)
}
}
}
fun createMyDataVOList(): List<MyDataVO> {
var myDataVoList = ArrayList<MyDataVO>()
for (i in 1 until 5) {
myDataVoList.add(
MyDataVO((i * 3).toLong(),"col2_" +i.toString(),i % 2 == 0,"col4","col5","col6","col7",i.toLong())
)
}
return myDataVoList
}
results in the log containing :-
D/DBINFO: Key is Blah
D/DBINFO-EXTRA: C1 = 3 C2 = col2_1 C3 = false etc....
D/DBINFO-EXTRA: C1 = 6 C2 = col2_2 C3 = true etc....
D/DBINFO-EXTRA: C1 = 9 C2 = col2_3 C3 = false etc....
D/DBINFO-EXTRA: C1 = 12 C2 = col2_4 C3 = true etc....
Bear with me, it's a tricky question and what resources I've found around don't really help me resolve my problem.
I'm trying to build a real estate-oriented app on Kotlin. It must show at some point a RecyclerView with multiple object classes (say: houses, flats, plots, buildings, etc.)
I've seen multiple examples of RVs designed to accept multiple classes, but I'm struggling to put together a DB and the intermediary classes translating between tables and POJOs.
So far I've figured the following:
I must have a Properties table that stores the unique ID for every object, along with another identifier for its type and a series of values common to every property (say, address, price, etc.)
I must have a table for each entity type that can be independently listed as a real estate item (say, a house, a flat, a plot of land, a building, what have you). Each row on those tables will have a primary foreign key referencing its equivalent on the Properties table.
Now for the unexpected habanero. I decided to start sketching out my project on the basis of the RecyclerView Kotlin codelabs Google put together for newbies like me. Therein data is retrieved from the DB in this fashion:
this.plots = Transformations.map(database.RealtorDao.getPlots()) { it.asDomainModel() }
This works smoothly enough when the objects on the list the DB spits at you are all of one single kind, but what happens if you need them to be of different classes so that the adapter can tell them apart?
Or the only way around is just to build a gigantic table with about a hundred columns that will have nulls everywhere, and sort out objects ONLY AFTER they've been parsed in the previously described fashion?
I smashed my head against this wall until I got tired of hearing the squishing sound. I could not get a Room DB to return a list of objects of multiple classes, so I had to adopt a dirtier approach.
If I had worked just with the database classes then probably I could have hacked it, but trying to translate objects of such classes into POJOs to use instead complicated things somewhat.
The workaround I found was to make a master real estate class and accept that it would have lots and lots of null fields on the database. While a far cry from ideal, it works.
Database object classes:
open class DatabaseProperty
{
#ColumnInfo(name = COL_TYPE)
#SerializedName(COL_TYPE)
#Expose
var type: String? = null
#ColumnInfo(name = COL_ADDRESS)
#SerializedName(COL_ADDRESS)
#Expose
var address: String? = null
#ColumnInfo(name = COL_OWNER)
#SerializedName(COL_OWNER)
#Expose
var owner: String? = null
#ColumnInfo(name = COL_PRICE_FINAL)
#SerializedName(COL_PRICE_FINAL)
#Expose
var priceFinal: Long? = null
#ColumnInfo(name = COL_PRICE_QUOTED)
#SerializedName(COL_PRICE_QUOTED)
#Expose
var priceQuoted: Long? = null
/**
* No args constructor for use in serialization
*/
constructor()
#Ignore
constructor
(
type: String,
address: String,
owner: String,
priceFinal: Long,
priceQuoted: Long
) : super() {
this.type = type
this.address = address
this.owner = owner
this.priceFinal = priceFinal
this.priceQuoted = priceQuoted
}
}
#Entity
(
tableName = TABLE_RE,
indices =
[
Index(value = [COL_RE_ID], unique = true)
],
foreignKeys =
[
ForeignKey
(
entity = DatabaseRealEstate::class,
parentColumns = arrayOf(COL_RE_ID),
childColumns = arrayOf(COL_PARENT_ID),
onDelete = ForeignKey.NO_ACTION
)
]
)
data class DatabaseRealEstate
(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = COL_RE_ID)
var id: Int? = null,
#ColumnInfo(name = COL_PARENT_ID)
var parent_id: Int? = null,
#Embedded(prefix = RE)
var property: DatabaseProperty? = null,
#ColumnInfo(name = COL_PARCEL_FRONT) // Plot front
#SerializedName(COL_PARCEL_FRONT)
#Expose
var front: Float? = null,
#ColumnInfo(name = COL_PARCEL_SIDE) // Plot side
#SerializedName(COL_PARCEL_SIDE)
#Expose
var side: Float? = null,
#ColumnInfo(name = COL_AREA) // Plot area
#SerializedName(COL_AREA)
#Expose
var area: Float? = null,
#ColumnInfo(name = COL_CATASTER)
#SerializedName(COL_CATASTER)
#Expose
var cataster: String? = null,
#ColumnInfo(name = COL_ZONIFICATION)
#SerializedName(COL_ZONIFICATION)
#Expose
var zonification: String? = null,
)
data class RealEstateWithSubunits
(
#Embedded
val re: DatabaseRealEstate? = null,
#Relation
(
parentColumn = COL_RE_ID,
entityColumn = COL_PARENT_ID,
entity = DatabaseRealEstate::class
)
var subunits: List<DatabaseRealEstate>? = null,
#Relation
(
parentColumn = COL_RE_ID,
entityColumn = COL_PARENT_ID,
entity = DatabaseChamber::class
)
var chambers: List<DatabaseChamber>? = null
)
fun List<RealEstateWithSubunits>.asRESUBDomainModel() : List<RealEstate>
{
return map { obj ->
RealEstate(
id = obj.re!!.id!!,
type = obj.re.property!!.type!!,
address = obj.re.property!!.address!!,
owner = obj.re.property!!.owner!!,
priceFinal = obj.re.property!!.priceFinal!!,
priceQuoted = obj.re.property!!.priceQuoted!!,
parent_id = obj.re.parent_id,
front = obj.re.front,
side = obj.re.side,
area = obj.re.area,
cataster = obj.re.cataster,
zonification = obj.re.zonification,
chambers = obj.chambers!!.asChamberDomainModel(),
subunits = obj.subunits!!.asREDomainModel()
)
}
}
fun List<DatabaseChamber>.asChamberDomainModel(): List<Chamber>
{
return map {
Chamber(
id = it.id,
parent_id = it.parent_id,
front = it.front,
side = it.side,
area = it.area
)
}
}
fun List<DatabaseRealEstate>.asREDomainModel(): List<RealEstate>
{
return map { obj ->
RealEstate(
id = obj.id!!,
type = obj.property!!.type!!,
address = obj.property!!.address!!,
owner = obj.property!!.owner!!,
priceFinal = obj.property!!.priceFinal!!,
priceQuoted = obj.property!!.priceQuoted!!,
parent_id = obj.parent_id,
front = obj.front,
side = obj.side,
area = obj.area,
cataster = obj.cataster,
zonification = obj.zonification,
chambers = ArrayList(),
subunits = ArrayList()
)
}
}
Model object classes:
interface BaseProperty {
var id: Int
var type: String
var address: String
var owner: String
var priceFinal: Long
var priceQuoted: Long
}
data class RealEstate(
override var id: Int = -1,
override var type: String = "",
override var address: String = "",
override var owner: String = "",
override var priceFinal: Long = 0,
override var priceQuoted: Long = 0,
var parent_id: Int?,
var front: Float?,
var side: Float?,
var area: Float?,
var cataster: String?,
var zonification: String?,
var subunits: List<RealEstate>? = null,
var chambers: List<Chamber>? = null
) : BaseProperty
{
fun hasParent() : Boolean
{
if (parent_id == null)
{
return false
}
return true
}
}
I haven't yet found a better approach, so if someone does, I'm welcoming it with open arms.
I'm using Room persistence library for Android and trying to make 'update' query for the boolean field.
#Update
suspend fun updateProduct(product: Product)
Product entity:
#Entity(tableName = "products")
data class Product(
#ColumnInfo(name = "name") val name: String = "",
#ColumnInfo(name = "price") val price: Int = 0,
#ColumnInfo(name = "count") val count: Int = 0,
#ColumnInfo(name = "description") val description: String = "",
#ColumnInfo(name = "isPurchased") val isPurchased : Boolean = false
) {
#PrimaryKey var id: String = UUID.randomUUID().toString()
#ColumnInfo(name = "date") var date: Long = Date().time
}
Similar queries as delete, insert work fine. The underhood query should find the id of product and update all fields but it doesn't work. Please don't write about insert query instead update, it's a dirty trick.
Update: update method returns 0 and it means it doesn't work, according to docs it should return num of updated record:
Although usually not necessary, you can have this method return an int
value instead, indicating the number of rows updated in the database.
you can try this
#Query("UPDATE products SET price=:price WHERE id = :id")
void update(Float price, int id);
Regarding the docs it says you have to do something like this:
#Update
fun updateProduct(product: Product) // no need of suspend
also you can control what happen onConflict. Note that if you don't specify by default it is OnConflictStrategy.ABORT which roll back the transaction and does nothing. So you might wanna add something like #Update(onConflict = OnConflictStrategy.REPLACE).
I have an Entity like this:
data class Person(
#PrimaryKey(autoGenerate = true) val id: Long = 0,
#ColumnInfo(name = "user_id", index = true) var userId: Long = 0,
#ColumnInfo(name = "first_name") var firstName: String = "",
#ColumnInfo(name = "mid_name") var midName: String = "",
#ColumnInfo(name = "last_name") var lastName: String = ""
) {
public fun fullName(): String {
return "$firstName $midName $lastName"
}
}
I know that I can set a "not null" attribute to each named property. But in my case, it's no need to fill up all the name properties, I only want to validate the full name is blank or not before this entity saves to the room database.
I'm practicing to use the MVVM framework, but now I'm not sure where should I put the validation. Activity/Fragment? ViewModel? Repository? or inside the Entity directly?
I think maybe I should do this in a repository so that I can prevent the wrong input before I save it to the database. But what if there is another use case that I need to validate the same thing in a different repository? If so, then the code will be duplicated in two repositories.
I've ever written ruby on rails before, there is validation function in the Model like:
# This is ruby on rails code
validate :name_validation
def name_validation
if first_name == "" && mid_name == "" && last_name == ""
errors.add(:name, "the name should not be totally blank")
end
end
I wonder if there is a similar way to validate a property in the Entity, or there is any better practice to resolve this?
Please help me figure it out.
I think maybe we can just use a fullName property directly and let it not null.
Then make a Name class for name processing, and NameConveter class for converting.
So the sample code may seems like this:
class Name(
var firstName: String = "",
var midName: String = "",
var lastName: String = ""
) {
public fun fullName(): String {
return "$firstName $midName $lastName".trim()
}
}
#Entity
data class Person(
#PrimaryKey(autoGenerate = true) val id: Long = 0,
#ColumnInfo(name = "user_id", index = true) var userId: Long = 0,
#ColumnInfo(name = "full_name") var fullName: Name
)
class NameStringConverter {
#TypeConverter
fun fromString(value: String): Name {
val nameStr = value.split(" ")
return Name(nameStr[0], nameStr[1], nameStr[2])
}
#TypeConverter
fun nameToString(name: Name): String? {
val fullName = name.fullName()
// Here is the trick
// to make a blank full name become an invalid name for a not null property
if (name.isNullOrBlank()) {
return null
} else {
return fullName
}
}
}
So that we can access multiple name by Name Class, and database will help us to check the fullname now.
I know that my sample code may cause some problem such as an extra blank character in a name string, but let us focus on the fullName validation issue for now.
I figured out this solution today, I'm not sure it's a good answer or not.
I post here and open for everyone to judge it.