I'm trying to pass a map from Firestore into a data class but I'm getting null instead of the data
This is how my data is accessed:
firestoreDb = FirebaseFirestore.getInstance()
val postsRef = firestoreDb
.collection("posts")
.limit(20)
.orderBy("creation_time_ms", Query.Direction.DESCENDING)
.orderBy("date", Query.Direction.DESCENDING)
postsRef.addSnapshotListener { snapshot, exception ->
if (exception != null || snapshot == null) {
Log.e(TAG,"Exception when querying post", exception )
return#addSnapshotListener
}
val postList = snapshot.toObjects(HousePost::class.java)
for (post in postList) {
Log.i(TAG, "Posts $post")
}
}
My model for the data
data class HousePost (
#get:PropertyName("image_url") #set:PropertyName("image_url") var postImage: String = "",
var rent: String = "",
var description: String = "",
#get:PropertyName("creation_time_ms") #set:PropertyName("creation_time_ms") var creationTimeMs: Long = 0L,
var location: String = "",
#get:PropertyName("post_id") #set:PropertyName("post_id")var postId: String? = "",
var rooms: String = "",
var caption: String = "",
var date: String = "",
var owner: Owner? = null
)
My Owner model class
class Owner(
var uid: String = "",
var username: String = "",
var email: String = "",
#get:PropertyName("profile_image") #set:PropertyName("profile_image") var profileImage: String = "",
#get:PropertyName("user_type") #set:PropertyName("user_type")var usertype: String = "owner"
)
Here is my firestore data I know some fields are an empty string but that shouldn't output null.
This is the result from logcat
2022-05-03 11:42:12.313 6581-6581/com.example.moveapplication I/MainActivity: Posts HousePost(postImage=https://firebasestorage.googleapis.com/v0/b/moveapplicationdb.appspot.com/o/Post%20Pictures%2Fian-dooley-_-JR5TxKNSo-unsplash.jpg?alt=media&token=6721ec57-7602-41ee-b7cd-b8b1838b27fc, rent=15000, description=Along Ngong road we have a studio room for you to rent. Located in a moder =n area with nice infrastrucure, creationTimeMs=1651561930185, location=Lenana, postId=, rooms=studio room, caption=studio room along Ngong road, date=03/05/2022, owner=null)
My index from firestore
Looking at your class definitions, all look fine. You're using the correct annotations. However, when you're using the following query:
val postsRef = firestoreDb
.collection("posts")
.limit(20)
You'll need to know that you have to create an index. It can be manually created inside the Firebase Console or if you are using Android Studio, you'll find a message in the logcat that sounds like this:
FAILED_PRECONDITION: The query requires an index. You can create it here: ...
Simply click on that link or copy and paste the URL into a web browser and your index will be created automatically for you.
Related
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.
This is my model class
#Parcel
data class ClientModel(
var name: String? = "",
var phone: String? = "",
var princpalAddresse: String? = "",
var homeAddresse: String? = "",
var travailleAddresse: String? = "",
var email: String? = "",
var userToken: String? = "",
var principalAddresseCoords: Pair<Double, Double>? = null,
var homeAddresseCoords: Pair<Double, Double>?= null,
var workAddresseCoords: Pair<Double, Double>? = null,
)
My proGuard file keep the class :
-keep class com.olivier.oplivre.models.ClientModel
But! when I try to get the snapshot with a singleValueEventListener I got exception because of the Pair<Double,Double> variables
val utilisationInfo = snapshot.getValue(ClientModel::class.java) //todo CRASH
Exception :
com.google.firebase.database.DatabaseException: Class kotlin.Pair does not define a no-argument constructor. If you are using ProGuard, make sure these constructors are not stripped.
Database Structure :
I think firebase Realtime database treat your principalAddresseCoords as a list of long so in your ClientModel change the value of principalAddresseCoords to emptyList() and the type List
As #Sami Shorman said , firebase took my Pair instance and transform it but not as list, as Hashmap<,> ! so I changed my class model like that :
var principalAddresseCoords: HashMap<String,Double>? = null,
var homeAddresseCoords: HashMap<String,Double >? = null,
var workAddresseCoords: HashMap<String,Double >? = null,
To put the data as Hashmap I just had to do :
clientModel.workAddresseCoords = HashMap<String,Double>().apply {
put("lat",lat)
put("long",long)
}
Can't see how to do this and getting rather confused!
I am saving 'site' objects to firestore, but I want to add a list of users associated to each site.
I have added a Map of users to my JSON object as below:
#IgnoreExtraProperties
data class SiteObject(
var siteReference: String,
var siteAddress: String,
var sitePhoneNumber: String,
var siteEmail: String,
var invoiceAddress: String,
var invoicePhoneNumber: String,
var invoiceEmail: String,
var website: String,
var companyNumber: String,
var vatNumber: String,
var recentProjectsText: String,
//not set up yet:
var sitePriority: Boolean,
var siteRating: Int,
var plusCode: String,
var users: Map<String, Boolean>?, // This is the map I have added
#ServerTimestamp
var dateCreatedTimestamp: Date?,
#ServerTimestamp
var dateEditedTimestamp: Date?,
#Exclude
var siteID: String?
) : Serializable {
private constructor() : this(
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
false,
1,
"",
null,
null,
null,
null
)
override fun toString(): String {
return "$siteReference"
}
}
And in my respository I am trying to add the current user to this list of users as below:
// save sites to firebase
fun saveSite(site: SiteObject) {
site.users?.plus(Pair(firebaseUser?.uid.toString(), true)) // This is where I expected the user Id to be added to Map of users..
val documentReference = firestore.collection("sites").document().set(site)
.addOnCompleteListener {
if(it.isSuccessful){
Log.d(TAG, "${site.toString()} saved")
lastOperationText.value = "New site, ${site.siteReference}, saved!"
} else {
Log.d(TAG, "${site.toString()} saved")
lastOperationText.value = "Save new site failed"
}
}
}
However, I still seeing null for users in the Firestore console.
Your code never gives an initial value to users. It starts off null. Since it doesn't get assigned a value, this code will not make a change to it, because it's first checking to see if users is null using the ?. operator:
site.users?.plus(Pair(firebaseUser?.uid.toString(), true))
You will need to assign it an initial value before trying to modify it. It should probably never be null and just start empty.
var users = HashMap<String, Boolean>()
For completeness, below is my updated data class. This initialises the values when it is created and also includes #Exclude #set:Exclude #get:Exclude on siteID to prevent this being saved to Firestore (used to store generated id when read from Firestore):
#IgnoreExtraProperties
data class SiteObject(
var siteReference: String = "",
var siteAddress: String = "",
var sitePhoneNumber: String = "",
var siteEmail: String = "",
var invoiceAddress: String = "",
var invoicePhoneNumber: String = "",
var invoiceEmail: String = "",
var website: String = "",
var companyNumber: String = "",
var vatNumber: String = "",
var recentProjectsText: String = "",
var sitePriority: Boolean = false,
var siteRating: Int = 1,
var plusCode: String = "",
var users: HashMap<String, Boolean> = hashMapOf(),
#ServerTimestamp
var dateCreatedTimestamp: Date? = null,
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var siteID: String = ""
) : Serializable {
override fun toString(): String {
return "$siteReference, $siteAddress, $siteRating, $siteID"
}
fun siteDetailsText(): String {
var siteDetailsText = siteAddress
if (sitePhoneNumber.isNotEmpty()) siteDetailsText.plus("\n\n$sitePhoneNumber")
if (website.isNotEmpty()) siteDetailsText.plus("\n\n$website")
return siteDetailsText
}
fun invoiceDetailsText(): String {
var invoiceDetailsText = invoiceAddress
if (invoicePhoneNumber.isNotEmpty()) invoiceDetailsText.plus("\n\n$invoicePhoneNumber")
if (companyNumber.isNotEmpty()) invoiceDetailsText.plus("\n\n$companyNumber")
if (vatNumber.isNotEmpty()) invoiceDetailsText.plus("\n\n$vatNumber")
return invoiceDetailsText
}
}
So in my android app I'm fetching all the documents from a collection by the following code :
log("Player Listener Started")
FirebaseFirestore.getInstance().collection("players")
.limit(4)
.addSnapshotListener { querySnapshot, firebaseFirestoreException ->
log("Player Listener firestore exception message ${firebaseFirestoreException?.localizedMessage}")
if (firebaseFirestoreException == null) {
querySnapshot?.documents!!.forEach {
val serverPlayer = it.toObject(Player::class.java)!!
snapshotPlayers.add(serverPlayer)
}
log("Player numbers : ${snapshotPlayers.size}")
I've 4 documents in "players" collection in the database :
But it always return just 1 document i.e. "Sheetal". Please check the log from the above code :
This exact code was working fine an hour ago and I was getting all 4 users.
I cleared database from the console and re-created these document using my app this code is not returning 4 documents.
Player class :
class Player(
var name: String = "",
var gameId: String = "",
var currentCard: Card = Card(),
var cardList: ArrayList<Card> = ArrayList(),
var made: String = "0",
var expected: String = "0",
var message: String = "",
var order: Int = 0,
var turn: Int? = null
)
Player document contents in the database
I tried these things but still getting same result :
Clear my app cache manually
Cleared Firestore persistence data using FireDB.db.clearPersistence()
Its my first project in firestore so I don't know what am I doing wrong. Please help me.
UPDATED LOGCAT:
I have a data class where I retrieve Firebase information. It looks similar to this:
#IgnoreExtraProperties
data class EventData(var eventEmoji: String = "",
var PlanType: PlanType = PlanType.CUSTOM_TYPE_1,
var accel: Boolean = false,
var latitude: String = "0.0",
var longitude: String = "0.0",
var initialDate: String = "",
var finalDate: String = "",
var initialHour: String = "",
var finalHour: String = "",
var plae: String = "",
var addr: String = "",
var description: String = "",
var adminProfileImageUrl: String = "") : Serializable
The problem that I'm facing now, is that exist a new data information that before was unused.
But this new field can exist or not depending on Business people. That field contains some translations, but... not always are the same or same number of it.
Example how is in Firebase:
The problem is that I don't know how to retrieve all this data in the EventData class.
I tried to add the new attribute as:
var localizable: Map<Any,Any>? = hashMapOf()
var localizable: Map<String,Any>? = hashMapOf()
var localizable: Map<String,String>? = hashMapOf()
var localizable: Map<String,SpecificClass>? = hashMapOf()
But... doesn't work.
Is possible to achieve?