Information is perhaps one of the most important resources for users of an application. It can be sensitive and contain the user’s private data. That’s why it’s important to properly manage an application’s data, so as to guarantee adequate integrity, privacy and storage.
When an application manages repetitive or structured data, the recommended approach is to store data in a local database on the user’s device. This way, when the device can’t access the network, the user can still use the app while being disconnected.
To store persistent information in Android, an available option is to use the relational database system, SQLite. The main advantage of SQLite is that it doesn’t function as an independent process, instead, it forms a part of the application that’s using it. Therefore, it’s neither necessary to be installed independently nor requires we manage its running state (started/stopped) apart from the main application. It doesn’t even have a configuration file. Although SQLite is powerful, it’s considered a low-level API. Its use requires time and effort, due to the following:
To create an authentically good implementation of an SQLite DB means having to deal with the aforementioned inconveniences. Alternatively, we can use an ORM (Object-Relational Mapping), but we would need to define and create manually the database, extending from SQLiteOpenHelper, and create contract classes.
So, our dilemma can be summed up as follows: Is Room the solution to simplify our process?
The general recommendation to avoid the earlier mentioned disadvantage is to use persistent libraries, which serve to add a layer of abstraction to access the data between our apps and the SQLite database.
In Google’s 2017 I/O conference, the Android team introduced Room, a persistence library which allows us to create and manage SQLite databases more easily. Since then, Room has evolved to become one of the most powerful and versatile tools in the market for managing information in Android apps, simplifying the workflow and at the same time guaranteeing the security and integrity of our data.
In essence, Room has the following characteristics which improve on SQLite’s disadvantages:
To use Room in our application, we can start by adding the following dependencies to our build.gradle file:
It’s not possible to have one single implementation method in our apps that works well in every scenario.
As the documentation indicates, the architectures recommended by Android Team are a starting point for the majority of situations.
However, if we have a good method to implement our Android apps that follow common Architectural Principles (Separation of Concerns (SoC)[1], DriveUI from a Model, etc.), we have no reason to modify it.
That being said, an application’s architecture can be structured using Architecture Components [2] as follows:
Notice that each component depends only on the component one level below it. For example, activities and fragments depend only on a view model. The repository is the only class that depends on multiple other classes; in this example, the repository depends on a persistent data model and a remote backend data source.
– Android Documentation
In this article, we focus on the architecture of the Persistent Data Model (Room).
In Android, it’s always encouraged to structure an application respecting the Architectural Components described here. Later, in relation to the level of complexity of an application, it’s possible to shape our design using diverse architectural patterns like MVP (Model View Presenter), MVVM (ModelView ViewModel), Clean Architecture, etc.
[1] SoC– Is a design principle that has the objective of dividing an application into different sections so that each section deals with a specific concern or function. A concern is a set of information that affects an application’s code and can be as general as “the hardware requirements for an application,” or as specific as “the name of the specific class that we need to instantiate.”
[2]: Architecture Components: A subset of libraries included in Android Jetpack [3] that are well-known for application development, including:
Room’s Architecture / Architecture Components
In relation to Architecture Components [2], we can highlight the following set of components: LiveData, ViewModel and Repository where the following diagram shows the basic form of the architecture.
ViewModel: Acts like a communications hub between the repository (data) and the UI. The UI doesn’t need to worry about the origin of the data. The instances of ViewModel survive the recreation of the Activity/Fragment.
Repository: Deals with an intermediate class whose function is to manage multiple sources of data.
LiveData: A class data holder that can be observed. It always contains/maintains a cache of the latest version of the data and notifies the observers when data have been modified. For this reason, LiveData is lifecycle aware.
Room is part of the Android Architecture Components, which means it works well with ViewModel, LIveData and Paging, but it doesn’t depend on these modules. As of Google I/O 2018, it also is part of Android Jetpack[3].
Through the use of annotations, we can define our database, tables and operations.
Room automatically translates these annotations into SQLite queries to execute the operations in the database engine. Room is made up of three principle components:
SQLite database: The database on the device. Room creates and maintains the database for us.
[3]: Android Jetpack: The set of recommended libraries to develop apps that make up the best practices as suggested by Android. These libraries include backward compatibility for previous versions of Android.
3. Annotations
Room works with an architecture whose classes specify predefined annotations. The most common annotations are the following (although there are more available):
Additionally, we can consult the Android documentation for more information on each item.
Entity
An entity is simply an annotated Kotlin or Java class with @Entity, which models a table in a database, where the instance variables establish a corresponding map with each of the columns in the database table, and the name of the class corresponds to the name of the table. One or more attributes can make up the primary key (PK) which allows the uniqueness of the instances/records in the table.
To be able to persist an attribute, Room should have access to the referenced variables, meaning we should have our variables declared as public, or adequately configured setter/getter methods.
In an application to track the properties of our employees, we might have a class that looks as follows:
@Entity
data class Employee(
@PrimaryKey(autoGenerate = true) val employeeId: Long,
@ColumnInfo(name = "FIRST_NAME") val firstName: String?,
@ColumnInfo(name = "LAST_NAME") val lastName: String?,
@ColumnInfo(name = "ADDRESS") val address: String?,
@ColumnInfo(name = "AGE" ) val age: String?,
@ColumnInfo(name = "PROFESSION") val profession: String?
)
What is a DAO? A structural overview
We use Room to be able to access and interact with the persisted data in the database from our application.
The most convenient way to do it is to define DAOs (Data Access Object) because they constitute a modular form for accessing a database in comparison with other query builders or directly writing queries themselves.
We can define a DAO as an interface or as an abstract class; the correct answer will depend on the complexity of the app we are developing. For simpler apps, in general, we can use an interface.
Whatever the case may be, we should write our DAOs with the annotation @DAO. In the DAOs we don’t define attributes, we only define one or more methods to access the database of our app.
What can a DAO offer us?
There are two types of methods that we define in a DAO to interact with the database:
An example of a DAO query for the entity Employee could be as follows:
@Dao
interface EmployeeDAO {
// (1)
@Insert
fun insertEmployee(vararg employee: Employee)
// (2)
@Update
fun updateEmployee(vararg employee: Employee)
// (3)
@Delete
fun eraseEmployee(employee: Employee)
// (4)
@Query( value: "SELECT * FROM Employee")
fun loadEmployees(): ArrayList<Employee>
// (5)
@Query( value: "SELECT * FROM Employee WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): ArrayList<EmployeeJava>
}
(1) – @Insert: Allows us to define a method to save data in a specific table. The example shows how to insert one or more objects as a type of Employee. If a method is annotated with @Insert, it receives only one parameter, which can return a long value, which is the new rowId [4] for the item inserted. If the parameter is an array or a collection, later the method returns an array or a collection of long values.
( 2 ) – @Update: Allows us to define methods to update specific records in a database table. The example of the method @Update shows how to update one or more objects of type Employee in the database.
( 3 ) – @Delete: Allows us to define methods that erase records in our database table. The example shows a method @Delete that is used to erase one or more objects of the type Employee in the table.
Each parameter of a method annotated with @Insert, @Update, or @Delete should be an instance of a data class annotated with @Entity or a collection of the same. The methods annotated with @Update or @Delete use the PK to establish the connection to the entity instances passed in as parameters with the database records. If a record doesn’t exist with the same PK, Room doesn’t make any change. Additionally, it can optionally return a whole value that indicates the number of rows that were updated or erased successfully.
( 4 ) – @Query: Allows us to write SQL statements and expose them as DAO methods. The example query defines a method that uses a simple SELECT statement to return all of the objects in the Employee table of the database.
( 5 ) – @Query: How to pass simple parameters to a query. It’s common that in DAO methods we need to accept parameters to be able to make filter operations. Room supports binding parameters to our queries. The example defines a method that returns all of the employees whose ages are above a certain number.
Relations between entities (1:1, 1:M, and M.M)
As SQLite is a relational database, we can define relations between entities. As we have mentioned before, with Room, Google decided that the “entities should not contain entities.” This focus tries to eliminate the possibility of executing operations with the database in the main thread.
Working with the relationships in Room is maybe the most tricky part about this technology, and to remain brief, we will only include here a brief introduction.
Common ORM patterns permit us to reference objects, and therefore, ORMs implement a type of lazy loading that is considered potentially dangerous in Android applications.
The reason for this decision is as follows: The majority of the ORM libraries make it possible to map relationships from a database to the respective data model in the application. This is a common practice and works well on the server, because the lazy loading of fields means that the work is actually carried out when the demand is lower (lazily), and offers performance improvements.
“However, on the client side, this type of lazy loading isn’t feasible because it usually occurs on the UI thread, and querying information on disk in the UI thread creates significant performance problems,” according to the Android Documentation.
Although we can’t use direct relations, Room still permits us to define restrictions on external foreign keys between entities. In Room, there are three approaches that can be used to define relations between entities.
We don’t need to consider these three approaches as mutually exclusive. In other words, in Room we have the relationships 1:1, 1:Many, and Many:Many between entities. These are not configured using only the first option, or only the second option, but rather a combination of options 2 and 3, for example. [See annotations for @Relation/@Embedded]
4] rowId – For more information related to the rowId values, see the SQLite reference documentation for the annotation @Insert, by clicking here.
@Embedded annotation: The use of the @Embedded annotation allows us to include POJO (Plain Old Java Object) classes in a Room entity. Room can’t map complex data types defined by a user to SQLite. Continuing our earlier example, this might be a mapping between Employer and Address.
@ForeignKey annotation: We use the ForeignKey annotation when all of the objects are stored in separate tables, or when we need a waterfall behavior. We could say that this type of annotation allows us to create part of a relation between two entities. Later, to be able to insert, update, or read entities through query methods, we will need to use the @Embedded/@Relation annotations.
@Relation/@Embedded annotations: The official Android documentation lists these as alternatives to establish relationships between entities. Therefore, it represents the only option of the three approaches mentioned here that can be used to map relationships between entities and execute CRUD operations without combining them with another option, (for example the @ForeignKey annotation). However, if we use these two annotations in this fashion, we will not have access to all of the features that are offered by @ForeignKey. It is for this last reason we mention that these two annotations can’t completely satisfy our requirements when relating two entities.
Creation of the 1:Many relation
Let’s say we have a hypothetical application that tracks which vehicles belong to each student at a university. To be able to link a Student and Vehicle entity, we must define a Foreign Key (FK) between the two tables. For this, we will need to specify three aspects:
Consulting a 1:Many Relationship
To be able to generate the necessary structure to retrieve the vehicles associated to a particular student, we will need to implement a query that returns the following type of response:
To be able to return this entity from a query, we need to be able to provide two annotations:
The resulting code would be the following:
Later, inside of our DAO, we can write the query to be able to retrieve each student and their associated vehicles.
Here, we can note two different annotations used here:
Conclusions
References