While both of these concepts are used to bring structure to your Room database, their use case differs in that:
@ForeignKey
is used to enforce relational structure when INSERTING / MODYFING your data
@Relation
is used to enforce relational structure when RETRIEVING / VIEWING your data.
To better understand the need for ForeignKeys
consider the following example:
@Entity
data class Artist(
@PrimaryKey val artistId: Long,
val name: String
)
@Entity
data class Album(
@PrimaryKey val albumId: Long,
val title: String,
val artistId: Long
)
The applications using this database are entitled to assume that for each row in the Album table there exists a corresponding row in the Artist table. Unfortunately, if a user edits the database using an external tool or if there is a bug in an application, rows might be inserted into the Album table that do not correspond to any row in the Artist table. Or rows might be deleted from the Artist table, leaving orphaned rows in the Album table that do not correspond to any of the remaining rows in Artist. This might cause the application or applications to malfunction later on, or at least make coding the application more difficult.
One solution is to add an SQL foreign key constraint to the database schema to enforce the relationship between the Artist and Album table.
@Entity
data class Artist(
@PrimaryKey val id: Long,
val name: String
)
@Entity(
foreignKeys = [ForeignKey(
entity = Artist::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("artistId"),
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)]
)
data class Album(
@PrimaryKey val albumId: Long,
val title: String,
val artistId: Long
)
Now whenever you insert a new album, SQL checks if there exists a artist with that given ID and only then you can go ahead with the transaction. Also if you update an artist's information or remove it from the Artist table, SQL checks for any albums of that artist and updates / deletes them. That's the magic of ForeignKey.CASCADE
!
But this doesn't automatically make them return together during a Query, so enter @Relation
:
// Our data classes from before
@Entity
data class Artist(
@PrimaryKey val id: Long,
val name: String
)
@Entity(
foreignKeys = [ForeignKey(
entity = Artist::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("artistId"),
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)]
)
data class Album(
@PrimaryKey val albumId: Long,
val title: String,
val artistId: Long
)
// Now embedded for efficient querying
data class ArtistAndAlbums(
@Embedded val artist: Artist,
@Relation(
parentColumn = "id",
entityColumn = "artistId"
)
val album: List<Album> // <-- This is a one-to-many relationship, since each artist has many albums, hence returning a List here
)
Now you can easily fetch list of artists and their albums with the following:
@Transaction
@Query("SELECT * FROM Artist")
fun getArtistsAndAlbums(): List<ArtistAndAlbums>
While previously you had to write long boilerplate SQL queries to join and return them.
Note: The @Transaction
annotation is required to make SQLite execute the two search queries (one lookup in the Artist table and one lookup in the Album table) in one go and not separately.
Sources:
Excerpts from Android Developers Documentations:
Sometimes, you'd like to express an entity or data object as a cohesive whole in your database logic, even if the object contains several fields. In these situations, you can use the @Embedded annotation to represent an object that you'd like to decompose into its subfields within a table. You can then QUERY the embedded fields just as you would for other individual columns.
Foreign keys allows you to specify constraints across Entities such that SQLite will ensure that the relationship is valid when you MODIFY the database.
SQLite's ForeignKey documentation.