Hi there! How are you? In my last article, I talked about how to implement a Room Database using Kotlin and the benefits over the SQL framework implementation, but this time I want to comment what I found most difficult during the Room usage, which is the object relationship.

Room and relationships

Because SQLite is a relational database, you can specify relationships between objects. Even though most Object Relational Mapping (ORM) libraries allow entity objects to reference each other, Room explicitly forbids this.

Mapping relationships from a database to the respective object model is a common practice and works very well on the server side where it’s performant to lazily load fields as they’re accessed. However, on the client side, lazy loading is not feasible because it’s likely to happen on the UI thread, and querying information on disk in the UI thread creates significant performance problems. For more details, see Addendum: No object references between entities.

So, even though you cannot use direct relationships, Room still allows you to define Foreign Key constraints between entities.

Imagine that we have 2 entities like these ones:

1 2 3 4 5 6 7 8 9 10 11 @Entity data class Owner ( @PrimaryKey var id : Int = 0 , var age : Int = 0 , var name : String = "" , var address : String = "" ) @Entity data class Pet ( @PrimaryKey var petId : Int = 0 , var petName : String = "" , var petAge : Int = 0 )

@ForeignKey

1 2 3 4 5 6 7 8 9 10 11 12 13 @Entity data class Owner ( @PrimaryKey var id : Int = 0 , var age : Int = 0 , var name : String = "" , var address : String = "" ) @Entity ( foreignKeys = arrayOf ( ForeignKey ( entity = Owner :: class , parentColumns = arrayOf ( "id" ) , childColumns = arrayOf ( "ownerId" ) ) ) ) data class Pet ( @PrimaryKey var petId : Int = 0 , var petName : String = "" , var petAge : Int = 0 , var ownerId : Int = 0 )

To define the relationship between a Pet and its Owner we need to use theannotation, as shown in the following code snippet:

@Query

1 2 3 @Query ( "SELECT * FROM Pet" + "INNER JOIN Owner ON Pet.ownerId = Owner.id WHERE Owner.id= :id" ) fun getOwnerAndPetDetails ( id : Int ) : Owner

And in the DAO when can define ato retrieve a User and its Pet:

@Query

1 2 3 4 5 6 7 8 9 10 11 12 data class OwnerAndPet ( var id : Int = 0 , var age : Int = 0 , var name : String = "" , var address : String = "" , var petId : Int = 0 , var petName : String = "" , var petAge : Int = 0 , var ownerId : Int = 0 ) @Query ( "SELECT * FROM Pet" + "INNER JOIN Owner ON Pet.ownerId = Owner.id WHERE Owner.id= :id" ) fun getOwnerAndPetDetails ( id : Int ) : OwnerAndPet

But the lasthas a problem, the return type. We put Owner and that is not correct. Look that instead of an Owner, we are getting an object with all the Owner fields, but also with all the Pet fields, to solve this we can a Custom Query Result Object, notice that it’s simply a POJO. The fields returned from the query must match the fields in the class. The POJO is verified at compile time and raises a warning if there is a mismatch.

@ForeignKey

1 2 3 4 5 6 7 8 @Entity ( foreignKeys = arrayOf ( ForeignKey ( entity = Owner :: class , onDelete = CASCADE , parentColumns = arrayOf ( "id" ) , childColumns = arrayOf ( "ownerId" ) ) ) ) data class Pet ( @PrimaryKey var petId : Int = 0 , var petName : String = "" , var petAge : Int = 0 , var ownerId : Int = 0 )

annotation is very powerfull, we can define all kind of relationships, 1-to-1, 1-to-many and many-to-many, and they also allow you to specify what occurs when the referenced entity is updated. For example, you can tell Room to delete the Pet from the database if its corresponding Owner is deleted:

@ForeignKey

Nested objects

You can read more aboutannotation here.

Sometimes, you’d like to express an entity as a cohesive whole in your database logic, even if the object contains several fields. In these situations, you can use the @Embedded annotation to represent an object that you’d like to decompose into its subfields within a table. You can then query the embedded fields just as you would for other individual columns.

For instance, we can modify our Owner class to instead of using a simple String to store the user address, to use an Address class with the following fields: street, city, postCode. To store the composed columns separately in the table, you must declare an Address field in the Owner class that is annotated with @Embedded , as shown in the following code snippet: (Notice that Address is NOT an entity)



1 2 3 4 5 6 7 8 9 data class Address ( var street : String = "" , var city : String = "" , var postCode : String = "" ) @Entity data class Owner ( @PrimaryKey var id : Int = 0 , var age : Int = 0 , var name : String = "" , @Embedded var address : Address = Address ( ) )

Conclusion:



We just saw that we can cover all the relationships using @ForeignKey and @Embedded annotations. It’s obvious that using Room is not easy as other ORM libraries but it still has 2 advantages in my opinion.

As you need to specify explicitly all the relationships you are more aware of what it’s happening and how the objects are stored when you create them. Room is forcing you to only request the data you need, in apps with large databases and complex queries this lead to significant increase in the app performance.

What do you think about Room Relationships? You can leave your comment below. Thanks!