I'm trying to load entity sublist, but i would like to avoid to do 2 queries.
I'm thinking about do query inside TypeConverter, but i really don't know if it's good idea.
My entities:
#Entity
class Region(
#PrimaryKey(autoGenerate = true)
var id: Int = 0,
var name: String = "",
var locales: List<Locale> = listOf())
#Entity(foreignKeys = arrayOf(ForeignKey(
entity = Region::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("regionId"),
onDelete = CASCADE,
onUpdate = CASCADE
)))
class Locale(
#PrimaryKey(autoGenerate = true)
var id: Int = 0,
var regionId: Int = 0,
var name: String = "")
DAOs :
#Dao
interface RoomRegionDao{
#Insert
fun insert(region: Region)
#Delete
fun delete(region: Region)
#Query("select * from region")
fun selectAll(): Flowable<List<Region>>
}
#Dao
interface RoomLocaleDao{
#Insert
fun insert(locale: Locale)
#Query("select * from locale where regionId = :arg0")
fun selectAll(regionId: Int): List<Locale>
}
Database:
#Database(entities = arrayOf(Region::class, Locale::class), version = 1)
#TypeConverters(RoomAppDatabase.Converters::class)
abstract class RoomAppDatabase : RoomDatabase() {
abstract fun regionDao(): RoomRegionDao
abstract fun localeDao(): RoomLocaleDao
inner class Converters {
#TypeConverter
fun toLocales(regionId: Int): List<Locale> {
return localeDao().selectAll(regionId)
}
#TypeConverter
fun fromLocales(locales: List<Locale>?): Int {
locales ?: return 0
if (locales.isEmpty()) return 0
return locales.first().regionId
}
}
}
It's not working because can't use inner class as converter class.
Is it a good way?
How can I load "locales list" automatically in region entity when I do RoomRegionDao.selectAll?
I think that TypeConverter only works for static methods.
I say this based on the example from here and here
From the relationships section here:
"Because SQLite is a relational database, you can specify relationships between objects. Even though most ORM libraries allow entity objects to reference each other, Room explicitly forbids this."
So I guess it's best if you add #Ignore on your locales property and make a method on RoomLocaleDao thats insert List<Locale> and call it after insert Region.
The method that inserts a Region can return the inserted id.
If the #Insert method receives only 1 parameter, it can return a long, which is the new rowId for the inserted item. If the parameter is an array or a collection, it should return long[] or List instead.
(https://developer.android.com/topic/libraries/architecture/room.html#daos-convenience)
You can get all your Regions wit a list of Locales inside each Region just with one Query.
All you need to do is create a new Relation like this:
data class RegionWithLocale(
#Embedded
val region: Region,
#Relation(
parentColumn = "id",
entity = Locale::class,
entityColumn = "regionId"
)
val locales: List<Locale>
)
And latter get that relation in your Dao like this:
#Query("SELECT * FROM region WHERE id IS :regionId")
fun getRegions(regionId: Int): LiveData<RegionWithLocale>
#Query("SELECT * FROM region")
fun getAllRegions(): LiveData<List<RegionWithLocale>>
This solution worked for me
ListConvert.kt
class ListConverter
{
companion object
{
var gson = Gson()
#TypeConverter
#JvmStatic
fun fromTimestamp(data: String?): List<Weather>?
{
if (data == null)
{
return Collections.emptyList()
}
val listType = object : TypeToken<List<String>>() {}.type
return gson.fromJson(data, listType)
}
#TypeConverter
#JvmStatic
fun someObjectListToString(someObjects: List<Weather>?): String?
{
return gson.toJson(someObjects)
}
}
}
The property inside of my Entity
#SerializedName("weather")
#TypeConverters(ListConverter::class)
val weather: List<Weather>,
Related
i'm android beginner developer. i am createing stationArrivalInformation App.
#Entity
data class StationEntity(
#PrimaryKey val stationName: String,
val isFavorite: Boolean = false
)
#Entity
data class SubwayEntity(
#PrimaryKey val subwayId: Int
)
#Entity(primaryKeys = ["stationName", "subwayId"])
data class StationSubwayCrossRefEntity(
val stationName: String,
val subwayId: Int
)
data class StationWithSubwaysEntity(
#Embedded val station: StationEntity,
#Relation(
parentColumn = "stationName",
entityColumn = "subwayId",
entity = SubwayEntity::class,
associateBy = Junction(
StationSubwayCrossRefEntity::class,
parentColumn = "stationName",
entityColumn = "subwayId"
)
)
val subways: List<SubwayEntity>
)
i have built a data class with a many-to-many relationship.
1. station Table
enter image description here
2. subway Table
enter image description here
3. cross Ref Table
enter image description here
if you look at the DAO File:
#Dao
interface StationDao {
#Transaction
#Query("SELECT * FROM StationEntity")
fun getStationWithSubways(): Flow<List<StationWithSubwaysEntity>>
#Insert(onConflict = REPLACE)
suspend fun insertStations(station: List<StationEntity>)
#Insert(onConflict = REPLACE)
suspend fun insertSubways(subway: List<SubwayEntity>)
#Insert(onConflict = REPLACE)
suspend fun insertCrossRef(refEntity: List<StationSubwayCrossRefEntity>)
#Transaction
#Insert(onConflict = REPLACE)
suspend fun insertStationSubways(stationSubways: List<Pair<StationEntity, SubwayEntity>>) {
insertStations(stationSubways.map { it.first })
insertSubways(stationSubways.map { it.second })
insertCrossRef(stationSubways.map { (station, subway) ->
StationSubwayCrossRefEntity(
station.stationName, subway.subwayId
)
})
}
#Update
suspend fun updateStation(station: StationEntity)
}
class StationRepositoryImpl #Inject constructor(
private val stationDao: StationDao
): StationRepository {
override val stations: Flow<List<Station>> =
stationDao.getStationWithSubways()
.distinctUntilChanged()
.map { stations -> stations.toStation().sortedByDescending { it.isFavorite } }
.flowOn(dispatcher)
}
here, the result of stationDao.getStationWithSubways() is null. I referred to the Android official documentation and applied it, but I am not getting the desired result. Why?
i expected getting multiple subways at one station
enter image description here
<Type of the parameter must be a class annotated with #Entity or a collection/array of it.>
-> how solve?
Room does not anything (according to the classes shown) about the Station class and thus it does not know how to handle:-
#Update
suspend fun updateStation(station: Station)
As there is no such #Entity annotated class.
The fix I believe is to use
#Update
suspend fun updateStation(station: StationEntity)
if that is what you are trying to update
However, you will then possibly have issues due to the warning that will be in the Build log:-
warning: The affinity of child column (subwayId : INTEGER) does not match the type affinity of the junction child column (subwayId : TEXT). - subways in a.a.so75052787kotlinroomretrievefrommanytomany.StationWithSubwaysEntity
Another warning that you may wish to consider is
warning: The column subwayId in the junction entity a.a.so75052787kotlinroomretrievefrommanytomany.StationSubwayCrossRefEntity is being used to resolve a relationship but it is not covered by any index. This might cause a full table scan when resolving the relationship, it is highly advised to create an index that covers this column. - subwayId in a.a.so75052787kotlinroomretrievefrommanytomany.StationSubwayCrossRefEntity
Both will not appear if you used:-
#Entity(primaryKeys = ["stationName", "subwayId"])
data class StationSubwayCrossRefEntity(
val stationName: String,
//val subwayId: String
#ColumnInfo(index = true)
val subwayId: Int
)
commented out line left in just to show before/after
You would then have to use :-
#Transaction
#Insert(onConflict = REPLACE)
fun insertStationSubways(stationSubways: List<Pair<StationEntity, SubwayEntity>>) {
insertStations(stationSubways.map { it.first })
insertSubways(stationSubways.map { it.second })
insertCrossRef(stationSubways.map { (station, subway) ->
StationSubwayCrossRefEntity(
station.stationName, subway.subwayId
)
})
}
subway.subwayId instead of subway.subwayId.toString()
i expected getting multiple subways at one station in normal operation
You would. With the changes suggested above (and some others to run on the main thread for brevity/convenience), an appropriate #Database annotated class as per:-
#Database(entities = [SubwayEntity::class,StationEntity::class, StationSubwayCrossRefEntity::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
abstract fun getStationDao(): StationDao
companion object {
private var instance: TheDatabase?=null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
And the following in an Activity, to demonstrate :-
class MainActivity : AppCompatActivity() {
lateinit var dbInstance: TheDatabase
lateinit var dao: StationDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
dbInstance = TheDatabase.getInstance(this)
dao = dbInstance.getStationDao()
dao.insertStationSubways(listOf(Pair(StationEntity("Station1",false),SubwayEntity(1))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station1",false),SubwayEntity(2))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station1",false),SubwayEntity(3))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station2",false),SubwayEntity(4))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station2",false),SubwayEntity(5))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station2",false),SubwayEntity(6))))
val sb = StringBuilder()
for (sws in dao.getStationWithSubways()) {
sb.clear()
for (subway in sws.subways) {
sb.append("\n\tSubway is ${subway.subwayId}")
}
Log.d("DBINFO","Station is ${sws.station.stationName} it has ${sws.subways.size} subways. They are:-${sb}")
}
}
}
Then the log (when run for the first time) includes (as expected):-
D/DBINFO: Station is Station1 it has 3 subways. They are:-
Subway is 1
Subway is 2
Subway is 3
D/DBINFO: Station is Station2 it has 3 subways. They are:-
Subway is 4
Subway is 5
Subway is 6
My Entity
#Entity(tableName = "doctor_table")
data class DoctorEntity(
#PrimaryKey(autoGenerate = true)
var id : Int = AppUtil.defId,
var doc_id : Int = AppUtil.defId,
var name : String = AppUtil.defString,
#field:TypeConverters(IntTypeConverter::class)
var days : List<Int> = AppUtil.emptyIntArray,
#field:TypeConverters(LongTypeConverter::class)
var numbers : List<Long> = AppUtil.emptyLongArray,
#field:TypeConverters(IntTypeConverter::class)
var clinic_ids : List<Int> = AppUtil.emptyIntArray
)
The empty int array : val emptyIntArray = arrayListOf<Int>()
IntTypeConverter class:
class IntTypeConverter {
#TypeConverter
fun saveIntList(list: List<Int>): String? {
return Gson().toJson(list)
}
#TypeConverter
fun getIntList(list: List<Int>): List<Int> {
return Gson().fromJson(
list.toString(),
object : TypeToken<List<Int?>?>() {}.type
)
}
DB class:
#Database(
entities = [DoctorEntity::class, ClinicEntity::class,
DocClinicEntity::class, DocTreatmentEntity::class,
TreatmentEntity::class],
version = 1,
exportSchema = false
)
#TypeConverters(LongTypeConverter::class,IntTypeConverter::class)
abstract class Db : RoomDatabase() {
abstract fun getDao(): DbDao
}
I am using hilt to inject the DB
#Module
#InstallIn(SingletonComponent::class)
object AppModules {
#Provides
#Singleton
fun provideDatabase(
#ApplicationContext app: Context
) = Room.databaseBuilder(
app,
Db::class.java,
"medical_db.db"
).fallbackToDestructiveMigration()
.build()
}
The error while trying to run the app.
\room\entities\DoctorEntity.java:15: error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
private java.util.List<java.lang.Integer> days;
What am I missing ??
In your second type convertor, you should input a String instead of a List because you are saving a String representation of you list (according to your first convertor).
Something like this:
#TypeConverter
fun getIntList(list: String): List<Int> {
return Gson().fromJson(
list,
object : TypeToken<List<Int>>() {}.type
)
}
Also, you need not worry about nullable types here because the list that you are saving in saveIntList is non-nullable and contains non-nullable Ints. So you can safely convert the stored string back to a list which is non-nullable.
I am trying to understand how to use Room with relational tables. I have created a Job model, that has a list of locations and therefore needs a 1-to-many relation between the Job and Location object. For that, I have created a JobWrapper data class to hold both the Job and the locations. But, when building I get the following error:
The class must be either #Entity or #DatabaseView. -
java.util.Collectionerror: 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). -
java.util.Collection \models\JobWrapper.java:12: error: Cannot find
the child entity column parentId in java.util.Collection.
Options:
private java.util.Collection<models.Location> locations;
public final class JobWrapper {
^ Tried the following constructors but they failed to match:
JobWrapper(models.Job,java.util.Collection<models.Location>)
-> [param:job -> matched field:job, param:locations -> matched field:unmatched] models\JobWrapper.java:9:
error: Cannot find setter for field.
I notice that it at least cannot find the locations table. But, I do not know how to handle the problem. The problem did not appear while reading from the database - it first appeared when I was trying to put data into the database with my JobDAO. I have already spent a day trying to solve it and are therefore searching for a solution or some advise on how to solve it.
Note: I have been following the following guides:
https://developer.android.com/training/data-storage/room/relationships#one-to-many
https://dev.to/normanaspx/android-room-how-works-one-to-many-relationship-example-5ad0
Here follows some relevant code snippets from my projects:
JobWrapper.kt
data class JobWrapper(
#Embedded val job: Job,
#Relation(
parentColumn = "jobid",
entityColumn = "parentId"
) var locations : Collection<Location>
)
Job
#Entity
data class Job (
#PrimaryKey
#NonNull
var jobid : String,
#NonNull
#ColumnInfo(name = "job_status")
var status : JobStatus,
#NonNull
#SerializedName("createdByAuth0Id")
var creator : String,
#SerializedName("note")
var note : String?,
#NonNull
var organisationId : String,
#NonNull
var type : JobType,
#SerializedName("atCustomerId")
#NonNull
#ColumnInfo(name = "working_at_customer_id")
var workingAtCustomerId : String,
#SerializedName("toCustomerId")
#NonNull
#ColumnInfo(name = "working_to_customer_id")
var workingToCustomerId : String,
)
JobStatus.kt
enum class JobStatus {
CREATED,
READY,
IN_PROGRESS,
FINISHED
}
Location.kt
#Entity
data class Location (
#PrimaryKey(autoGenerate = true)
var entityId: Long,
#NonNull
var parentId: String,
#NonNull
var locationId: String,
#NonNull
var type: String
) {
constructor() : this(0, "", "", "")
}
JobDao.kt
#Dao
interface JobDAO {
#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(job: JobWrapper)
#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(jobs: List<JobWrapper>)
#Transaction
#Update
fun update(job: JobWrapper)
#Transaction
#Delete
fun delete(job: JobWrapper)
#Transaction
#Query("DELETE FROM Job")
fun deleteAll()
#Transaction
#Query("SELECT * FROM Job")
fun getAll(): LiveData<List<JobWrapper>>
}
As Kraigolas has pointed out you can only use JobWrapper directly to extract data you need to insert/delete/update via the actual underlying Entities.
Consider the following
(unlike Kraigolas's solultion the extended functions are in the JobDao rather than in a repository(swings and roundabouts argument as to which is better))
note to test I've made some changes for brevity and convenience so the you would have to amend to suit.
JobDao
#Dao
interface JobDAO {
/* Core/Underlying DAO's */
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(job: Job): Long
#Insert()
fun insert(location: List<Location>): List<Long>
#Transaction
#Delete
fun delete(location: List<Location>)
#Delete
fun delete(job: Job)
#Update
fun update(job: Job)
#Update
fun update(location: List<Location>)
#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(job: JobWrapper): Long {
val rv =insert(job.job)
insert(job.locations)
return rv
}
#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(jobs: List<JobWrapper>) {
for (jw: JobWrapper in jobs) {
insert(jw)
}
}
#Transaction
#Update
fun update(job: JobWrapper) {
update(job.locations)
update(job.job)
}
/* Delete according to JobWrapper allowing optional deletion of locations */
#Transaction
#Delete
fun delete(job: JobWrapper, deleteChildLocations: Boolean) {
if (deleteChildLocations) {
delete(job.locations)
}
delete(job.job)
}
/* will orphan locations as is */
/* Note using Foreign Keys in Location (to Job) with ON DELETE CASCADE */
#Transaction
#Query("DELETE FROM Job")
fun deleteAll()
#Transaction
#Query("SELECT * FROM Job")
fun getAll(): List<JobWrapper>
#Transaction
#Query("SELECT * FROM job WHERE jobid = :jobid")
fun getJobWrapperByJobId(jobid: String ): JobWrapper
}
As can be seen the Core Dao's include Job and Location actions
The JobWrapper actions invoke the Core actions
List have been used instead of Collections for my convenience (aka I only dabble with Kotlin)
Instead of JobType, type has been changed to use String for convenience
As I've tested this the demo/example used follows (obviously JobDao is as above)
The POJO's and Entities used are/were :-
JobWrapper
data class JobWrapper(
#Embedded val job: Job,
#Relation(
parentColumn = "jobid",
entityColumn = "parentId",
entity = Location::class
) var locations : List<Location>
)
List instead of Collection
Job
#Entity
data class Job (
#PrimaryKey
#NonNull
var jobid : String,
#NonNull
#ColumnInfo(name = "job_status")
var status : String,
#NonNull
var creator : String,
var note : String?,
#NonNull
var organisationId : String,
#NonNull
var type : String,
#NonNull
#ColumnInfo(name = "working_at_customer_id")
var workingAtCustomerId : String,
#NonNull
#ColumnInfo(name = "working_to_customer_id")
var workingToCustomerId : String,
)
largely the same but String rather than objects for convenience
Location
#Entity(
foreignKeys = [
ForeignKey(
entity = Job::class,
parentColumns = ["jobid"],
childColumns = ["parentId"],
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)
])
data class Location (
#PrimaryKey(autoGenerate = true)
var entityId: Long,
#NonNull
var parentId: String,
#NonNull
var locationId: String,
#NonNull
var type: String
) {
constructor() : this(0, "", "", "")
}
Foreign Key added for referential integrity and also CASCADE deletes (UPDATE CASCADE not really required unless you change a jobid, other updates aren't cascaded (and wouldn't need to be))
#Database used JobDatabase
#Database(entities = [Location::class,Job::class],version = 1)
abstract class JobDatabase: RoomDatabase() {
abstract fun getJobDao(): JobDAO
companion object {
var instance: JobDatabase? = null
fun getInstance(context: Context): JobDatabase {
if (instance == null) {
instance = Room.databaseBuilder(context, JobDatabase::class.java, "job.db")
.allowMainThreadQueries()
.build()
}
return instance as JobDatabase
}
}
}
allowMainThreadQueries used to allow testing on main thread
The test/demo activity MainActivity
class MainActivity : AppCompatActivity() {
lateinit var db: JobDatabase
lateinit var dao: JobDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = JobDatabase.getInstance(this)
dao = db.getJobDao()
var jobId: String = "Job1"
var jw = JobWrapper(
Job(
jobId,
"x",
"Fred",
"Note for Job1",
"Org1",
"A","Cust1",
"Cust1"),
listOf(
Location(0,jobId,"loc1","J"),
Location(0,jobId,"Loc2","K"),
Location(0,jobId,"Loc3","L")
)
)
dao.insert(jw)
dao.insertAll(createJobWrapperList(10))
dao.delete(dao.getJobWrapperByJobId("job6"),true)
var jobWrapper = dao.getJobWrapperByJobId("job7")
jobWrapper.job.creator = "update creator"
for (l in jobWrapper.locations) {
if (l.type == "M") l.type= "UPDATED"
}
dao.update(jobWrapper)
}
fun createJobWrapperList(numberToCreate: Int): List<JobWrapper> {
val l = mutableListOf<JobWrapper>()
for(i in 1..numberToCreate) {
var jid = "job$i"
l.add(
JobWrapper(
Job(jid,"X","Creator$i","Note for $jid","org$jid","T","custA","custT"),
arrayListOf(
Location(0,jid,"loc_$jid.1","L"),
Location(0,jid,"loc_$jid.2","M"),
Location(0,jid,"loc_$jid.3","N")
)
)
)
}
return l.toList()
}
}
This :-
gets DB instance and the dao.
adds a job and it's location via a single JobWrapper
adds x (10) jobs and 3 locations per Job via a List of JobWrappers generated via the createJobWrapperList function.
4.deletes the a JobWrapper obtained via getJobWrapperByJobId including deleting the underlying locations (true) using delete for the JobWrapper associated with the jobid "job6".
obtains the JobWrapper associated with "job7" changes the creator and changes the locations that have a type of "M" to "UPDATED" (just the one) and then uses the update(JobWrapper) to apply the updates.
WARNING
Inserting using a JobWrapper, as it has REPLACE conflict strategy will result in additional locations if it replaces as the entityId will always be generated.
Result
Runinng the above results in :-
Job Table :-
As can be seen job6 has been deleted (11 rows added 10 left) and job7's creator has been updated.
Location Table :-
As can be seen no job6 locations (was 33 locations (11 * 3) now 30) and the location that was type M has been updated according to the JobWrapper passed.
You asked :-
How do I ensure the relationship to the right parent (job) when inserting the childs (locations)?
I think the above demonstrates how.
JobWrapper does not have an associated table with it. Note that it is nice for pulling from your database because it will grab from the Location table and the Job table, but it makes no sense to insert into your database with a JobWrapper because there is no associated table for it.
Instead, you need to insert Jobs, and Locations separately. The database can query for them together because they are related by jobid and parentid, so you don't have to worry about them losing their relationship to each other, and you can still query for your JobWrapper.
If given my above explanation you still think that you should be able to insert a JobWrapper, then you might create a repository with a method like:
suspend fun insertWrapper(wrapper : JobWrapper){
dao.insertJob(wrapper.job)
dao.insertLocations(wrapper.locations)
}
Where insertJob inserts a single Job, and insertLocations inserts a list of Location.
I am building a database using Room and I can't figure out how to insert the new elements that have a relationship (one to many in my case) into the database. No solution has ever talked about the insertion (they only talk about querying the data).
Here is the DAO:
#Dao
abstract class ShoppingListsDao {
#Insert
abstract suspend fun addNewShoppingList(newShoppingList: ShoppingList)
#Insert
abstract suspend fun addNewItem(newItem: Item)
// This is how I thought it would work but it didn't
#Insert
#Transaction
abstract suspend fun addNewShoppingListWithItems(newShoppingListWithItems: ShoppingListWithItems)
}
Here are my entities:
#Entity
class ShoppingList(
#PrimaryKey(autoGenerate = true)
val listID: Int,
val ListName: String
)
#Entity(foreignKeys = [ForeignKey(
entity = ShoppingList::class,
parentColumns = ["listID"],
childColumns = ["parentListID"]
)])
class Item(
#PrimaryKey(autoGenerate = true)
var itemID: Int,
val name: String,
val quantity: Int,
val parentListID: Int
)
There isn't a way that I am aware of that lets you directly insert a compound entity (like ShoppingListWithItems). You have to just insert the individual entities to their tables.
In your example, you would want to define an insert method for your ShoppingList entity which returns the generated primary key (so you can use it for your other items) and an insert method for your Item entities which can insert a whole list of them.
#Insert
suspend fun addNewShoppingList(newShoppingList: ShoppingList): Long
#Insert
suspend fun addNewItems(newItems: List<Item>)
Then you can run a transaction to insert them in a batch.
#Transaction
suspend fun addNewShoppingListWithItems(shoppingList: ShoppingList, items: List<Item>) {
val listId = addNewShoppingList(shoppingList)
items.forEach { it.parentListId = listId }
addNewItems(items)
}
Here's a good resource to understand one-to-many relationships-> https://developer.android.com/training/data-storage/room/relationships#one-to-many
You can create an embedded object for 'ShoppingListWithItems' as (more on embedded objects - https://developer.android.com/training/data-storage/room/relationships#nested-objects):
data class ShoppingListWithItems(
#Embedded val shoppingList: ShoppingList,
#Relation(parentColumn = "listID", entityColumn = "parentListID") val itemList: List<Item>
)
To store them in the database, you can simply use a transaction:
#Transaction
suspend fun createTransaction(shoppingList: ShoppingList, itemList: List<Item>) {
addNewShoppingList(shoppingList)
addNewItem(*itemList) // or create an alternate to accept the list itself.
}
To retrieve the 'ShoppingListWithItems' instance:
#Query("SELECT * from ShoppingList where listID =:id")
suspend fun getShoppingListWithItemsById(id: String): List<ShoppingListWithItems>
I am implementing a local cache using Room. I have created typeconverter to convert list of objects to json and back. But I am receiving mapping issue while retrieving data from json with error:
The columns returned by the query does not have the fields [title,media] in
com.example.theApp.data.FlickrImage even though they are annotated as non-null or
primitive. Columns returned by the query: [items]
Another one like this:
error: Cannot figure out how to read this field from a cursor.
private final com.example.theApp.data.Media media = null;
I tried other answers here but its not associated directly with this issue.
Here is my typeconverter:
class FlickrImageConverters {
#TypeConverter
fun fromImageListToJson(stat: List<FlickrImage>): String {
return Gson().toJson(stat)
}
/**
* Convert a json to a list of Images
*/
#TypeConverter
fun fromJsonToImagesList(jsonImages: String): List<FlickrImage> {
val type = object : TypeToken<List<FlickrImage>>() {}.type
return Gson().fromJson<List<FlickrImage>>(jsonImages, type)
}
}
Here is my entity class:
#Entity
data class DatabaseImagesEntity(
#PrimaryKey
#TypeConverters(FlickrImageConverters::class)
#SerializedName("item")
val items: List<FlickrImage>)
Dao class
#Dao
interface ImagesDao {
#Query("select * from DatabaseImagesEntity")
fun getImages(): List<FlickrImage>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(images: List<FlickrImage>)
}
FlickrImage class
data class FlickrImage(val title: String, val media: Media)
Media class
data class Media(val m: String)
LatestImage class
data class LatestImages(val items: List<FlickrImage>)
Please let me know if you faced this issue and if you know the solution for this.
Room database implementation
#Database(entities = [DatabaseImagesEntity::class], version = 1,
exportSchema = false)
#TypeConverters(FlickrImageConverters::class)
abstract class FlickrDatabase: RoomDatabase() {
abstract val imagesDao: ImagesDao
}
private lateinit var INSTANCE: FlickrDatabase
fun getDatabase(context: Context): FlickrDatabase{
synchronized(FlickrDatabase::class.java){
if(!::INSTANCE.isInitialized){
INSTANCE = Room.databaseBuilder(context.applicationContext,
FlickrDatabase::class.java,
"flickerImages").build()
}
}
return INSTANCE
}
The issue was I was saving data in the wrong entity, wrong TypeConverters and as a result, I was using the wrong Entity class at the time of database creation.
Here are the necessary changes I had to make to store the list of objects:
Flickr data class
#Entity(tableName = "FlickerImage")
data class FlickrImage(
#PrimaryKey(autoGenerate = true)
val id: Int,
val title: String,
#TypeConverters(MediaConverter::class)
val media: Media)
TypeConvertors for Media class
class MediaConverter {
#TypeConverter
fun fromMediaToJson(stat: Media): String {
return Gson().toJson(stat)
}
/**
* Convert a json to a list of Images
*/
#TypeConverter
fun fromJsonToMedia(jsonImages: String): Media {
val type = object : TypeToken<Media>() {}.type
return Gson().fromJson<Media>(jsonImages, type)
}
}
DAO class
#Dao
interface ImagesDao {
#Query("select * from FlickerImage")
fun getImages(): LiveData<List<FlickrImage>>
Database class
#Database(entities = [FlickrImage::class], version = 1, exportSchema = false)
#TypeConverters(MediaConverter::class)
abstract class FlickrDatabase: RoomDatabase() {
abstract val imagesDao: ImagesDao
}
private lateinit var INSTANCE: FlickrDatabase
fun getDatabase(context: Context): FlickrDatabase{
synchronized(FlickrDatabase::class.java){
if(!::INSTANCE.isInitialized){
INSTANCE = Room.databaseBuilder(context,
FlickrDatabase::class.java,
"flickerImages").build()
}
}
return INSTANCE
}
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(images: List<FlickrImage>)
}
You need to add the appropriate annotations to your data class - e.g. for Gson you need to add the annotations #SerializedName("field_name") Otherwise, there's no way for the converters to know how to translate the json.
To clarify, the current annotations you have are only for Room. Just check with whatever json library you are using for the necessary logic.
#Entity(tableName = "images")
data class DatabaseImagesEntity(
#PrimaryKey(autoGenerate = true)
var id: Int? = 0,
#TypeConverters(FlickrImageConverters::class)
#SerializedName("item")
val items: MutableList<FlickrImage>? = null
)
or
#Entity(tableName = "images")
class DatabaseImagesEntity {
#PrimaryKey(autoGenerate = true)
var id: Int? = 0
#TypeConverters(FlickrImageConverters::class)
#SerializedName("item")
val items: MutableList<FlickrImage>? = null
}
then update your DAO query to #Query("select * from images")
I named it images as an example - you can choose whatever you want.
class ListConverter {
//from List to String
#TypeConverter
fun fromList(list : List<Object>): String {
return Gson().toJson(list)
}
// from String to List
#TypeConverter
fun toList(data : String) : List<Object> {
if (data == null){
return Collections.emptyList()
}
val typeToken = object : TypeToken<List<Object>>() {}.type
return Gson().fromJson(data,typeToken)
}
}