Room Database
Room Database
Room acts as a layer on top of SQLite. It makes our coding lives much
easier and efficient.
Primary Key
● Use the @PrimaryKey on the variable selected as the primary key of
the table.
● If you want the primary key to auto generate set autoGenerate =
true
DAO stands for “Data Access Object”. This interface is where we define
functions to deal with the database.
Function names
Function names are not important. You can give any name you like.
Annotations
But, annotating the function with correct annotation is very important.
Suspend keyword
“Kotlin coroutines” is the current best practice to manage threads in android
development. So, we are going to use them to execute database
operations in a background thread. Therefore, we need to define these
functions as suspending functions.
But, it is not required for query functions. Because, Room library uses its
own dispatcher to run queries on a background thread.
SQL Statement
For basic Insert, Update and Delete functions we don’t need to write SQL
statements.
But, we need to write a SQL statement for Query functions and for
customized Update and Delete functions.
This class should be an abstract class. Most importantly, this should extend
the “RoomDatabase” class.
And, also provide the version number . Database’s version number is very
important when we are going to migrate the database.
Finally, we need to define abstract functions to get Dao interfaces inside the
class.
REFERENCE
https://appdevnotes.com/android-room-db-tutorial-for-beginners-in-kotlin/
https://appdevnotes.com/android-mvvm-project-example/
AutoMigration in Room DB
First we need to do the following changes in app level build.gradle
defaultconfig {
//……
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
}
@ColumnInfo(name = "subject_name")
var course: String?
@RenameColumn(tableName = "student_info",
fromColumnName = "course_name", toColumnName =
"subject_name")
class Migration2TO3: AutoMigrationSpec
Last do changes in
@Database(entities = [Student::class],version = 3,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3, spec =
StudentDatabase.Migration2TO3::class)
]
)
Like if we want to remove the column name student_email
@Database(entities = [Student::class],version = 4,
autoMigrations =
[
AutoMigration(from = 1, to = 2)
AutoMigration(from = 2, to = 3, spec =
StudentDatabase.Migration2TO3::class),
AutoMigration(from = 3, to = 4, spec =
StudentDatabase.Migration3TO4::class)
]
)
Interview Questions:
Code Example:
Code Example:
@Entity(tableName = "user")
data class User(
@PrimaryKey val id: Int,
val name: String
)
@Entity(tableName = "book")
data class Book(
@PrimaryKey val id: Int,
val title: String,
@ColumnInfo(name = "user_id") val userId: Int
)
data class UserWithBooks(
@Embedded val user: User,
@Relation(
parentColumn = "id",
entityColumn = "user_id"
)
val books: List<Book>
)
@Dao
interface UserDao {
@Transaction
@Query("SELECT * FROM user")
fun getUsersWithBooks(): List<UserWithBooks>
}
Question 6: How can you observe database changes using LiveData with Room?
Answer: Room provides the LiveData class from the Android Architecture Components,
which can be used to observe changes in the database. By returning a LiveData object
from a DAO method, Room automatically updates the returned LiveData whenever
there are changes in the database.
Code Example:
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAllUsers(): LiveData<List<User>>
}
// Observe the LiveData in an Activity or Fragment
userDao.getAllUsers().observe(this) { users ->
// Update UI with the latest user data
}
Question 7: How can you execute complex database queries using Room?
Answer: Room supports complex database queries using the @Query annotation in
DAO methods. You can write custom SQL queries and pass parameters using
placeholders.
Code Example:
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE age > :minAge")
fun getUsersOlderThan(minAge: Int): List<User>
}
Question 8: What is the purpose of the @TypeConverter annotation in Room?
Answer: The @TypeConverter annotation allows you to define custom type converters
in Room. It is useful when you need to store complex data types (such as Date, List, or
custom objects) in the database as primitive types.
Code Example:
class Converters {
@TypeConverter
fun fromTimestamp(value: Long): Date {
return Date(value)
}
@TypeConverter
fun dateToTimestamp(date: Date): Long {
return date.time
}
}
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
// Database definition
}
Question 9: What are the main components of Android Room, and how do they
interact with each other?
Android Room has three main components: Database, Entity, and DAO.
1. Database: It is an abstract class extending RoomDatabase, acting as the main
access point for underlying SQLite database connections. Annotated with @Database,
it lists all entities and defines a version number.
2. Entity: Represents a table in the SQLite database. A POJO class annotated with
@Entity, its fields define columns while primary keys are specified using @PrimaryKey.
3. DAO (Data Access Object): Interface containing methods to interact with the
database. Annotated with @Dao, it includes query, insert, update, and delete
operations.
Interaction:
Entities are defined within the Database class, which also provides DAO instances. The
Room library generates code based on annotations, creating a concrete implementation
of the Database class. This implementation manages SQLiteOpenHelper and creates
DAO implementations. Clients use these DAOs to perform CRUD operations on entities,
ensuring type-safety and compile-time checks.
Question 10: Can you explain the differences between Room, SQLite, and Realm,
and when you would use each for Android app development?
Room, SQLite, and Realm are data persistence solutions for Android app development.
Room is an abstraction layer over SQLite, providing a more convenient API and
compile-time checks. SQLite is a lightweight relational database management system
embedded in the Android framework. Realm is a third-party, object-oriented database
with its own storage engine.
Use Room when you need a robust, easy-to-use solution that integrates well with other
Android components (LiveData, ViewModel). It’s suitable for most apps requiring local
data storage.
Choose SQLite if you prefer working directly with SQL queries or require fine-grained
control over the database. However, it lacks some of the convenience features provided
by Room.
Opt for Realm when you need a high-performance, cross-platform solution not limited to
Android. It excels in real-time applications and complex data models but may have a
steeper learning curve compared to Room.
Question 11: How can you handle version updates and schema migrations in
Android Room?
To handle version updates and schema migrations in Android Room, follow these steps:
1. Increment the database version number in the @Database annotation.
2. Create a Migration class that extends the abstract class “Migration” and implement its
“migrate()” method to define the necessary schema changes.
3. In the “migrate()” method, use SQLite commands like ALTER TABLE, CREATE
TABLE, or DROP TABLE to modify the schema as needed.
4. Add the migration object(s) to your RoomDatabase.Builder using the
“addMigrations()” method.
For example:
@Database(entities = {MyEntity.class}, version = 2)
public abstract class MyDatabase extends RoomDatabase {
// ...
}
// Define migration from version 1 to 2
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Schema modification code here
}
};
// Build the database with the migration
MyDatabase db = Room.databaseBuilder(context, MyDatabase.class, "my-db")
.addMigrations(MIGRATION_1_2)
.build();
Question 12: What is the role of DAO (Data Access Object) in Android Room, and
how do you create a DAO?
In Android Room, DAO (Data Access Object) plays a crucial role in abstracting
database access and enabling communication between the app components and
SQLite. It defines methods for performing CRUD operations on the database, ensuring
separation of concerns and testability.
To create a DAO:
1. Define an interface or abstract class.
2. Annotate it with @Dao.
3. Declare methods for required operations.
4. Use annotations like @Insert, @Update, @Delete, and @Query to specify SQL
statements or actions.
Example:
@Dao
public interface UserDao {
@Insert
void insert(User user);
@Update
void update(User user);
@Delete
void delete(User user);
@Query("SELECT * FROM users WHERE id = :userId")
User getUserById(int userId);
}
Question 13: How do you handle complex relationships between entities, such as
many-to-many relationships?
To handle complex relationships like many-to-many in Android Room, use associative
entity (also known as join table) to break down the relationship into two one-to-many
relationships. Define entities and their primary keys, then create an associative entity
with foreign keys referencing the primary keys of related entities.
For example, consider Student and Course entities with a many-to-many relationship:
1. Create Student and Course entities with respective primary keys.
2. Create an associative entity, e.g., StudentCourseJoin, containing foreign keys for
both Student and Course.
3. Annotate the foreign keys with @ForeignKey annotation to enforce referential
integrity.
4. Use @Relation annotation in DAOs to retrieve related data across multiple tables.
Here’s a code snippet illustrating this approach:
@Entity
data class Student(@PrimaryKey val id: Int, val name: String)
@Entity
data class Course(@PrimaryKey val id: Int, val title: String)
@Entity(primaryKeys = [“studentId”, “courseId”])
data class StudentCourseJoin(
@ForeignKey(entity = Student::class,
parentColumns = [“id”],
childColumns = [“studentId”])
val studentId: Int,
@ForeignKey(entity = Course::class,
parentColumns = [“id”],
childColumns = [“courseId”])
val courseId: Int
)
Question 14: Can you explain the LiveData integration in Android Room and its
benefits in your application?
LiveData integration in Android Room allows observing changes in the database and
automatically updating UI components. It is a lifecycle-aware component, ensuring data
updates are only sent to active observers, preventing memory leaks and crashes.
Benefits include:
1. Real-time UI updates: LiveData emits data changes, enabling automatic UI refresh.
2. Lifecycle awareness: Observations stop when activity/fragment is destroyed, avoiding
memory leaks.
3. Thread safety: LiveData ensures data modifications occur on the main thread,
preventing concurrency issues.
4. Simplified code: Reduces boilerplate code for handling data updates and managing
lifecycles.
Example usage:
@Entity
data class User(val id: Int, val name: String)
@Dao
interface UserDao {
@Query(“SELECT * FROM user”)
fun getAllUsers(): LiveData>
}
Question 15: How do you ensure thread safety when using Android Room in a
multi-threaded environment?
To ensure thread safety with Android Room in a multi-threaded environment, follow
these steps:
1. Use LiveData or Flow: These components automatically handle threading and notify
observers on the main thread when data changes.
2. Utilize ViewModel: Encapsulate UI-related data within a ViewModel to separate it
from lifecycle-aware components like Activities and Fragments.
3. Implement Coroutines or RxJava: Employ asynchronous programming techniques for
background tasks, such as database operations, without blocking the main thread.
4. Apply Synchronization: If necessary, use synchronization mechanisms (e.g.,
synchronized blocks, locks) to protect shared resources from concurrent access.
5. Design Thread-safe DAOs: Ensure Data Access Objects (DAOs) are designed to be
thread-safe by using appropriate annotations, such as @Transaction, to manage
transactions.
6. Avoid Static Variables: Refrain from using static variables that can lead to
concurrency issues.
Question 18: How can you optimize performance in Android Room using query
optimization techniques?
To optimize performance in Android Room, apply the following query optimization
techniques:
1. Use indices: Create indices on frequently queried columns to speed up searches. Be
cautious as excessive indexing can slow down insertions and updates.
2. Limit data retrieval: Utilize LIMIT and OFFSET clauses to fetch only required data,
reducing memory consumption and improving response time.
3. Opt for compile-time query verification: Annotate DAO methods with @Query to
enable SQLite syntax checking during compilation, preventing runtime errors.
4. Leverage LiveData or Flow: Employ LiveData or Kotlin’s Flow to observe database
changes, ensuring UI updates are efficient and thread-safe.
5. Choose appropriate threading strategy: Execute queries on a background thread
using AsyncTask, Executors, or coroutines to prevent blocking the main thread.
6. Minimize JOIN operations: Reduce complex JOINs by denormalizing tables or
utilizing embedded objects and relations when designing the schema.
7. Use Projections: Select specific columns instead of fetching entire entities to
minimize memory usage and improve query performance.
Question 19: What are the different types of conflict resolution strategies
available in Android Room, and when would you use them?
Android Room offers five conflict resolution strategies: IGNORE, REPLACE, ABORT,
FAIL, and ROLLBACK.
1. IGNORE: Use when you want to skip inserting a new row if it conflicts with an existing
one based on the unique constraint.
2. REPLACE: Choose this strategy to replace the conflicting row entirely with the new
data.
3. ABORT: Utilize this option to cancel the current transaction or statement if there’s a
conflict, preserving the original data.
4. FAIL: Similar to ABORT, but only cancels the specific statement causing the conflict,
allowing other statements in the transaction to proceed.
5. ROLLBACK: Select this strategy to revert the entire transaction back to its initial state
upon encountering a conflict.
Choose the appropriate strategy based on your application’s requirements for handling
duplicate or conflicting data.
Question 20: How do you handle on-demand or on-change database backups and
restore scenarios with Android Room?
To handle on-demand or on-change database backups and restore scenarios with
Android Room, follow these steps:
1. Create a backup: Use the framework’s
BackupAgentHelper class to create a custom backup agent that copies the SQLite
database file to the backup destination.
2. Register the backup agent: In the app manifest, add the <application> element’s
android:backupAgent attribute pointing to your custom backup agent.
3. Implement LiveData observers: Utilize LiveData in your DAOs to observe changes in
data and trigger backup when necessary.
4. Restore the backup: Override the onRestore() method in your custom backup agent
to copy the backed-up database file back to its original location.
5. Notify the app: After restoring the backup, use WorkManager or other means to notify
the app components about the restored data, so they can refresh their UI accordingly.
Question 21: How can you test your Room database in a Unit test or an
Instrumented test?
To test Room database in a Unit test or an Instrumented test, follow these steps:
1. Use In-Memory database: Create an in-memory version of the database for testing
purposes, as it’s faster and doesn’t persist data between tests.
2. Test setup: Initialize the database and DAOs before each test using @Before
annotation, and close them after each test using @After annotation.
3. Unit tests: For unit tests, use Robolectric to run tests on JVM without needing an
emulator or device. Add required dependencies and configure your test class with
@RunWith(RobolectricTestRunner.class) and @Config(sdk =
Build.VERSION_CODES.P).
4. Instrumented tests: For instrumented tests, use AndroidJUnit4 runner. Configure your
test class with @RunWith(AndroidJUnit4.class), and add necessary dependencies.
5. LiveData testing: To test LiveData objects, use InstantTaskExecutorRule JUnit rule to
execute tasks synchronously. Add this rule at the beginning of your test class: @Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new
InstantTaskExecutorRule();
6. Test cases: Write test cases for CRUD operations (Create, Read, Update, Delete)
and any custom queries defined in your DAOs. Use Assert methods to verify expected
results.
Question 22: Can you explain how Paging Library works with Room?
Paging Library, part of Android Jetpack, efficiently loads and displays large data sets in
chunks. It works seamlessly with Room by integrating LiveData and RxJava
components.
To use Paging Library with Room:
1. Define a DataSource.Factory: In your DAO, create a method returning
DataSource.Factory for the desired query.
2. Configure PagedList.Builder: Set up PagedList configuration, specifying page size,
prefetch distance, and initial load size.
3. Create LivePagedListBuilder: Combine DataSource.Factory and PagedList.Config to
build a LivePagedListBuilder.
4. Observe PagedList: Retrieve LiveData from LivePagedListBuilder and observe it in
your ViewModel or Activity/Fragment.
5. Implement PagedListAdapter: Extend RecyclerView.Adapter with PagedListAdapter,
handling placeholders and item binding.
6. Submit data to adapter: When observing LiveData, submit new PagedList to
PagedListAdapter using submitList().
Room handles database queries on background threads, ensuring smooth UI
performance.
Question 24: How do you perform database transactions with Android Room, and
how does it handle rollback scenarios?
To perform database transactions with Android Room, use the @Transaction annotation
on a method within your DAO (Data Access Object). This ensures that all operations
within the method are executed atomically. If any operation fails, Room automatically
rolls back the transaction to maintain data integrity.
For rollback scenarios, Room handles them by default when using the @Transaction
annotation. If an exception occurs during the execution of the annotated method, Room
will catch it and roll back the transaction, preventing partial updates or inconsistent data
states.
In cases where you need more control over the transaction process, you can use the
SupportSQLiteDatabase.beginTransaction() and endTransaction() methods. To
manually handle rollback, call the setTransactionSuccessful() method before
endTransaction() if everything is successful, otherwise, don’t call it, and Room will roll
back the changes.
Example:
@Dao
public interface MyDao {
@Transaction
public void insertAndUpdate(MyEntity1 entity1, MyEntity2 entity2) {
// Perform multiple operations here
insert(entity1);
update(entity2);
}
}
Question 25: How do you execute a raw SQL query with Room, and what would
be the advantages and challenges of doing so?
To execute a raw SQL query with Room, use the @RawQuery annotation in your DAO
interface. Create a method that returns LiveData or List of desired objects and pass a
SupportSQLiteQuery as a parameter.
Advantages:
1. Flexibility: Raw queries allow complex operations not supported by default.
2. Performance: Customized queries can optimize performance for specific cases.
Challenges:
1. Safety: Raw queries are prone to SQL injection attacks if not sanitized properly.
2. Maintainability: Harder to refactor and maintain compared to standard Room queries.
3. Type-safety: No compile-time checks; errors may occur at runtime.
Example:
@Dao
public interface UserDao {
@RawQuery(observedEntities = User.class)
LiveData> getUsersWithCustomQuery(SupportSQLiteQuery query);
}
Question 26: What is the FTS (Full-Text Search) support in Android Room, and
how can you implement it?
FTS (Full-Text Search) support in Android Room enables efficient text-based search
functionality within SQLite databases. It uses virtual tables and tokenizes data for faster
querying.
To implement FTS in Room:
1. Add dependencies: Include ‘androidx.room:room-compiler’ and
‘androidx.sqlite:sqlite-framework’ in build.gradle.
2. Create an FTS entity: Annotate a class with @Entity(tableName =
“your_table_name”, indices = {@Index(value = {“column_name”}, unique = true)},
inheritSuperIndices = true, ftsOptions = @FtsOptions(languageId = “lid”)) to define the
FTS table schema.
3. Define DAO: Create an interface annotated with @Dao, containing methods for
inserting, updating, deleting, and querying the FTS table.
4. Implement database: Extend RoomDatabase, annotate with @Database(entities =
{YourFtsEntity.class}, version = 1), and declare abstract methods for accessing your
DAOs.
5. Initialize database: Use Room.databaseBuilder(context, YourDatabaseClass.class,
“database_name”).build() to create an instance of your database.
6. Perform operations: Access the DAO methods through the database instance to
interact with the FTS table.
Question 27: How can you handle large datasets and improve query performance
in Android Room?
To handle large datasets and improve query performance in Android Room, consider
the following strategies:
1. Pagination: Use Paging Library to load data in chunks, reducing memory usage and
improving responsiveness.
2. Indexing: Create indices on frequently queried columns to speed up searches.
3. Compile-time Query Verification: Utilize @Query annotations for SQL queries,
enabling compile-time checks and preventing runtime errors.
4. Relationship Handling: Opt for explicit JOIN statements instead of embedded or
relation fields to avoid unnecessary data loading.
5. Asynchronous Operations: Execute database operations off the main thread using
Kotlin coroutines or RxJava to prevent UI blocking.
6. Efficient Data Structures: Choose appropriate data structures like LiveData or Flow to
observe changes and update UI efficiently.
Question 29: How can you use Dependency Injection frameworks like Dagger2 or
Hilt with Android Room?
To use Dependency Injection frameworks like Dagger2 or Hilt with Android Room, follow
these steps:
1. Add required dependencies for Dagger2/Hilt and Room in the build.gradle file.
2. Create a Database class extending RoomDatabase and annotate it with @Database.
3. Define DAO interfaces for data access operations.
4. In your AppModule (for Dagger2) or Application class (for Hilt), create a method
annotated with @Provides/@Singleton (Dagger2) or
@InstallIn(ApplicationComponent::class)/@Singleton (Hilt) to provide an instance of the
database.
5. Inject the database instance into repositories or view models using constructor
injection by annotating the constructor with @Inject.
6. Use the injected database instance to access DAOs and perform data operations.
Question 30: Can you explain the concept of Multi-Module architecture and how
Android Room fits into modularization?
Multi-Module architecture is a design pattern that divides an application into multiple,
smaller modules to improve scalability, maintainability, and reusability. Each module
focuses on a specific functionality or feature, allowing for independent development and
testing.
Android Room fits into modularization by providing a robust database layer within a
module. It simplifies data persistence with minimal boilerplate code, enabling
developers to create separate modules for different app components while maintaining
a consistent data storage solution across all modules. This approach promotes clean
architecture principles, separating concerns and reducing dependencies between
modules.
Question 31: How do you cache data with Android Room and what kind of
caching strategies can you implement?
To cache data with Android Room, create a local database using Room persistence
library. Define entities representing tables, Data Access Objects (DAOs) for queries, and
a RoomDatabase instance to access the database.
Caching strategies:
1. Offline-first: Prioritize cached data; fetch from network only if unavailable or outdated.
2. Online-first: Fetch from network first; use cache as fallback.
3. Cache-then-refresh: Display cached data while fetching updated data from network.
4. Two-layer cache: Combine in-memory cache (e.g., LRU) with Room for faster access
and persistence.
5. Time-based expiration: Invalidate cache after a specific duration.
6. Version-based invalidation: Invalidate cache when app version changes or
server-side schema updates.
7. Smart-sync: Sync cached data with remote source based on user actions or
connectivity status.
An embedded entity in Room allows for flattening of object hierarchy by including fields
from another class directly into the main table, while a relationship represents
associations between tables using foreign keys. Embedded entities are useful when
there’s a one-to-one mapping and no need to query related data separately.
Relationships, on the other hand, cater to one-to-many or many-to-many mappings and
enable efficient querying of related data.
For example, consider an Address class with fields street, city, and zipCode. Using
@Embedded, these fields can be included in a User table without creating a separate
Address table:
@Entity
data class User(
@PrimaryKey val id: Int,
val name: String,
@Embedded val address: Address
)
In contrast, relationships require defining separate tables and establishing connections
via foreign keys. For instance, if a user has multiple addresses, we’d create a separate
Address table and use @Relation annotation to establish a connection:
@Entity
data class User(@PrimaryKey val id: Int, val name: String)
@Entity(foreignKeys = [ForeignKey(entity = User::class, parentColumns = [“id”],
childColumns = [“userId”])])
data class Address(@PrimaryKey val id: Int, val userId: Int, val street: String, val city:
String, val zipCode: String)
Question 32: How would you perform concurrency control in Android Room to
avoid conflicts between read and write operations?
To perform concurrency control in Android Room, use transactions and LiveData.
Transactions ensure atomicity, consistency, isolation, and durability (ACID) properties
while LiveData provides real-time updates.
1. Use @Transaction annotation for complex read/write operations to guarantee ACID
properties.
2. Utilize coroutines or RxJava for asynchronous execution of database operations.
3. Implement conflict resolution strategies using @Insert(onConflict =
OnConflictStrategy.REPLACE) or similar annotations.
4. Employ LiveData or Flow to observe changes in the data and automatically update UI
components.
5. For multi-threading, use Executors or Dispatchers to manage background threads.
6. Optimize queries by indexing columns and limiting fetched data with pagination
techniques.