I am working with room persistence library in android, i would appreciate if someone can help me in using foreign key, how to get data by using foreign key.
Just to summarize the above posts for future readers:
The foreign key syntax in Kotlin is
#Entity(foreignKeys = arrayOf(ForeignKey(entity = ParentClass::class,
parentColumns = arrayOf("parentClassColumn"),
childColumns = arrayOf("childClassColumn"),
onDelete = ForeignKey.CASCADE)))
The foreign key syntax in Java is:
#Entity(foreignKeys = {#ForeignKey(entity = ParentClass.class,
parentColumns = "parentClassColumn",
childColumns = "childClassColumn",
onDelete = ForeignKey.CASCADE)
})
Note: foreignKeys is an array, so in Java enclose #ForeignKey elements in { and }
You can refer to the official documentation for more information.
https://developer.android.com/reference/androidx/room/ForeignKey
Here how you can define and access a One-to-many (Foreign Key) relationship in Android Jetpack Room. Here the Entities are Artist and Album and the foreign key is Album.artist
#Entity
data class Artist(
#PrimaryKey
val id: String,
val name: String
)
#Entity(
foreignKeys = [ForeignKey(
entity = Artist::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("artist"),
onDelete = ForeignKey.CASCADE
)]
)
data class Album(
#PrimaryKey
val albumId: String,
val name: String,
#ColumnInfo(index = true)
val artist: String
)
then the embedded object (read official Android documentation: Define relationships between objects)
data class ArtistAndAlbums(
#Embedded
val artist: Artist,
#Relation(
parentColumn = "id",
entityColumn = "artist"
)
val albums: List<Album>
)
And finally the DAO
#Dao
interface Library {
#Insert
suspend fun save(artist: Artist)
#Insert
suspend fun save(vararg album: Album)
#Transaction
#Query("SELECT * FROM artist")
suspend fun getAll(): List<ArtistAndAlbums>
#Transaction
#Query("SELECT * FROM artist WHERE id = :id")
suspend fun getByArtistId(id: String): ArtistAndAlbums
}
trying this out, (all these operations happens in a Coroutine)
// creating objects
val artist = Artist(id="hillsongunited", name="Hillsong United" )
val artist2 = Artist(id="planetshakers", name="Planet Shakers" )
val album1 = Album(albumId = "empires", name = "Empires", artist = artist.id)
val album2 = Album(albumId = "wonder", name = "Wonder", artist = artist.id)
val album3 = Album(albumId = "people", name = "People", artist = artist.id)
val album4 = Album(albumId = "rain", name = "Rain", artist = artist2.id)
val album5 = Album(albumId = "itschristmas", name = "Its Christmas", artist = artist2.id)
val album6 = Album(albumId = "overitall", name = "Over It All", artist = artist2.id)
// saving to database
SongDatabase.invoke(applicationContext).library().save(artist)
SongDatabase.invoke(applicationContext).library().save(artist2)
SongDatabase.invoke(applicationContext).library().save(album1, album2, album3, album4, album5, album6)
Logging out all Artists
val all = SongDatabase.invoke(applicationContext).library().getAll()
Log.d("debug", "All Artists $all ")
D/debug: All Artists [ArtistAndAlbums(artist=Artist(id=hillsongunited, name=Hillsong United), albums=[Album(albumId=empires, name=Empires, artist=hillsongunited), Album(albumId=wonder, name=Wonder, artist=hillsongunited), Album(albumId=people, name=People, artist=hillsongunited)]), ArtistAndAlbums(artist=Artist(id=planetshakers, name=Planet Shakers), albums=[Album(albumId=rain, name=Rain, artist=planetshakers), Album(albumId=itschristmas, name=Its Christmas, artist=planetshakers), Album(albumId=overitall, name=Over It All, artist=planetshakers)])]
Logging out albums by a specific artist,
val hillsongAlbums = SongDatabase.invoke(applicationContext).library().getByArtistId(artist.id)
Log.d("debug", "Albums by artist ID: $hillsongAlbums ")
D/debug: Albums by artist ID: ArtistAndAlbums(artist=Artist(id=hillsongunited, name=Hillsong United), albums=[Album(albumId=empires, name=Empires, artist=hillsongunited), Album(albumId=wonder, name=Wonder, artist=hillsongunited), Album(albumId=people, name=People, artist=hillsongunited)])
#ForeignKey annotations are not used to define relations when getting data but to define relations when modifying data. To get relational data from a Room databse, Google recommends the #Relation along with the #Embedded annotation.
You can check out my answer here for more explanation if you're interested.
Related
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.
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
I have the typical one-to-one relationship between two tables
One entity/table has documents in it
#Entity(
tableName = "documents",
foreignKeys = [
(ForeignKey(
entity = Document::class,
parentColumns = ["id"],
childColumns = ["parentId"],
onDelete = ForeignKey.CASCADE
)),
(ForeignKey(
entity = Attachment::class,
parentColumns = ["id"],
childColumns = ["attachmentId"],
onDelete = ForeignKey.CASCADE
))
],
indices = [
Index(value = ["parentId"], unique = true),
Index(value = ["attachmentId"])
]
)
class Document(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
#Nullable
val parentId: Long? = null,
val title: String,
val description: String? = "",
#Nullable
var attachmentId: Long? = null
)
and then I have this for attachments
#Entity(tableName = "attachments")
class Attachment(
#PrimaryKey(autoGenerate = true)
val id: Long,
val type: FileType = FileType.RAW,
val title: String? = "",
#ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val data: ByteArray? = null
)
enum class FileType {
RAW, PDF, IMAGE
}
My DAO for documents is this:
#Dao
interface DocumentDao {
#Query("SELECT * FROM documents")
fun getAll(): LiveData<List<Document>>
#Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(doc: Document)
#Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(attachment: Attachment): Long
#Transaction
suspend fun insert(doc: Document, attachment: Attachment) {
val id = insert(attachment)
doc.attachmentId= id
insert(doc)
}
#Update
suspend fun update(vararg doc: Document?)
#Update
suspend fun update(vararg attachment: Attachment?)
#Transaction
suspend fun update(doc: Document, attachment: Attachment) {
if (attachment.id == 0L) {
val id = insert(attachment)
doc.attachmentId= id
} else {
update(attachment)
}
update(doc)
}
#Delete
suspend fun delete(doc: Document)
#Delete
suspend fun delete(attachment: Attachment)
}
I can add documents with attachments, update them, add child documents (for which I use the parentId foreign key).
My problem is that attachments are not deleted when parent documents are deleted, even if I can clearly see in the table that the id of the attachment has been correctly added. Child documents, referenced by parentId are deleted in accordance to onDelete = ForeignKey.CASCADE rule but attachments are not.
The generated SQL that creates the table looks like this:
CREATE TABLE `documents` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `parentId` INTEGER, `title` TEXT NOT NULL, `description` TEXT, `attachmentId` INTEGER, FOREIGN KEY(`parentId`) REFERENCES `documents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`attachmentId`) REFERENCES `attachments`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )
I really do not understand why one foreign key (parentId) works when deleting parent rows but the other one (attachmentId) does not
Let's say you have:
Table Attachment, one row with id = 1, second with id = 2
Table Document, row with id = 1, attachmentId = 2.
As I understood, you want after deleting row in Document table - row in Attachment table with id = 2 to be deleted as well. But for that you have to change your db structure. Instead add foreign key documentId to Attachment table.
#Entity(tableName = "attachments",
foreignKeys = [
(ForeignKey(
entity = Document::class,
parentColumns = ["id"],
childColumns = ["documentId"],
onDelete = ForeignKey.CASCADE
)]
)
class Attachment(
#PrimaryKey(autoGenerate = true)
val id: Long,
val documentId: Long, // <-- Foreign key to Document Entity
val type: FileType = FileType.RAW,
val title: String? = "",
#ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val data: ByteArray? = null
)
Since while deleting some row Sqlite looks all foreign keys in all other tables that are binded to that row and then deletes them.
UPDATE: Addition below is misleading and shouldn't be taken into account
And, in addition, are you sure your child rows really were deleted after deleting parent one? It seems you have to change parentColums and childColumns
ForeignKey(
entity = Document::class,
parentColumns = ["id"], // <-- should be childColumns instead?
childColumns = ["parentId"], // <-- should be parentColumns instead?
onDelete = ForeignKey.CASCADE
))
I ended up removing the foreign key for attachments and deleting them in a transaction
#Query("DELETE FROM attachments WHERE id = :attachmentId")
fun deleteAttachment(attachmentId: Long)
#Transaction
suspend fun deleteWithAttachment(doc: Document) {
if (doc.attachmentId != null) {
deleteAttachment(doc.attachmentId!!)
}
delete(doc)
}
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.
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