Check if an entry is already in a livedata list wihout looping through the list - android

I am trying to build an app to help me track some of the tasks we have to do in the game.
I have a Firebase Firestore database that store all the tasks and I download at the application launch the data and add only the one I don't have.
Here is my entry model:
#Entity(tableName = "entry_table")
data class Entry(
#PrimaryKey(autoGenerate = true) var uid: Long?,
#ColumnInfo(name = "title") val title: String,
#ColumnInfo(name = "description") val description: String,
#ColumnInfo(name = "target") val target: Int = 0,
#ColumnInfo(name = "position") val position: Int = 0,
#ColumnInfo(name = "starred") val starred: Boolean = false
) {
constructor(): this(null, "", "", 0, 0, starred = false)
}
Since I download the document from the firestore database I cannot set an ID before inserting the entries in my SQLite database.
This means that I cannot use the "contains" method on my livedata list (since the entries I recieve has a "null" id and the one from the database has an id). I need to loop though all the data, here is the code:
#WorkerThread
suspend fun insertEntry(entry: Entry) {
for (doc in entriesList.value!!){
if (doc.description == entry.description && doc.title == entry.title) {
Log.d("MAIN_AC", "Entry already saved $entry")
return
}
}
entryDAO.insertEntry(entry)
}
My code works but I am not satisfied with it, is there a better way to make this happen? I was hoping that the contains method could ignore some arguments (in my case the autogenerated ID)

One way you can go about, assuming you are using Room, it is to annotate your insert function (in the relevant DAO) with OnConflictStrategy.IGNORE.
e.g.
#Dao
interface EntryDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(list: List<Entry>)
// or (if you want the inserted IDs)
// fun insert(list: List<Entry>) : LongArray
}
Be sure to also annotate your entity with the relevant unique index.
e.g.
#Entity(tableName = "entry_table",
indices = [Index(value = ["title", "description"], unique = true)]
)
data class Entry(
#PrimaryKey(autoGenerate = true) var uid: Long,
#ColumnInfo(name = "title") val title: String,
#ColumnInfo(name = "description") val description: String
//...
)
Primary keys should not be null-able, you can .map to Entry wit uid = 0. If you are using the same entity model both locally and remotely that is probably not the best idea.

Related

How to define values in code block uses data class with room?

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

Null Data Returned with Nested Relation in Room Database Android

Given that I have 3 entities, Order contains list of LineItem, each LineItem will associates with one Product by productId.
The problem that when I get data from OrderDao, it returns null for the product field, but in the lineItem field, it has data. While I can data with ProductWithLineItem.
Already tried a lot of work arounds but it does not work.
Here is my code for entities and dao
Entities
#Entity(tableName = DataConstant.ORDER_TABLE)
data class Order(
#PrimaryKey
#ColumnInfo(name = "orderId")
val id: String,
#ColumnInfo(name = "status")
var status: String
)
#Entity(tableName = DataConstant.LINE_ITEM_TABLE)
data class LineItem(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "lineItemId")
val id: Long,
#ColumnInfo(name = "productId")
val productId: String,
#ColumnInfo(name = "orderId")
val orderId: String,
#ColumnInfo(name = "quantity")
var quantity: Int,
#ColumnInfo(name = "subtotal")
var subtotal: Double
)
#Entity(tableName = DataConstant.PRODUCT_TABLE)
data class Product(
#PrimaryKey
#NonNull
#ColumnInfo(name = "productId")
val id: String,
#ColumnInfo(name = "name")
var name: String?,
#ColumnInfo(name = "description")
var description: String?,
#ColumnInfo(name = "price")
var price: Double?,
#ColumnInfo(name = "image")
var image: String?,
)
Relations POJOs
data class ProductAndLineItem(
#Embedded val lineItem: LineItem?,
#Relation(
parentColumn = "productId",
entityColumn = "productId"
)
val product: Product?
)
data class OrderWithLineItems(
#Embedded var order: Order,
#Relation(
parentColumn = "orderId",
entityColumn = "orderId",
entity = LineItem::class
)
val lineItemList: List<ProductAndLineItem>
)
Dao
#Dao
interface OrderDao {
#Transaction
#Query("SELECT * FROM `${DataConstant.ORDER_TABLE}` WHERE orderId = :id")
fun getById(id: String): Flow<OrderWithLineItems>
}
Result after running with Dao
Result after running query
Here is my code for entities and dao
You code appears to be fine, with the exception of returning a Flow, testing, using your code, but on the main thread using List (and no WHERE clause) i.e the Dao being :-
#Query("SELECT * FROM ${DataConstant.ORDER_TABLE}")
#Transaction
abstract fun getOrderWithLineItemsAndWithProduct(): List<OrderWithLineItems>
Results in :-
The data being loaded/tested using :-
db = TheDatabase.getInstance(this)
orderDao = db.getOrderDao()
orderDao.clearAll()
orderDao.insert(Product("product1","P1","desc1",10.01,"image1"))
orderDao.insert(Product("product2","P2","desc2",10.02,"image2"))
orderDao.insert(Product("product3","P3","desc3",10.03,"image3"))
orderDao.insert(Product("product4","P4","desc4",10.04,"image4"))
orderDao.insert(Product("","","",0.0,""))
val o1 = orderDao.insert(Order("Order1","initiaited"))
val o2 = orderDao.insert(Order("Order2","finalised")) // Empty aka no List Items
val o1l1 = orderDao.insert(LineItem(10,"product3","Order1",1,10.01))
val o1l2 = orderDao.insert(LineItem(20,"product4","Order1",2,20.08))
val o1l3 = orderDao.insert(LineItem(30,"","Order1",3,30.09))
val o1l4 = orderDao.insert(LineItem(40,"","x",1,10.01))
//val o1l3 = orderDao.insert(LineItem(30,"no such product id","Order1",10,0.0))
// exception whilst trying to extract if not commented out at test = ....
val TAG = "ORDERINFO"
val test = orderDao.getOrderWithLineItemsAndWithProduct()
for(owl: OrderWithLineItems in orderDao.getOrderWithLineItemsAndWithProduct()) {
Log.d(TAG,"Order is ${owl.order.id} status is ${owl.order.status}")
for(pal: ProductAndLineItem in owl.lineItemList) {
Log.d(TAG,"\tLine Item is ${pal.lineItem.id} " +
"for Order ${pal.lineItem.orderId} " +
"for ProductID ${pal.lineItem.productId} " +
"Quantity=${pal.lineItem.quantity} " +
"Product description is ${pal.product.description} Product Image is ${pal.product.image} Price is ${pal.product.price}")
}
}
As such I believe the issue might be that for some reason the Flow is detecting when the first query has completed but prior to the underlying queries.
That is when using #Relation the core objects (Order's) are extracted via the query and the core objects created then the related objects are extracted by a another query and used to build ALL the related objects as a List (unless just the one when it doesn't have to be a list). So prior to this underlying query the core object will have a null or an empty list for the underlying objects. Of course with a hierarchy of #Relations then this is replicated along/down the hierarchy.
I would suggest temporarily adding .allowMainThreadQueires to the databaseBuilder and using a List<OrderWithLineItems> or just a sole OrderWithLineItems. If using this then you get the Product(s) then the issue is with the Flow (which is what I suspect).

How to insert a set of nullable entites in Room?

I have an Room Entity called City:
#Entity(tableName = "cities")
class City(
#PrimaryKey
#ColumnInfo(name = "unique_city_id")
val id: Long,
#ColumnInfo(name = "city_name")
val name: String,
#ColumnInfo(name = "city_code")
val code: String,
)
And I have a list of objects of this class type:
data class CoffeeHouse(
override val id: Long,
override val latitude: Double,
override val longitude: Double,
override val city: City?,
override val address: String,
)
I need to save both CoffeeHouse and City classes. Because there are a lot of identical cities, I map a list of coffeehouses to a set of cities to get only unique ones:
val cities = coffeeHouses.map { it.city?.toPersistenceType() }.toSet()
(.toPersistenceType() just maps domain type to persistence)
And then I'm inserting coffeeHouses and cities into Room Database using these DAOs:
#Dao
abstract class CoffeeHouseDao(val cacheDatabase: CacheDatabase) {
private val cityDao = cacheDatabase.cityDao()
#Insert(onConflict = REPLACE)
abstract suspend fun insertAllCoffeeHouses(coffeeHouses: List<CoffeeHouse>)
#Transaction
open suspend fun insertAllCoffeeHousesInfo(
coffeeHouses: List<CoffeeHouse>,
cities: Set<City?>,
) {
insertAllCoffeeHouses(coffeeHouses)
cityDao.setCities(cities)
}
}
#Dao
interface CityDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun setCities(cities: Set<City?>)
The problem is when I'm trying to insert Set<City?> app crashes with an exception:
Uncaught exception java.lang.NullPointerException:
Attempt to invoke virtual method 'long com.coffeeapp.android.persistence.entity.City.getId()'
on a null object reference
Stacktrace points on the line of cities insertion, so I don't understand how to make it right.
This is happening because you have set the ID field in city as the Primary Key for that table and it cannot be null.
You can try changing your annotation to
#PrimaryKey(autoGenerate = true)
Or if you do not want auto increment you have to make sure that the id is not null whenever you are inserting a City.
I think it is because of the city: City? and cities: Set<City?> in the CofeeHouse entity. Try to make them not nullable.
To allow for inserting a with null you can use :-
#Entity(tableName = "cities")
class City(
#PrimaryKey
#ColumnInfo(name = "unique_city_id")
val id: Long?, //<<<<<<<< ? ADDED
#ColumnInfo(name = "city_name")
val name: String,
#ColumnInfo(name = "city_code")
val code: String,
)
As such id will be generated when it is inserted. e.g. the following (based upon for reduced for convenience).
However, the REPLACE conflict strategy will never result in replacement as null will generate a unique id.
What I believe you want is that either city name, the city code or both (together or independently) constitutes a unique entry.
As such :-
#Entity(
tableName = "cities",
indices = [
/*
probably just one of these all three is overkill
*/
Index(value = ["city_name"],unique = true),
Index(value = ["city_code"], unique = true),
Index(value = ["city_name","city_code"],unique = true)
]
)
class City(
#PrimaryKey
#ColumnInfo(name = "unique_city_id")
val id: Long?,
#ColumnInfo(name = "city_name")
val name: String,
#ColumnInfo(name = "city_code")
val code: String,
)
As an example consider the following :-
cityDao.setCities(setOf<City>(City(null,"Sydney","SYD1"),City(null,"New York","NY1")))
cityDao.setCities(setOf<City>(City(null,"Sydney","SYD1"),City(null,"New York","NY1")))
So an attempt is made to add the same set of cities The result is:-
i.e. The first added Sydney and New York with id's 1 and 2, the second attempt replaced due to the conflict which deletes the originals so you end up with id's 3 and 4. Without the unique index(s) then the result would have been 4 rows with id's 1,2,3 and 4.

Room update query

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

How to autogenerate a Room database id without providing an id

I am trying to create a room database and I want each item inserted into it to have its own unique id without me having to provide it, The problem is when I try to insert new items into the database I get an error asking me to provide an id.
Here is my entity:
#Entity(tableName = "notes_table")
data class Note(
#PrimaryKey(autoGenerate = true)
val id: Int = 0,
#ColumnInfo(name = "description")
val description: String,
#ColumnInfo(name = "priority")
var priority: Int)
Is there a way to have the database create its own auto-generated auto-increasing id column without having me having to add it like this:
val item = Note(id, item, priority)
insert(item)
And instead do this:
val item = Note(item, priority)
insert(item)
Create a constructor that takes item and priority as arguments
#Entity(tableName = "notes_table")
data class Note (var item: String,
#ColumnInfo(name = "priority")
var priority: String) {
#PrimaryKey(autoGenerate = true)
var id: Long = 0,
//.....
}
You can just simply give the id a default value and put that at the end:
#Entity(tableName = "notes_table")
data class Note(
#ColumnInfo(name = "description")
val description: String,
#ColumnInfo(name = "priority")
var priority: Int)
#PrimaryKey(autoGenerate = true) //must be at the end
val id: Int = 0 //Long type recommend
)
Then you can:
val item = Note(item, priority)
insert(item)
Because your data class Note has three parameter.
So you you have to create Note by passing three parameter.
It is nothing to do with autogenerate or room.

Categories

Resources