How to make PrimaryKey to generate ids for multiple tables in one order - android

I have 3 entities(Dishes, Places, Artifacts) which inherit from one abstract class called Memory. Every Memory can store up to 3 photos(string paths) so I set one to many relation Memory-Photo. Photo row contains memory Id as a foreign key so they can be linked together.
The problem is that Android Room creates separate tables for every type of memory so the #AutoGenerate = true annotation generates ids in 3 separate orders for every type of memory.
It causes conflicts because in database there might be 3 memories of different types with the same id so new set of photos would be linked to more than one memory.
I came up with an idea to make primary key out of creation timestamp but it is not the best idea. Maybe there is some way to synchronise key generation mechanism or change the plan of my database.
abstract class Memory(#ColumnInfo(name = "favorite") var favorite: Boolean,
#ColumnInfo(name = "title") var title: String,
#ColumnInfo(name = "date") var date: Date,
#ColumnInfo(name = "description") var description: String,
#Embedded var mapMarker: MapMarker) : Serializable {
#ColumnInfo(name="id")
#PrimaryKey(autoGenerate = true)
var id: Long = 0 }
#Entity(tableName = "artifact_memories")
class ArtifactMemory(favorite: Boolean,
title: String,
date: Date,
description: String,
mapMarker: MapMarker) : Memory(favorite, title,
date, description, mapMarker){}
#Entity(tableName = "dish_memories")
class DishMemory(#ColumnInfo(name = "country_of_origin") var originCountry: String,
#ColumnInfo(name = "type") var dishType: String,
favorite: Boolean,
title: String,
date: Date,
description: String,
mapMarker: MapMarker) : Memory(favorite, title,
date, description, mapMarker) {}
#Entity(tableName = "photos")
data class Photo(#ColumnInfo(name="photo_path") var photoPath: String,
#ColumnInfo(name="memory_id") var memoryId: Long = 0,
#ColumnInfo(name="is_main") var main: Boolean = false) : Serializable {
#PrimaryKey(autoGenerate = true) var id: Long = 0 }
How to deal with such relationships? The goal is to make photos be saved within only one memory and/or remove conflicts with id duplicates.

you can use inheritSuperIndices property to make sure that every child class inherits super class's primary key
#Entity(tableName = "artifact_memories",inheritSuperIndices = true)
class ArtifactMemory(favorite: Boolean,
title: String,
date: Date,
description: String,
mapMarker: MapMarker) : Memory(favorite, title,
date, description, mapMarker){}
#Entity(tableName = "dish_memories",inheritSuperIndices = true)
class DishMemory(#ColumnInfo(name = "country_of_origin") var originCountry:
String,
#ColumnInfo(name = "type") var dishType: String,
favorite: Boolean,
title: String,
date: Date,
description: String,
mapMarker: MapMarker) : Memory(favorite, title,
date, description, mapMarker) {}
#Entity(tableName = "photos",inheritSuperIndices = true)
data class Photo(#ColumnInfo(name="photo_path") var photoPath: String,
#ColumnInfo(name="memory_id") var memoryId: Long = 0,
#ColumnInfo(name="is_main") var main: Boolean = false) : Serializable {
#PrimaryKey(autoGenerate = true) var id: Long = 0 }

Related

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.

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.

Why do I have to make a setter for an auto-incremented column in a database?

In my android app I have created a database using the room persistance library. It contains of a table called stack with the columns stack_id, col_1, col_2 and stack_name. Here's the code for the entity class:
#Entity
class Stack(
#ColumnInfo(name = "stack_name") val stackName: String,
#ColumnInfo(name = "col_1") val column1: String,
#ColumnInfo(name = "col_2") val column2: String
) {
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "stack_id") val
stackId: Int = 0
}
Question 1: Is the stack_id correctly implemented? I found this solution to set en auto-incremented column to 0 and it will automaticly auto-increment the value, but it doesn't make sense to me. So, is this correct?
Question 2: Whe I want to build the app, it throws the error:
Cannot find setter for field. private final int stackId = 0;
But it would be nonsense to make a setter for an auto-incremented value. So, should I make a setter or is there another solution?
You are using concrete class so you need to provide get() and set() for properties.
Use data class which are build for this purpose only .
#Entity
data class Stack(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "stack_id") val id: Int? = null,
#ColumnInfo(name = "stack_name") val stackName: String,
#ColumnInfo(name = "col_1") val column1: String,
#ColumnInfo(name = "col_2") val column2: String)
You need to move
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "stack_id") val
stackId: Int = 0
to the constructor.
#Entity
class Stack(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "stack_id") val
stackId: Int = 0,
#ColumnInfo(name = "stack_name") val stackName: String,
#ColumnInfo(name = "col_1") val column1: String,
#ColumnInfo(name = "col_2") val column2: String
)
Why this works? The property is val, so is final/immutable, but the assignment in the constructor is a default value which can be overridden by passing in a value.
However in your example the property is in the class body and is assigned the value 0 on init, and cannot be changed as property is immutable.
The second option, as pointed out, is making the property mutable by using var instead of val.

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

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.

Categories

Resources