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;
}
Explanation:
  • @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);
}
Explanation:
  • @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;
    }
}
Explanation:
  • @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);
    });
}
Explanation: Executors.newSingleThreadExecutor() creates a background worker thread; execute(() -> { ... }) hands it a block of code to run there. Because reads via LiveData-returning DAO methods are observed asynchronously, Room handles those for you automatically — but one-shot writes like insert/update/delete still need this manual executor wrapper in Java.

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();
Migrations are covered here only briefly — the key idea is that every schema change needs a corresponding Migration, or existing users' recipes would be lost on the next app update.

Room vs Raw SQLiteOpenHelper

ConcernRaw SQLiteOpenHelperRoom
Table creationHand-written CREATE TABLE stringGenerated from @Entity fields
Reading rowsManual Cursor + column indicesRows mapped straight to Java objects
Live updatesYou re-query manually after every changeLiveData-returning DAO methods update automatically
SQL mistakesCaught only at runtime (if at all)Many caught at compile time
Room replaces raw database access, but structured data isn't the only kind of data-sharing Android supports. The next lesson, Content Providers, covers how apps expose and consume data between each other — including the system Contacts and MediaStore.