Room and Custom Types
So far, all of our fields have been basic primitives (int
, float
, etc.) or String
. There is a good reason for that: those are all that Room understands “out of the box”. Everything else requires some amount of assistance on our part.
Sometimes, a field in an entity will be related to another entity. Those are relations, and we will consider those in the next chapter.
However, other times, a field in an entity does not map directly to primitives and String
types, or to another entity. For example:
- What do we do with a Java
Date
orCalendar
object? Do we want to store that as a milliseconds-since-the-Unix-epoch value as a Javalong
? Do we want to store a string representation in a standard format, for easier readability (at the cost of disk space and other issues)? - What do we do with a
Location
object? Here, we have two pieces: a latitude and a longitude. Do we have two columns that combine into one field? Do we convert theLocation
to and from aString
representation? - What do we do with collections of strings, such as lists of tags?
- What do we do with enums?
And so on.
In this chapter, we will explore two approaches for handling these things without creating another entity class: type converters and embedded types.
Type Converters
Type converters are a pair of methods, annotated with @TypeConverter
, that map the type for a single database column to a type for a Java field. So, for example, we can:
- Map a
Date
field to aLong
, which can go in a SQLiteINTEGER
column - Map a
Location
field to aString
, which can go in a SQLiteTEXT
column - Map a collection of
String
values to a singleString
(e.g., comma-separated values), which can go in a SQLiteTEXT
column - And so forth
However, type converters offer only a 1:1 conversion: a single Java field to and from a single SQLite column. If you have a single Java field that should map to several SQLite columns, the @Embedded
approach can handle that, as we will see later in this chapter.
Setting Up a Type Converter
First, define a Java class somewhere. The name, package, superclass, etc. do not matter.
Next, for each type to be converted, create two public static
methods that convert from one type to the other. So for example, you would have one public static
method that takes a Date
and returns a Long
(e.g., returning the milliseconds-since-the-Unix-epoch value), and a counterpart method that takes a Long
and returns a Date
. If the converter method is passed null
, the proper result is null
. Otherwise, the conversion is whatever you want, so long as the “round trip” works, so that the output of one converter method, supplied as input to the other converter method, returns the original value.
Then, each of those methods get the @TypeConverter
annotation. The method names do not matter, so pick a convention that works for you.
Finally, you add a @TypeConverters
annotation, listing this and any other type converter classes, to… something. What the “something” is controls the scope of where that type converter can be used.
The simple solution is to add @TypeConverters
to the RoomDatabase
, which means that anything associated with that database can use those type converters. However, sometimes, you may have situations where you want different conversions between the same pair of types, for whatever reason. In that case, you can put the @TypeConverters
annotations on narrower scopes:
@TypeConverters Location |
Affected Areas |
---|---|
Entity class | all fields in the entity |
Entity field | that one field in the entity |
DAO class | all methods in the DAO |
DAO method | that one method in the DAO, for all parameters |
DAO method parameter | that one parameter on that one method |
POJO | all fields on the POJO |
The General/RoomTypes
sample project illustrates the use of type converters. As with the RoomDao
project from the preceding chapter, this project contains a single library module with an associated instrumentation test case. In fact, it is a clone of the RoomDao
project, just with some type converters.
Example: Dates and Times
A typical way of storing a date/time value in a database is to use the number of milliseconds since the Unix epoch (i.e., the number of milliseconds since midnight, 1 January 1970). Date
has a getTime()
method that returns this value.
So, the project has a TypeTransmogrifiers
class that contains two methods, each annotated with @TypeConverter
, for converting Date
to and from a Long
:
@TypeConverter
public static Long fromDate(Date date) {
if (date==null) {
return(null);
}
return(date.getTime());
}
@TypeConverter
public static Date toDate(Long millisSinceEpoch) {
if (millisSinceEpoch==null) {
return(null);
}
return(new Date(millisSinceEpoch));
}
StuffDatabase
then has the @TypeConverters
annotation, listing TypeTransmogrifier
as the one class that has type conversion methods:
package com.commonsware.android.room.dao;
import android.arch.persistence.room.Database;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.TypeConverters;
import android.content.Context;
@Database(
entities={Customer.class, VersionedThingy.class},
version=1
)
@TypeConverters({TypeTransmogrifier.class})
abstract class StuffDatabase extends RoomDatabase {
abstract StuffStore stuffStore();
private static final String DB_NAME="stuff.db";
private static volatile StuffDatabase INSTANCE=null;
synchronized static StuffDatabase get(Context ctxt) {
if (INSTANCE==null) {
INSTANCE=create(ctxt, false);
}
return(INSTANCE);
}
static StuffDatabase create(Context ctxt, boolean memoryOnly) {
RoomDatabase.Builder<StuffDatabase> b;
if (memoryOnly) {
b=Room.inMemoryDatabaseBuilder(ctxt.getApplicationContext(),
StuffDatabase.class);
}
else {
b=Room.databaseBuilder(ctxt.getApplicationContext(), StuffDatabase.class,
DB_NAME);
}
return(b.build());
}
}
Now, classes like Customer
can use Date
fields, which will be stored in INTEGER
columns in the database.
CREATE TABLE IF NOT EXISTS Customer (id TEXT, postalCode TEXT, displayName TEXT, creationDate INTEGER, PRIMARY KEY(`id`))
Example: Locations
A Location
object contains a latitude, longitude, and perhaps other values (e.g., altitude). If we only care about the latitude and longitude, we could save those in the database in a single TEXT
column, so long as we can determine a good format to use for that string. If we use Locale.US
formatting for the latitude and longitude, so that the decimal place is denoted by a .
, we could use a two-element comma-separated values list for the string.
That is what these two type converter methods on TypeTransmogrifiers
do:
@TypeConverter
public static String fromLocation(Location location) {
if (location==null) {
return(null);
}
return(String.format(Locale.US, "%f,%f", location.getLatitude(),
location.getLongitude()));
}
@TypeConverter
public static Location toLocation(String latlon) {
if (latlon==null) {
return(null);
}
String[] pieces=latlon.split(",");
Location result=new Location("");
result.setLatitude(Double.parseDouble(pieces[0]));
result.setLongitude(Double.parseDouble(pieces[1]));
return(result);
}
Since TypeTransmogrifiers
is registered on the StuffDatabase
, a Customer
could have a Location
field, which would be mapped to a TEXT
column in the database:
CREATE TABLE IF NOT EXISTS Customer (id TEXT, postalCode TEXT, displayName TEXT, creationDate INTEGER, officeLocation TEXT, PRIMARY KEY(`id`))
However, the downside of using this approach is that we cannot readily search based on location. If your location data is not a searchable field, and it merely needs to be available when you load your entities from the database, using a type converter like this is fine. Later in this chapter, we will see another approach (@Embedded
) that allows us to store the latitude and longitude as separate columns while still mapping them to a single POJO in Java.
Example: Simple Collections
TEXT
and BLOB
columns are very flexible. So long as you can marshal your data into a String
or byte array, you can save that data in TEXT
and BLOB
columns. As with the comma-separated values approach in the preceding section, though, columns used this way are poor for searching.
So, suppose that you have a Set
of String
values that you want to store, perhaps representing tags to associate with an entity. One approach is to have a separate Tag
entity and set up a relation. This is the best approach in many cases. But, perhaps you do not want to do that for some reason.
You can use a type converter, but you need to decide how to represent your data in a column. If you are certain that the tags will not contain some specific character (e.g., a comma), you can use the delimited-list approach demonstrated with locations in the preceding section. If you need more flexibility than that, you can always use JSON encoding, as these type converters do:
@TypeConverter
public static String fromStringSet(Set<String> strings) {
if (strings==null) {
return(null);
}
StringWriter result=new StringWriter();
JsonWriter json=new JsonWriter(result);
try {
json.beginArray();
for (String s : strings) {
json.value(s);
}
json.endArray();
json.close();
}
catch (IOException e) {
Log.e(TAG, "Exception creating JSON", e);
}
return(result.toString());
}
@TypeConverter
public static Set<String> toStringSet(String strings) {
if (strings==null) {
return(null);
}
StringReader reader=new StringReader(strings);
JsonReader json=new JsonReader(reader);
HashSet<String> result=new HashSet<>();
try {
json.beginArray();
while (json.hasNext()) {
result.add(json.nextString());
}
json.endArray();
}
catch (IOException e) {
Log.e(TAG, "Exception parsing JSON", e);
}
return(result);
}
Here, we use the JsonReader
and JsonWriter
classes that have been part of Android since API Level 11. Alternatively, you could use a third-party JSON library (e.g., Gson).
Note that type converter methods cannot throw checked exceptions, as the Room code generator does not wrap type converter calls in a try
/catch
block. Here, the IOExceptions
should never happen, since we are working with strings, not files or other types of streams. In other cases, though, you may need to wrap the checked exception in some form of RuntimeException
and throw that, to trigger your app’s unhandled-exception logic, as it is unlikely that you can recover from within a type converter method.
Given these type conversion methods, we can now use a Set
of String
values in Customer
:
package com.commonsware.android.room.dao;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import android.location.Location;
import android.support.annotation.NonNull;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity(indices={@Index(value="postalCode", unique=true)})
class Customer {
@PrimaryKey
@NonNull
public final String id;
public final String postalCode;
public final String displayName;
public final Date creationDate;
public final Location officeLocation;
public final Set<String> tags;
@Ignore
Customer(String postalCode, String displayName, Location officeLocation,
Set<String> tags) {
this(UUID.randomUUID().toString(), postalCode, displayName, new Date(),
officeLocation, tags);
}
Customer(String id, String postalCode, String displayName, Date creationDate,
Location officeLocation, Set<String> tags) {
this.id=id;
this.postalCode=postalCode;
this.displayName=displayName;
this.creationDate=creationDate;
this.officeLocation=officeLocation;
this.tags=tags;
}
}
…where the tags
will be stored in a TEXT
column:
CREATE TABLE IF NOT EXISTS Customer (id TEXT, postalCode TEXT, displayName TEXT, creationDate INTEGER, officeLocation TEXT, tags TEXT, PRIMARY KEY(`id`))
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.