Skip to content

Room

Room Relations

So far we've only talked about entities by themselves. But we really need relationships to make things work.

There are two general approaches to making relationships work

  • Helper class: You define a non-entity class to hold data and explicitly pull columns
    • @Embedded objects can group attributes into objects
    • @Relation objects run secondary queries automatically
  • Multi-map: Specify a Map<Entity, List<OtherEntity>> as the return type on a DAO function

We'll look into using some of these in a few examples, which should be enough for the class assignment. For a more detailed discussion see https://developer.android.com/training/data-storage/room/relationships.

Fetching Partial Data

You can use a helper class to fetch partial data from an Entity.

Let's start by only fetching the name and age of Person entities. Recall that our Person looks like

@Entity
data class Person(
    @PrimaryKey var id: String = UUID.randomUUID().toString(),
    var name: String,
    var age: Int,
    var ssn: String,
)

If we only want the name and age, we can define a data class with just those properties:

data class NameAndAge(
    val name: String,
    val age: Int,
)

Note

Note that NameAndAge does not have the @Entity annotation! It's just a normal data class!

And then in the DAO, you can define

@Query("SELECT name, age FROM Person")
fun getNamesAndAges(): Flow<List<NameAndAge>>

Room will fill the NameAndAge instances, not returning the id or ssn. (This would also work with a "SELECT * FROM Person" query, but that would fetch all the data from the database before creating NameAndAge objects with just the name and age)

One-to-One

Let's set up support to fetch a BirthCertificate while we're fetching a Person. Suppose we have entities

@Entity
data class Person(
    @PrimaryKey var id: String = UUID.randomUUID().toString(),
    var name: String,
    var age: Int,
    var ssn: String,
)

@Entity
data class BirthCertificate(
    @PrimaryKey var id: String = UUID.randomUUID().toString(),
    var owningPersonId: String,
    var fatherName: String,
    var motherName: String,
    var birthTime: Long,
)

We want to fetch a Person and its associated BirthCertificate at the same time. So we create

data class PersonAndBirthCertificate(
    @Embedded
    val person: Person,
    @Relation(
        parentColumn = "id", 
            // the id attribute of the "owner"
        entityColumn = "owningPersonId",
            // the foreign key in the related item that holds the 
            //   owner id
    )
    val birthCertificate: BirthCertificate
)

// (in the DAO)
@Transaction
@Query("SELECT * FROM Person")
fun getPeopleWithBirthCertificates(): Flow<List<PersonAndBirthCertificate>>

Our helper class, PersonAndBirthCertificate contains two objects - a Person and a BirthCertificate. Surprise!

Let's talk through how they get filled in.

  1. We run the getPeopleWithBirthCertificates function in the Dao.
  2. The "SELECT * FROM Person" query is run, fetching all Person data from the table.
  3. Room looks at the return type and creates a PersonAndBirthCertificate instance for each row in the table.
  4. Room sees the @Embedded annotation on person and copies the data from all columns matching properties in the Person to a new Person instance. Note that the @Embedded object doesn't have to be the same type as table we're querying. It could be a partial-data class that contains properties that match columns in the table. Note that you can use @ColumnInfo annotations to create custom mappings between column names and properties, but we're not covering that in this class.

  5. Room sees a @Relation annotation on the birthCertificates property, and runs another query to fetch the BirthCertificates.

    • The @Relation annotation specifies a parentColumn and entityColumn.
    • parentColumn is the column in the original query that is considered the id of the owner
    • entityColumn is the foreign key inside the related object (the one specified as the type of the annotated property, BirthCertificate in this example)
    • Room's query fetches from the BirthCertificate table grabbing entities that have a owningPersonId that matches the id of each Person
  6. Room fills in the BirthCertificate for each PersonWithBirthCertificate

You can have multiple @Relation annotations inside the helper class, but note that they each result in running an extra query. You can also nest relations, where related objects can contain their own @Relation annotations to create a larger graph of results.

Note

Did you notice the @Transaction annotation on the getPeopleWithBirthCertificates function? This ensures that no matter how many queries are run when the function is called, they're all treated atomically. No updates can happen between any of the queries, keeping the returned data consistent. Whenever you use @Relation, make sure any DAO functions that reference the class containing it are marked with @Transaction.

@Transaction can also be used when defining non-abstract functions in a DAO. For example, suppose you want to define a transfer function in a banking DAO that removes funds from one account and deposits them in another account. You would need a @Transaction annotation on such a function so the operation is atomic. You'd never want it to be able to only deposit or withdraw. You could write (if the DAO is an abstract class):

@Transaction
fun transfer(
    from: Account,
    to: Account,
    amount: Long
) {
    withdraw(from, amount)
    deposit(to, amount)
}

One-to-Many

One-to-Many works almost exactly like One-to-One. The only difference is the declaration of the related property in the helper object. For our Organs example, we could define

@Entity
data class Person(
    @PrimaryKey var id: String = UUID.randomUUID().toString(),
    var name: String,
    var age: Int,
    var ssn: String,
)

@Entity
data class Organ(
    @PrimaryKey var id: String = UUID.randomUUID().toString(),
    var owningPersonId: String,
    var type: String,
)

for our entities, and

data class PersonAndOrgans(
    @Embedded
    val person: Person,
    @Relation(
        parentColumn = "id",
        // the id attribute of the "owner"
        entityColumn = "owningPersonId",
        // the foreign key in the related item that holds the
        //   owner id
    )
    val organs: List<Organ>
)

// and in the DAO
@Transaction
@Query("SELECT * FROM Person")
fun getPeopleAndOrgans(): Flow<List<PersonAndOrgans>>

The only difference here is that our related property, organs is a List rather than a single-value property.

Many-to-Many

Things get more complex when we define a many-to-many relationship. Take our example of Person and Address:

@Entity
data class Person(
    @PrimaryKey var id: String = UUID.randomUUID().toString(),
    var name: String,
    var age: Int,
    var ssn: String,
)

@Entity
data class Address(
    @PrimaryKey var id: String = UUID.randomUUID().toString(),
    var street: String,
    var city: String,
    var state: String,
    var zipCode: String,
)

@Entity(
    primaryKeys = ["personId", "addressId"]
)
data class PersonAddress(
    var personId: String,
    var addressId: String,
)

These three entities give us a many-to-many relationship:

  • A Person can have multiple Address entities (home, work, etc)
  • An Address can host multiple Person entities.

All three are entities, and we can use helper objects to fetch a Person or Address and the related other:

data class PersonAndAddresses(
    @Embedded
    val person: Person,
    @Relation(
        parentColumn = "id",
            // which property represents the id of the person 
            //    (the main entity we're fetching)
        entityColumn = "id",
            // which property represents the id of the Address (the related entity)
        associateBy = Junction(
            PersonAddress::class,
            parentColumn = "personId",
                // which property **in the association table** represents
                //   the id of the person (the main entity we're fetching)
            entityColumn = "addressId",
                // which property **in the association table** represents
                //   the id of the Address (the related entity)
        ),
    )
    val addresses: List<Address>
)

// in the DAO
@Transaction
@Query("SELECT * FROM Person")
fun getPeopleAndAddresses(): Flow<List<PersonAndAddresses>>

Similar to before, but we add an associateBy attribute to describe the association table. Room maps the parentColumn of the @Relation to the parentColumn in the Junction, and the entityColumn in the @Relation to the entityColumn in the Junction.

This ends up running two queries. First, get all the Person entities.

SELECT * FROM Person

Room now has all the Person data, including the ids of each Person. Room will now run its second query, passing those ids (I'm representing them as $personIds in this query).

SELECT 
    Address.id, Address.street, Address.city, 
    Address.state, Address.zip
FROM PersonAddress 
INNER JOIN Address ON (PersonAddress.addressId = Address.id)
WHERE PersonAddress.personId IN ($personIds)