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:

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:

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.