The Dao of Entities
Two chapters ago, we went through the basic steps for setting up Room:
- Create and annotate your entity classes
- Create, annotate, and define operator methods on your DAO(s)
- Create a subclass of
RoomDatabase
to tie the entities and DAO(s) together - Create an instance of that
RoomDatabase
at some likely point in time, while you are safely on a background thread - Use the
RoomDatabase
instance to retrieve your DAO and from there work with your entities
However, we only scratched the surface of what can be configured on entities and DAOs. In this chapter — and the subsequent chapters on custom types and relations — we will explore the rest of the configuration for entities and DAOs.
Many of the code snippets shown in this chapter come from the General/RoomDao
sample project. This contains a library module (stuff
) with entity and DAO code along with instrumentation tests for bits of that code.
Configuring Entities
The only absolute requirements for a Room entity class is that it be annotated with the @Entity
annotation and have a field identified as the primary key, typically by way of a @PrimaryKey
annotation. Anything above and beyond that is optional.
However, there is a fair bit that is “above and beyond that”. Some — though probably not all — of these features will be of interest in larger apps.
Primary Keys
If you have a single field that is the primary key for your entity, using the @PrimaryKey
annotation is simple and helps you clearly identify that primary key at a later point.
However, you do have some other options.
Auto-Generated Primary Keys
In SQLite, if you have an INTEGER
column identified as the PRIMARY KEY
, you can optionally have SQLite assign unique values for that column, by way of the AUTOINCREMENT
keyword.
In Room, if you have an int
or Integer
field that is your @PrimaryKey
, you can optionally apply AUTOINCREMENT
to the corresponding column by adding autoGenerate=true
to the annotation:
@Entity
public class Constant {
@PrimaryKey(autoGenerate=true)
@NonNull
public int id;
String title;
double value;
@Override
public String toString() {
return(title);
}
}
By default, autoGenerate
is false
. Setting that property to true
gives you AUTOINCREMENT
in the generated CREATE TABLE
statement:
CREATE TABLE IF NOT EXISTS Constant (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, value REAL NOT NULL)
However, this starts to get complicated in the app. You do not know your primary key until you insert the entity into a database. That presents “trickle-down” complications — for example, you cannot make the primary key field final
, as then you cannot create an instance of an entity that is not yet in the database. While you can try to work around this (e.g., default the id
to 0
), then you have to keep checking to see whether you have a valid identifier.
Most of the samples in this book will use a UUID
instead. While these take up much more room than a simple int
, they can be uniquely generated outside of the database. For your production apps, you will need to decide if the headaches surrounding database-generated identifiers are worth their benefits.
Also, notice that the value
column has NOT NULL
applied to it. Room’s rule is that primitive fields (int
, double
, etc.) will be NOT NULL
, while their object equivalents (Integer
, Double
, etc.) will allow null values.
Composite Primary Keys
In some cases, you may have a composite primary key, made up of two or more columns in the database. This is particularly true if you are trying to design your entities around an existing database structure, one that used a composite primary key for one of its tables (for whatever reason).
If, logically, those are all part of a single object, you could combine them into a single field, as we will see in the next chapter. However, it may be that they should be individual fields in your entity, but they happen to combine to create the primary key. In that case, you can skip the @PrimaryKey
annotation and use the primaryKeys
property of the @Entity
.
One scenario for this is data versioning, where we are tracking changes to data over time, the way a version control system tracks changes to source code and other files over time. There are several ways of implementing data versioning. One approach has all versions of the same entity in the same table, with a version code attached to the “natural” primary key to identify a specific version of that content. In that case, you could have something like:
@Entity(primaryKeys={"id", "versionCode"})
class VersionedThingy {
@NonNull public final String id;
public final int versionCode;
VersionedThingy(String id, int versionCode) {
this.id=id;
this.versionCode=versionCode;
}
}
Room will then use the PRIMARY KEY
keyword in the CREATE TABLE
statement to set up the composite primary key:
CREATE TABLE IF NOT EXISTS VersionedThingy (id TEXT NOT NULL, versionCode INTEGER NOT NULL, PRIMARY KEY(id, versionCode))
Even though we are using primaryKeys
rather than @PrimaryKey
, the @NonNull
requirement still holds. We need to add that to any of our primaryKeys
fields that are of object types. Since id
is a String
, we need @NonNull
. However, versionCode
is an int
, and an int
cannot be null
, so we do not need @NonNull
(though having it will not cause a problem). If versionCode
were an Integer
, we would need @NonNull
, as an Integer
field could be null
.
Adding Indexes
Your primary key is indexed automatically by SQLite. However, you may wish to set up other indexes for other columns or collections of columns, to speed up queries. To do that, use the indices
property on @Entity
. This property takes a list of @Index
annotations, each of which declares an index.
For example, as part of a Customer
entity, you might have an address, which might contain a postalCode
. You might be querying directly on postalCode
as part of a search form, and so having an index on that would be useful. To do that, add the appropriate @Index
to indices
:
@Entity(indices={@Index("postalCode")})
class Customer {
@PrimaryKey
@NonNull
public final String id;
public final String postalCode;
public final String displayName;
Customer(String id, String postalCode, String displayName) {
this.id=id;
this.postalCode=postalCode;
this.displayName=displayName;
}
}
Room will add the requested index:
CREATE INDEX index_Customer_postalCode ON Customer (postalCode)
If you have a composite index, consisting of two or more fields, @Index
takes a comma-delimited list of column names and will generate the composite index.
If the index should also enforce uniqueness — only one entity can have the indexed value — add unique=true
to the @Index
. This requires you to assign the column(s) for the index to the value
property, due to the way Java annotations work:
@Entity(indices={@Index(value="postalCode", unique=true)})
class Customer {
@PrimaryKey
@NonNull
public final String id;
public final String postalCode;
public final String displayName;
Customer(String id, String postalCode, String displayName) {
this.id=id;
this.postalCode=postalCode;
this.displayName=displayName;
}
}
This causes Room to add the UNIQUE
keyword to the CREATE INDEX
statement:
CREATE UNIQUE INDEX index_Customer_postalCode ON Customer (postalCode)
Ignoring Fields
If there are fields in the entity class that should not be persisted, annotate them with @Ignore
:
@Entity(primaryKeys={"id", "versionCode"})
class VersionedThingy {
@NonNull public final String id;
public final int versionCode;
@Ignore
private String something;
VersionedThingy(String id, int versionCode) {
this.id=id;
this.versionCode=versionCode;
}
}
That annotation is required. For example, this does not work:
@Entity(primaryKeys={"id", "versionCode"})
class VersionedThingy {
@NonNull public final String id;
public final int versionCode;
private String something;
VersionedThingy(String id, int versionCode) {
this.id=id;
this.versionCode=versionCode;
}
}
You might think that since the field is private and has no setter, that Room would ignore it automatically. Room, instead, generates a build error, as it cannot tell if you want to ignore that field or if you simply forgot to add it properly.
With Room, transient
fields are ignored automatically by default, so in the following code snippet, something
will be ignored:
@Entity(primaryKeys={"id", "versionCode"})
class VersionedThingy {
@NonNull public final String id;
public final int versionCode;
public transient String something;
VersionedThingy(String id, int versionCode) {
this.id=id;
this.versionCode=versionCode;
}
}
As seen earlier in the book, you can also @Ignore
constructors. This may be required to clear up Room build errors, if the code generator cannot determine what constructor to use:
@Entity(primaryKeys={"id", "versionCode"})
class VersionedThingy {
@NonNull public final String id;
public final int versionCode;
@Ignore
private String something;
@Ignore
VersionedThingy() {
this(UUID.randomUUID().toString(), 1);
}
VersionedThingy(String id, int versionCode) {
this.id=id;
this.versionCode=versionCode;
}
}
NOT NULL Fields
As noted earlier, primitive fields get converted into NOT NULL
columns in the table, while object fields allow null values.
If you want an object field to be NOT NULL
, apply the @NonNull
annotation:
@Entity(indices={@Index("postalCode")})
class Customer {
@PrimaryKey
@NonNull
public final String id;
@NonNull
public final String postalCode;
public final String displayName;
Customer(String id, String postalCode, String displayName) {
this.id=id;
this.postalCode=postalCode;
this.displayName=displayName;
}
}
This will make the associated column have NOT NULL
applied to it.
Custom Table and Column Names
By default, Room will generate names for your tables and columns based off of the entity class names and field names. In general, it does a respectable job of this, and so you may just leave them alone. However, you may find that you need to control these names, particularly if you are trying to match an existing database schema (e.g., you are migrating an existing Android app to use Room instead of using SQLite directly). And for table names in particular, setting your own name can simplify some of the SQL that you have to write for @Query
-annotated methods.
To control the table name, use the tableName
property on the @Entity
attribute, and give it a valid SQLite table name. For example, while in Java we might want to call the class VersionedThingy
, we might prefer the table to just be thingy
:
@Entity(tableName="thingy", primaryKeys={"id", "versionCode"})
class VersionedThingy {
@NonNull public final String id;
public final int versionCode;
@Ignore
private String something;
@Ignore
VersionedThingy() {
this(UUID.randomUUID().toString(), 1);
}
VersionedThingy(String id, int versionCode) {
this.id=id;
this.versionCode=versionCode;
}
}
To rename a column, add the @ColumnInfo
annotation to the field, with a name
property that provides your desired name for the column:
@Entity(tableName="thingy", primaryKeys={"id", "versionCode"})
class VersionedThingy {
@NonNull public final String id;
@ColumnInfo(name="version_code")
public final int versionCode;
@Ignore
private String something;
@Ignore
VersionedThingy() {
this(UUID.randomUUID().toString(), 1);
}
VersionedThingy(String id, int versionCode) {
this.id=id;
this.versionCode=versionCode;
}
}
Here, we changed the versionCode
field’s column to version_code
, along with specifying the table name.
However, this fails. The values in the primaryKeys
property are the column names, not the field names. Since we renamed the column, we need to update primaryKeys
to match:
package com.commonsware.android.room.dao;
import android.arch.persistence.room.ColumnInfo;
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="thingy", primaryKeys={"id", "version_code"})
class VersionedThingy {
@NonNull public final String id;
@ColumnInfo(name="version_code")
@NonNull
public final int versionCode;
@Ignore
private String something;
@Ignore
VersionedThingy() {
this(UUID.randomUUID().toString(), 1);
}
VersionedThingy(String id, int versionCode) {
this.id=id;
this.versionCode=versionCode;
}
}
Also note that adding @ColumnInfo
to a transient
field means that this field will be included when creating the table structure. By default, transient
fields are ignored, but adding @ColumnInfo
indicates that you want that default behavior to be overridden.
Other @ColumnInfo Options
Beyond specifying the column name to use, you can configure other options on a @ColumnInfo
annotation.
Indexing
You can add an index
property to indicate that you want to index the column, as an alternative to listing the column in the indices
property of the @Entity
annotation. For example, we could replace:
@Entity(indices={@Index("postalCode")})
class Customer {
@PrimaryKey
public final String id;
public final String postalCode;
public final String displayName;
Customer(String id, String postalCode, String displayName) {
this.id=id;
this.postalCode=postalCode;
this.displayName=displayName;
}
}
with:
@Entity
class Customer {
@PrimaryKey
public final String id;
@ColumnInfo(index=true)
public final String postalCode;
public final String displayName;
Customer(String id, String postalCode, String displayName) {
this.id=id;
this.postalCode=postalCode;
this.displayName=displayName;
}
}
and have the same result.
Collation
You can specify a collate
property to indicate the collation sequence to apply to this column. Here, “collation sequence” is a fancy way of saying “comparison function for comparing two strings”.
There are four options:
-
BINARY
andUNDEFINED
, which are equivalent, the default value, and indicate that case is sensitive -
NOCASE
, which indicates that case is not sensitive (more accurately, that the 26 English letters are converted to uppercase) -
RTRIM
, which indicates that trailing spaces should be ignored on a case-sensitive collation
There is no full-UTF equivalent of NOCASE
in SQLite.
Type Affinity
Normally, Room will determine the type to use on the column in SQLite based upon the type of the field (e.g., int
or Integer
turn into INTEGER
columns). If, for some reason, you wish to try to override this behavior, you can use the typeAffinity
property on @ColumnInfo
to specify some other type to use.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.