Room Database, structuring and querying entity with list - android

Here is my setup. I have a WorkoutPlan object that can contain a list of Workout objects in it. The way I'm currently modeling it is by have a third table that handles mapping the two together. A WorkoutPlan can contain many workouts, and a Workout can be used by many WorkoutPlans.
#Entity(tableName = "workoutPlans")
data class DbWorkoutPlan(#ColumnInfo(name = "name")
val name: String,
#ColumnInfo(name = "date")
val date: Date) {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
var id: Int = 0
}
#Entity(tableName = "workouts")
data class DbWorkout(#ColumnInfo(name = "name")
val name: String,
#ColumnInfo(name = "date")
val data: Date) {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
var id: Int = 0
}
#Entity(tableName = "DbWorkoutPlanWorkoutJoin",
primaryKeys = arrayOf("workoutPlanId", "workoutId"),
foreignKeys = arrayOf(ForeignKey(entity = DbWorkoutPlan::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("workoutPlanId")),
ForeignKey(entity = DbWorkout::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("workoutId"))))
data class DbWorkoutPlanWorkoutJoin(#ColumnInfo(name = "workoutPlanId")
val workoutPlanId: Int,
#ColumnInfo(name = "workoutId")
val workoutId: Int)
So that is my data setup for the tables. I'm not sure if it's correct. On the returned data side I have this.
data class DbWorkoutPlanResult(#Embedded
val workoutPlan: WorkoutPlan,
#Relation(parentColumn = "id", entityColumn = "workoutId")
val workoutIds: List<DbWorkout>)
So I want to get back a DbWorkoutPlanResult containing one WorkoutPlan and a list of all the Workouts it has.
I know I'm not doing this right, and the complexity is increasing quickly. Does anyone know what I have done wrong in my setup? And what would I have for a query? My best attempt is this
#Query("SELECT * " +
"FROM DbWorkoutPlanWorkoutJoin " +
"INNER JOIN workoutPlans " +
"ON DbWorkoutPlanWorkoutJoin.workoutPlanId = workoutPlans.id " +
"INNER JOIN workouts " +
"ON DbWorkoutPlanWorkoutJoin.workoutId = workouts.id ")
fun getWorkoutPlans(): Flowable<List<DbWorkoutPlanResult>>
Thanks in advance.

Using #Relation annotation you can create a 1:N (one to many) relation. Like, in your case, a single plan can have multiple workouts, but each workout can belong to only a single plan. This is clearly not what you want!
For your needs, which I assume are like this: get a list of POJOs containing a planand list of associated workouts, you need to use a separate JOIN Table (which I guess you already are).
A simple way to get the results would be to divide the operations into two queries:
Get List<DbWorkoutPlan> of all plans
Query the Join Table and fetch all List<DbWorkout> for each DbWorkoutPlan
Example code
First define the models
#Entity(tableName="plans") class DbWorkoutPlan {
#PrimaryKey
private long id;
// ...
#Ignore private List<DbWorkout>; // do not persist this, also create getter/setter for this field
}
#Entity(tableName="workouts") class DbWorkout {
#PrimaryKey
private long id;
// ...
}
#Entity(
tableName="plan_workout_join"
primaryKeys = {"workoutPlanId", "workoutId"},
foreignKeys = {
#ForeignKey(entity = DbWorkoutPlan.class, parentColumns = "id", childColumns = "plan"),
#ForeignKey(entity = DbWorkout.class, parentColumns = "id", childColumns = "workout")
}
) class PlanWorkoutJoin {
private long plan;
private long workout;
}
Now in the DAO,
#Query("SELECT * FROM plans")
List<DbWorkoutPlan> getAllPlans();
#Query("SELECT * FROM workouts WHERE workouts.id IN (SELECT workout FROM plan_workout_join WHERE plan_workout_join.plan=:plan)")
List<DbWorkout> getWorkoutsForPlan(long plan);
Now you can query like,
List<DbWorkoutPlan> plans = dao.getAllPlans();
for(DbWorkoutPlan plan : plans){
List<DbWorkout> workouts = dao.getWorkoutsForPlan(plan.getId());
plan.setWorkouts(workouts);
}
// ... continue
P.S. You'll obviously need to modify this a bit if you are using RxJava, but the core idea remains the same

Related

How to query from Multiple Tables

I have three Models
#Entity(foreignKeys = [ForeignKey(entity = SelfHelpGroup::class, parentColumns = ["shgId"], childColumns = ["shgId"], onDelete = CASCADE), ForeignKey(entity = Member::class, parentColumns = ["memberId"], childColumns = ["memberId"], onDelete = CASCADE)])
data class Committee(
#PrimaryKey(autoGenerate = true)
#SerializedName("committeeId")
val committeeId: Int?= null,
#SerializedName("shgId")
val shgId: Int?,
#SerializedName("memberId")
val memberId: Int?,
#SerializedName("date")
val date: String?
)
#Entity(tableName = "Member", foreignKeys = [ForeignKey(entity = SelfHelpGroup::class, parentColumns = ["shgId"], childColumns = ["shgId"], onDelete = CASCADE), ForeignKey(entity = Role::class, parentColumns = ["roleId"], childColumns = ["roleId"], onDelete = CASCADE)])
data class Member(
#PrimaryKey(autoGenerate = true)
#SerializedName("memberId")
val memberId: Int ?= null,
#SerializedName("shgId")
val shgId: Int,
#SerializedName("name")
val name: String,
#SerializedName("address")
val address: String,
#SerializedName("phoneNumber")
val phoneNumber: String,
#SerializedName("emailId")
val emailId: String,
#SerializedName("roleId")
val roleId: Int?,
#SerializedName("password")
val password: String?
)
#Entity(foreignKeys = [
ForeignKey(entity = Committee::class, parentColumns = ["committeeId"], childColumns = ["committeeId"], onDelete = CASCADE),
ForeignKey(entity = Member::class, parentColumns = ["memberId"], childColumns = ["memberId"], onDelete = CASCADE),
])
data class Attendance(
#PrimaryKey(autoGenerate = true)
#SerializedName("attendanceId")
val attendanceId: Int?= null,
#SerializedName("committeeId")
val committeeId: Int,
#SerializedName("memberId")
val memberId: Int,
/*#SerializedName("status")
val status: AttendanceStatus,*/
#SerializedName("isPresent")
var isPresent: Boolean = false,
#SerializedName("leaveApplied")
var leaveApplied: Boolean = false
)
Relation between 3 models :
Any member can host a committee.
The hosted memberId is saved in the table Member.
Other members can join the committee.
To track the attendance of these members, we are using the Table Attendance.
So I need help queriying the data in such a way that the result structure would look like below
data class CommitteeDetails (
val committeeId: Int,
val member: Member,
val attendances: List<Attendance>,
val dateTime: String
)
Since there are more than many committees, I need to query to get Listof CommitteeDetails
val committees = List<CommitteeDetails>()
The easiest way would be to use:-
data class CommitteeDetails (
//val committeeId: Int, /* part of the Committee so get the Committee in it's entireity */
#Embedded
val committee: Committee,
#Relation(entity = Member::class, parentColumn = "memberId", entityColumn = "memberId")
val member: Member,
#Relation(entity = Attendance::class, parentColumn = "committeeId", entityColumn = "committeeId")
val attendances: List<Attendance>
//val dateTime: String
)
This does then retrieve a little more information but the Query is very simple as you just query the committee table.
e.g. to get ALL Committees with the Member and the attendances then you can use
#Transaction
#Query("SELECT * FROM committee")
#Tranaction is not mandatory but if not used will result in a warning e.g.
warning: The return value includes a POJO with a `#Relation`. It is usually desired to annotate this method with `#Transaction` to avoid possibility of inconsistent results between the POJO and its relations. See https://developer.android.com/reference/androidx/room/Transaction.html for details.
This is because #Relation results in Room effectively running subqueries to obtain the related items. Note that using #Relation will return ALL the related items for the #Embedded.
IF you wanted to get exactly what you have asked for then it's a little more convoluted as you would then have to not use #Embedded for the Committee and thus you could then not use #Relation.
In theory you would SELECT FROM the committee table, JOIN the member table and also JOIN the attendance table. The issue is that the result is the cartesian map so for every attendance per committee you would get a result that contained the committee columns (id and date) the member columns (all to build the full Member class) and all the columns from the attendance. However, the issue, is then building the CommitteeDetails.
However, you can mimic how room works and just get the desired committee column along with the member columns and then invoke a subquery to obtain the related attendances (potentially filtering them).
So say you have (wanting a List of these as the end result):-
data class CommitteeDetailsExact (
val committeeId: Int,
val member: Member,
val attendances: List<Attendance>,
val dateTime: String
)
The to facilitate the initial extraction of the committee and members columns you could have another POJO such as:-
data class CommitteeIdAndDateAsDateTimeWithMember(
val committeeId: Int,
#Embedded
val member: Member,
val dateTime: String
)
To facilitate extracting the data you could have functions such as:-
#Query("SELECT committee.committeeId, committee.date AS dateTime, member.* FROM committee JOIN member ON committee.memberId = member.memberId")
fun getCommitteeExactLessAttendances(): List<CommitteeIdAndDateAsDateTimeWithMember>
#Query("SELECT * FROM attendance WHERE committeeId=:committeeId")
fun getCommitteeAttendances(committeeId: Int): List<Attendance>
To obtain the end result then the above functions need to be used together, so you could have a function such as:-
#Transaction
#Query("")
fun getExactCommitteeDetails(): List<CommitteeDetailsExact> {
var rv = arrayListOf<CommitteeDetailsExact>()
for (ciadadwm in getExactCommitteeDetails()) {
rv.add(CommitteeDetailsExact(ciadadwm.committeeId,ciadadwm.member,getCommitteeAttendances(ciadadwm.committeeId),ciadadwm.dateTime))
}
return rv.toList()
}
This will:-
return the desired list of committee details (albeit List<CommitteeDetailsExact> to suite the two answers given)
run as a single transaction (the #Query(") enables Room to apply the #Transaction)
Obtains the list of committee and member columns an then
Loops through the list extract the respective list of attendances
in short it, in this case, is very much similar to the first answer other than limiting the columns extracted from the committee table.

How to Query in a many to many relationship Room database?

I have a many to many relationship Room database with three tables:
First one :
data class Name(
#PrimaryKey(autoGenerate = true)
var nameId : Long = 0L,
#ColumnInfo(name = "name")
var name : String = "",
#ColumnInfo(name = "notes")
var notes: String=""
)
Second:
#Entity(tableName = "tags_table")
data class Tag(
#PrimaryKey(autoGenerate = true)
var tagId : Long = 0L,
#ColumnInfo(name = "tag_name")
var tagName : String = ""
)
Third:
#Entity(
tableName = "tagInName_table",
primaryKeys = ["nameId", "tagId"],
foreignKeys = [
ForeignKey(
entity = Name::class,
parentColumns = ["nameId"],
childColumns = ["nameId"]
),
ForeignKey(
entity = Tag::class,
parentColumns = ["tagId"],
childColumns = ["tagId"]
)
]
)
data class TagInName(
#ColumnInfo(name = "nameId")
var nameId: Long = 0L,
#ColumnInfo(name = "tagId")
var tagId: Long = 0L
)
The data class I use for a return object in a Query:
data class NameWithTags(
#Embedded
val name: Name,
#Relation(
parentColumn = "nameId",
entityColumn = "tagId",
associateBy = Junction(value = TagInName::class)
)
val listOfTag : List<Tag>
)
This is how I query to get all NamesWithTags:
#Query("SELECT * FROM names_table")
#Transaction
fun getNamesWithTags() : LiveData<List<NameWithTags>>
So the thing I need to do is, I need to Query to return LiveData<List<NameWithTags>> where every NamesWithTags has a list which contains the Tag ID that I Query for.
From my interpretation of what you say you need to do, then :-
#Transaction
#Query("SELECT names_table.* FROM names_table JOIN tagInName_table ON names_table.nameId = tagInName_table.nameId JOIN tags_table ON tagInName_table.tagId = tags_table.tagId WHERE tags_table.tagId=:tagId ")
fun getNameWithTagsByTagId(tagId: Long): LiveData<List<NamesWithTags>>
Note the above is in-principle code and has not been compiled or tested, so it may contain some errors.
A NameWithTags will contain ALL related tags whcih should be fine according to (where every NamesWithTags has a list which contains the Tag ID ), if you wanted just certain Tags in the List of Tags then it's a little more complex, this is explained in a recent answer at Android Room query with condition for nested object

Android Room "Entities and POJOs must have a usable public constructor"

I have 3 entities:
product_table
#Entity(tableName = "products_table")
data class Product (
#PrimaryKey(autoGenerate = true) val id: Int,
val name: String,
val price: Double
)
planned_lists_table
#Entity(tableName = "planned_lists_table")
data class PlannedList (
#PrimaryKey(autoGenerate = true) val id: Int,
val name: String,
val budget: Double
)
and selected_products_table
#Entity(tableName = "selected_products_table",
foreignKeys = [
ForeignKey(
entity = PlannedList::class,
parentColumns = ["id"],
onDelete = ForeignKey.CASCADE,
childColumns = ["productListId"])
])
data class SelectedProduct (
#PrimaryKey(autoGenerate = true) val id: Int,
val productListId: Int,
#Embedded(prefix = "ref_") val product: Product,
val amount: Int,
val checked: Boolean // checked if a user has gotten a product
)
I'm trying to figure out how to fetch data from both tables - product_table and selected_products_table. I've chosen #Embedded annotation, but now when I compile the project, Room says next:
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). - kotlin.Unit
I'm aware that I can't use embedded classes that contain something like List and my tables seem not to have any.
What do I need to do to fix the problem?
It appears that you are getting mixed up with the selected_products_table.
I believe that what you are looking for is like (see note) :-
#Entity(tableName = "selected_products_table",
foreignKeys = [
ForeignKey(
entity = Product::class,
parentColumns = ["id"],
onDelete = ForeignKey.CASCADE,
childColumns = ["productListId"])
])
data class SelectedProduct (
#PrimaryKey(autoGenerate = true) val id: Int,
#ColumnInfo(index = true) val productListId: Int,
/* you get the product via the join/relationship according to the productListId*/
//#Embedded(prefix = "ref_") val product: Product, /* you get the product via a join*/
val amount: Int,
val checked: Boolean // checked if a user has gotten a product
)
#ColumnInfo added to add an index on the ProductListId column
comments explain why you don't want the embedded here.
Note it is unclear whether you are relating to a PlannedList or a Product so Product has been used (easy to switch)
You probably want to present the list including the products details.
To extract this data you then have a POJO for the joined/related data, such as:-
data class SelectedProductList(
#Embedded
val selectedProduct: SelectedProduct,
#Relation(entity = Product::class,parentColumn = "productListId",entityColumn = "id")
val product: Product
)
This is what undertakes the JOIN and populates the product with the data from the products_table.
As an example using your code, the above code and the following :-
dao.insert(Product(id = 1,name = "Apples", price = 30.55))
dao.insert(Product(id = 2,name = "Pears", price = 25.70))
dao.insert(Product(id = 3,"Oranges",price = 47.23))
dao.insert(SelectedProduct(1,1,25,false))
dao.insert(SelectedProduct(2,3,10,true))
for (spl: SelectedProductList in dao.getSelectedList()) {
Log.d(
"DBINFO",
"Product is ${spl.product.name} " +
"price is ${spl.product.price} " +
"Order is for ${spl.selectedProduct.amount} " +
"cost is ${spl.selectedProduct.amount * spl.product.price} " +
"Checked is ${spl.selectedProduct.checked}"
)
}
The the result output to the log is :-
D/DBINFO: Product is Apples price is 30.55 Order is for 25 cost is 763.75 Checked is false
D/DBINFO: Product is Oranges price is 47.23 Order is for 10 cost is 472.29999999999995 Checked is true

Android Room cascade delete for a one to many relationship

Say I have two entities, Workout and Exercise and a one to many relationship exists between Workout (one) and Exercise (many). The entities are setup like this
Workout Entity:
#Entity(
tableName = "workouts",
indices = [Index("startDate")]
)
data class Workout(
#PrimaryKey
val startDate: String,
val workoutName: String
)
Exercise Entity:
#Entity
data class Exercise(
#PrimaryKey(autoGenerate = true)
val exerciseId: Long = 0,
val workoutId: String,
val name: String
)
Workout with Exercises:
#Entity(
foreignKeys = [ForeignKey(
entity = Workout::class,
parentColumns = arrayOf("startDate"),
childColumns = arrayOf("workoutId"),
onDelete = ForeignKey.CASCADE
)]
)
data class Exercise(
#PrimaryKey(autoGenerate = true)
val exerciseId: Long = 0,
val workoutId: String,
val name: String
)
This is how I get the exercises related to a workout:
#Transaction
#Query("SELECT * FROM workouts WHERE startDate = :startDate")
suspend fun getWorkoutWithExercises(startDate: String): WorkoutWithExercises
So my question is, if the workout instance containing exercises is deleted, will the related exercises also be deleted? If not, how would this be accomplished?
Thanks
The exercises will also be deleted as you have created a Foreign Key for the table Exercise.

Get column from another entity using foreign keys in Room

I have two different entities. One has two references to the other one and I need to get a attribute of the reference.
my_main_table.primary_type is a foreign key of types._id and my_main_table.secondary_type is a foreign key of types._id that can be null.
Is a prepopulated database copied using RoomAsset library, so the scheme is already done in the database. Here is the diagram of the database:
Here is my main entity:
#Entity(
tableName = "my_main_table",
foreignKeys = [
ForeignKey(
entity = Type::class,
parentColumns = ["_id"],
childColumns = ["secondary_type"],
onDelete = ForeignKey.RESTRICT,
onUpdate = ForeignKey.RESTRICT
),
ForeignKey(
entity = Type::class,
parentColumns = ["_id"],
childColumns = ["primary_type"],
onDelete = ForeignKey.RESTRICT,
onUpdate = ForeignKey.RESTRICT
)
]
)
data class MainTable(
#PrimaryKey
#ColumnInfo(name = "_id", index = true)
val id: Int,
#ColumnInfo(name = "number")
val number: String,
#ColumnInfo(name = "name")
val name: String,
#ColumnInfo(name = "primary_type")
val primaryType: String,
#ColumnInfo(name = "secondary_type")
val secondaryType: String?
)
And here is my reference:
#Entity(tableName = "types")
data class Type(
#PrimaryKey
#ColumnInfo(name = "_id")
val id: Int,
#ColumnInfo(name = "name")
val name: String
)
Finally the SQL code for #Query:
SELECT p._id AS _id,
p.number AS number,
p.name AS name,
pt.name AS primary_type,
st.name AS secondary_type
FROM my_main_table p
INNER JOIN types pt ON p.primary_type == pt._id
LEFT JOIN types st ON p.secondary_type == st._id
What I want is to get the value of types.name throught the relation. But I can't figure out how. Should I need another method in my repository to get the value of the name?
Thanks.

Categories

Resources