Room can convert primitive and boxed types, but doesn't allow object references between entities. Learn how to use type converters and why Room doesn't support object references.
Use type converters
Sometimes, you need your app to store a custom data type in a single database
column. You support custom types by providing type converters. These are
functions that tell Room how to convert custom types to and from known types
that Room can persist. You identify type converters by using the
@ColumnTypeConverter annotation.
Suppose you need to persist instances of Date in
your Room database. Room can't natively persist Date objects, so you need
to define type converters:
object Converters { @ColumnTypeConverter fun fromTimestamp(value: Long?): Date? { return value?.let { Date(it) } } @ColumnTypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time } }
This example defines two type converter functions: one that converts a Date
object to a Long object, and one that converts a Long object back to a
Date object. Because Room can persist Long objects, it can use these
converters to persist Date objects.
Next, you add the @ColumnTypeConverters
annotation to the AppDatabase class so that Room can use the converter
class that you've defined:
@Database(entities = [User::class], version = 1) @ColumnTypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }
With these type converters defined, you can use your custom type in your entities and DAOs just as you would use primitive types:
@Entity data class User( @PrimaryKey val id: Long, val name: String, val birthday: Date? ) @Dao interface UserDao { @Query("SELECT * FROM user WHERE birthday = :targetDate") suspend fun findUsersBornOnDate(targetDate: Date): List<User> }
Because you annotated AppDatabase with @ColumnTypeConverters in this
example, Room can use the defined type converter everywhere. To scope type
converters to specific entities or DAOs instead, annotate your @Entity or
@Dao classes with @ColumnTypeConverters.
Control type converter initialization
Ordinarily, Room instantiates type converters for you. However, if you need to
pass additional dependencies to your type converter classes, your app must
directly control their initialization. In that case, annotate your converter
class with @ProvidedColumnTypeConverter:
@ProvidedColumnTypeConverter class ExampleConverter { @ColumnTypeConverter fun stringToExample(string: String?): ExampleType? { return string?.let { ExampleType() } } @ColumnTypeConverter fun exampleToString(example: ExampleType?): String? { return example?.toString() } }
In addition to declaring your converter class in @ColumnTypeConverters, use
the RoomDatabase.Builder.addColumnTypeConverter function to pass an
instance of your converter class to the RoomDatabase builder:
val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name") .addColumnTypeConverter(exampleConverterInstance) .build()
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 properties 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 property 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, as shown in the following code snippet:
Text(text = book.author.name)
However, this seemingly innocent change causes the Author table to be queried
on the main thread.
If you query author information ahead of time but don't need it, it's difficult
to change how data is loaded. For example, if your app's UI no longer needs to
display Author information, your app effectively loads data that it doesn't
display, 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, create a data object that contains each entity, and 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.