Room Database (Java)
Room is Jetpack's ORM (object-relational mapper), sitting directly on top of the same SQLite engine used by SQLiteOpenHelper in the previous lesson. Instead of hand-writing CREATE TABLE strings, manually walking a Cursor, and remembering to close everything yourself, Room lets you describe your data with annotations and generates all of that boilerplate at compile time — while still catching many mistakes the raw approach could not, like malformed SQL, right at compile time.
@Entity
A Java class describing one table row
@Dao
An interface describing queries/inserts
@Database
Ties entities + DAOs into one database
The Entity: @Entity
An entity is a plain Java class annotated with @Entity. Room reads its fields and generates the matching table for you — no SQL string required.
// Recipe.java — the Room entity
@Entity
public class Recipe {
@PrimaryKey(autoGenerate = true)
public int id;
public String title;
public String ingredients;
public String steps;
public String photoUri;
public String category;
public int prepMinutes;
}
- @Entity tells Room this class maps to a table (named Recipe by default).
- @PrimaryKey(autoGenerate = true) replaces the manual INTEGER PRIMARY KEY AUTOINCREMENT SQL from the previous lesson.
- Every public field becomes a column automatically — no ContentValues, no column-name constants.
The DAO: @Dao
A DAO (Data Access Object) is an interface where each method describes one database operation. Room generates the implementation for you at compile time.
// RecipeDao.java
@Dao
public interface RecipeDao {
@Insert
void insertRecipe(Recipe recipe);
@Update
void updateRecipe(Recipe recipe);
@Delete
void deleteRecipe(Recipe recipe);
@Query("SELECT * FROM Recipe ORDER BY title ASC")
LiveData<List<Recipe>> getAllRecipes();
@Query("SELECT * FROM Recipe WHERE category = :category")
LiveData<List<Recipe>> getRecipesByCategory(String category);
}
- @Insert, @Update, and @Delete need no SQL at all — Room infers them from the entity's primary key.
- Returning LiveData<List<Recipe>> directly from a DAO method means Room automatically re-runs the query and pushes a new list to observers whenever the Recipe table changes — no manual refresh code, no re-querying after every insert.
- This connects straight back to ViewModel & LiveData: a ViewModel exposes this LiveData, and the Activity/Fragment just observes it.
The Database: @Database
// AppDatabase.java
@Database(entities = {Recipe.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract RecipeDao recipeDao();
private static volatile AppDatabase INSTANCE;
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"recipebox.db"
).build();
}
}
}
return INSTANCE;
}
}
- @Database(entities = {...}, version = 1) lists every entity table and the current schema version.
- The singleton pattern (getInstance()) ensures the whole app shares one AppDatabase instance, avoiding the overhead — and bugs — of opening the database repeatedly.
- synchronized plus the double-null-check keeps this thread-safe if two parts of the app request the instance at the same time.
Running Room Off the Main Thread
Room deliberately throws an exception if you call a blocking insert/update/delete on the main thread — database I/O is too slow to risk freezing the UI. Kotlin coroutines have suspend functions for this, but plain Java has no language-level equivalent, so the standard approach is a manual background thread via ExecutorService.
// A single background thread dedicated to database writes
private static final ExecutorService databaseExecutor = Executors.newSingleThreadExecutor();
// Inserting a recipe off the main thread
public void addRecipe(Recipe recipe) {
databaseExecutor.execute(() -> {
appDatabase.recipeDao().insertRecipe(recipe);
});
}
Migrations: Evolving the Schema Safely
When RecipeBox eventually needs a new column — say, a servings count — bumping version alone isn't enough; without a migration, Room would simply wipe and recreate the database, deleting every saved recipe. A Migration object describes exactly how to transform the old schema into the new one, in raw SQL, without losing data.
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase db) {
db.execSQL("ALTER TABLE Recipe ADD COLUMN servings INTEGER NOT NULL DEFAULT 1");
}
};
// Registered when building the database:
// Room.databaseBuilder(...).addMigrations(MIGRATION_1_2).build();
Room vs Raw SQLiteOpenHelper
| Concern | Raw SQLiteOpenHelper | Room |
|---|---|---|
| Table creation | Hand-written CREATE TABLE string | Generated from @Entity fields |
| Reading rows | Manual Cursor + column indices | Rows mapped straight to Java objects |
| Live updates | You re-query manually after every change | LiveData-returning DAO methods update automatically |
| SQL mistakes | Caught only at runtime (if at all) | Many caught at compile time |