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.
- We run the
getPeopleWithBirthCertificates
function in the Dao. - The
"SELECT * FROM Person"
query is run, fetching allPerson
data from the table. - Room looks at the return type and creates a
PersonAndBirthCertificate
instance for each row in the table. -
Room sees the
@Embedded
annotation onperson
and copies the data from all columns matching properties in thePerson
to a newPerson
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. -
Room sees a
@Relation
annotation on thebirthCertificates
property, and runs another query to fetch theBirthCertificates
.- The
@Relation
annotation specifies aparentColumn
andentityColumn
. parentColumn
is the column in the original query that is considered the id of the ownerentityColumn
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 aowningPersonId
that matches theid
of eachPerson
- The
- Room fills in the
BirthCertificate
for eachPersonWithBirthCertificate
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 multipleAddress
entities (home, work, etc) - An
Address
can host multiplePerson
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)