Introduction
When working with Room, there might come a situation during bulk CRUD operations that it would be useful to add a Foreign Key to one of our tables. This provides two main benefits: cascading delete (must be configured manually) and enforcing data integrity on linked entities.
In this tutorial, we will learn how to add Foreign Keys into Room entities.
Goals
At the end of the tutorial, you would have learned:
- How to add Foreign Keys to Room entities.
Tools Required
- Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.
Prerequisite Knowledge
- Intermediate Android.
- SQL.
- Basic Room database.
- Kotlin coroutines.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Add the dependencies below for Room into the Module build.gradle.
def room_version = "2.4.2" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
-
In the same file, add the kapt plugin under plugins
id 'kotlin-kapt'
-
Create a new @Entity called Student with the code below.
@Entity data class Student( @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String )
-
Create another @Entity called ReportCard with the code below.
@Entity(tableName = "report_card") data class ReportCard( @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo(name = "student_id") val studentId: Long )
-
Create a new empty DAO for the Student entity.
@Dao interface StudentDao { @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertStudent(student: Student): Long @Query("SELECT * FROM student WHERE id=:id") suspend fun getStudentById(id: Long): Student? }
-
Create the abstract class MyRoomDB with the code below.
@Database(entities = [Student::class, ReportCard::class], version = 1) abstract class MyRoomDB : RoomDatabase() { abstract fun studentDao(): StudentDao }
-
Append the code below to MainActivity
onCreate()
. This creates an instance of the database and then attempts to perform a query on the empty database.val db = Room.databaseBuilder( applicationContext, MyRoomDB::class.java, "my-room-db" ).build() lifecycleScope.launch { db.studentDao().getStudentById(1) }
-
For most of the tutorial, we will interact with the database via the Database Inspector, and not in code. The DAO query above performs a dummy connection so that the Database Inspector can keep the database connection open in debugging mode. Run the app in Debug mode and then switch to the App Inspection window. Verify that the database connection stays open. Later on, we can interact with the database directly using a Query tab.
Project Overview
For this tutorial, we are completely ignoring the frontend. We will only focus on the interaction with the database via Room.
We currently have two entities, Student and ReportCard. The Student entity is not aware of the ReportCard entity, but the ReportCard entity is dependent on the Student entity. Each ReportCard contains its own ID as well as the associated Student ID.
There is no foreign key constraint to Student in ReportCard, so it is possible that a ReportCard might be referencing a Student who does not exist in the student
table. At the end of the tutorial, we should have created a Foreign Key for the ReportCard entity; this way we can ensure that each ReportCard can only reference a valid Student.
The Problem
First, in order to have a better understanding of the problems that a Foreign Key can solve, let us walk through an example where data integrity is violated.
The current student
table is empty, but we can INSERT
new ReportCard into report_card
just fine, referencing non-existent students.
INSERT INTO report_card (id, student_id) VALUES
(1, 30),
(2, 2)
If we query the join for the two tables, we would receive nothing because those student_id
do not exist in the student
table.
SELECT * FROM report_card INNER JOIN student ON report_card.student_id=student.id
Foreign Key
Fortunately for us, we can create a Foreign Key to an entity so that the database can throw errors when we try to violate this constraint. One thing to keep in mind is that this does not prevent developers from writing code that violates the constraint; only via runtime testing that data inconsistencies will show up with a SQL exception.
To apply a Foreign Key, we can simply pass in ForeignKey objects to Entity’s foreignKey
parameter. entity
, childColumns
, and parentColumns
are required by ForeignKey.
@Entity(tableName = "report_card",
foreignKeys = [ForeignKey(
entity = Student::class,
childColumns = ["student_id"],
parentColumns = ["id"]
)])
data class ReportCard(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "student_id") val studentId: Long
)
After adding the Foreign Key, you can re-install the App so Room can make use of the new constraint.
If we attempt to run the same INSERT into report_card
again, Room will not allow us and throw an exception instead.
E/SQLiteQuery: exception: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY); query: INSERT INTO report_card (id, student_id) VALUES
(1, 30),
(2, 2)
In order for the INSERT into report_card
to work, valid students must exist. INSERT new Students with the statement below.
INSERT INTO student VALUES
(2, "Mary", "Anne"),
(30, "John", "Doe")
After this, you can INSERT into report_card
for the valid student IDs.
INSERT INTO report_card (id, student_id) VALUES
(1, 30),
(2, 2)
If we run the JOIN query again, we can see that it returns all of the data correctly.
Solution Code
MainActivity.kt
package com.codelab.daniwebandroidroomforeignkey
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.room.Room
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val db = Room.databaseBuilder(
applicationContext,
MyRoomDB::class.java, "my-room-db"
).build()
lifecycleScope.launch {
db.studentDao().getStudentById(1)
}
}
}
MyRoomDB.kt
package com.codelab.daniwebandroidroomforeignkey
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [Student::class, ReportCard::class], version = 1)
abstract class MyRoomDB : RoomDatabase() {
abstract fun studentDao(): StudentDao
abstract fun reportCardDao(): ReportCardDao
}
ReportCard.kt
package com.codelab.daniwebandroidroomforeignkey
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(tableName = "report_card",
foreignKeys = [ForeignKey(
entity = Student::class,
childColumns = ["student_id"],
parentColumns = ["id"]
)])
data class ReportCard(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "student_id") val studentId: Long
)
Student.kt
package com.codelab.daniwebandroidroomforeignkey
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "student")
data class Student(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "first_name") val firstName: String,
@ColumnInfo(name = "last_name") val lastName: String
)
StudentDao.kt
package com.codelab.daniwebandroidroomforeignkey
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface StudentDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertStudent(student: Student): Long
@Query("SELECT * FROM student WHERE id=:id")
suspend fun getStudentById(id: Long): Student?
}
Module build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.codelab.daniwebandroidroomforeignkey"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
//Room deps
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary
We have learned how to create Foreign Keys on Room Entities. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidRoomForeignKey