9.
The DAO Pattern
Written by Subhrajyoti Sen
In the previous chapter, you learned about different kinds of relations, such as one-to-one, one-to-many and many-to-many. You also learned how to create them using annotations.
In this chapter, you’ll learn how to retrieve, insert, delete and update data from your database using Database Access Objects (DAO).
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
@Queryto read data from the database.
Ready? Dive in!
Getting started
Download the starter project attached to this chapter and open it using Android Studio 4.2 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:
SplashActivity,MainActivity,QuestionActivity, andResultActivity.
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 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 @Transaction on a method, and calling multiple different methods within.
If you are wondering why DAOs are defined as abstract classes or interfaces, it’s 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’ll 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’ll need to get the list of all the questions currently stored in your database.
- You’ll also need to be able to create new questions when your app is created.
- Finally, you’ll have to delete questions at some point.
With the above in mind, create a DAO in your project that performs CREATE, READ, and DELETE queries in your database. You’ll 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)
@Insert 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. 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 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.
- IGNORE makes the transaction ignore the conflict and continue as expected.
- REPLACE replaces the old data with the new values and continues the transaction.
That’s all you need to create a DAO to insert new questions and answers into your Database. Now, create the method to delete questions.
Add the following method to the same class:
@Query("DELETE FROM question")
fun clearQuestions()
That code uses @Query to execute a DELETE FROM query in your question table. Since there is no WHERE clause, this will just delete all the records in your question table, which is what you’ll 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 them. 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 @Delete to automatically create DELETE FROM queries. 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/androidx/room/Delete.
Finally, add the following methods:
@Query("SELECT * FROM question ORDER BY question_id") // 1
fun getAllQuestions(): List<Question>
@Transaction // 2
@Query("SELECT * FROM question") // 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 especially useful when you want to query multiple tables like in the case of your one-to-many relation between thequestionstable and theanswerstable. - Finally, you’re 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 @Update. With @Update, the implementation of the annotated method will simply update its parameters in the database if they already exist or won’t 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 instance for usage, you simply need to add a function to QuizDatabase. Open 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 QuizDaoand 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’ll 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 MutableList of two questions with the respective id and text fields. You’ll 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 MutableList, but with Answer objects. Each answer corresponds to a Question object of the previous method, using the questionId property to match each of them.
Now, add the following properties at the top of your class:
val questionList = initQuestionList()
val 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:
object QuestionInfoProvider { ...}
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 isn’t 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 insert or delete data and print 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’ll 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 named Espresso.
In your project, there’s a com.raywenderlich.android.droidquiz package with (androidTest) next to it.
Under this package, create a new class named QuizDaoTest and add the following code to it:
@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
}
While adding the import for AndroidJUnit4, use androidx.test.ext.junit.runners.AndroidJUnit4.
Briefly, the code above does the following:
- Creates a test class for your
QuizDaonamedQuizDaoTest. - 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 = 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
}
When adding the import for InstrumentationRegistry, use androidx.test.platform.app.InstrumentationRegistry.
@Before is used to specify a method that should be executed before any test is run. Here’s 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 newNumberOfQuestions = quizDao.getAllQuestions().size
//4
val changeInQuestions = newNumberOfQuestions - previousNumberOfQuestions
// 5
Assert.assertEquals(1, changeInQuestions)
}
When adding the import for Assert, use org.junit.Assert.
@Test specifies that this method is a test that you want to execute. Here’s what is going on:
- Calls
getAllQuestions()of your DAO and store the size of currently available questions. - Creates a
Questionand inserts it into your database. - Gets the new amount of questions from the database.
- Calculates the delta from the 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 theQuestionobject you created wasn’t stored in the database, or it was stored more than once.
Next, 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 clearQuestions() by inserting and deleting all the question records in 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 @Before 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’ll install your app on a device or emulator, then execute all the code in your tests. Under the com.raywenderlich.android.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.
-
@Insertis 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”.
- The official documentation about Room DAOs.
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.