8.
The DAO Pattern
Written by Aldo Olivares
In the previous chapter, you learned about different kind of relations, such as one-to-one, one-to-many and many-to-many. You also learned how to create them using annotations.
Now, you are going to learn how to retrieve, insert, delete and update data from your database using Database Access Objects.
Along the way, you will also learn:
- What DAOs are and how they work.
- How to create DAOs using Room annotations.
- How to prepopulate the database using a provider class.
- How to perform INSERT INTO queries using
@Insertannotated methods. - How to perform DELETE FROM queries using
@Deleteannotated methods. - How to use the
@Queryannotation, to read data from the database.
Ready? Dive in!
Getting started
Download the starter project attached to this chapter and open it using Android Studio 3.4 or above. Once Gradle finishes building your project, take some time to familiarize yourself with the code. If you have been following along, to this point, you should already be familiar with the project since it is the same as the final project from the last chapter. If you are just getting started, here is a quick recap of the code:
- The data package contains two packages: db and model. The db package contains the
QuestionDatabaseclass, which defines your Room database. The model package contains your entities:QuestionandAnswer. - The view package contains all your activities:
MainActivity,QuestionActivityand theResultActivity.
Now Build and Run the app to verify that everything is working properly:
Cool! Now you are ready to start creating some Database Access Objects in order to manipulate the data.
Using DAOs to query your data
Database Access Objects are commonly known as DAOs. DAOs are objects that provide access to your app’s data, and they are what make Room so powerful since they abstract most of the complexity of communicating to the actual database. Using DAOs instead of query builders or direct queries makes it very easy to interact with your database. You avoid all the hardship of debugging query builders, if something breaks, and we all know how tricky SQL can be! They also provide a better separation of concerns to create a more structured application and improve its testability.
In Room, the DAOs are defined as interfaces or abstract classes. The only difference between both implementations is that the abstract class can optionally accept a RoomDatabase instance, as a constructor parameter. They are also convenient for defining large database transactions, using the @Transaction annotation on a method, and calling multiple different methods within.
If you are wondering why DAOs are defined as abstract classes or interfaces, it is because Room takes care of creating each DAO implementation at compile time, by generating the business logic code for your definitions.
Note: Remember that Room does not support database access on the main thread by default since performing long running operations such as database transactions might cause your app to freeze or crash. If you still want to take this risk, you will need to explicitly call
allowMainThreadQueries()on your database builder.
But, alright, that’s enough theory. Think about all the operations that you will need to perform on your database to make the DroidQuiz app work:
- You will need to get the list of all the questions currently stored in your database.
- You will also need to be able to create new questions when your app is created.
- Finally, you will also have to delete questions at some point.
With the above in mind, create a DAO in your project that perform CREATE, READ, and DELETE queries in your database. You will see how easy it is to create DAOs in Room.
Create a new interface under the db package and name it QuizDao. To turn your new interface into a DAO, simply add the @Dao annotation like below:
@Dao
interface QuizDao {
}
Now, add the following code to the interface:
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(question: Question)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(answer: Answer)
The @Insert annotation is a shortcut that allows you to automatically create an INSERT INTO query, that creates a new record in the appropriate table, with the object passed as a parameter. In this case, the first method is going to create a new record in your Question table using the question object passed as a parameter and the second method is going to create a new record in your Answer table.
You might have also noticed the onConflict parameter in the @Insert annotation. It allows you to define an OnConflictStrategy, which will be used to specify what happens in case there is a conflict when creating a new entry. There are several options:
- ABORT this is the default option and instructs Room to abort the transaction, return an error and roll back any changes made by the current SQL statement.
- FAIL makes the transaction fail and return an error.
- IGNORE makes the transaction ignore the conflict and continue as expected.
- REPLACE replaces the old data with the new values and continues the transaction.
- ROLLBACK aborts the current transaction and rolls back any changes made.
That is all you need to create a DAO to insert new questions and answers into your Database. Now, create the methods to delete questions.
Add the following method:
@Query("DELETE FROM questions")
fun clearQuestions()
The above code uses the @Query annotation to execute a DELETE FROM query in your questions table. Since there is no WHERE clause, this will just delete all the records in your questions table, which is what you will need later.
Note: Notice how, if you examine the query
String, there’s some degree of autocomplete available. Room knows which entity definitions you’ve created and which fields exist within. You can use this to write queries, and to easily connect to those definitions, knowing that autocomplete is here to help you write them.
Room also offers the @Delete annotation to automatically create DELETE FROM querys. A method annotated with @Delete will delete its parameter objects from the database. For example, if you wanted to delete a single question from your database, you could do something like this:
@Delete
fun deleteQuestion(question: Question)
Note: If you want to learn more about
@Delete, you can check out the official documentation here: https://developer.android.com/reference/android/arch/persistence/room/Delete.html.
Finally, add the following methods:
@Query("SELECT * FROM questions ORDER BY question_id") // 1
fun getAllQuestions(): List<Question>
@Transaction // 2
@Query("SELECT * FROM questions") // 3
fun getQuestionAndAllAnswers(): List<QuestionAndAllAnswers>
Just like deleteQuestion(), the above methods use @Query to create SQL statements. Taking each commented section in turn:
-
This statement is retrieving all the question records in your database and ordering them by
question_id. The response is returned as aListofQuestionobjects. -
@Transactiontells Room that the following SQL statements should be executed in a single transaction. This is specially useful when you want to query multiple tables like in the case of your one to many relation between thequestionstable and theanswerstable. -
Finally, you are creating a select statement to retrieve all the questions from your
questionstable. Notice that this method is returning a list of yourQuestionAndAllAnswersclass so Room will immediately take the answers associated with each question and store them inside the properties of your class.
And that’s it! Those are all the DAO definitions you will need.
For this app, you won’t be doing any updates to the data; therefore, you didn’t use the @Update annotation. With @Update, the implementation of the annotated method will simply update its parameters in the database if they already exist or wont create any if they don’t. But since you’re using the OnConflictStrategy.REPLACE when inserting data anyways, you have a way to update the data, after all.
Now, to provide the QuizDao for usage, you simply need to add a function to QuizDatabase. Open up QuizDatabase.kt, and add the following code within:
abstract fun quizDao(): QuizDao
Your code should now look like this:
@Database(
entities = [(Question::class), (Answer::class)],
version = 1
)
abstract class QuizDatabase : RoomDatabase() {
abstract fun quizDao(): QuizDao
}
Now, when you need to access the QuizDao and its functions, you simply have to retrieve it from the QuizDatabase instance you create on app startup.
Creating a provider class
Now that your DAO methods are ready, you will create a provider class, that you will need later on, to prepopulate your database. Create a new class under the data package, name it QuestionInfoProvider and add the following method:
private fun initQuestionList(): MutableList<Question> {
val questions = mutableListOf<Question>()
questions.add(
Question(
1,
"Which of the following languages is not commonly used to develop Android Apps")
)
questions.add(
Question(
2,
"What is the meaning of life?")
)
return questions
}
This method creates a MutableList of two questions with the respective id and text fields. You will use these questions later when prepopulating your database.
But you will also need to have answers for the above questions, so add the following method:
private fun initAnswersList(): MutableList<Answer> {
val answers = mutableListOf<Answer>()
answers.add(Answer(
1,
1,
true,
"Java"
))
answers.add(Answer(
2,
1,
false,
"Kotlin"
))
answers.add(Answer(
3,
1,
false,
"Ruby"
))
answers.add(Answer(
4,
2,
true,
"42"
))
answers.add(Answer(
5,
2,
false,
"35"
))
answers.add(Answer(
6,
2,
false,
"7"
))
return answers
}
Just like initQuestionsList(), this method is creating a MutableList, but with Answer objects. Each answer corresponds to a Question of the previous method, using the questionId property to match each of them.
Now, add the following properties at the top of your class:
var questionList = initQuestionList()
var answerList = initAnswersList()
The above properties are immediately initialized with the list of questions and answers that initQuestionList() and initAnswersList() return.
Finally, make this class a singleton by changing the class keyword to object. Your code should now look like this:
object QuestionInfoProvider {
var questionList = initQuestionList()
var answerList = initAnswersList()
private fun initQuestionList(): MutableList<Question> {
val questions = mutableListOf<Question>()
questions.add(
Question(
1,
"Which of the following languages is not commonly used to develop Android Apps"))
questions.add(
Question(
2,
"What is the meaning of life?"))
return questions
}
private fun initAnswersList(): MutableList<Answer> {
val answers = mutableListOf<Answer>()
answers.add(Answer(
1,
1,
true,
"Java"
))
answers.add(Answer(
2,
1,
false,
"Kotlin"
))
answers.add(Answer(
3,
1,
false,
"Ruby"
))
answers.add(Answer(
4,
2,
true,
"42"
))
answers.add(Answer(
5,
2,
false,
"35"
))
answers.add(Answer(
6,
2,
false,
"7"
))
return answers
}
}
The reason you use the object keyword is to make this class a singleton so that we never really have to instantiate this class ourselves since you are only interested in the utilities that this class provides.
And that’s it! Build and Run your app to verify that it is still working as expected:
Sweet! Although the UI is still not working, your database and DAOs are pretty much ready to use. You’ll connect the business logic in the following chapters.
Testing your database
Although your app’s UI is not working yet, you can still interact with your database by performing some tests such as adding or deleting questions to verify its functionality.
Now, to test your database, you could start writing code in your activities that inserts or deletes data and prints the results. You could also wait until you have all your ViewModels and wire it to your UI. The problem with this approach is that you might end up with a ton of code that you will have to delete at the end anyway. Also, you might forget to delete some of your print() statements and expose sensitive data from your users.
To avoid the above issues, you are going to use a very useful testing framework for Andriod called Espresso.
In your project, there’s a com.raywenderlich.droidquiz package with (androidTest) next to it.
Under that package, there is a QuizDaoTest.kt file. Open it by double-clicking and replace the contents inside with the following code:
@RunWith(AndroidJUnit4::class)
class QuizDaoTest { // 1
@Rule
@JvmField
val rule: TestRule = InstantTaskExecutorRule() // 2
private lateinit var database: QuizDatabase // 3
private lateinit var quizDao: QuizDao // 4
}
Briefly, the above:
- Creates a test class for your
QuizDaocalledQuizDaoTest. - Specifies that all tasks executed using Google’s Architecture Components should be executed synchronously on the main thread. This is very important since unit tests should be executed sequentially and synchronously.
- Creates a
lateinit varthat will hold a reference to your Room database. - Creates a
lateinit varthat will hold a reference to yourQuizDao.
Next, add the following code:
@Before
fun setUp() {
val context: Context = InstrumentationRegistry.getInstrumentation().context // 1
try {
database = Room.inMemoryDatabaseBuilder(context, QuizDatabase::class.java) //2
.allowMainThreadQueries() //3
.build()
} catch (e: Exception) {
Log.i(this.javaClass.simpleName, e.message) //4
}
quizDao = database.quizDao() //5
}
@Before is used to specify a method that should be executed before any test is run. Here is what’s happening in the previous code:
- Gets the context for this test and assigns it to
context. - Creates an in-memory version of your database. This means that all data will safely be deleted at the end of your test.
-
allowMainThreadQueries()allows you to execute queries on the main thread. You need to call this method on your database builder, or Room will throw an error. - Logs an exception if the database can’t be built.
- Initializes your
QuizDao.
Next, add the following code:
@Test
fun testInsertQuestion() {
// 1
val previousNumberOfQuestions = quizDao.getAllQuestions().size
//2
val question = Question(1, "What is your name?")
quizDao.insert(question)
//3
val numberOfQuestions = quizDao.getAllQuestions().size
// 4
val numberOfNewQuestions =
numberOfQuestions - previousNumberOfQuestions
// 5
Assert.assertEquals(1, numberOfNewQuestions)
// 6
quizDao.clearQuestions()
// 7
Assert.assertEquals(0, quizDao.getAllQuestions().size)
}
The @Test annotation specifies that this method is a test that you want to execute. Here’s what is going on:
- Calls the
getAllQuestions()method of your DAO and store the size of currently available questions. - Creates a
Questionand inserts it in your database. - Gets the new amount of questions from the database.
- Calculates the delta from new and previous amount of questions in the database.
- Uses
assert()to let the test know you’re expecting only one new question in the database. If the assertion fails, it means theQuestionyou created was not stored in the database, or it was stored more than once. - Clears all the questions from the database after the first assertion.
- Verifies that there are no questions in the database, after you’ve cleared it.
Add the following method:
@Test
fun testClearQuestions() {
for (question in QuestionInfoProvider.questionList) {
quizDao.insert(question)
}
Assert.assertTrue(quizDao.getAllQuestions().isNotEmpty())
Log.d("testData", quizDao.getAllQuestions().toString())
quizDao.clearQuestions()
Assert.assertTrue(quizDao.getAllQuestions().isEmpty())
}
This test simply verifies the correct functionality of your clearQuestions() method by inserting and deleting all the question records in your QuestionInfoProvider. It also logs the data saved, so you can see the data does exist within the database.
Finally, add the following method:
@After
fun tearDown() {
database.close()
}
Contrary to the @Before annotation that specifies methods that should be executed before any test is run, @After specifies which methods should be executed after each of the tests has concluded. Generally speaking, methods annotated with @After are used to release resources allocated with @Before. In this case, you are closing the connection to your database.
When you run an Espresso test, it will install your app on a device or emulator, then execute all the code in your tests. Under the com.raywenderlich.droidquiz with the (androidTest) notation, right-click your QuizDaoTest and select Run ‘QuizDaoTest’. You can also click the green arrow next to the test class name, or click the arrow next to an individual test.
When asked to select a deployment target select your emulator and click OK.
Your project will build, install, and execute all your tests. If you followed every step properly all the tests should pass.
You have now interacted with your database without adding a single line of code to your activities. You also know that your DAOs work as expected and can confidently proceed to focus on the other layers of your architecture such as your ViewModels and Views.
If you need even more proof of the awesome work you did in this chapter, you can add some Log.d() statements to the tests, to print out the data being read from the in-memory database. But the tests should be proof enough!
Key points
-
Database Access Objects are commonly referred to as DAOs.
-
DAOs are objects that provide access to your app’s data by abstracting most of the complexity behind querying and updating your database.
-
In Room, DAOs can be defined as interfaces or abstract classes.
-
The
@Insertannotation is a marker, which allows you to automatically create an INSERT INTO query. -
@Insertcan take anOnConflictStrategyparameter, that allows you to specify what happens in case there is a conflict when creating a new database entry. -
@Queryallows you to perform any kind of queries in your database. You can also use autocomplete, to easily connect to entities and their property definitions. -
@Transactiontells Room that the following SQL statements should be executed in a single transaction. -
@Deleteallows you to automatically create DELETE FROM queries, but it requires a parameter to be removed; e.g., aQuestionobject. -
@Updateupdates a record in the database if it already exists, or omits the changes, if it doesn’t, leaving the database unchanged. -
Writing tests with Espresso is a good way to see if your database code works properly.
-
You can run Espresso tests, without manually going through the app, in less than a few seconds.
-
Inserting the data and reading from the databse in Espresso is safe, because you can work with an in-memory version of the database.
-
In-memory databases clear up after tests end, so there’s no need to do extra cleanup, other than to
close()the database, to avoid leaks.
Where to go from here?
You now know how to create DAOs to interact with your database. You can download the final project by opening the attachment on this chapter, and if you want to learn more about DAOs in Room, you can explore the following resources:
- Google’s guide, “Accessing data using Room DAOs,” found at https://developer.android.com/training/data-storage/room/accessing-data.
- The official documentation about Room DAOs, found at https://developer.android.com/reference/android/arch/persistence/room/Dao.
In the next chapter, you are finally going to see your UI working by integrating your Room database and DAOs with other architecture components such as LiveData and ViewModels.