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>
Related
I am experimenting with Room, Live Data and Recycler View in Android Kotlin.
My question is, I am trying to make an expense tracking APP, and I have 2 Table:
one for the expense
one for expense type
I joined the table as indicated in Room documentation for 1:N relationship.
Example of my table:
**Expense**
*ID = 1
expenseName = MyExpense1
expenseAmount = 100
expenseTypeID = 1*
**ExpenseType**
*ID= 1
ExpenseType= Home Expenses*
**Result expected from JOIN:**
*expenseName = MyExpense1
expenseAmount = 100
expenseType = Home Expenses*
But in this way, when I get the data for recycler view, I get a list that contain:
Expense Type Class
Expense list of Expense Class
How can I have data as if **I JOINED ** the table? Since my **ExpenseTypeWithExpense **class contains a class and a List of class
Usually I use a **RecycleView **on just one table and it is easy since I have a list of my Entity Class and I can access the single instance with list[position] in my **onBindViewHolder **class
EXPENSE Class
#Entity(
foreignKeys =[
ForeignKey(
entity = ExpenseType::class,
parentColumns = ["id"],
childColumns = ["expenseTypeID"]
)]
)
data class Expense (
#PrimaryKey(autoGenerate = true)
val id:Int,
val expenseName: String,
val expenseAmount: Double,
val expenseTypeID:Int
)
EXPENSE TYPE Class
#Entity(tableName = "expense_type")
data class ExpenseType (
#PrimaryKey(autoGenerate = true)
val id:Int,
val expenseType: String
)
EXPENSE TYPE Join with EXPENSE Class (as per documentation of joining 1:n table)
data class ExpenseTypeWithExpense (
#Embedded val expenseType: ExpenseType,
#Relation(
parentColumn ="id",
entityColumn ="id"
)
val expense: List<Expense>
)
My DAO Interface
#Dao
interface ExpenseDao {
#Insert
suspend fun insertExpense(expense: Expense)
#Insert
suspend fun insertExpenseType(expenseType:ExpenseType)
#Transaction
#Query("SELECT * FROM expense_type")
fun getExpenseWithType():LiveData<List<ExpenseTypeWithExpense>>
}
Expense Repository
class ExpenseRepository(private val expenseDao: ExpenseDao) {
val readAllData: LiveData<List<ExpenseTypeWithExpense>> = expenseDao.getExpenseWithType()
suspend fun insertExpense(expense: Expense){
expenseDao.insertExpense(expense)
}
suspend fun insertExpenseType(expenseType: ExpenseType){
expenseDao.insertExpenseType(expenseType)
}
}
ExpenseViewModel
class ExpenseViewModel(application: Application):AndroidViewModel(application) {
val readAllData: LiveData<List<ExpenseTypeWithExpense>>
private val repository: ExpenseRepository
init {
val expenseDao: ExpenseDao = ExpenseDatabase.getDatabase(application).expenseDao()
repository = ExpenseRepository(expenseDao)
readAllData = expenseDao.getExpenseWithType()
}
fun insertExpense(expense: Expense){
viewModelScope.launch(Dispatchers.IO){
repository.insertExpense(expense)
}
}
fun insertExpenseType(expenseType: ExpenseType){
viewModelScope.launch(Dispatchers.IO){
repository.insertExpenseType(expenseType)
}
}
}
My Adapter
class ListAdapter(): RecyclerView.Adapter<ListAdapter.MyViewHolder>() {
private val expenseList = emptyList<ExpenseTypeWithExpense>()
class MyViewHolder(itemView:View):RecyclerView.ViewHolder(itemView){
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.custom_row, parent, false))
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentItem = expenseList[position]
holder.itemView.findViewById<TextView>(R.id.expenseName).text =
# ** //currentItem is an instance of ExpenseTypeWithExpense and contains ExpenseType class and an Expense List**
}
override fun getItemCount(): Int {
return expenseList.size
}
}
** //currentItem is an instance of ExpenseTypeWithExpense and contains ExpenseType class and an Expense List** **
I do not Know how to handle this...
When you use #Relation there is no actual JOIN, rather for each parent (the #Embedded) the children are obtained as a list, as you have found.
If you want the true cartesian product then you would use a POJO without the #Relation but rather #Embeded and the query would have the JOIN.
e.g.
data class ExpenseTypeWithExpense (
#Embedded val expenseType: ExpenseType,
#Embedded
val expense: Expense
)
The query could then be:-
#Query("SELECT * FROM expense_type JOIN expense ON expense_type.id = expense.expenseTypeID;")
However, as the id field is common to both and that rather than objects (ExpenseType and Expense being included in the output) You probably want the POJO to be
When Room maps output columns to the fields, if there are any liked named columns the last is mapped.
:-
data class ExpenseTypeWithExpense (
val idOfExpenseType:Int,
val expenseType: String,
val id:Int,
val expenseName: String,
val expenseAmount: Double,
val expenseTypeID:Int
)
And the Query to (to rename the output columns so they can be assigned to the fields) as
#Query("SELECT expense_type.id AS idOfExpenseType, expense_type.expenseType, expense.* FROM expense_type JOIN expense ON expense_type.id = expense.expenseTypeID;;")
as you can see the id column of the expense_type has been given a different name to map to the POJO. As * is not being used the expenseType has to be selected. However rather than specify all the columns of the expense table individually,expense.* is used to output all of the columns of the expense table.
If you had expense_type as:-
id expenseType
1 T1
2 T2
3 T3
and expense as:-
id expenseName expenseAmount expenseTypeID
1 EXP1 100.11 1
2 EXP2 200.22 1
3 EXP3 300.33 1
4 EXP4 400.44 2
5 EXP5 500.55 2
Then the cartesian product output would be:-
which would equate to 5 ExpenseTypeWithExpense objects being returned in the LiveData<List>
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
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 have list of custom object that i wanna save it in the database.
So I have to use TypeConverters to make this possible.
My problem that i get an error when I implement the functionality and I noticed
that a function annotated with TypeConverter never used
Here's the error:
A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
error: The columns returned by the query does not have the fields [avatar,email,first_name,id,last_name] in com.hraa.fakeusers.model.UserData even though they are annotated as non-null or primitive. Columns returned by the query: [data]
public abstract androidx.lifecycle.LiveData<java.util.List<com.hraa.fakeusers.model.UserData>> getUsers();
And here's the code:
#Entity(tableName = USER_DATA_TABLE)
data class DataModel(
val data: List<UserData>,
val page: Int,
val per_page: Int,
val total: Int,
val total_pages: Int
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
}
data class UserData(
val avatar: String,
val email: String,
val first_name: String,
val id: Int,
val last_name: String
)
class Converters {
#TypeConverter
fun toUsersData(value: String): List<UserData> {
val type = object : TypeToken<List<UserData>>() {}.type
return Gson().fromJson(value, type)
}
#TypeConverter
fun fromUsersData(usersData: List<UserData>): String {
return Gson().toJson(usersData)
}
}
#Database(entities = [DataModel::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
abstract fun dataDao(): DataDao
}
#Dao
interface DataDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertData(data: DataModel)
#Delete
suspend fun deleteData(data: DataModel)
#Query("SELECT data FROM USER_DATA_TABLE")
fun getUsers(): LiveData<List<UserData>>
}
Note: toUsersData() function never used.. I don't know why.
Note: toUsersData() function never used
How can you be sure of that? My guess is that this function could work well, but you have two type's transformations here:
#Query("SELECT data FROM USER_DATA_TABLE")
fun getUsers(): LiveData<List<UserData>>
Transformation #1 (row level). Input: String (saved in db). Output: data (List).
That should be processed well thanks to your toUsersData() method (may be not, I've not checked, but it seems it should do)
Transformation #2 (row level). Input: data (List). Output: UserData (According to your desired return type). For that Room doesn't know how to do this transformation, so you have en error.
To check if your toUsersData() really works you can test next query:
#Query("SELECT * FROM USER_DATA_TABLE")
fun getUsers(): LiveData<List<DataModel>>
If your build is successful, then there is no problem with this function. You can also find this function in Java-class, that was autogenerated by Room during build.
You can try to add another data class:
data class UserDataList(
val data: List<UserData>
)
and change your data method to:
#Query("SELECT data FROM USER_DATA_TABLE")
fun getUsers(): LiveData<List<UserDataList>>
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>,