Introduction
Room is an abstraction layer over SQLite that provides a robust database framework while harnessing the full power of SQLite. It simplifies database operations and provides compile-time verification of SQL queries, making it an essential component for Android app development.
Core Components
1. Entity - Defines Database Tables
@Entity(tableName = "users")
public class User {
@PrimaryKey(autoGenerate = true)
private int id;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
private String email;
@Ignore
private transient String temporaryData; // Not persisted
// Constructors
public User() {}
public User(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
// Composite Primary Key Example
@Entity(primaryKeys = {"firstName", "lastName"})
public class UserWithCompositeKey {
private String firstName;
private String lastName;
private int age;
// Constructors, getters, setters
}
// Index Example
@Entity(indices = {
@Index(value = {"email"}, unique = true),
@Index(value = {"last_name", "first_name"})
})
public class UserWithIndex {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
public String email;
}
2. DAO (Data Access Object) - Database Operations
@Dao
public interface UserDao {
// INSERT operations
@Insert
long insert(User user);
@Insert
long[] insertAll(User... users);
@Insert
List<Long> insertAll(List<User> users);
// UPDATE operations
@Update
int update(User user);
@Update
int updateAll(User... users);
// DELETE operations
@Delete
int delete(User user);
@Query("DELETE FROM users")
int deleteAll();
@Query("DELETE FROM users WHERE id = :userId")
int deleteById(int userId);
// SELECT operations
@Query("SELECT * FROM users")
List<User> getAll();
@Query("SELECT * FROM users WHERE id = :userId")
User getById(int userId);
@Query("SELECT * FROM users WHERE first_name LIKE :firstName AND last_name LIKE :lastName")
List<User> findByName(String firstName, String lastName);
@Query("SELECT * FROM users WHERE email = :email LIMIT 1")
User findByEmail(String email);
// Complex queries with JOIN
@Query("SELECT users.*, orders.order_date " +
"FROM users " +
"INNER JOIN orders ON users.id = orders.user_id " +
"WHERE users.id = :userId")
List<UserWithOrders> getUserWithOrders(int userId);
// LiveData for reactive updates
@Query("SELECT * FROM users ORDER BY first_name ASC")
LiveData<List<User>> getAllLiveData();
// RxJava support
@Query("SELECT * FROM users WHERE id = :userId")
Maybe<User> getByIdRx(int userId);
// Coroutines support
@Query("SELECT * FROM users WHERE age > :minAge")
suspend List<User> getUsersOlderThan(int minAge);
}
// Custom return type for complex queries
class UserWithOrders {
@Embedded
public User user;
@ColumnInfo(name = "order_date")
public Date orderDate;
}
3. Database - Database Holder
@Database(
entities = {User.class, Product.class, Order.class},
version = 1,
exportSchema = false
)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
public abstract ProductDao productDao();
public abstract OrderDao orderDao();
// Singleton pattern
private static volatile AppDatabase INSTANCE;
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"app_database.db"
)
.addCallback(roomCallback)
.addMigrations(MIGRATION_1_2)
.build();
}
}
}
return INSTANCE;
}
private static RoomDatabase.Callback roomCallback = new RoomDatabase.Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
// Pre-populate database
new Thread(() -> {
UserDao dao = INSTANCE.userDao();
dao.insert(new User("Admin", "User", "[email protected]"));
}).start();
}
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
// Database opened
}
};
}
Advanced Entity Relationships
One-to-Many Relationship
// User entity
@Entity
public class User {
@PrimaryKey
public int id;
public String name;
}
// Order entity (many orders per user)
@Entity(
foreignKeys = @ForeignKey(
entity = User.class,
parentColumns = "id",
childColumns = "user_id",
onDelete = ForeignKey.CASCADE
)
)
public class Order {
@PrimaryKey
public int orderId;
@ColumnInfo(name = "user_id")
public int userId;
public Date orderDate;
public double totalAmount;
}
// Relationship class
class UserWithOrders {
@Embedded
public User user;
@Relation(
parentColumn = "id",
entityColumn = "user_id"
)
public List<Order> orders;
}
// DAO for relationships
@Dao
public interface UserWithOrdersDao {
@Transaction
@Query("SELECT * FROM User")
List<UserWithOrders> getUsersWithOrders();
}
Many-to-Many Relationship
// Student entity
@Entity
public class Student {
@PrimaryKey
public int studentId;
public String name;
}
// Course entity
@Entity
public class Course {
@PrimaryKey
public int courseId;
public String title;
}
// Cross-reference entity
@Entity(primaryKeys = {"studentId", "courseId"})
public class StudentCourseCrossRef {
public int studentId;
public int courseId;
}
// Relationship class
class StudentWithCourses {
@Embedded
public Student student;
@Relation(
parentColumn = "studentId",
entityColumn = "courseId",
associateBy = @Junction(StudentCourseCrossRef.class)
)
public List<Course> courses;
}
// DAO for many-to-many
@Dao
public interface StudentDao {
@Transaction
@Query("SELECT * FROM Student")
List<StudentWithCourses> getStudentsWithCourses();
}
Type Converters
Custom Type Conversion
public class Converters {
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
@TypeConverter
public static String listToString(List<String> list) {
return list == null ? null : String.join(",", list);
}
@TypeConverter
public static List<String> stringToList(String data) {
return data == null ? null : Arrays.asList(data.split(","));
}
@TypeConverter
public static Address fromAddressString(String address) {
if (address == null) return null;
Gson gson = new Gson();
return gson.fromJson(address, Address.class);
}
@TypeConverter
public static String addressToString(Address address) {
if (address == null) return null;
Gson gson = new Gson();
return gson.toJson(address);
}
}
// Entity using converters
@Entity
public class UserProfile {
@PrimaryKey
public int id;
public Date birthDate;
@TypeConverters(Converters.class)
public List<String> tags;
@TypeConverters(Converters.class)
public Address address;
}
// Address class
class Address {
public String street;
public String city;
public String zipCode;
public Address(String street, String city, String zipCode) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
}
}
Database Migrations
Handling Schema Changes
@Database(entities = {User.class, Product.class}, version = 3)
public abstract class AppDatabase extends RoomDatabase {
// Migration from version 1 to 2
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Add new column to users table
database.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT");
}
};
// Migration from version 2 to 3
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Create new products table
database.execSQL(
"CREATE TABLE products (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"name TEXT, " +
"price REAL, " +
"created_date INTEGER)"
);
// Add foreign key to orders table
database.execSQL(
"CREATE TABLE orders_new (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"user_id INTEGER, " +
"product_id INTEGER, " +
"quantity INTEGER, " +
"FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, " +
"FOREIGN KEY(product_id) REFERENCES products(id) ON DELETE CASCADE)"
);
database.execSQL("INSERT INTO orders_new (id, user_id, quantity) " +
"SELECT id, user_id, quantity FROM orders");
database.execSQL("DROP TABLE orders");
database.execSQL("ALTER TABLE orders_new RENAME TO orders");
}
};
public static AppDatabase getInstance(Context context) {
return Room.databaseBuilder(context, AppDatabase.class, "app.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.fallbackToDestructiveMigration() // Use carefully in development
.build();
}
}
Complex Query Examples
Advanced DAO Operations
@Dao
public interface AdvancedUserDao {
// Pagination with LIMIT and OFFSET
@Query("SELECT * FROM users ORDER BY id LIMIT :pageSize OFFSET :offset")
List<User> getUsersPaginated(int pageSize, int offset);
// Search with multiple conditions
@Query("SELECT * FROM users WHERE " +
"(:name IS NULL OR first_name LIKE '%' || :name || '%') AND " +
"(:minAge IS NULL OR age >= :minAge) AND " +
"(:maxAge IS NULL OR age <= :maxAge) " +
"ORDER BY first_name, last_name")
List<User> searchUsers(String name, Integer minAge, Integer maxAge);
// Aggregate functions
@Query("SELECT COUNT(*) FROM users")
int getUserCount();
@Query("SELECT AVG(age) FROM users")
double getAverageAge();
@Query("SELECT MAX(age) FROM users")
int getMaxAge();
@Query("SELECT first_name, COUNT(*) as count FROM users GROUP BY first_name")
List<NameCount> getFirstNameCounts();
// Subqueries
@Query("SELECT * FROM users WHERE id IN " +
"(SELECT user_id FROM orders WHERE total_amount > :minAmount)")
List<User> getUsersWithLargeOrders(double minAmount);
// CASE statements
@Query("SELECT *, " +
"CASE " +
"WHEN age < 18 THEN 'Minor' " +
"WHEN age BETWEEN 18 AND 65 THEN 'Adult' " +
"ELSE 'Senior' " +
"END as age_group " +
"FROM users")
List<UserWithAgeGroup> getUsersWithAgeGroup();
// Date range queries
@Query("SELECT * FROM users WHERE created_date BETWEEN :startDate AND :endDate")
List<User> getUsersCreatedBetween(Date startDate, Date endDate);
// Bulk operations
@Query("UPDATE users SET last_login = :loginTime WHERE id IN (:userIds)")
int updateLastLoginForUsers(List<Integer> userIds, Date loginTime);
}
// Custom result classes for complex queries
class NameCount {
public String firstName;
public int count;
}
class UserWithAgeGroup {
@Embedded
public User user;
public String ageGroup;
}
Repository Pattern with Room
Repository Implementation
@Singleton
public class UserRepository {
private final UserDao userDao;
private final Executor executor;
public UserRepository(UserDao userDao, Executor executor) {
this.userDao = userDao;
this.executor = executor;
}
// LiveData for UI observation
public LiveData<List<User>> getAllUsers() {
return userDao.getAllLiveData();
}
// Async operations
public void insertUser(User user, OnInsertListener listener) {
executor.execute(() -> {
try {
long id = userDao.insert(user);
listener.onSuccess(id);
} catch (Exception e) {
listener.onError(e);
}
});
}
// RxJava operations
public Flowable<List<User>> getUsersOlderThan(int age) {
return userDao.getUsersOlderThanRx(age)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
// Coroutines operations
public suspend List<User> searchUsers(String query) {
return userDao.searchUsers("%" + query + "%");
}
// Complex business logic
public LiveData<UserWithOrders> getUserWithOrders(int userId) {
return userDao.getUserWithOrdersLiveData(userId);
}
// Batch operations
public void insertUsersBatch(List<User> users, OnBatchCompleteListener listener) {
executor.execute(() -> {
try {
userDao.insertAll(users);
listener.onComplete();
} catch (Exception e) {
listener.onError(e);
}
});
}
public interface OnInsertListener {
void onSuccess(long id);
void onError(Exception e);
}
public interface OnBatchCompleteListener {
void onComplete();
void onError(Exception e);
}
}
Testing Room Database
Comprehensive Testing Strategy
@RunWith(AndroidJUnit4.class)
public class UserDaoTest {
private AppDatabase database;
private UserDao userDao;
@Before
public void createDb() {
Context context = ApplicationProvider.getApplicationContext();
database = Room.inMemoryDatabaseBuilder(context, AppDatabase.class)
.allowMainThreadQueries()
.build();
userDao = database.userDao();
}
@After
public void closeDb() throws IOException {
database.close();
}
@Test
public void insertAndGetUser() {
User user = new User("John", "Doe", "[email protected]");
long userId = userDao.insert(user);
User loaded = userDao.getById((int) userId);
assertThat(loaded.getFirstName(), equalTo("John"));
assertThat(loaded.getLastName(), equalTo("Doe"));
}
@Test
public void getAllUsers() {
User user1 = new User("John", "Doe", "[email protected]");
User user2 = new User("Jane", "Smith", "[email protected]");
userDao.insertAll(user1, user2);
List<User> allUsers = userDao.getAll();
assertThat(allUsers.size(), equalTo(2));
}
@Test
public void updateUser() {
User user = new User("John", "Doe", "[email protected]");
long userId = userDao.insert(user);
user.setId((int) userId);
user.setFirstName("Johnny");
userDao.update(user);
User updated = userDao.getById((int) userId);
assertThat(updated.getFirstName(), equalTo("Johnny"));
}
@Test
public void deleteUser() {
User user = new User("John", "Doe", "[email protected]");
long userId = userDao.insert(user);
user.setId((int) userId);
userDao.delete(user);
List<User> allUsers = userDao.getAll();
assertThat(allUsers.size(), equalTo(0));
}
@Test
public void searchUsers() {
User user1 = new User("John", "Doe", "[email protected]");
User user2 = new User("Jane", "Smith", "[email protected]");
userDao.insertAll(user1, user2);
List<User> johns = userDao.findByName("John", "Doe");
assertThat(johns.size(), equalTo(1));
assertThat(johns.get(0).getEmail(), equalTo("[email protected]"));
}
}
// Testing with LiveData
@Test
public void testLiveData() throws InterruptedException {
User user = new User("John", "Doe", "[email protected]");
userDao.insert(user);
LiveData<List<User>> liveData = userDao.getAllLiveData();
List<User> users = LiveDataTestUtil.getValue(liveData);
assertThat(users.size(), equalTo(1));
}
// LiveData test utility
class LiveDataTestUtil {
public static <T> T getValue(LiveData<T> liveData) throws InterruptedException {
final Object[] data = new Object[1];
CountDownLatch latch = new CountDownLatch(1);
Observer<T> observer = new Observer<T>() {
@Override
public void onChanged(T t) {
data[0] = t;
latch.countDown();
liveData.removeObserver(this);
}
};
liveData.observeForever(observer);
latch.await(2, TimeUnit.SECONDS);
return (T) data[0];
}
}
Best Practices and Performance
Optimization Techniques
public class RoomBestPractices {
// 1. Use transactions for bulk operations
@Dao
public interface OptimizedUserDao {
@Transaction
default void insertUsersWithTransaction(List<User> users) {
for (User user : users) {
insert(user);
}
}
@Insert
void insert(User user);
}
// 2. Use indices for frequently queried columns
@Entity(indices = {
@Index(value = "email", unique = true),
@Index(value = {"last_name", "first_name"})
})
public class OptimizedUser {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
public String email;
}
// 3. Proper database configuration
public static AppDatabase getOptimizedDatabase(Context context) {
return Room.databaseBuilder(context, AppDatabase.class, "optimized.db")
.setQueryExecutor(Executors.newFixedThreadPool(4))
.setJournalMode(JournalMode.TRUNCATE)
.openHelperFactory(new SafeHelperFactory("password".toCharArray())) // Encryption
.build();
}
// 4. Use Room with other architecture components
@Singleton
public class UserRepository {
private final UserDao userDao;
private final Executor executor;
public UserRepository(UserDao userDao, @Named("diskIO") Executor executor) {
this.userDao = userDao;
this.executor = executor;
}
public LiveData<Resource<List<User>>> loadUsers() {
return new NetworkBoundResource<List<User>, List<User>>(executor) {
@Override
protected LiveData<List<User>> loadFromDb() {
return userDao.getAllLiveData();
}
@Override
protected boolean shouldFetch(List<User> data) {
return data == null || data.isEmpty();
}
@Override
protected LiveData<ApiResponse<List<User>>> createCall() {
return userService.getUsers();
}
@Override
protected void saveCallResult(List<User> items) {
userDao.insertAll(items);
}
}.asLiveData();
}
}
// 5. Handle database encryption
public static AppDatabase getEncryptedDatabase(Context context) {
SafeHelperFactory factory = SafeHelperFactory.fromUser(
new SafeHelperFactory.Options.Builder()
.setPassword("secure_password")
.build()
);
return Room.databaseBuilder(context, AppDatabase.class, "encrypted.db")
.openHelperFactory(factory)
.build();
}
}
// Custom SQLiteOpenHelper for advanced configurations
public class CustomRoomOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
@Override
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
return new FrameworkSQLiteOpenHelper(
configuration.context,
configuration.name,
configuration.callback
) {
@Override
public void onConfigure(SQLiteDatabase db) {
super.onConfigure(db);
// Custom configurations
db.setForeignKeyConstraintsEnabled(true);
db.enableWriteAheadLogging();
}
};
}
}
Common Issues and Solutions
Troubleshooting Guide
public class RoomTroubleshooting {
// 1. Resolve schema export issues
@Database(
entities = {User.class},
version = 1,
exportSchema = true, // Enable schema export
autoMigrations = {
@AutoMigration(from = 1, to = 2)
}
)
public abstract class TroubleshootingDatabase extends RoomDatabase {
// Default directory for schema: app/schemas/your.app.package.AppDatabase/version/
}
// 2. Handle database conflicts
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertOrReplace(User user);
@Insert(onConflict = OnConflictStrategy.IGNORE)
long insertOrIgnore(User user);
@Insert(onConflict = OnConflictStrategy.ABORT)
long insertOrAbort(User user);
// 3. Custom conflict resolution
@Query("INSERT OR REPLACE INTO users (id, first_name, last_name) " +
"VALUES (:id, :firstName, :lastName)")
void upsertUser(int id, String firstName, String lastName);
// 4. Handle large datasets with paging
@Dao
public interface PagedUserDao {
@Query("SELECT * FROM users ORDER BY id")
DataSource.Factory<Integer, User> getUsersPaged();
@Query("SELECT * FROM users WHERE name LIKE :query")
DataSource.Factory<Integer, User> searchUsersPaged(String query);
}
// 5. Debugging queries
public static AppDatabase getDebugDatabase(Context context) {
return Room.databaseBuilder(context, AppDatabase.class, "debug.db")
.setQueryCallback(new RoomDatabase.QueryCallback() {
@Override
public void onQuery(@NonNull String sqlQuery,
@NonNull List<Object> bindArgs) {
Log.d("ROOM_SQL", "SQL: " + sqlQuery + " | Args: " + bindArgs);
}
}, Executors.newSingleThreadExecutor())
.build();
}
}
Conclusion
Room Persistence Library provides a robust, type-safe abstraction over SQLite with these key benefits:
- Compile-time verification of SQL queries
- Reduced boilerplate code through annotations
- Seamless integration with other Architecture Components
- Excellent performance with minimal overhead
- Built-in support for LiveData, RxJava, and Coroutines
By following these patterns and best practices, developers can build efficient, maintainable, and scalable data persistence layers for Android applications. Room's compile-time safety and intuitive API make it the preferred choice for modern Android database management.