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:
- How to embed an Entity inside of another Entity.
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 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 )
-
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, )
-
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> }
-
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> }
-
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 }
-
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:
- One-to-one (probably too tightly coupled because the head coach can be replaced)).
- Include a nullable ID of the
head_coach
in thesoccer_team
table, and vice versa, with a Foreign Key constraint tosoccer_team
. - Embed a
head_coach
inside thesoccer_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:
- 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.
- The child Entity. It is not required to know of the parent.
- 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. - 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.
-
Open SoccerTeam.kt.
-
Add a
headCoach
member to it (typed to HeadCoach). And then annotate it with @Embedded. -
Because both HeadCoach and SoccerTeam have an identical
id
column, pass a value ofhead_coach_
toprefix
for the @Embedded annotation. Do not forget the underscore (_) at the end of theprefix
string; we want our column to look likehead_coach_id
instead ofhead_coachid
. -
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.
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