Room Furnishings

Roughly speaking, your use of Room is divided into three sets of classes:

  1. Entities, which are POJOs that model the data you are transferring into and out of the database
  2. The data access object (DAO), that provides the description of the Java API that you want for working with certain entities
  3. The database, which ties together all of the entities and DAOs for a single SQLite database

If you have used Square’s Retrofit, some of this will seem familiar:

In this chapter, we will look at the Trips/RoomBasics sample project. This app is the first of a linked series of apps that we will examine in this book, containing some code for a travel itinerary manager. Right now, though, we are settling for being able to see some very rudimentary trips get into and out of a database.

Entities

In many ORM systems, the entity (or that system’s equivalent) is a POJO that you happen to want to store in the database. It usually represents some part of your overall domain model, so a payroll system might have entities representing departments, employees, and paychecks.

With Room, a better description of entities is that they are POJOs representing:

That difference may sound academic. It starts to come into play a bit more when we start thinking about relations.

However, it also more closely matches the way Retrofit maps to Web services. With Retrofit, we are not describing the contents of the Web service’s database. Rather, we are describing how we want to work with defined Web service endpoints. Those endpoints have a particular set of content that we can work with, courtesy of whoever developed the Web service. We are simply mapping those to methods and POJOs, both for input and output. Room is somewhere in between a Retrofit-style “we just take what the Web service gives us” approach and a full ORM-style “we control everything about the database” approach.

From a coding standpoint, an entity is a Java class marked with the @Entity annotation. For example, here is a Trip class that serves as a Room entity:

package com.commonsware.android.room;

import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
import java.util.UUID;

@Entity(tableName = "trips")
class Trip {
  @PrimaryKey
  @NonNull
  public final String id;
  public final String title;
  final int duration;

  @Ignore
  Trip(String title, int duration) {
    this(UUID.randomUUID().toString(), title, duration);
  }

  Trip(@NonNull String id, String title, int duration) {
    this.id=id;
    this.title=title;
    this.duration=duration;
  }

  @Override
  public String toString() {
    return(title);
  }
}

There is no particular superclass required for entities, and the expectation is that often they will be simple POJOs, as we see here.

Sometimes, your fields will be marked with annotations describing their roles. In this example, the id field has the @PrimaryKey annotation, telling Room that this is the unique identifier for this entity. Room will use that to know how to update and delete Trip objects by their primary key values. Room also requires that any @PrimaryKey field of an object type — like String — be annotated with @NonNull, as primary keys in SQLite cannot be null.

Similarly, sometimes your methods will be marked with annotations. In this case, Trip has two constructors: one that generates the id from a UUID, and one that takes the id as a constructor parameter. Room needs to know which constructor(s) are eligible for its use; you mark the other constructors with the @Ignore annotation.

For Room to work with a field, it needs to be public or have JavaBean-style getter and setter methods, so Room can access them. If the fields are final, as they are on Trip, Room will try to find a constructor to use to populate the fields, as final fields will lack setters.

We will explore entities in greater detail in an upcoming chapter.

DAO

“Data access object” (DAO) is a fancy way of saying “the API into the data”. The idea is that you have a DAO that provides methods for the database operations that you need: queries, inserts, updates, deletes, whatever.

In Room, the DAO is identified by the @Dao annotation, applied to either an abstract class or an interface. The actual concrete implementation will be code-generated for you by the Room annotation processor.

The primary role of the @Dao-annotated abstract class or interface is to have one or more methods, with their own Room annotations, identifying what you want to do with the database and your entities. This serves the same role as the methods annotated @GET or @POST in Retrofit.

The sample app has a TripStore that is our DAO:

package com.commonsware.android.room;

import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;

@Dao
interface TripStore {
  @Query("SELECT * FROM trips ORDER BY title")
  List<Trip> selectAll();

  @Query("SELECT * FROM trips WHERE id=:id")
  Trip findById(String id);

  @Insert
  void insert(Trip... trips);

  @Update
  void update(Trip... trips);

  @Delete
  void delete(Trip... trips);
}

Besides the @Dao annotation on the TripStore interface, we have five methods, each with their own annotations. Your four main annotations for these methods are @Query, @Insert, @Update, and @Delete, which map to the corresponding database operations.

Two TripStore methods — selectAll() and findById() — have the @Query annotation. Principally, @Query will be used for SQL SELECT statements, where you put the actual SQL in the annotation itself. To a large extent, any valid SQLite query can be used here. However, instead of using ? as placeholders for arguments, as we would in traditional SQLite, you use :-prefixed method parameter names. So, in findById(), we have a String parameter named id, so we can use :id in the SQL statement wherever we might have used ? to indicate the value to bind in.

The remaining three methods use the @Insert, @Update, and @Delete annotations, mapped to methods of the same name. Here, the methods take a varargs of Trip, meaning that we can insert, update, or delete as many Trip objects as we want (passing in zero Trip objects works, though that would be rather odd).

If you want custom code on your DAO, beyond the code-generated implementations of your Room-annotated methods, use an abstract class and mark all the Room-annotated methods as abstract. If, on the other hand, all you need on the DAO are the Room-annotated methods, you can use an interface and skip all the abstract keywords, as we did with TripStore.

We will explore the DAO in greater detail in an upcoming chapter.

Database

In addition to entities and DAOs, you will have at least one @Database-annotated abstract class, extending a RoomDatabase base class. This class knits together the database file, the entities, and the DAOs.

In the sample project, we have a TripDatabase serving this role:

package com.commonsware.android.room;

import android.arch.persistence.room.Database;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.content.Context;

@Database(entities={Trip.class}, version=1)
abstract class TripDatabase extends RoomDatabase {
  abstract TripStore tripStore();

  private static final String DB_NAME="trips.db";
  private static volatile TripDatabase INSTANCE=null;

  synchronized static TripDatabase get(Context ctxt) {
    if (INSTANCE==null) {
      INSTANCE=create(ctxt, false);
    }

    return(INSTANCE);
  }

  static TripDatabase create(Context ctxt, boolean memoryOnly) {
    RoomDatabase.Builder<TripDatabase> b;

    if (memoryOnly) {
      b=Room.inMemoryDatabaseBuilder(ctxt.getApplicationContext(),
        TripDatabase.class);
    }
    else {
      b=Room.databaseBuilder(ctxt.getApplicationContext(), TripDatabase.class,
        DB_NAME);
    }

    return(b.build());
  }
}

The @Database annotation configures the code generation process, including:

@Database(entities={Trip.class}, version=1)

Here, we are saying that we have just one entity class (Trip), and that this is schema version 1.

You also need abstract methods for each DAO class that return an instance of that class:

  abstract TripStore tripStore();

In this app, we have only one DAO (TripStore), so we have an abstract method to return an instance of TripStore.

Extending RoomDatabase, having the @Database annotation, and having the abstract method(s) for your DAOs are the requirements. Anything beyond that is up to you, and some apps may elect to have nothing more here.

In our case, we have a bit more logic.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.