Room provides functionality for converting between primitive and boxed types but doesn't allow for object references between entities. This document explains how to use type converters and why Room doesn't support object references.
Use type converters
Sometimes, your app needs to use a custom data type whose value you would like
to store in a single database column. To add this kind of support for custom
types, you provide a
TypeConverter
,
which converts a custom class to and from a known type that Room can persist.
For example, if we want to persist instances of Date
, we can
write the following
TypeConverter
to store the equivalent Unix timestamp in the database:
Kotlin
class Converters { @TypeConverter fun fromTimestamp(value: Long?): Date? { return value?.let { Date(it) } } @TypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time?.toLong() } }
Java
public class Converters { @TypeConverter public static Date fromTimestamp(Long value) { return value == null ? null : new Date(value); } @TypeConverter public static Long dateToTimestamp(Date date) { return date == null ? null : date.getTime(); } }
The preceding example defines 2 functions, one that converts a Date
object to a Long
object and another that
performs the inverse conversion, from Long
to Date
. Since Room already knows how to persist Long
objects, it can use this converter to persist values of type
Date
.
Next, you add the
@TypeConverters
annotation to the AppDatabase
class so that Room can use the converter that
you've defined for each entity
and DAO in that
AppDatabase
:
AppDatabase
Kotlin
@Database(entities = arrayOf(User::class), version = 1) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }
Java
@Database(entities = {User.class}, version = 1) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); }
Using these converters, you can then use your custom types in other queries, just as you would use primitive types, as shown in the following code snippet:
User
Kotlin
@Entity data class User(private var birthday: Date?)
Java
@Entity public class User { private Date birthday; }
UserDao
Kotlin
@Dao interface UserDao { @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to") fun findUsersBornBetweenDates(from: Date, to: Date): List<User> }
Java
@Dao public interface UserDao { @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to") List<User> findUsersBornBetweenDates(Date from, Date to); }
You can also limit the
@TypeConverters
to different scopes, including individual entities, DAOs, and DAO methods. For
more details, see the reference documentation for the
@TypeConverters
annotation.
Understand why Room doesn't allow object references
Key takeaway: Room disallows object references between entity classes. Instead, you must explicitly request the data that your app needs.
Mapping relationships from a database to the respective object model is a common practice and works very well on the server side. Even when the program loads fields as they're accessed, the server still performs well.
However, on the client side, this type of lazy loading isn't feasible because it usually occurs on the UI thread, and querying information on disk in the UI thread creates significant performance problems. The UI thread typically has about 16 ms to calculate and draw an activity's updated layout, so even if a query takes only 5 ms, it's still likely that your app will run out of time to draw the frame, causing noticeable visual glitches. The query could take even more time to complete if there's a separate transaction running in parallel, or if the device is running other disk-intensive tasks. If you don't use lazy loading, however, your app fetches more data than it needs, creating memory consumption problems.
Object-relational mappings usually leave this decision to developers so that they can do whatever is best for their app's use cases. Developers usually decide to share the model between their app and the UI. This solution doesn't scale well, however, because as the UI changes over time, the shared model creates problems that are difficult for developers to anticipate and debug.
For example, consider a UI that loads a list of Book
objects, with each book
having an Author
object. You might initially design your queries to use lazy
loading to have instances of Book
retrieve the author. The first retrieval of
the author
field queries the database. Some time later, you realize that you
need to display the author name in your app's UI, as well. You can access this
name easily enough, as shown in the following code snippet:
Kotlin
authorNameTextView.text = book.author.name
Java
authorNameTextView.setText(book.getAuthor().getName());
However, this seemingly innocent change causes the Author
table to be queried
on the main thread.
If you query author information ahead of time, it becomes difficult to change
how data is loaded if you no longer need that data. For example, if your app's
UI no longer needs to display Author
information, your app effectively loads
data that it no longer displays, wasting valuable memory space. Your app's
efficiency degrades even further if the Author
class references another table,
such as Books
.
To reference multiple entities at the same time using Room, you instead create a POJO that contains each entity, then write a query that joins the corresponding tables. This well-structured model, combined with Room's robust query validation capabilities, allows your app to consume fewer resources when loading data, improving your app's performance and user experience.