Need for NoSQL Android Cache - android

I am running several MongoDb based API's implemented with Spring Data
My problem is to have a Android Cache Db that can handle Documents, especially Documents with Embedded Documents seamlessly!
MongoDb seem to have a solution (MongoDb Realm) BUT it is connected to a Paid for hosted Db
As a sample with Fake data received as JSON from the API call to the Back-End could look like this
{
"detail": "Page 1 of 1",
"count": 1,
"page": 1,
"results": [
{
"prospectId": "5cb2e9424274072ec4bb419c",
"name": "Bill",
"lastName": "Gates",
"phone": "0108101081",
"email": "gates.william#gmail.com",
"company": {
"companyId": "60847dc8ba7e6a4ae0fa5f93",
"name": "Microsoft",
"email": "info#Microsoft.com",
"address": {
"addressId": "5cb2e9424274072ec4bb4199",
"label": "Home",
"number": "1",
"street": "Microsoft Way",
"timestamp": 1631087921460
},
"timestamp": 1631087921855
},
"address": {
"addressId": "5cb2e9424274072ec4bb4199",
"label": "Home",
"number": "1",
"street": "Microsoft Way",
"timestamp": 1631087921460
},
"timestamp": 1631087922537
}
]
}
Prospect Class
class Prospect {
#Id
var prospectId: String? = null
var name: String? = null
var lastName: String? = null
var phone: String? = null
var email: String? = null
#DBRef
var company: Company? = null
#DBRef
var address: Address? = null
var timestamp: Long = nowToLongUTC()
}
My Company
class Company {
#Id
var companyId: String? = null
var name: String? = null
var email: String? = null
#DBRef
var address: Address? = null
var timestamp: Long? = null
}
My Address
class Address {
#Id
var addressId: String? = null
var label: String? = null
var number: String? = null
var street: String? = null
var timestamp: Long = nowToLongUTC()
}
It is generated from a classes that includes #dbRef Company and #dbRef Address
Company also include #dbRef Address
Both the Prospect and the Company includes an Address, combined it has Document in Document in Document structure
Which is quite a natural reference document in noSql type documents
I need to have an Android Cache that understands noSql (MongoDb) and can natively handle that document embedded in document embedded in document,
Room fails galactically! being a SQL and suffers doing it properly. Using room's #Embedded notation helps for one level of embedded document but the next level need to be using a TypeConverter. That is how SQL fails me.

You can take a look at the Nitrite database here. Nitrite is an open source nosql embedded document store written in Java which is ideal for Android or Java desktop applications. Fortunately for you, it supports mongodb like api.
In your particular use case, there are two options here.
You remove the #DBRef annotations (nitrite does not support db reference) and insert the Prospect object into a nitrite object repository with all its embedded object.
class Prospect {
#Id
var prospectId: String? = null
var name: String? = null
var lastName: String? = null
var phone: String? = null
var email: String? = null
var company: Company? = null
var address: Address? = null
var timestamp: Long = System.currentTimeMillis()
}
class Company {
var companyId: String? = null
var name: String? = null
var email: String? = null
var address: Address? = null
var timestamp: Long? = null
}
class Address {
var addressId: String? = null
var label: String? = null
var number: String? = null
var street: String? = null
var timestamp: Long = System.currentTimeMillis()
}
val db = nitrite {
file = File(fileName)
autoCommitBufferSize = 2048
compress = true
autoCompact = false
}
// create repository
val prospects = db.getRepository<Prospect>()
// create/get your object
val obj = Prospect()
...
// insert the object into the database
prospects.insert(obj)
// retrieve like
val cursor = prospects.find(Prospect::prospectId eq "xyz")
// or
val cursor = prospects.find("prospectId.company.address.number" eq 123)
You create 3 object repositories for Prospects, Company and Address separately and store the ids in place of reference. But there you have to make call to appropriate object repositories to fetch the actual object.
class Prospect {
#Id
var prospectId: String? = null
var name: String? = null
var lastName: String? = null
var phone: String? = null
var email: String? = null
var companyId: String? = null
var addressId: String? = null
var timestamp: Long = System.currentTimeMillis()
}
class Company {
#Id
var companyId: String? = null
var name: String? = null
var email: String? = null
var addressId: String? = null
var timestamp: Long? = null
}
class Address {
#Id
var addressId: String? = null
var label: String? = null
var number: String? = null
var street: String? = null
var timestamp: Long = System.currentTimeMillis()
}
val db = nitrite {
file = File(fileName)
autoCommitBufferSize = 2048
compress = true
autoCompact = false
}
// create repositories
val prospects = db.getRepository<Prospect>()
val companies = db.getRepository<Company>()
val addresses = db.getRepository<Address>()
// create/get your objects
val pObj = Prospect()
val cObj = Company()
val aObj = Address()
...
// insert the object into the database
prospects.insert(pObj)
companies.insert(cObj)
addresses.insert(aObj)
// retrieve like
val p = prospects.find(Prospect::prospectId eq "xyz").firstOrNull()
val a = addresses.find(Address::addressId eq p.addressId)
Here is the full documentation to help you, if you get stuck. I'll recommend you to use version 3.4.2 as it is production ready and stable.
Disclaimer: I am the developer of Nitrite database.

Room fails galactically! being a SQL and suffers doing it properly. Using room's #Embedded notation helps for one level of embedded document but the next level need to be using a TypeConverter. That is how SQL fails me.
Perhaps the following is along the lines of what you are looking for based upon the "Fake" data. This ignores the #DBRef's but instead has the Entities (tables) Address, Company and Prospect and has supportive POJO's CompanyWithAddress and (for want of a better name) ProspectWithCompanyWithAddressAndWithProspectAddress, these embedding the underlying respective entities using #Relation for the child objects.
So the Entities are :-
Address
#Entity /* This indicates that this is a table */
class Address {
//#Id
#PrimaryKey /* Room requires a Primary Key for a table */
var addressId: String = ""
var label: String? = null
var number: String? = null
var street: String? = null
var timestamp: Long = /* nowToLongUTC() */ System.currentTimeMillis() /* used for convenience */
}
Company
#Entity(
foreignKeys = [
ForeignKey(
entity = Address::class,
parentColumns = ["addressId"],
childColumns = ["addressRef"],
/* You may or may not want onDelete and onUpdate actions */
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
/* you may wish to refer to https://sqlite.org/foreignkeys.html */
)
],
indices = [
Index("addressRef") /* Room will warn if a FK column is not indexed */
]
)
class Company {
//#Id
#PrimaryKey
var companyId: String = ""
var name: String? = null
var email: String? = null
//#DBRef
//var address: Address? = null
// Rather than de-normalise DB, waste space,
// here we reference the address rather than include the address
// the Foreign Key above adds a rule saying that the value of addressRef
// MUST be a value that is in an addressId column in the address table
// Foreign Keys are optional but recommended to enforce referential integrity
// break the rule and an error occurs
var addressRef: String? = null
var timestamp: Long? = null
}
and Prospect
#Entity(
foreignKeys = [
ForeignKey(
entity = Company::class,
parentColumns = ["companyId"],
childColumns = ["companyRef"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = Address::class,
parentColumns = ["addressId"],
childColumns = ["prospectAddressRef"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
],
indices = [
Index("companyRef"),
Index("prospectAddressRef")
]
)
class Prospect {
//#Id
#PrimaryKey
var prospectId: String = ""
var name: String? = null
var lastName: String? = null
var phone: String? = null
var email: String? = null
//#DBRef
var companyRef: String? = null
//#DBRef
#ColumnInfo(name = "prospectAddressRef") /* defines the column name in the table to avoid ambiguities */
var addressRef: String? = null
var timestamp: Long = /* nowToLongUTC() */ System.currentTimeMillis()
}
The POJO's :-
CompanyWithAddress
class CompanyWithAddress {
#Embedded
var company: Company? = null
#Relation(
entity = Address::class,
parentColumn = "addressRef",
entityColumn = "addressId"
)
var address: Address? = null
}
So a company with it's address noting that the Address object is obtained via the addressRef that holds the addressId of the related address (no need for a TypeConverter)
and ProspectWithCompanyWithAddressAndWithProspectAddress
class ProspectWithCompanyWithAddressAndWithProspectAddress {
#Embedded
var prospect: Prospect? = null
#Relation(
entity = Company::class,
parentColumn = "companyRef",
entityColumn = "companyId"
)
var companyWithAddress: CompanyWithAddress? = null
#Relation(
entity = Address::class,
parentColumn = "prospectAddressRef",
entityColumn = "addressId"
)
var address: Address? = null
}
The Dao's for a demo of the above in a class named AllDao
#Dao
abstract class AllDao {
#Insert
abstract fun insert(address: Address): Long
#Insert
abstract fun insert(company: Company): Long
#Insert
abstract fun insert(prospect: Prospect): Long
#Transaction
#Query("SELECT * FROM prospect")
abstract fun getALlFullProspects(): List<ProspectWithCompanyWithAddressAndWithProspectAddress>
}
These allow the insertion into respective tables
Note that due to the Foreign Keys addresses must be inserted before companies and prospects that use the address(es) and Companies must be inserted before the Prospect is inserted.
with Foreign Keys (which are optional) it wouldn't matter.
A pretty basic #Database TheDatabase
#Database(
entities = [Address::class,Company::class,Prospect::class],
version = 1
)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
#Volatile
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase::class.java,
"thedatabase.db"
)
.allowMainThreadQueries() /* for convenience/brevity of demo */
.build()
}
return instance as TheDatabase
}
}
}
Putting it all together in an Activity (run on the main thread for brevity/convenience) MainActivity :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
val a1Id = "5cb2e9424274072ec4bb4199"
val c1Id = "60847dc8ba7e6a4ae0fa5f93"
val p1Id = "5cb2e9424274072ec4bb419c"
var a1 = Address()
a1.addressId = a1Id
a1.label = "Home"
a1.number = "1"
a1.street = "MS Way"
dao.insert(a1)
var c1 = Company()
c1.companyId = c1Id
c1.email = "info#Microsoft.com"
c1.name = "Microsoft"
c1.addressRef = a1Id
dao.insert(c1)
var p1 = Prospect()
p1.prospectId = p1Id
p1.name = "Bill"
p1.lastName = "Gates"
p1.email = "gates.william#gmail.com"
p1.phone = "0108101081"
p1.companyRef = c1Id
p1.addressRef = a1Id
dao.insert(p1)
/* Going to add a 2nd Prospect with a 2nd Company with separate addresses */
val a2Id = "6cb2e9424274072ec4bb4199"
val a3Id = "7cb2e9424274072ec4bb4199"
val c2Id = "70847dc8ba7e6a4ae0fa5f93"
val p2Id = "6cb2e9424274072ec4bb419c"
/* change the initial address ready for the insert of the 2nd*/
a1.addressId = a2Id
a1.street = "Apple lane"
a1.number = "33"
a1.label = "Work"
dao.insert(a1)
/* change the 2nd address ready for the insert of the 3rd */
a1.addressId = a3Id
a1.label = "Holiday"
a1.number = "111"
a1.street = "Lazy Street"
dao.insert(a1)
/* Change the company and insert a new one */
c1.companyId = c2Id
c1.name = "Apple Inc"
c1.email = "info#apple.inc"
c1.addressRef = a2Id
dao.insert(c1)
/* Change the prospect and insert a new one */
p1.addressRef = a3Id
p1.companyRef = c2Id
p1.prospectId = p2Id
dao.insert(p1)
/* Now the data has been added extract it and output the extracted to the log*/
var TAG = "PROSPECTINFO"
for(p: ProspectWithCompanyWithAddressAndWithProspectAddress in dao.getALlFullProspects()) {
Log.d(TAG,
"Prospect: Name = ${p.prospect!!.name}, ${p.prospect!!.lastName} Phone is ${p.prospect!!.phone}" +
"\n\tCompany is ${p.companyWithAddress!!.company!!.name} " +
"\n\t\tCompany Address is ${p.companyWithAddress!!.address!!.number} ${p.companyWithAddress!!.address!!.street}" +
"\n\tProspect Address is ${p.address!!.number} ${p!!.address!!.street}"
)
}
}
}
Result
The log when the above is run (designed to just run the once, running a second time would result in UNIQIUE conflicts) :-
D/PROSPECTINFO: Prospect: Name = Bill, Gates Phone is 0108101081
Company is Microsoft
Company Address is 1 MS Way
Prospect Address is 1 MS Way
D/PROSPECTINFO: Prospect: Name = Bill, Gates Phone is 0108101081
Company is Apple Inc
Company Address is 33 Apple lane
Prospect Address is 111 Lazy Street
Via App Inspector the database is:-
The Address table :-
The Company table :-
timestamps are NULL as they weren't set (as per your original code)
The Prospect table :-
Name/Last Name etc are the same values as these weren't changed. Importantly though the companyRef and prospectAddressRef values reference the respective companies/addresses.

Related

How to Query in a many to many relationship Room database?

I have a many to many relationship Room database with three tables:
First one :
data class Name(
#PrimaryKey(autoGenerate = true)
var nameId : Long = 0L,
#ColumnInfo(name = "name")
var name : String = "",
#ColumnInfo(name = "notes")
var notes: String=""
)
Second:
#Entity(tableName = "tags_table")
data class Tag(
#PrimaryKey(autoGenerate = true)
var tagId : Long = 0L,
#ColumnInfo(name = "tag_name")
var tagName : String = ""
)
Third:
#Entity(
tableName = "tagInName_table",
primaryKeys = ["nameId", "tagId"],
foreignKeys = [
ForeignKey(
entity = Name::class,
parentColumns = ["nameId"],
childColumns = ["nameId"]
),
ForeignKey(
entity = Tag::class,
parentColumns = ["tagId"],
childColumns = ["tagId"]
)
]
)
data class TagInName(
#ColumnInfo(name = "nameId")
var nameId: Long = 0L,
#ColumnInfo(name = "tagId")
var tagId: Long = 0L
)
The data class I use for a return object in a Query:
data class NameWithTags(
#Embedded
val name: Name,
#Relation(
parentColumn = "nameId",
entityColumn = "tagId",
associateBy = Junction(value = TagInName::class)
)
val listOfTag : List<Tag>
)
This is how I query to get all NamesWithTags:
#Query("SELECT * FROM names_table")
#Transaction
fun getNamesWithTags() : LiveData<List<NameWithTags>>
So the thing I need to do is, I need to Query to return LiveData<List<NameWithTags>> where every NamesWithTags has a list which contains the Tag ID that I Query for.
From my interpretation of what you say you need to do, then :-
#Transaction
#Query("SELECT names_table.* FROM names_table JOIN tagInName_table ON names_table.nameId = tagInName_table.nameId JOIN tags_table ON tagInName_table.tagId = tags_table.tagId WHERE tags_table.tagId=:tagId ")
fun getNameWithTagsByTagId(tagId: Long): LiveData<List<NamesWithTags>>
Note the above is in-principle code and has not been compiled or tested, so it may contain some errors.
A NameWithTags will contain ALL related tags whcih should be fine according to (where every NamesWithTags has a list which contains the Tag ID ), if you wanted just certain Tags in the List of Tags then it's a little more complex, this is explained in a recent answer at Android Room query with condition for nested object

(Android) How to match child ID to parent in one-to-many relationship

I'm working on an image-like, one-to-many database.
To explain the flow of functions, WorkoutSetInfo is added to the list whenever the Add button is pressed. (It is not inserted into DB.)
Finally, when the Save button is pressed, the Workout and WorkoutSetInfo list are inserted into the DB.
I have completed setting up the relationship between Workout and WorkoutSetInfo, but I don't know how to match the ID of each Workout with Id(parendWorkoutId) of WorktoutSetInfo.
(Now, all IDs of parendWorkoutId are set to 1.)
How should I set it up?
Workout
#Entity
data class Workout(
#PrimaryKey(autoGenerate = true)
val workoutId: Long,
val title: String = "",
var unit: String = "kg",
val memo: String = "",
)
WorkoutSetInfo
#Entity(
foreignKeys = [
ForeignKey(
entity = Workout::class,
parentColumns = arrayOf("workoutId"),
childColumns = arrayOf("parentWorkoutId"),
onDelete = ForeignKey.CASCADE
)
]
)
data class WorkoutSetInfo(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
val set: Int,
var weight: String = "",
var reps: String = "",
val parentWorkoutId: Long = 1 // How do I set this up?
)
WorkoutWithSets
data class WorkoutWithSets(
#Embedded val workout: Workout,
#Relation (
parentColumn = "workoutId" ,
entityColumn = "parentWorkoutId"
)
val sets: List<WorkoutSetInfo>
)
Repository
class WorkoutRepository(private val workoutDao : WorkoutDao, title: String) {
val workout = Workout(0, title)
private val setInfoList = ArrayList<WorkoutSetInfo>()
fun add() {
val item = WorkoutSetInfo(set = setInfoList.size + 1)
setInfoList.add(item)
}
fun delete() {
if(setInfoList.size != 0)
setInfoList.removeLast()
return
}
fun save() {
workoutDao.insertWorkout(workout)
workoutDao.insertSetInfoList(setInfoList)
}
}
In the workoutDao interface/abstract class, have the insertWorkout function return a Long. This will be the id of the inserted Workout or -1 if the insert was ignored.
Retrieve/Use this returned value and use it to set the parentWorkoutId of each element in setInfoList within the save function.
of course if the value is -1 then you should handle this appropriately as the Workout would not have been inserted.
E.G.
if you change parentWorkoutId from val to var then the save function could be something like:-
fun save(): Boolean {
var rv = false /* indicate nothing done as insert was ignored */
val id = workoutDao.insertWorkout(workout)
if (id > 0) {
rv = true
for (wi in setInfoList) {
wi.parentWorkoutId = id
}
workoutDao.insertSetInfoList(setInfoList)
}
return rv
}

How to build and query a Room DB to return a list of objects of multiple classes?

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.

How to join multiple instances of same object in Android Room?

NOTE: I cannot use relation as we had performance issue which is not reproduced on direct join query.
Until I added target user and from group user and corresponding
LEFT JOIN chat_user ON chat_user.chat_user_id = message_item.messages_target_user
LEFT JOIN chat_user ON chat_user.chat_user_id = message_item.messages_from_group_user
It worked well. But after addition, I can not figure out how to make those prefixes map in the query.
class ReadMessageEntity(
#Embedded
var message: MessageEntity,
#Embedded
var block: BlockEntity?,
#Embedded
var user: ChatUserRelationEntity,
#Embedded(prefix = "target_user_")
var targetUser: ChatUserEntity?,
#Embedded(prefix = "from_group_user_")
var fromGroupUser: ChatUserEntity?
)
This is which I'm trying to query:
#Transaction
#Query("""
SELECT * FROM message_item
LEFT JOIN block_item ON block_item.block_message_id = message_item.message_local_id
LEFT JOIN chat_user_relation ON chat_user_relation.chat_user_id = message_item.message_user_id
LEFT JOIN chat_user ON chat_user.chat_user_id = message_item.messages_target_user
LEFT JOIN chat_user ON chat_user.chat_user_id = message_item.messages_from_group_user
WHERE message_item.message_chat_id = :chatId
ORDER BY message_created_at ASC
""")
fun getMessagesByChat(chatId: String): Single<List<ReadMessageEntity>>
The error:
e: error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (ambiguous column name: main.chat_user.chat_user_id)
Here is my join solution:
ChatsEntity
#Entity(tableName = "Chats",foreignKeys = [ForeignKey(entity = UserEntity::class,
parentColumns = ["id"], childColumns = ["userId"], onDelete = NO_ACTION),ForeignKey(entity = LastMessageEntity::class,
parentColumns = ["id"], childColumns = ["roomId"], onDelete = NO_ACTION)])
data class ChatsEntity(
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
#ColumnInfo(name = "roomId") var roomId: String,
#ColumnInfo(name = "userId") var userId: String,
#ColumnInfo(name = "count") var count: Int
)
LastMessageEntity
#Entity(tableName = "LastMessages")
data class LastMessageEntity(
#PrimaryKey #ColumnInfo(name = "id") var id: String = "",
#ColumnInfo(name = "message") var message: String = "",
#ColumnInfo(name = "type") var type: String = ""
)
UserEntity
#Entity(tableName = "Users")
data class UserEntity(
#PrimaryKey #ColumnInfo(name = "id") var id: String = "",
#ColumnInfo(name = "username") var username: String = "",
#ColumnInfo(name = "token") var token: String = ""
)
1. using relation
class ChatUserMessage {
#Embedded
var chat : ChatsEntity? = null
#Relation(parentColumn = "userId", entityColumn = "id")
var user : UserEntity? = null
#Relation(parentColumn = "roomId", entityColumn = "id")
var lastMessage : LastMessageEntity? = null
}
SQL Query
#Query("SELECT * FROM Chats")
fun getAllChats(): List<ChatUserMessage?>?
2. without using relation
class ChatUserMessage
{
#Embedded
var chat: ChatsEntity? = null
#Embedded(prefix = "user_")
var user: UserEntity? = null
#Embedded(prefix = "message_")
var lastMessage: LastMessageEntity? = null
}
Query
#Query("SELECT Chats.*, LastMessages.id as message_id,LastMessages.message as message_message, LastMessages.type as message_type, Users.id as user_id, Users.username as user_username, Users.token as user_token FROM Chats INNER JOIN LastMessages ON LastMessages.id = Chats.roomId INNER JOIN Users ON Users.id = Chats.userId")
fun getAllChats(): List<ChatUserMessage?>?
In the query, you have to set an alias for the table chat_user as you are joining it twice in the same name which will confuse database engine so it tells you that the field is ambiguous.
EDIT:
I found something that may be similar to your issue: https://github.com/JetBrains/Exposed/issues/177
Also they referred to this code as an example to fix this issue:
object Users : Table() {
val id = varchar("id", 10).primaryKey()
val name = varchar("name", length = 50)
val residentialCityId = optReference("resid_city_id", Cities)
val bornCityId = optReference("born_city_id", Cities)
}
object Cities : IntIdTable() {
val name = varchar("name", 50) // Column
}
fun test() {
val userTable1 = Users.alias("u1")
val userTable2 = Users.alias("u2")
Cities
.innerJoin(userTable1, {Cities.id}, {userTable1[Users.residentialCityId]})
.innerJoin(userTable2, {Cities.id}, {userTable2[Users.bornCityId]})
.selectAll()
}
as mentioned Moayad .AlMoghrabi I should use table aliases. So I renamed LEFT JOIN chat_user to LEFT JOIN chat_user target_user. Note there is no AS between chat-user and target_user. (my mistake was that I tried to put it there). Behavior of prefixes is still a mystery to me as the prefix for chat_user works properly, but if I put the prefix to ChatUserRelationEntity then I receive unknown column error.
SELECT * FROM message_item
LEFT JOIN block_item
ON block_item.block_message_id = message_item.message_local_id
LEFT JOIN chat_user_relation
ON chat_user_relation.chat_user_relation_chat_user_id = message_item.message_user_id
AND chat_user_relation.chat_user_relation_chat_user_chat_id = :chatId
LEFT JOIN chat_user target_user
ON target_user.chat_user_id = message_item.messages_target_user
LEFT JOIN chat_user from_group_user
ON from_group_user.chat_user_id = message_item.messages_from_group_user
WHERE message_item.message_chat_id = :chatId
ORDER BY message_created_at ASC

Android Room Select #Relation Annotation With Conditions one-to-many Issue

I have 2 Entities: User and Messages. Every User can have some Messages.
Class User
#Entity(tableName = DBConstants.TABLE_USER)
open class User(
#field:PrimaryKey(autoGenerate = true)
var id: Int = 0,
var appUserId: Int = 0,
var name: String = "",
var phoneNumber: String = "",
var appPackage: #AppPackage String = IN_APP,
var userType: #UserType Int = CUSTOMER
) : Serializable ,BaseObservable(){
#field:Ignore
var messages: ArrayList<Messages> = ArrayList()
#Bindable
fun getNumMessages() : Int {
return messages.size
}
}
And Class Messages
#Entity(tableName = TABLE_USER_MESSAGES,
foreignKeys = [ForeignKey(entity = User::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("userId"),
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE)])
data class Messages(#field:PrimaryKey(autoGenerate = true) var id: Int = 0,
#field:ColumnInfo(name = "userId")
var userId: Int = 0,
var appUserId: Int = 0,
var appPackage: #AppPackage String = IN_APP,
var ticker: String = "",
var title: String = "",
var text: String = "",
var date : String)
For Select One User - Many Messages. I Created Class UserMessagesView.kt
class UserMessagesView {
#Embedded
var user : User = User()
#Relation(parentColumn = "id", entityColumn = "userId",entity = Messages::class)
var msg: List<Messages> = ArrayList()
}
I use Select Inner Join with Conditions follow this
#Dao
abstract class UserMessagesDao : BaseDao<Messages>() {
#Query("SELECT * FROM user INNER JOIN messages ON messages.user_id = user.id WHERE messages.date like '%18-11-2018%'")
abstract fun getAllMessagesByToday(): List<UserMessagesView>
}
In databse, I have 1 user in table users with id = 1
and 4 record messages in table messages foreign key with id user = 1. Follow 2 image
TableUser
[Table Messages][2]
My Issue is When I call method
getAllMessagesByToday
I received List is size = 4, But First element 'UserMessagesView ' object 'msg' have size = 4 and Others Element object 'msg' size = 0.
And object User in UserMessagesView in List They are the same all field and difference each ID
Follow Image Result
In this case I think Room will return 1 record containing a user object and a msg of 4 elements. But it's not exactly what I thought. And I do not know where I went wrong.
Please help me. It took me 3 days to find out but it did not work. Maybe Room is new and no one has problems like me. Thanks everyone.
Sorry my bad English.

Categories

Resources