Android Native - Define one-to-many relationship in Room

dimitrilc 1 Tallied Votes 1K Views Share

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:

  1. How to define one-to-many relationship for entities in Room.

Tools Required

  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.

Prerequisite Knowledge

  1. Intermediate Android.
  2. SQL.
  3. Basic Room database.
  4. Kotlin coroutines.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. 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'
  3. In the same file, add the kapt plugin under plugins

     id 'kotlin-kapt'
  4. 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
     )
  5. 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
     )
  6. Create a new StudentDao using the code below.

     @Dao
     interface StudentDao {
        @Insert
        suspend fun insertStudents(vararg students: Student)
     }
  7. Create a new ClassRoomDao using the code below.

     @Dao
     interface ClassRoomDao {
        @Insert
        suspend fun insertClassRoom(classRoom: ClassRoom)
     }
  8. 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.

  1. 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 the classRoomId 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
     )
  2. 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.

  3. 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 class ClassRoomWithStudent using the code below.

     data class ClassRoomWithStudent(
        @Embedded val classRoom: ClassRoom,
        val students: List<Student>
     )
  4. If you are not sure what @Embedded does, you can check out the tutorial on it here.
    Annotate the child students with @Relation and fill out its parentColumn and entityColumn 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:

  1. 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>
  2. This is a query, so add a @Query annotation to it. Notice that we are simply querying the class_room table here, not class_room_with_student (does not exist) or student.

     @Query("SELECT * FROM class_room")
     suspend fun getAllClassRoomWithStudent(): List<ClassRoomWithStudent>
  3. 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>
  4. Append the code below to MainActivity#onCreate() to create the database instance.

     val db = Room.databaseBuilder(
        applicationContext,
        MyDatabase::class.java, "my-database"
     ).build()
  5. Now, launch a new coroutine with the code below.

     lifecycleScope.launch(Dispatchers.IO) {
    
     }
  6. 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
     )
  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")
     }
  8. 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.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.