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.
Related
I'm trying to implement caching of a JSON API response with Room.
The response I get in JSON follows this data class structure:
#Serializable
data class ApiDataResponse(
val success: Boolean,
val message: String? = null,
val albums: List<AlbumResponse> = emptyList()
)
#Serializable
data class AlbumResponse(
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicResponse>
)
#Serializable
data class PicResponse(
val picUrl: String,
val emojis: List<String>
)
Notes:
#Serializable is from kotlinx.serialization library to parse the JSON response.
These response data classes are only used inside my datasource layer, the view layer doesn't care about an ApiDataResponse and only knows a "pure" version of AlbumResponse called Album and a "pure" version of PicResponse called Pic (by "pure" I mean a data class without external library annotations).
So to implement this cache with Room I could discard the ApiDataResponse and save only the contents of AlbumResponse (and consequently PicResponse), having new data classes for Room entities following this idea:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>, // obstacle here
val pics: List<PicEntity> // obstacle here
)
// obstacle here
// #Entity
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
I already know how to save simple data in Room, with the simplest JSON I was able to do this task, the problem is that in this more complex scenario I have no idea how to achieve this goal. So I wish someone could guide me in this situation.
Maybe it's a little late, but I would still like to add some interesting information regarding MikeT's answer.
It is not necessary to create a new data class just to transform a custom object into a JSON with TypeConverter, for example:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicEntity> // can be converted directly
)
import kotlinx.serialization.Serializable
#Serializable // to be able to do the serialize with the kotlinx.serialization
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
With just these two data classes we can build the TypeConverters as follows:
import androidx.room.TypeConverter
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseConverter {
private val json = Json
#TypeConverter
fun convertStringListToString(strings: List<String>): String =
json.encodeToString(strings)
#TypeConverter
fun convertStringToStringList(string: String): List<String> =
json.decodeFromString(string)
#TypeConverter
fun convertPicEntityListToString(picsEntity: List<PicEntity>): String =
json.encodeToString(picsEntity)
#TypeConverter
fun convertStringToPicEntityList(string: String): List<PicEntity> =
json.decodeFromString(string)
}
Code to create an example dummy list:
object DummyAlbums {
fun createList(): List<AlbumEntity> = listOf(
AlbumEntity(
id = "0001",
title = "Album AB",
createdBy = "Created by AB",
enabled = true,
keywords = listOf("ab"),
pics = dummyPics(albumId = "0001", size = 0)
),
AlbumEntity(
id = "0002",
title = "Album CD",
createdBy = "Created by CD",
enabled = false,
keywords = listOf("cd", "c", "d"),
pics = dummyPics(albumId = "0002", size = 1)
),
AlbumEntity(
id = "0003",
title = "Album EF",
createdBy = "Created by EF",
enabled = true,
keywords = listOf(),
pics = dummyPics(albumId = "0003", size = 2)
)
)
private fun dummyPics(
albumId: String,
size: Int
) = List(size = size) { index ->
PicEntity(
picUrl = "url.com/$albumId/${index + 1}",
emojis = listOf(":)", "^^")
)
}
}
So we can have the following data in table:
I wanted to highlight this detail because maybe it can be important for someone to have a table with the cleanest data. And in even more specific cases, to have it clean, you can do the conversion manually using Kotlin functions, such as joinToString(), split(), etc.
I believe the issue is with columns as lists.
What you could do is add the following classes so the Lists are embedded within a class:-
data class StringList(
val stringList: List<String>
)
data class PicEntityList(
val picEntityList: List<PicEntity>
)
and then change AlbumEntity to use the above instead of the Lists, as per:-
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
//val keywords: List<String>, // obstacle here
val keywords: StringList, /// now not an obstacle
//val pics: List<PicEntity> // obstacle here
val emojis: PicEntityList// now not an obstacle
)
To be able to store the "complex" (single object) you need to convert this so some TypeConverters e.g.
class RoomTypeConverters{
#TypeConverter
fun convertStringListToJSON(stringList: StringList): String = Gson().toJson(stringList)
#TypeConverter
fun convertJSONToStringList(json: String): StringList = Gson().fromJson(json,StringList::class.java)
#TypeConverter
fun convertPicEntityListToJSON(picEntityList: PicEntityList): String = Gson().toJson(picEntityList)
#TypeConverter
fun convertJSONToPicEntityList(json: String): PicEntityList = Gson().fromJson(json,PicEntityList::class.java)
}
note this utilises the dependency com.google.code.gson
You then need to have the #TypeConverters annotation to cover the appropriate scope (at the #Database level is the most scope). Note the plural rather than singular, they are different.
To demonstrate the above works, First some functions in an interface annotated with #Dao :-
#Dao
interface AlbumDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(albumEntity: AlbumEntity): Long
#Query("SELECT * FROM albums")
fun getAllAlbums(): List<AlbumEntity>
}
Second an #Database annotated class (note the #TypeConverters annotation) :-
#TypeConverters(RoomTypeConverters::class)
#Database(entities = [AlbumEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAlbumDao(): AlbumDao
companion object {
#Volatile
private var instance: TheDatabase?=null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"album.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Third some activity code to actually do something (insert some Albums and then extract them writing the extracted data to the Log) :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AlbumDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAlbumDao()
dao.insert(AlbumEntity(
"Album001", "The First Album","Fred",false,
StringList(listOf("The","First","Album")),
PicEntityList(
listOf(
PicEntity("PE001", listOf("emoji1","emoji2","emoji3")),
PicEntity("PE002",listOf("emoji10")),
PicEntity("PE003", listOf("emoji20","emoji21"))
))
))
dao.insert(AlbumEntity(
"Album002","This is the Second Album","Mary", true,
StringList(listOf("keya","keyb","keyc","keyd","keye")),
PicEntityList(
listOf(
PicEntity("PE011", listOf("emoji30","emoji31")),
PicEntity("PE012", listOf("emoji1","emoji10","emoji20","emoji30"))
))
))
for (a in dao.getAllAlbums()) {
logAlbum(a)
}
}
fun logAlbum(albumEntity: AlbumEntity) {
val keywords = StringBuilder()
for(s in albumEntity.keywords.stringList) {
keywords.append("\n\t$s")
}
val pelog = StringBuilder()
for (pe in albumEntity.emojis.picEntityList) {
pelog.append("\n\tURL is ${pe.picUrl}")
for (emoji in pe.emojis) {
pelog.append("\n\t\tEmoji is ${emoji}")
}
}
Log.d(
"ALBUMINFO",
"Album id is ${albumEntity.id} " +
"Title is ${albumEntity.title} " +
"CreateBy ${albumEntity.createdBy} " +
"Enabled=${albumEntity.enabled}. " +
"It has ${albumEntity.keywords.stringList.size} keywords. " +
"They are $keywords\n. " +
"It has ${albumEntity.emojis.picEntityList.size} emojis. " +
"They are ${pelog}"
)
}
}
Run on the main thread for convenience and brevity
When run then the log contains:-
D/ALBUMINFO: Album id is Album001 Title is The First Album CreateBy Fred Enabled=false. It has 3 keywords. They are
The
First
Album
. It has 3 emojis. They are
URL is PE001
Emoji is emoji1
Emoji is emoji2
Emoji is emoji3
URL is PE002
Emoji is emoji10
URL is PE003
Emoji is emoji20
Emoji is emoji21
D/ALBUMINFO: Album id is Album002 Title is This is the Second Album CreateBy Mary Enabled=true. It has 5 keywords. They are
keya
keyb
keyc
keyd
keye
. It has 2 emojis. They are
URL is PE011
Emoji is emoji30
Emoji is emoji31
URL is PE012
Emoji is emoji1
Emoji is emoji10
Emoji is emoji20
Emoji is emoji30
i.e. the 2 albums have been extracted along with the appropriate embedded lists.
The Albums table itself (via App Inspection) consists of :-
An Alternative, and from a Database perspective, better approach, instead of embedding lists as a single value (String), would have the lists as related tables (with a one-many or a many-many relationship).
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).
I have implemented a DB where I have two one-to-many relationships but it would seem that room does not allow it. Is that so?
The entities are:
#Entity(tableName = "arete_sheet")
data class EAreteSheet(
#PrimaryKey val id: Long,
#ColumnInfo(name = "sheet") val form: Sheets,
#ColumnInfo(name = "version") val version: Int,
)
#Entity(tableName = "arete_sheet_paragraph")
data class EAreteSheetParagraph(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_id") val sheet: Long,
#ColumnInfo(name = "name") val name: String
)
#Entity(tableName = "arete_sheet_form")
data class EAreteSheetForm(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_paragraph_id") val paragraph: Long,
#ColumnInfo(name = "fieldType") val fieldType: FieldType,
#ColumnInfo(name = "cell") val cell: String,
#ColumnInfo(name = "label") val label: String
)
To solve the schema I have implemented these join classes:
data class EAreteSheetWithParagraph(
#Embedded val sheet: EAreteSheet,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_id"
)
val paragraph: List<EAreteSheetParagraphWithForm>
)
data class EAreteSheetParagraphWithForm(
#Embedded val paragraph: EAreteSheetParagraph,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_paragraph_id"
)
val forms: List<EAreteSheetForm>
)
This is the DAO implementation:
#Transaction
#Query("SELECT * FROM arete_sheet")
suspend fun getSheetWithParagraphsAndForms(): List<EAreteSheetWithParagraph>
This is the mistake he gives me in the building phase:
app/build/generated/source/kapt/debug/it/ximplia/agri2000/model/db/dao/AreteSheetDAO_Impl.java:203: error: constructor EAreteSheetWithParagraph in class EAreteSheetWithParagraph cannot be applied to given types;
_item = new EAreteSheetWithParagraph();
^
required: EAreteSheet,List<EAreteSheetParagraphWithForm>
found: no arguments
reason: actual and formal argument lists differ in length
app/build/generated/source/kapt/debug/it/ximplia/agri2000/model/db/dao/AreteSheetDAO_Impl.java:204: error: sheet has private access in EAreteSheetWithParagraph
_item.sheet = _tmpSheet;
I think that Room does not allow to resolve dependencies in cascade but I would like to know if someone was successful or if I made a mistake before changing the code.
In EAreteSheetWithParagraph you are specifying a list of EAreteSheetParagraphWithForm's
as per :-
val paragraph: List<EAreteSheetParagraphWithForm>
The #Relation will try to ascertain the columns as per the EAreteSheetParagraphWithForm form class, which is not an Entity. You should change the #Relation to specify the appropriate entity (i.e. the EAreteSheetParagraph) using the entity parameter.
So EAreteSheetWithParagraph should be something like :-
data class EAreteSheetWithParagraph(
#Embedded val sheet: EAreteSheet,
#Relation(
entity = EAreteSheetParagraph::class,
parentColumn = "id",
entityColumn = "arete_sheet_id"
)
val paragraph: List<EAreteSheetParagraphWithForm>
)
However, I don't believe that you are getting that far, as the messages appear to be complaining about the sheet variable which has a type of Sheets (and that the variable is private).
Without adding the entity= parameter then the compile, if it reached that stage, would fail with :-
> Task :app:kaptDebugKotlin FAILED
error: The class must be either #Entity or #DatabaseView. - a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm
E:\AndroidStudioApps\SO68953488KotlinRoomMany1N\app\build\tmp\kapt3\stubs\debug\a\a\so68953488kotlinroommany1_n\EAreteSheetWithParagraph.java:12: error: Cannot find the child entity column `arete_sheet_id` in a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm. Options:
private final java.util.List<a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm> paragraph = null;
^
The class must be either #Entity or #DatabaseView. - a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm
I think that Room does not allow to resolve dependencies in cascade but I would like to know if someone was successful or if I made a mistake before changing the code.
As indicated above, I believe the issue is mistakes so :-
Proof of concept
Based upon your code the following shows that nested/multiple 1-n's do work:-
Using your code BUT with the following changes (to avoid issues and simplify) :-
#Entity(tableName = "arete_sheet_form")
data class EAreteSheetForm(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_paragraph_id") val paragraph: Long,
#ColumnInfo(name = "fieldType") val fieldType: /* FieldType */ String, // changed for convenience/brevity
#ColumnInfo(name = "cell") val cell: String,
#ColumnInfo(name = "label") val label: String
)
fieldType' stype changed from FieldType to String so no need for extra class and type converters.
and
#Entity(tableName = "arete_sheet")
data class EAreteSheet(
#PrimaryKey val id: Long,
#ColumnInfo(name = "sheet") val form: /*Sheets*/ String, // changed for convenience/brevity
#ColumnInfo(name = "version") val version: Int,
)
Sheets type substituted with String
EAreteSheetWithParagraph as above
EAreteSheetParagraphWithForm changed to
data class EAreteSheetParagraphWithForm(
#Embedded val paragraph: EAreteSheetParagraph,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_paragraph_id",
entity = EAreteSheetForm::class
)
val forms: List<EAreteSheetForm>
)
i.e. the entity parameter has been added according to my preference to always code the entity parameter.
AreteSheetDao used :-
#Dao
abstract class AreteSheetDAO {
#Insert
abstract fun insert(eAreteSheet: EAreteSheet): Long
#Insert
abstract fun insert(eAreteSheetParagraph: EAreteSheetParagraph): Long
#Insert
abstract fun insert(eAreteSheetForm: EAreteSheetForm): Long
#Transaction
#Query("SELECT * FROM arete_sheet")
abstract fun getSheetWithParagraphsAndForms(): List<EAreteSheetWithParagraph>
}
Code in an Activity :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AreteSheetDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getDao()
var s1 = dao.insert(EAreteSheet(10,"Sheet1",1))
var s2 = dao.insert(EAreteSheet(20,"Sheet2",1))
var p1 = dao.insert(EAreteSheetParagraph(100,s1,"Para1 (Sheet1)"))
var p2 = dao.insert(EAreteSheetParagraph(101,s1,"Para2 (Sheet1)"))
var p3 = dao.insert(EAreteSheetParagraph(201,s2,"Para3 (Sheet2)"))
var p4 = dao.insert(EAreteSheetParagraph(202,s2,"Para4 (Sheet2)"))
var f1 = dao.insert(EAreteSheetForm(1000,p1,"typex","cellx","Form1"))
var f2 = dao.insert(EAreteSheetForm(1001,p1,"typex","cellx","Form2"))
var f3 = dao.insert(EAreteSheetForm(1002,p1,"typex","cellx","Form3"))
var f4 = dao.insert(EAreteSheetForm(1010,p2,"typex","cellx","Form4"))
var f5 = dao.insert(EAreteSheetForm(1011,p2,"typex","cellx","Form5"))
var f6 = dao.insert(EAreteSheetForm(1020,p3,"typex","cellx","Form6"))
val TAG = "ARETEINFO"
for(sw: EAreteSheetWithParagraph in dao.getSheetWithParagraphsAndForms()) {
Log.d(TAG,"Sheet ID is ${sw.sheet.id} Form is ${sw.sheet.form} Version is ${sw.sheet.version}" )
for(pf: EAreteSheetParagraphWithForm in sw.paragraph) {
Log.d(TAG,"\tPara is ${pf.paragraph.name} etc")
for(f: EAreteSheetForm in pf.forms) {
Log.d(TAG,"\t\tForm is ${f.label}")
}
}
}
}
}
As can be seen, some data is loaded, 2 Sheets, each with 2 paragraphs and then 4 forms distributed unevenly (3 to para1, 2 to para2, 1 to para3, 0 to para4).
The data is then extracted (as array of EAreteSheetWithParagraph), the array traversed (traversing the underlying arrays of paragraphs and forms within paragraphs) and output to the log the Result being :-
D/ARETEINFO: Sheet ID is 10 Form is Sheet1 Version is 1
D/ARETEINFO: Para is Para1 (Sheet1) etc
D/ARETEINFO: Form is Form1
D/ARETEINFO: Form is Form2
D/ARETEINFO: Form is Form3
D/ARETEINFO: Para is Para2 (Sheet1) etc
D/ARETEINFO: Form is Form4
D/ARETEINFO: Form is Form5
D/ARETEINFO: Sheet ID is 20 Form is Sheet2 Version is 1
D/ARETEINFO: Para is Para3 (Sheet2) etc
D/ARETEINFO: Form is Form6
D/ARETEINFO: Para is Para4 (Sheet2) etc
I have two related entities:
Station
#Entity(tableName = "stations")
data class Station(
#PrimaryKey
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "latitude")
val latitude: Double,
#ColumnInfo(name = "longitude")
val longitude: Double,
#ColumnInfo(name = "connectors")
val connectors: List<Connector>, // this field has a type converter
)
Connector
#Entity(
tableName = "connectors",
primaryKeys = ["station_id", "station_connector_id"]
)
class Connector(
#ColumnInfo(name = "station_connector_id")
val stationConnectorId: Int,
#ColumnInfo(name = "station_id")
val stationId: Long,
#ColumnInfo(name = "type")
val type: ConnectorType,
)
When I insert data, I fill up both entites and it seems kinda okay, but when I'm trying to receive stations with particular types of connectors it duplicates rows.
For example, I have an object
Station(
id = 100,
latitude = 56.565,
longitude = 34.565,
connectors = [
Connector(stationConnectorId=1, stationId=100, type=TYPE_2),
Connector(stationConnectorId=2, stationId=100, type=CHADEMO),
Connector(stationConnectorId=3, stationId=100, type=TYPE_1)
]
)
And if I want to filter stations only by one connector type I receive one row with this station(and it's right), but if I want to reset filters and look up for stations that can contain many connectors, I receive duplicates of this stations(in this example if I request for station with TYPE_1, TYPE_2 and CHADEMO connector types it will be three equal rows).
I'm using this query to request stations from my database:
SELECT * FROM simple_stations
INNER JOIN connectors ON simple_stations.id = connectors.station_id
WHERE connectors.type IN (:connectorTypesList)
I've tried to use DISTINCT in the query, ForeignKeys and Indexes in these Entities, but it was not working, so now I'm completely lost.
If you just want Stations then you have various options then your have various options.
is to use a GROUP BY clause such as GROUP BY simple_stations.id
However, the issue you may then encounter is that Station would be incomplete/unreliable as you have a List of Connectors and if you GROUP by Station then you will only get a single arbitrary Connector (there again that may depend upon you TypeConvertor).
to use DISTINCT you would have to only include the Station columns (similar problem as above).
I'd suggest that your schema is at fault by including the List of Connectors related to the Station you are duplicating data (aka it's not normalised).
rather if you removed
#ColumnInfo(name = "connectors")
val connectors: List<Connector>, // this field has a type converter
from the Station Entity the data itself would still be available for retrieval.
You may then wish to have a POJO that Embeds the Station and has a List of Connector's, perhaps one with and one without the Connector List have an #Relationship (with and you would get all connectors irrespective of the WHERE clause as that's how #Relationship works). Without and you could have the constructor get only the Connectors with the types you want.
Perhaps consider the following based upon your code:-
The Station Entity
#Entity(tableName = "stations")
data class Station(
#PrimaryKey
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "latitude")
val latitude: Double,
#ColumnInfo(name = "longitude")
val longitude: Double
/*
#ColumnInfo(name = "connectors")
val connectors: List<Connector> // this field has a type converter
NO NEED IMPLIED BY RELATIONSHIP
*/
)
The Connector Entity
#Entity(
tableName = "connectors",
primaryKeys = ["station_id", "station_connector_id"]
)
class Connector(
#ColumnInfo(name = "station_connector_id")
val stationConnectorId: Int,
#ColumnInfo(name = "station_id")
val stationId: Long,
#ColumnInfo(name = "type")
val type: String //<<<<< changed for convenience
)
The StationWithConnectors POJO NEW
class StationWithConnectors {
#Embedded
var station: Station? = null
var connectors: List<Connector> = emptyList()
constructor(allDao: AllDao, station: Station, connectorTypeList: List<String>) {
this.station = station
this.connectors = allDao.getConnectorsOfSpecifiedTypesByStationId(station.id,connectorTypeList)
}
}
note the embedded query to build the list of connectors of only the specfified types
The Dao used i.e. AllDao
#Dao
interface AllDao {
#Insert
fun insert(station: Station): Long
#Insert
fun insert(connector: Connector): Long
#Query("SELECT * FROM stations")
fun getAllStations(): List<Station>
#Query("SELECT * FROM stations WHERE stations.id = :stationId")
fun getStationById(stationId: Long): Station
// Gets the Connectors per Station of the requested Type (i.e. NOT ALL CONNECTORS necessarily)
#Query("SELECT * FROM connectors WHERE station_id = :stationId AND connectors.type IN( :connectorTypesList)")
fun getConnectorsOfSpecifiedTypesByStationId(stationId: Long, connectorTypesList: List<String>): List<Connector>
// Gets the Stations that have Connectors of the requested type DISTINCT used along with only the station columns
#Query("SELECT DISTINCT stations.id, stations.latitude, stations.longitude " +
"FROM stations INNER JOIN connectors ON connectors.station_id = stations.id " +
"WHERE connectors.type IN(:connectorTypesList)")
fun getStationsWithSpecificConnectorTypes(connectorTypesList: List<String>): List<Station>
}
The #Database TheDatabase
#Database(entities = [Connector::class,Station::class],version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
}
and finaly an Activity to test/demonstrate
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 = Room.databaseBuilder(this,TheDatabase::class.java,"thedb.db")
.allowMainThreadQueries()
.build()
dao = db.getAllDao()
// Define some types
var type1 = "TYPE_1"
var type2 = "TYEP_2"
var type3 = "CHADEMO"
var type4 = "ANOTHER"
// Define stations with Connectors
var station1 = Station(100,56.565,34.565)
dao.insert(station1)
dao.insert(Connector(10,station1.id,type1))
dao.insert(Connector(20,station1.id,type4))
dao.insert(Connector(30,station1.id,type3))
dao.insert(Connector(40,station1.id ,type2))
var station2 = Station(200,33.333,22.222)
dao.insert(station2)
dao.insert(Connector(100,station2.id,type2))
dao.insert(Connector(110,station2.id,type4))
dao.insert(Connector(120,station2.id,type3))
// Define the search types
var listOfTypes = listOf(type1,type2) // Types to search for
// prepare the StationWithConnectors list
var allswcList: ArrayList<StationWithConnectors> = ArrayList()
// Get the stations with connectors of the required types
var stationsWithCertainTYpes = dao.getStationsWithSpecificConnectorTypes(listOfTypes)
// Build the StationWithCertainTypes POJOs
for(s: Station in stationsWithCertainTYpes) {
allswcList!!.add(StationWithConnectors(dao,s, listOfTypes))
}
var count = stationsWithCertainTYpes.size //<<<< just so breakpoint can be added
}
}
When run in debug mode then:-
StationWithCertainTypes gets both Stations (station 1 has type1 and type2, station2 has type2) as per :-
allswcList has the 2 StationWithConnectors built from the 2 Stations as per :-
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.