Android Room database

theplebdev

Tristan Elliott

Posted on July 31, 2021

Android Room database

Introduction

  • This series is going to be dedicated to the basics of an SQLite database. I will be following the official google guide, HERE but I will be working in an order that makes more sense to me.

Before we start

  • This section is pretty thread heavy so if you are unfamiliar with threads I would recommend that you start reading the Oracle thread tutorial HERE and end HERE
  • I would also recommend watching THIS video on the Java singleton pattern, because we will be using that pattern in this tutorial.

What is a Room Database?

  • Simply put it is an abstraction layer on top of the SQLite database that makes working with SQLite a lot easier by internally handling the implementation details.

  • When we create a Room database we must satisfy 3 conditions.

1) : The class must be annotated with a @Database annotation that includes an entity array that lists all of the data entities associated with the database.

2) : The class must be an abstract class that extends RoomDatabase.

3) : For each DAO class that is associated with the database, we must define an abstract method that has zero arguments and returns an instance of a DAO class.

  • With that being said we can paste the code and walk through it line by line
@Database(entities = {Word.class}, version = 1, exportSchema = false)
public abstract class WordRoomDatabase extends RoomDatabase {

    public abstract WordDAO WordDAO(); 

    private static volatile WordRoomDatabase INSTANCE;
    private static final int NUMBER_OF_THREADS = 4;
    static  final ExecutorService databaseWriteExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);

    static WordRoomDatabase getDatabase(final Context context){
        if(INSTANCE == null){
            synchronized (WordRoomDatabase.class){
                if (INSTANCE == null){
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            WordRoomDatabase.class,"word_database")
                            .build();
                }
            }
        }
        return INSTANCE;
    }
}

Enter fullscreen mode Exit fullscreen mode
  • This code might look complicated and it is but don't worry we are going to take it slow and go line by line.

@Database(entities = {Word.class}, version = 1, exportSchema = false)

  • The @Database(entities = {Word.class} is part of the mandatory code we must include in order for the Room library to properly operate. @Database tells the Room library that this class is going to represent our database. entities = {Word.class} is our entity array and is used to represent all of the entities in our database system. version = 1, exportSchema = false) has to do with with database migrations. However, database migration is a little much for this tutorial. So just know that we use them to avoid build errors.

public abstract class WordRoomDatabase extends RoomDatabase

  • This is just a normal class declaration and we use extends to indicate RoomDatabase is the super class. This is mandatory for any class that is annotated with @Database. You should also note that our subclass is abstract. A subclass is made abstract if it does not implement all of the abstract methods inherited from the parent class. You can prove this if you delete abstract and read the error messages.

public abstract WordDAO WordDAO()

  • This is also mandatory for our Database class, it is actually through our Data Access Objects(DAO) that we will be able to interact with the database. This method must also be abstract because Room will handle the implementations for us.

private static volatile WordRoomDatabase INSTANCE;

  • Private is straight forward, we use it to promote encapsulation and information hiding. It is static because the method that will set this instance is static, a static method can only access static variables. This is a key property of the singleton pattern. However, volatile is a little more tricky and we need some background information to fully understand it.

Volatile

  • When we mark a variable as volatile it does a few things but its main goal is to make sure that changes to this variable on one thread are seen by all other threads. If we didn't mark this variable as volatile we could get a memory consistency error. So now we need to talk about memory consistency errors

Memory consistency errors

  • This type of error occurs when different threads have inconsistent views of what should be the same data.
  • A way to avoiding these types of errors is to establish a happens-before relationship. This type of relationship is simply a guarantee that memory writes by one specific statement are visible to another specific statement. A common way to establish this type of relationship on a variable that is changing on multiple threads is to declare it as volatile.

  • So basically we use volatile to prevent memory consistency errors by establishing a happens-before relationship. This lets all threads see the current state and changes of the variable

private static final int NUMBER_OF_THREADS = 4;

  • This is pretty standard we are just using modifiers to declare a class variable of type int that never changes and then we set its value to 4.

static final ExecutorService databaseWriteExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);

  • Before we can fully understand this statement we need to understand the two basics strategies for creating threads.

1) Directly control: thread creation and management simply instantiate a thread ourselves each time our application needs to initiate one. As you can imagine this is complex and a lot of word.

2) Abstract thread management: for the rest of the application, pass the job to an executor. This executor will handle all the thread management for us. We are using this technique.

  • The return type of ExecutorService is an interface that has methods for creating, tracking and destroying threads. Executors is a class that provides us with utility methods for creating things such as a fixed thread pool. Speaking of fixed thread pools, lets talk about thread pools.

Thread Pools

  • Thread pools consist of worker threads, it is important to point out that worker threads are a special kind of thread. They are significantly smaller in memory than a regular thread object, so the memory overhead is lower. Also, worker threads are often used to carry out asynchronous tasks(what we are using them for).
  • A common type of thread pool is called the fixed thread pool (what we are using). This type of thread pool always has a specific number of threads running. Any sort of tasked issued to this pool will be submitted to an internal queue, which holds tasks whenever there are more tasks then the active number of threads. Once a thread becomes available it will be given a task by the pool

  • So basically with our statement we are creating a fixed thread pool consisting of 4 worker threads that we are able to use and issue asynchronous tasks that will interact with our database.

static WordRoomDatabase getDatabase(final Context context)

  • This method is singleton pattern 101, so I won't go into it too much but it is mainly constructed to only return one instance of INSTANCE. You probably will notice the keyword synchronized and this also needs some explaining.

Synchronized

  • When threads communicate with each other they do so, primarily by sharing access to fields of the objects that they are using. This way of communication is extremely efficient, however, it makes two types of of errors possible.

1) : Memory Consistency Errors(We already talked about these)

2) : Thread Interference, this occurs when two operation on two different threads end on the same data. When this happens the threads are said to interleave. Thread interleaving leads to memory consistency errors. The way that we deal with this thread interleaving is to declare our method as synchronized and doing so has two effects

1) : It is not possible for two invocations of a synchronized method to interleave when working on the same object. When one thread is executing a synchronized method on an object, all other methods on the same object must wait until it is finished.

2) : When a synchronized method finishes it guarantees that the change made to the object is visible to all other threads performing operations on that object.

if (INSTANCE == null)

  • I am not 100% sure why we have to do a double check to see if the instance is null but I assume it is something we have to do with us working with threads. However, if the instance is null then we get to create our actual database. To create our database we use the Room.databaseBuilder method and we provide it with the global context, an instance of our database class and the name that we are going to call our database. Lastly we call build() to actually create our database and return INSTANCE. INSTANCE now represents our database. If the instance is not null, then we simply return instance, this is stereotypical of the singleton pattern.

  • With that we have successfully created a Room database.

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
💖 💪 🙅 🚩
theplebdev
Tristan Elliott

Posted on July 31, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related