Easy Way to Copy Between Two Dao
7 Pro-tips for Room
Room is an abstraction layer on top of SQLite that makes it easier and nicer to persist data. If you're new to Room then check out this primer:
In this article, I'd like to share some pro-tips on getting the most out of Room:
- Pre-populate your database via
RoomDatabase#Callback
- Use DAO's inheritance capability
- Execute queries in transactions with minimal boilerplate code via
@Transaction
- Read only what you need
- Enforce constraints between entities with foreign keys
- Simplify one-to-many queries via
@Relation
- Avoid false positive notifications for observable queries
1. Pre-populate the database
Do you need to add default data to your database, right after it was created or when the database is opened? Use RoomDatabase#Callback
! Call the addCallback
method when building your RoomDatabase and override either onCreate
or onOpen
.
onCreate
will be called when the database is created for the first time, after the tables have been created. onOpen
is called when the database was opened. Since the DAOs can only be accessed once these methods return, we're creating a new thread where we're getting a reference to the database, get the DAO, and then insert the data.
Room.databaseBuilder(context.applicationContext,
DataDatabase::class.java, "Sample.db")
// prepopulate the database after onCreate was called
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// moving to a new thread
ioThread {
getInstance(context).dataDao()
.insert(PREPOPULATE_DATA)
}
}
})
.build()
See a full example here.
Note: When using the ioThread
approach, if your app crashes at the first launch, in between database creation and insert, the data will never be inserted.
2. Use DAO's inheritance capability
Do you have multiple tables in your database and find yourself copying the same Insert
, Update
and Delete
methods? DAOs support inheritance, so create a BaseDao<T>
class, and define your generic @Insert
, @Update
and @Delete
methods there. Have each DAO extend the BaseDao
and add methods specific to each of them.
interface BaseDao<T> {
@Insert
fun insert(vararg obj: T)
} @Dao
abstract class DataDao : BaseDao<Data>() {
@Query("SELECT * FROM Data")
abstract fun getData(): List<Data>
}
See more details here.
The DAOs have to be interfaces or abstract classes because Room generates their implementation at compile time, including the methods from BaseDao
.
3. Execute queries in transactions with minimal boilerplate code
Annotating a method with @Transaction
makes sure that all database operations you're executing in that method will be run inside one transaction. The transaction will fail when an exception is thrown in the method body.
@Dao
abstract class UserDao {@Transaction
@Insert
open fun updateData(users: List<User>) {
deleteAllUsers()
insertAll(users)
}
abstract fun insertAll(users: List<User>) @Query("DELETE FROM Users")
abstract fun deleteAllUsers()
}
You might want to use the @Transaction
annotation for @Query
methods that have a select statement, in the following cases:
- When the result of the query is fairly big. By querying the database in one transaction, you ensure that if the query result doesn't fit in a single cursor window, it doesn't get corrupted due to changes in the database between cursor window swaps.
- When the result of the query is a POJO with
@Relation
fields. The fields are queries separately so running them in a single transaction will guarantee consistent results between queries.
@Delete
, @Update
and @Insert
methods that have multiple parameters are automatically run inside a transaction.
4. Read only what you need
When you're querying the database, do you use all the fields you return in your query? Take care of the amount of memory used by your app and load only the subset of fields you will end up using. This will also improve the speed of your queries by reducing the IO cost. Room will do the mapping between the columns and the object for you.
Consider this complex User
object:
@Entity(tableName = "users")
data class User(@PrimaryKey
val id: String,
val userName: String,
val firstName: String,
val lastName: String,
val email: String,
val dateOfBirth: Date,
val registrationDate: Date)
On some screens we don't need to display all of this information. So instead, we can create a UserMinimal
object that holds only the data needed.
data class UserMinimal(val userId: String,
val firstName: String,
val lastName: String)
In the DAO class, we define the query and select the right columns from the users table.
@Dao
interface UserDao {
@Query("SELECT userId, firstName, lastName FROM Users)
fun getUsersMinimal(): List<UserMinimal>
}
5. Enforce constraints between entities with foreign keys
Even though Room doesn't directly support relations, it allows you to define Foreign Key constraints between entities.
Room has the @ForeignKey
annotation, part of the @Entity
annotation, to allow using the SQLite foreign key features. It enforces constraints across tables that ensure the relationship is valid when you modify the database. On an entity, define the parent entity to reference, the columns in it and the columns in the current entity.
Consider a User
and a Pet
class. The Pet
has an owner, which is a user id referenced as foreign key.
@Entity(tableName = "pets",
foreignKeys = arrayOf(
ForeignKey(entity = User::class,
parentColumns = arrayOf("userId"),
childColumns = arrayOf("owner"))))
data class Pet(@PrimaryKey val petId: String,
val name: String,
val owner: String)
Optionally, you can define what action to be taken when the parent entity is deleted or updated in the database. You can choose one of the following: NO_ACTION
, RESTRICT
, SET_NULL
, SET_DEFAULT
or CASCADE
, that have same behaviors as in SQLite.
Note: In Room, SET_DEFAULT
works as SET_NULL
, as Room does not yet allow setting default values for columns.
6. Simplify one-to-many queries via @Relation
In the previousUser
-Pet
example, we can say that we have a one-to-many relation: a user can have multiple pets. Let's say that we want to get a list of users with their pets: List<UserAndAllPets>
.
data class UserAndAllPets (val user: User,
val pets: List<Pet> = ArrayList())
To do this manually, we would need to implement 2 queries: one to get the list of all users and another one to get the list of pets based on a user id.
@Query("SELECT * FROM Users")
public List<User> getUsers(); @Query("SELECT * FROM Pets where owner = :userId")
public List<Pet> getPetsForUser(String userId);
We would then iterate through the list of users and query the Pets table.
To make this simpler, Room's @Relation
annotation automatically fetches related entities. @Relation
can only be applied to a List
or Set
of objects. The UserAndAllPets
class has to be updated:
class UserAndAllPets { @Embedded
var user: User? = null @Relation(parentColumn = "userId",
entityColumn = "owner")
var pets: List<Pet> = ArrayList() }
In the DAO, we define a single query and Room will query both the Users
and the Pets
tables and handle the object mapping.
@Transaction
@Query("SELECT * FROM Users")
List<UserAndAllPets> getUsers();
7. Avoid false positive notifications for observable queries
Let's say that you want to get a user based on the user id in an observable query:
@Query("SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): LiveData<User> // or @Query("SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): Flowable<User>
You'll get a new emission of the User
object whenever that user updates. But you will also get the same object when other changes (deletes, updates or inserts) occur on the Users
table that have nothing to do with the User
you're interested in, resulting in false positive notifications. Even more, if your query involves multiple tables, you'll get a new emission whenever something changed in any of them.
Here's what's going on behind the scenes:
- SQLite supports triggers that fire whenever a
DELETE
,UPDATE
orINSERT
happens in a table. - Room creates an
InvalidationTracker
that usesObservers
that track whenever something has changed in the observed tables. - Both
LiveData
andFlowable
queries rely on theInvalidationTracker.Observer#onInvalidated
notification. When this is received, it triggers a re-query.
Room only knows that the table has been modified but doesn't know why and what has changed. Therefore, after the re-query, the result of the query is emitted by the LiveData
or Flowable
. Since Room doesn't hold any data in memory and can't assume that objects have equals()
, it can't tell whether this is the same data or not.
You need to make sure that your DAO filters emissions and only reacts to distinct objects.
If the observable query is implemented using Flowables
, use Flowable#distinctUntilChanged
.
@Dao abstract class UserDao : BaseDao<User>() { /**
* Get a user by id.
* @return the user from the table with a specific id.
*/ @Query("SELECT * FROM Users WHERE userid = :id")
protected abstract fun getUserById(id: String): Flowable<User> fun getDistinctUserById(id: String):
Flowable<User> = getUserById(id)
.distinctUntilChanged() }
If your query returns a LiveData
, you can use a MediatorLiveData
that only allows distinct object emissions from a source.
fun <T> LiveData<T>.getDistinct(): LiveData<T> {
val distinctLiveData = MediatorLiveData<T>()
distinctLiveData.addSource(this, object : Observer<T> {
private var initialized = false
private var lastObj: T? = null override fun onChanged(obj: T?) {
if (!initialized) {
initialized = true
lastObj = obj
distinctLiveData.postValue(lastObj)
} else if ((obj == null && lastObj != null)
|| obj != lastObj) {
lastObj = obj
distinctLiveData.postValue(lastObj)
}
}
}) return distinctLiveData
}
In your DAOs, make method that returns the distinct LiveData
public
and the method that queries the database protected
.
@Dao
abstract class UserDao : BaseDao<User>() { @Query("SELECT * FROM Users WHERE userid = :id")
protected abstract fun getUserById(id: String): LiveData<User> fun getDistinctUserById(id: String):
LiveData<User> = getUserById(id).getDistinct() }
See more of the code here.
Note: if you're returning a list to be displayed, consider using the Paging Library and returning a LivePagedListBuilder since the library will help with automatically computing the diff between list items and updating your UI.
New to Room? Check out our previous articles:
Source: https://medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1
0 Response to "Easy Way to Copy Between Two Dao"
Post a Comment