Embedded Types

With type converters, we are teaching Room how to deal with custom types, but we are limited to mapping from one property to one column. That property might be complex, but it still goes into one column in the table.

What happens, though, when we have multiple columns that should combine to create a single property?

In that case, we can use the @Embedded annotation on some data class or other simple Kotlin class, then use that class as a type in an entity.

Example: Locations

For example, as was noted earlier in this chapter, cramming a location into a single TEXT column works, but we cannot readily query on the resulting column. If we want to query for locations near some location in the database, it would be much more convenient to have the latitude and longitude stored as individual REAL columns. But, using type converters, we cannot map two columns to one property.

With @Embedded, we can, as we can see in the EmbeddedLocationEntity class in the the MiscSamples module:

package com.commonsware.room.misc

import android.location.Location
import android.util.JsonReader
import android.util.JsonWriter
import androidx.room.*
import org.threeten.bp.Instant
import java.io.StringReader
import java.io.StringWriter

data class LocationColumns(
  val latitude: Double,
  val longitude: Double
) {
  constructor(loc: Location) : this(loc.latitude, loc.longitude)
}

@Entity(tableName = "embedded")
data class EmbeddedLocationEntity(
  @PrimaryKey(autoGenerate = true)
  val id: Long = 0,
  val name: String,
  @Embedded
  val location: LocationColumns
) {
  @Dao
  interface Store {
    @Query("SELECT * FROM embedded")
    fun loadAll(): List<EmbeddedLocationEntity>

    @Insert
    fun insert(entity: EmbeddedLocationEntity)
  }
}

Here, we have a LocationColumns class that wraps our latitude and longitude. The entity itself has a LocationColumns property, named location, marked with the @Embedded annotation. Now, Room will use individual REAL columns for our latitude and longitude:

CREATE TABLE IF NOT EXISTS embedded (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, latitude REAL NOT NULL, longitude REAL NOT NULL)

As a result, we could construct queries on those columns, if we wished.

Note that even though a class used in @Embedded, like LocationColumns, is not an entity, you can still use @ColumnInfo annotations on it to rename columns, if desired. Also, you are subject to the same rules for data types: the properties need to be types that Room understands, natively or via type converters that you create.

Simple vs. Prefixed

What happens if we need two locations, though? Perhaps we need officeLocation and affiliateLocation, or something like that.

By default, Room generates column names based on the @Embedded class’ property names, perhaps modified by @ColumnInfo annotations on those properties. In this case, though, if we have two LocationColumns properties in the same entity class, we would wind up with two latitude and two longitude columns, which neither Room nor SQLite will support.

To address this, the @Embedded annotation accepts an optional prefix property:

@Embedded(prefix = "office_")
val officeLocation: LocationColumns

The columns for that entity will have the prefix added:

CREATE TABLE IF NOT EXISTS embedded (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, office_latitude REAL NOT NULL, office_longitude REAL NOT NULL)

Hence, having two LocationColumns simply means that one or both need to use distinct prefix values.

However, bear in mind that this changes the column names, so you will also need to adjust any @Query function that references those names, so that you use the appropriate prefix.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.