12 Kwiecień 2019

Room Persistence Library Introduction – part 3

android programowanie

Autor: Michał Konkel

Room Persistence Library Introduction – part 3

This article is the third and the last part of the three-part series that will smoothly introduce Room Persistence Library to you. The previous part was focused on @Embedded entities and @TypeConverters, during the work we also needed to add DB migration which was discussed as well.

In this part, I will show you how to add relations between tables/entities and how to query them properly. (the second part can be found here).

All sources can be found in related GitHub project.

The Room doesn’t allow object references

Because SQLite is a relational database, you can specify relationships between objects. Even though most object-relational mapping libraries allow entity objects to reference each other, Room explicitly forbids this. Working with Room relations is a bit tricky. Common ORMs allow you to use object references and thus ORMs implement lazy loading which was considered as potentially dangerous in Android apps.

UI needs approximately ~16ms to calculate and draw an updated Activity layout – so if you add to this a lazy loading call for some entity field that can take ~5ms you can run of out time to draw an updated frame for Activity.

For example, lets assume that we can add object reference

and

In such construction and lazy loading, Book will use getAuthor() method to return the author - the first call of this method will query DB for the corresponding Author.
Now if you try to set a text on the TextView and you need an authors name you will write something like this:

This will invoke another query to get the author’s name - on the main thread! This can end up with glitches in your app UI.

So, on the other hand instead of lazy loading we can use eager loading and load all necessary data earlier - but this will cause the overloading of the data and we will load something that isn’t needed in current point of the app.

To sum this up:
We can use Lazy Loading that will allow our app to query faster, load fewer data and improve the performance - but we can struggle with querying on UI thread and risk the glitches.
We can also use Eager loading where we lose all the performance gain, and we will load big amounts of useless data.

The Room comes across with something that mixes these two approaches. To reference multiple entities at the same time we need to create a POJO that contains each entity and write a query that will join corresponding tables.

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

Let’s add some code! We will add Book entity.

Even though you cannot use direct relationships, Room still allows you to define ForeignKey constraints between entities. Using the ForeingKey allows you to decide what should happen with the entity when the corresponding parent is deleted.

Don’t forget to add a migration to the DB (or you can just remove the existing app from the device or emulator and run the project again!)

The database:

Dont forget to create corresponding DAO and a prepopulating object.

One-to-many relation

First of all, let’s focus on one-to-many relation – this will require a special POJO (because Room doesn’t allow object relations) – so we should create the UserWithAllBooks class and simple query.

Lets start with a POJO:

The class above will use a @Relation annotation which can be used in a POJO to fetch relation entities automatically. When the POJO is returned from a query, all of its relations are also fetched by Room. This annotation must be a List or Set.

@Transaction annotation should be used in two cases:

  • If the query is fairly big, it is better to run it inside a transaction to receive a consistent result. Otherwise, the query result does not fit into a single CursorWindow.
    When you use @Relation annotation in POJO class because then all fields are queried separately, so when you want to receive consistent results between these queries it’s a good idea to do them in the transaction.
  • If the result of the query is a POJO with Relation fields, these fields are queried separately. To receive consistent results between these queries, you probably want to run them in a single transaction.

The UserWithAllBooks is just a common POJO but all of the fields are fetched from the entity defined in the @Relation (Book).
The @Embedded annotation will give us a direct access to the fields of the embedded data type (User). Usage of @Embedded annotation is dictated by Kotlin.

@Relation annotation can be used only on POJO classes - that means that you cant use it with @Entity (it’s a room design decision) – so to build a relation as above you can just simply extend the desired entity so it can look like this:

But in Kotlin you can’t inherit from data class – so usage of @Embedded annotation is a Kotlin workaround for @Relation limitation workaround…

Now let’s construct a proper query. The Room allows you to pass parameters into queries to perform filtering operations, such as finding a user by Id or displaying only users live in a certain city, it is as simple as that:

When this query is processed at compile time, Room matches the :userId bind parameter with the userId method parameter. Room performs the match using the parameter names. If there is a mismatch, an error occurs as your app compiles. The keyword LIMIT will assure us that we will get only one user in this query.

One-to-one relation

Now we will add Category entity and create one-to-one relation – with an assumption that one book can have only one category. The process will be much similar to the one-to-many relation but instead of a List, we will add a relation to the single object. This is a rather rare DB relation type, but still, you might need it.

We also need to add another ForeignKey to Book entity:

And a migration

We also need to add a POJO class for catching the query result:

As you can see now we are not using the @Relation annotation, because this is a one-to-one relation and mentioned annotation can be applied only to List or Set.

As you can easily notice we are using @ColumnInfo on non-entity class – this is for our convenience – without this annotation, Room wouldn’t know which column from returned query it should use, also thanks to this we can easily change the returning values names.

We also need to use @TypeConverter with proper class. We can avoid adding it here when we move this converter to Database level, not Entity or POJO.

Used query:

This case will also work fine without @ColumnInfo annotation, it just needs some small modifications:

And we also need to upgrade our query:

The results in our TestActivity will be the same.
It’s your choice which way you want to design your queries, and how to handle relations, but remember about @Transaction.

Please don’t forget to update PrepopulateData and create the CategoryDao – it should be needed while prepopulating DB.

Many-to-many relation

In SQL, implementing M:N relations requires a join table of some form, where the join table has foreign keys back to the entities being related. Room, using SQL at its core, does not change this. And since Room does not model relations, but only foreign keys, to create an M:N relation, you have to create a “join entity” that winds up creating the associated join table.

Regarding our simple App let’s assume that every user can attend a class (subjects) – we need to create a simple entity that will represent the subject, and joined entity users_with_subjects.

It can look a bit scary but if you have a closer look everything will work well. We are using composite primary key that is created with the user_id and subject_id. This will make every pair of user_id and subject_id unique and we will avoid the duplicates. Foreign keys are straightforward. They represent both User and Subject entities ids.

Now with the proper DB query, we can find for example all subjects for a given user:

or, a list of users that should attend a given subject:

Please remember about adding proper migration, prepopulated data set, entities classes to DB and increasing the DB version.

To sum up the relations:
The Room provides the same relation types as in SQL, however, unlike the majority of ORM’s it will not allow you to create object relation – this makes you write additional POJO class that will aggregate query results into objects. Nevertheless, you can use @Relation annotation to avoid complicated SELECT queries.

Things you should see too

  • First of all, you should always query things you really need. In most cases you won’t even need the object relation and simple “id” and “name” will be suitable to meet the business rules of the app.
  • If possible try to use @Relation annotation to simplify the one-to-many relation.
  • Always use the foreign keys constraints between entities. This will allow you to validate data consistency when you are modifying the database use onDelete and onUpdate methods with proper actions SET_NULL, CASCADE, NO_ACTION etc…

Returning subsets of columns:

We did it in previous queries but I want to show you how to work with POJO’s that are returned by the query where we don’t need the whole objects. Let’s think about users books. In most cases, we will need only the title, author and category – to show on a list. All we need is a simple POJO and a good query:

The first line is the book title, the second line will represent the author as the combination of two columns author_firstName and author_lastName, the last one is the book category.

If the returned column name matches the field name in POJO we don’t have to use the @ColumnInfo(name= “…”) annotation. In other cases we need to specify how we named the queried table, just like in the following query:

As you can see we didn’t need to create a new TypeConverter to creating the author full name field – we can use the SQLite functionalities and create one field from two queried columns.

We can also do a similar trick with the @Relation annotation, let’s check how we can use projection field when defining POJO. Let’s create another class for UserWithAllBooks which contains only a book title.

We can also pass multiple parameters to the query:

As simple as that!
Everything is a matter of how you create your query.

Abstract DAO class

Detailed gist can be found here.

In a nutshell, instead of using interface for creating specifics DAO’s you can choose the abstract class. Then you can create methods like this:

Furthermore, if you are tired of creating same DAO methods (insert, delete etc..) you can create one generic Interface BaseDao<T> then create abstract DAO that implements this interface so you will end up like this:

Usage:

Observable queries

Why would we need any observable queries?

The simplest and the most obvious answer is that we want our UI to update automatically when the data changes. To achieve this we can simply use LiveData or RxJava2 as returning value of our query methods. The Room is generating all necessary code to update the values when a database changed.

LiveData

We need to add some dependencies to use the LiveData and AndroidViewModel. It’s better to use LiveData with lifecycle aware component.

Now we need to modify the method for getting the user by Id.

To get all benefits of the architecture components we need to create a separate activity with the corresponding AndroidViewModel

And the activity where in order to observe database for changes, we need to register an observer on the LiveData object by using the method observe() as shown below:

That’s all! Now when we run the LiveDataTestActivity we will see a similar log output:

The timber log will be now called every time Room detects that the result of that query you wrote has changed.

RxJava2

For using RxJava2 we also need some more dependencies. RxJava, RxKotlin and RxJava support for Room

Similarly, as in the previous case, we need to modify the query in UserDao

Usage:

Log output will be the same as in LiveData.
If you are using RxJava2 in your application make your database reactive too! We can use the advantage of Rx returning also types such as Maybe, Single, Flowable.

@Flowable

  • No data (no rows were returned) neither onNext or onError will be emitted
  • Data – onNext will be triggered
  • Update – every data that will be updated and is a part of a query will trigger another onNext

@Single

  • No data (no rows were returned) onError(EmptyResultSetException) will be triggered
  • Data – onSucces will be triggered
  • Update – nothing happens

@Maybe

  • No data (no rows were returned) Maybe will complete
  • Data – onSuccess will be triggered and Maybe will complete
  • Update – nothing happens

Watch out!
You need to be aware that every update on the field used in DB query will trigger the observable query and UI will be updated (or deleted) – this is OK if you design your app like that, but it can be also – unwanted behaviour. You should control your code. When you don’t need to update your UI try to use Single instead of Flowable. Make sure to handle the emission in your DAO.

With flowable you can simply use .distinctUntilChanged() link
With LiveData it’s a bit harder and you can see more here.

That’s All Folks! We’ve reached the end of the third part of the introduction to the Room Persistence Library. I hope you’ve enjoyed this post. In case of any questions don’t hesitate to write a comment.

(October 8.2018 edit)
During work on this post, a new version of Room was released (2.1.0-alpha01). You can find the details here

Cheers!

W celu poprawy jakości naszych usług używamy ciasteczek.

Możesz je zablokować poprzez zmianę ustawień przeglądarki.