Android Native - How to embed an Entity inside of another Entity

dimitrilc 1 Tallied Votes 152 Views Share

Introduction

There are many ways to describe relationships between Entities in Room, one of which is to embed an entity inside of another. When embedding an Entity, the columns of the embedded Entity are extracted as member columns of the enclosing entity.

In this tutorial, we will learn how to embed an Entity in a Room database.

Goals

At the end of the tutorial, you would have learned:

  1. How to embed an Entity inside of another Entity.

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 new Kotlin data class for an Entity called SoccerTeam using the code below.

     @Entity(tableName = "soccer_team")
     data class SoccerTeam(
        @PrimaryKey(autoGenerate = true) val id: Long
     )
  5. Create a new Kotlin data class for an Entity called HeadCoach using the code below.

     @Entity(tableName = "head_coach")
     data class HeadCoach(
        @PrimaryKey(autoGenerate = true)  val id: Long,
        val name: String,
        val age: Int,
     )
  6. Create a new Dao for SoccerTeam using the code below.

     @Dao
     interface SoccerTeamDao {
        @Insert
        suspend fun insert(team: SoccerTeam)
    
        @Query("SELECT * FROM soccer_team")
        suspend fun getAll(): List<SoccerTeam>
    
     }
  7. Create a new Dao for HeadCoach using the code below.

     @Dao
     interface HeadCoachDao {
        @Insert
        suspend fun insert(coach: HeadCoach)
    
        @Query("SELECT * FROM head_coach")
        suspend fun getAll(): List<HeadCoach>
    
     }
  8. Create the abstract class MyDatabase using the code below.

     @Database(entities = [SoccerTeam::class, HeadCoach::class], version = 1)
     abstract class MyDatabase : RoomDatabase() {
        abstract fun soccerTeamDao(): SoccerTeamDao
        abstract fun headCoachDao(): HeadCoachDao
     }
  9. 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,
        MyDatabase::class.java, "my-database"
     ).build()
    
     lifecycleScope.launch(Dispatchers.IO) {
        db.soccerTeamDao().getAll()
     }

Project Overview

For this tutorial, we will only be working with the database, so we are not concerned about the frontend at all.

Currently, we have two entities, SoccerTem and HeadCoach, that are not related in any way. In the real world, every soccer team should have one head coach, so we would need to add a HeadCoach as a member of the team somehow. There are a couple of ways to describe this relationship:

  1. One-to-one (probably too tightly coupled because the head coach can be replaced)).
  2. Include a nullable ID of the head_coach in the soccer_team table, and vice versa, with a Foreign Key constraint to soccer_team.
  3. Embed a head_coach inside the soccer_team. This approach should work for our simple use case. Note that HeadCoach does not have to be an Entity itself to be embedded.

At the end of the tutorial, we should have written code to embed HeadCoach inside of SoccerTeam.

How to embed an Entity

Embedding an Entity is quite straight forward. We only need a few things:

  1. The parent Entity. It is aware of the child (embedded) Entity. It needs to have a member with the same type as the embedded Entity.
  2. The child Entity. It is not required to know of the parent.
  3. The @Embedded annotation. You will have to apply this to the member field in the parent Entity. You might be familiar with this if you have worked with JPA before.
  4. Be aware of the columns in both Entities, you might want to prefix columns of the embedded entities by passing a value to the prefix parameter of @Embedded. This prevents duplicate column names from clashing.

Follow the steps below to embed HeadCoach inside of SoccerTeam.

  1. Open SoccerTeam.kt.

  2. Add a headCoach member to it (typed to HeadCoach). And then annotate it with @Embedded.

  3. Because both HeadCoach and SoccerTeam have an identical id column, pass a value of head_coach_ to prefix for the @Embedded annotation. Do not forget the underscore (_) at the end of the prefix string; we want our column to look like head_coach_id instead of head_coachid.

  4. Your SoccerTeam Entity should now look like the code below.

     @Entity(tableName = "soccer_team")
     data class SoccerTeam(
        @PrimaryKey(autoGenerate = true) val id: Long,
        @Embedded(prefix = "head_coach_") val headCoach: HeadCoach
     )

Launch the App

Upon starting the app and using the Database Inspector, we can see that the database looks like below.

database.jpg

One thing that you will have to be aware of is that, If we insert a row in soccer_team with the command below,

    INSERT INTO soccer_team VALUES (1, 1, "John", "Doe")

head_coach would still be empty.

Solution Code

MainActivity.kt

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) {
           db.soccerTeamDao().getAll()
       }

   }
}

HeadCoach.kt

@Entity(tableName = "head_coach")
data class HeadCoach(
   @PrimaryKey(autoGenerate = true)  val id: Long,
   val name: String,
   val age: Int,
)

HeadCoachDao.kt

@Dao
interface HeadCoachDao {
   @Insert
   suspend fun insert(coach: HeadCoach)

   @Query("SELECT * FROM head_coach")
   suspend fun getAll(): List<HeadCoach>

}

MyDatabase.kt

@Database(entities = [SoccerTeam::class, HeadCoach::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
   abstract fun soccerTeamDao(): SoccerTeamDao
   abstract fun headCoachDao(): HeadCoachDao
}

SoccerTeam.kt

@Entity(tableName = "soccer_team")
data class SoccerTeam(
   @PrimaryKey(autoGenerate = true) val id: Long,
   @Embedded(prefix = "head_coach_") val headCoach: HeadCoach
)

SoccerTeamDao.kt

@Dao
interface SoccerTeamDao {
   @Insert
   suspend fun insert(team: SoccerTeam)

   @Query("SELECT * FROM soccer_team")
   suspend fun getAll(): List<SoccerTeam>

}

build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   id 'kotlin-kapt'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebembedentity"
       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.3.0'
   implementation 'com.google.android.material:material:1.4.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
   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 embed an Entity inside of another Entity in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebEmbedEntity

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.