Introduction
When working with Room, you might have wondered how to describe one-to-many relationships between entities. In this tutorial, we will learn how to do just that.
Goals
At the end of the tutorial, you would have learned:
- How to define one-to-many relationship for entities in Room.
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 ClassRoom entity using the code below.
@Entity(tableName = "class_room") data class ClassRoom( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "class_room_id") val classRoomId: Long = 0 )
-
Create a new Student entity using the code below.
@Entity(tableName = "student") data class Student( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "student_id") val studentId: Long = 0, val name: String, val age: Int )
-
Create a new StudentDao using the code below.
@Dao interface StudentDao { @Insert suspend fun insertStudents(vararg students: Student) }
-
Create a new ClassRoomDao using the code below.
@Dao interface ClassRoomDao { @Insert suspend fun insertClassRoom(classRoom: ClassRoom) }
-
Create a MyDatabase class using the code below.
@Database(entities = [ClassRoom::class, Student::class], version = 1) abstract class MyDatabase : RoomDatabase() { abstract fun classRoomDao(): ClassRoomDao abstract fun studentDao(): StudentDao }
Project Overview
Our project so far only contains two entities, Student and ClassRoom. One crucial step for our tutorial is that we must specify which entity is the parent and which is the child. For simplicity, we will choose ClassRoom as the parent and Student as the child; this means that one ClassRoom can contain many students.
Defining one-to-many relationship
To define a one-to-many relationship in Room, there is a little bit of boilerplate involved. Follow the steps below to define a one-ClassRoom-to-many-Students relationship for our Project.
-
The child must reference the primary key of the parent as a property. In this case, the primary key of ClassRoom would just be
classRoomId
. Add theclassRoomId
to Student with the code below.@Entity(tableName = "student") data class Student( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "student_id") val studentId: Long = 0, val name: String, val age: Int, @ColumnInfo(name = "class_room_id") val classRoomId: Long )
-
Optionally, you can add a foreign key here, if it makes sense, to improve data integrity, since each Student must belong to an existing ClassRoom anyways.
-
In order to map this relationship, we will need to create a new data class to act as a glue between ClassRoom and
List<Student>
. Create a new data classClassRoomWithStudent
using the code below.data class ClassRoomWithStudent( @Embedded val classRoom: ClassRoom, val students: List<Student> )
-
If you are not sure what
@Embedded
does, you can check out the tutorial on it here.
Annotate the childstudents
with@Relation
and fill out itsparentColumn
andentityColumn
parameters.data class ClassRoomWithStudent( @Embedded val classRoom: ClassRoom, @Relation( parentColumn = "class_room_id", entityColumn = "class_room_id" ) val students: List<Student> )
And that is it for the relationship mapper, but there is still more to do if you want to make use of the relationship wrapper class.
Interact with the wrapper relationship class
To use the wrapper class, follow the steps below:
-
Add the function
getAllClassRoomWithStudent()
to ClassRoomDao using the code below. The most important part of this function is its return type. It must return the relationship wrapper class.suspend fun getAllClassRoomWithStudent(): List<ClassRoomWithStudent>
-
This is a query, so add a
@Query
annotation to it. Notice that we are simply querying theclass_room
table here, notclass_room_with_student
(does not exist) orstudent
.@Query("SELECT * FROM class_room") suspend fun getAllClassRoomWithStudent(): List<ClassRoomWithStudent>
-
Because the individual fields of the relationship wrapper will be queried individually, we need to add
@Transaction
to it.@Transaction @Query("SELECT * FROM class_room") suspend fun getAllClassRoomWithStudent(): List<ClassRoomWithStudent>
-
Append the code below to
MainActivity#onCreate()
to create the database instance.val db = Room.databaseBuilder( applicationContext, MyDatabase::class.java, "my-database" ).build()
-
Now, launch a new coroutine with the code below.
lifecycleScope.launch(Dispatchers.IO) { }
-
Inside of the coroutine, add the code below to create sample Student and ClassRoom objects.
val classRoomId = 1L val classRoom = ClassRoom(classRoomId) val studentA = Student( name = "John", age = 6, classRoomId = classRoomId ) val studentB = studentA.copy( name = "Mary", age = 7 )
-
Run the Dao functions in the order below. We obviously must wait for the Student and ClassRoom to be persisted first before we can query them.
db.withTransaction { db.classRoomDao().insertClassRoom(classRoom) db.studentDao().insertStudents(studentA, studentB) val result = db.classRoomDao().getAllClassRoomWithStudent() Log.d(TAG, "$result") }
-
Upon running the app, we should see the ClassRoomWithStudent object printed to the console.
2022-03-22 19:52:20.953 7980-8016/com.example.daniwebandroidroomonetomany D/MAIN_ACTIVITY: [ClassRoomWithStudent(classRoom=ClassRoom(classRoomId=1), students=[Student(studentId=1, name=John, age=6, classRoomId=1), Student(studentId=2, name=Mary, age=7, classRoomId=1)])]
Solution Code
MainActivity.kt
private const val TAG = "MAIN_ACTIVITY"
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val db = Room.databaseBuilder(
applicationContext,
MyDatabase::class.java, "my-database"
).build()
lifecycleScope.launch(Dispatchers.IO) {
val classRoomId = 1L
val classRoom = ClassRoom(classRoomId)
val studentA = Student(
name = "John",
age = 6,
classRoomId = classRoomId
)
val studentB = studentA.copy(
name = "Mary",
age = 7
)
db.withTransaction {
db.classRoomDao().insertClassRoom(classRoom)
db.studentDao().insertStudents(studentA, studentB)
val result = db.classRoomDao().getAllClassRoomWithStudent()
Log.d(TAG, "$result")
}
}
}
}
ClassRoom.kt
@Entity(tableName = "class_room")
data class ClassRoom(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "class_room_id")
val classRoomId: Long = 0
)
ClassRoomDao.kt
@Dao
interface ClassRoomDao {
@Insert
suspend fun insertClassRoom(classRoom: ClassRoom)
@Transaction
@Query("SELECT * FROM class_room")
suspend fun getAllClassRoomWithStudent(): List<ClassRoomWithStudent>
}
ClassRoomWithStudent.kt
data class ClassRoomWithStudent(
@Embedded val classRoom: ClassRoom,
@Relation(
parentColumn = "class_room_id",
entityColumn = "class_room_id"
)
val students: List<Student>
)
MyDatabase.kt
@Database(entities = [ClassRoom::class, Student::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
abstract fun classRoomDao(): ClassRoomDao
abstract fun studentDao(): StudentDao
}
Student.kt
@Entity(tableName = "student")
data class Student(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "student_id")
val studentId: Long = 0,
val name: String,
val age: Int,
@ColumnInfo(name = "class_room_id") val classRoomId: Long
)
StudentDao.kt
@Dao
interface StudentDao {
@Insert
suspend fun insertStudents(vararg students: Student)
}
Module build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.example.daniwebandroidroomonetomany"
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 {
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 map one-to-many relationships in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidRoomOneToMany.