Table of Contents
Why Room Uses Entities
Room is a library that helps you work with a SQLite database using Kotlin objects. The main way you tell Room what your database looks like is through entities. An entity describes a table in the database and each property of the entity describes a column in that table.
You do not work directly with SQL table definitions. Instead, you create Kotlin data classes annotated with Room annotations. Room then generates all the necessary SQLite code behind the scenes.
In this chapter you focus only on how to define these entity classes, how Room maps them to database tables, and which annotations are important on the entity layer.
Defining a Basic Entity
A Room entity is usually a Kotlin data class annotated with @Entity. At minimum, an entity must have a primary key. Room uses reflection and code generation to map the class and its properties to a table and its columns.
Here is a very simple entity that represents a user:
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class User(
@PrimaryKey val id: Long,
val name: String,
val age: Int
)
By default, Room will create a SQLite table named User and columns id, name, and age. The @PrimaryKey annotation tells Room which column is the primary key of the table.
Every Room entity must have a primary key. If you do not define one, Room will report a compile time error.
Customizing Table and Column Names
Often you do not want the table name to be exactly the same as the class name. You can change it using the tableName parameter in the @Entity annotation.
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Long,
val name: String,
val age: Int
)
Now Room will create a users table instead of User.
You can also customize individual column names. For this you use @ColumnInfo.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "users")
data class User(
@PrimaryKey
@ColumnInfo(name = "user_id")
val id: Long,
@ColumnInfo(name = "full_name")
val name: String,
val age: Int
)
This creates a users table with columns user_id, full_name, and age. If you do not specify @ColumnInfo, Room uses the property name as the column name.
Primary Keys and Auto Generation
The primary key uniquely identifies each row in the table. You can choose any supported type, such as Int, Long, or even String. If you want Room to generate an integer primary key for you, you set autoGenerate = true on the @PrimaryKey annotation.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0L,
val name: String,
val age: Int
)
In this case you usually pass 0L when inserting a new User. Room will replace it with an automatically generated id.
When autoGenerate = true, do not try to manually assign unique values to the primary key. Let Room and SQLite handle the id generation.
If an entity class does not declare a primary key, Room returns a compilation error, because many operations depend on having a stable identifier for each row.
Supported Property Types
Room maps Kotlin types to SQLite column types. SQLite is loosely typed, but Room enforces some structure on top of it. Common mappings include:
Kotlin integer types map to SQLite INTEGER. This includes Int, Long, Short, and Boolean. Kotlin floating point types map to SQLite REAL. This includes Float and Double. Kotlin String maps to SQLite TEXT. ByteArray maps to SQLite BLOB.
Room understands collections and more complex types only if a type converter is defined, which is handled separately and not inside the entity itself. For entities, you must ensure that each property type is either directly supported by Room or has a converter.
If you use nullable types like String? or Int?, Room will create a column that can store NULL values. A non nullable property always becomes a column that room treats as not null.
Room enforces nullability based on Kotlin types. A non nullable property must never receive a null value from the database. If it does, Room throws an exception at runtime.
Ignoring Properties in Entities
Sometimes an entity class has properties you do not want to store in the database. Room would try to map every property to a column unless you explicitly tell it to ignore some properties.
You use @Ignore on properties or constructors to exclude them from the table mapping.
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
@Entity
data class User(
@PrimaryKey val id: Long,
val name: String,
val age: Int
) {
@Ignore
var isSelected: Boolean = false
}
isSelected is a purely in memory property. Room will not add a column for it and will not attempt to read or write this property when interacting with the database.
If you define multiple constructors, Room needs to know which one to use. @Ignore can also mark secondary constructors that Room should not use for instantiation. The default or main constructor is the one Room uses.
Indices and Uniqueness Constraints
To improve query performance or enforce uniqueness, you can define indices on one or more columns in an entity. Room exposes this through the indices parameter on the @Entity annotation.
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "users",
indices = [
Index(value = ["name"]),
Index(value = ["email"], unique = true)
]
)
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0L,
val name: String,
val email: String
)
Here, Room creates an index on the name column to speed up lookups by name. It also creates a unique index on email. If you try to insert another user with the same email, SQLite will reject the insert with a constraint error.
Use unique indices for columns that must not contain duplicates, such as emails or usernames. This enforces data integrity at the database layer.
Indices add overhead during inserts and updates because SQLite needs to maintain them, but they can greatly speed up SELECT queries that use the indexed columns.
Defining Composite Primary Keys
Sometimes you need more than one column to uniquely identify a row. For this, Room supports composite primary keys. Instead of putting @PrimaryKey on a single property, you define the primary key at the @Entity level using primaryKeys.
@Entity(
tableName = "user_books",
primaryKeys = ["userId", "bookId"]
)
data class UserBookCrossRef(
val userId: Long,
val bookId: Long,
val addedAt: Long
)
In this case, the combination of userId and bookId must be unique in the table. Neither column alone identifies a row. This is a common pattern in many to many relationships.
When you use primaryKeys at the entity level, do not also annotate individual properties with @PrimaryKey. Room expects you to choose one approach.
Relationships and Foreign Keys in Entities
Even though the detailed handling of relationships is covered elsewhere, you should understand how entities can define foreign key constraints.
Room provides a ForeignKey annotation that you use in the foreignKeys parameter of @Entity. It ties a column in this entity to the primary key or another column of a parent entity.
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "books",
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["userOwnerId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Book(
@PrimaryKey(autoGenerate = true)
val id: Long = 0L,
val title: String,
val userOwnerId: Long
)
Here, userOwnerId references the id of the User entity. The onDelete = ForeignKey.CASCADE setting means that if a user is deleted, all books that belong to that user are deleted too.
Foreign keys do not automatically create Kotlin object relationships. They only create database constraints between tables. Higher level relationship handling uses additional Room features that build on these definitions.
Embedded Objects inside Entities
Sometimes you have a logical group of fields that you want to reuse in multiple entities. Room supports this with @Embedded. An embedded object does not become a separate table. Instead, its fields become columns in the same table as the parent entity.
For example, you can embed an Address inside a User entity.
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.PrimaryKey
data class Address(
val street: String,
val city: String,
val postalCode: String
)
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0L,
val name: String,
@Embedded
val address: Address
)
Room will create a users table with columns id, name, street, city, and postalCode. There is no separate Address table. The Address type is only a convenience to structure your code.
If you embed two objects that have properties with the same names, you can provide a prefix for one of them using the prefix parameter of @Embedded. This avoids column name conflicts.
data class Coordinates(
val lat: Double,
val lng: Double
)
@Entity
data class Place(
@PrimaryKey(autoGenerate = true)
val id: Long = 0L,
@Embedded(prefix = "home_")
val homeLocation: Coordinates,
@Embedded(prefix = "work_")
val workLocation: Coordinates
)
The generated columns will include home_lat, home_lng, work_lat, and work_lng. The prefix values are just literal strings that Room places in front of the property names.
Using Default Values in Entities
Kotlin allows default values for constructor parameters. Room uses these defaults when it has no value for a column during object creation. This can happen when you add a new column with a default to an existing table.
For example:
@Entity
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0L,
val name: String,
val isActive: Boolean = true
)
If an older row in the database lacks the isActive column and you handle the migration correctly, Room can use the default true when constructing a User from that row.
Defaults also make it easier to instantiate entities in code without always specifying every property, especially when some properties are optional or have common values.
Entity Configuration and Migrations
Entities and their annotations directly affect your database schema. When you change an entity, for example by adding or renaming a property, changing a type, or altering indices, the underlying table definition must also change. This is where migrations in Room become necessary, which is discussed separately.
From the entity perspective you should be careful when changing annotations related to primary keys, table names, and column names. Removing or changing these definitions without proper migration steps leads to runtime errors or data loss.
A typical safe workflow is to design entities, run the app, and let Room create the initial database. Any significant change to entity structure after that must be accompanied by an explicit migration.
Organizing Entity Classes in a Project
In a real Android project, it is common to keep entity classes in a dedicated package such as data.local.entities or data.db.entities. This improves clarity. Each entity usually represents a core concept in your app domain, for example User, Note, Task, or Product.
Entities are part of the data layer. They should not contain Android specific components or complex business logic. They are simple data holders that represent how information is stored in the database.
By keeping entities small, focused, and well annotated, you make Room easier to work with and your database schema easier to understand and maintain.