Room Furnishings
Roughly speaking, your use of Room is divided into three sets of classes:
- Entities, which are POJOs that model the data you are transferring into and out of the database
- The data access object (DAO), that provides the description of the Java API that you want for working with certain entities
- 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:
- The DAO is roughly analogous to your Retrofit
interface
on which you declare your Web service API - Your entities are the POJOs that you are expecting Gson (or whatever) to create based on the Web service response
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:
- the data that you want to store into the database, and
- a typical unit of a result set that you are trying to retrieve from the database
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:
- Identifying all of the entity classes that you care about in the
entities
collection - Identifying the schema version of the database (as you see with
SQLiteOpenHelper
in conventional Android SQLite development)
@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.