Room, an ORM library for SQLite, makes it easy to turn Java objects into SQLite table data without writing boilerplate CODE for the SQLite API in the traditional way. Room also provides compile-time checking of SQLite syntax and returns RxJava, Flowable, and LiveData Observables.

Add the dependent

    // Room (use 1.1.0-beta2 for latest beta)
    implementation "Android. Arch. Persistence. Room: the runtime: 1.0.0"
    annotationProcessor "Android. Arch. Persistence. Room: the compiler: 1.0.0"
    // Test helpers for Room
    testImplementation "Android. Arch. Persistence. Room: testing: 1.0.0"
Copy the code

The basic use

Room mainly consists of three components:

  • Database: Contains the Database holder as the primary access point for the underlying connection to application persistence-related data. This class needs to be used@DatabaseNote, and satisfy the following conditions:
    • It has to be an inheritanceRoomDatabaseThe abstract class
    • The annotation contains a list of entity classes associated with the database
    • The included abstract method cannot have arguments, and the return value must be@DaoAnnotate the class
  • Entity: represents a table in the database
  • DAO: Contains a set of methods for accessing a database

Their relationship to the application is shown below:

@Entity(tableName = "products")
public class ProductEntity {

    @PrimaryKey
    private int id;
    private String name;
    privateString description; . }Copy the code
@Dao
public interface ProductDao {

    @Query("select * from products")
    List<ProductEntity> getAllProducts(a);

    @Query("select * from products where id = :id")
    ProductEntity findProductById(int id);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertProduct(ProductEntity product);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertAllProducts(List<ProductEntity> products);

    @Delete
    void deleteProduct(ProductEntity product);
}
Copy the code
@Database(entities = {ProductEntity.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract ProductDao productDao(a);
}
Copy the code
    AppDatabase appDatabase = Room.databaseBuilder(this, AppDatabase.class, "product.db").build(); ProductDao productDao = appDatabase.productDao(); . List<ProductEntity> allProducts = productDao.getAllProducts(); . productDao.insertProduct(productEntity);Copy the code

Each entity represents a table in which a field represents a column. The annotation handler automatically generates the corresponding implementation classes AppDatabase_Impl and ProductDao_Impl for AppDatabase and ProductDao. By calling the Room. DatabaseBuilder () or Room. InMemoryDatabaseBuilder () access to the Database instance at runtime, but note that instantiation RoomDatabase is quite expensive, It is best to create only one Database instance in singleton mode.

Define the Entity

In order for Room to access an entity, the fields in the Entity must be public or provide getter/setter methods. By default, Room treats each field in entity as a column in a database table, and you can use the @ignore annotation if you don’t want to persist a field. The default database tableName is the entity class name, which you can change via the tableName property of the @entity annotation. The default column name is the field name, which you can change via the @columninfo annotation.

A primary key

Each Entity must have at least one field as the primary key, even if the entity has only one field. Use the @primarykey annotation to specify the PrimaryKey. If you want SQLite to automatically generate this unique PrimaryKey for you, set @primarykey’s autoGenerate property to true, but change the column to be of type INTEGER. The Insert method defaults to 0 if the field type is long or int, and null if the field type is Integer or LONG. If the entity’s primary key is composite primary key, you need to define this constraint using the primaryKeys property of the @Entity annotation, as in:

@Entity(primaryKeys = {"firstName"."lastName"})
class User {
    public String firstName;
    public String lastName;
    public String avatar;
}
Copy the code

The index

Indices can be created using the @Entity attribute. If a field or group of fields is unique, set the @index attribute to true to enforce this uniqueness.

@Entity(indices = {@Index(value = {"first_name"."last_name"},
        unique = true)})
class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    Bitmap picture;
}
Copy the code

Relationship between

SQLite is a relational database, and many times we need to specify relationships between objects. Although most ORM libraries allow entity-class objects to refer to each other, Room explicitly forbids this. Because cascading queries cannot occur on the UI thread, which only takes 16 ms to compute and draw the layout, even if a query only takes 5 ms, your application could still time out and cause significant visual problems. And if there are other database transactions running at this time or if the device is running other disk-sensitive tasks, the query will take more time. If you don’t use lazy loading, your application will have to fetch more data than it needs, causing memory problems. ORM libraries often leave this decision up to the developer to take action based on their application, and developers often decide to share models between the application and the UI. However, this solution does not scale well because as the UI changes, Sharing models can create problems that are difficult for developers to predict and debug. For example, if the UI loads a list of Book objects and each Book has an Author object, you might initially want to get the Book instance lazily (using the getAuthor() method to get the Author), The first call to getAuthor() invokes the database query. After a while, you realize that you need to display the author name on the UI, and you write code like this:

    authorNameTextView.setText(user.getAuthor().getName());
Copy the code

This seemingly normal change causes the Author table to be queried in the main thread. Is it ok to inquire the author information in advance? Obviously not, it’s hard to change the way data is loaded if you don’t need it anymore. For example, if your UI no longer needs to display Author information, your application will still load the data that is not needed, wasting expensive memory space, and your application will be even less efficient if the Author references other tables. So in order for Room to reference multiple entities simultaneously, you need to create a POJO containing each entity and then write a query that joins the corresponding tables. This well-structured model, combined with Room’s robust query validation capabilities, allows your application to spend less resources loading data, improving application performance and user experience. Although you cannot specify relationships between objects directly, you can specify Foreign Key constraints. For example, for a Book entity that has an author’s ForeignKey reference User, you can specify this ForeignKey constraint via the @foreignkey annotation:

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id"))
class Book {
    @PrimaryKey
    public int bookId;

    public String title;

    @ColumnInfo(name = "user_id")
    public int userId;
}
Copy the code

Cascading operations such as cascading updates and cascading deletes can be specified through the onDelete and onUpdate attributes of the @foreignKey annotation:

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id",
                                  onUpdate = ForeignKey.CASCADE,
                                  onDelete = ForeignKey.CASCADE))
Copy the code

Sometimes, an Entity or POJO containing a nested object represents a complete database logic, and you can use the @Embedded annotation to decompose the fields of that nested object into a table, such as the User table that needs to contain address-related fields, You can use the @embedded annotation to indicate that this is a composite column:

public class Address {
    public String street;
    public String state;
    public String city;
    @ColumnInfo(name = "post_code")
    public int postCode;
}

@Entity
public class User {
    @PrimaryKey
    public int id;
    public String firstName;
    @Embedded
    public Address address;
}
Copy the code

That is, the User table contains the ID, firstName, street, state, city, and post_code columns. Embedded fields can also contain other Embedded fields. If you have another composite column that is also of type Address, you can use the prefix attribute of the @Embedded annotation to prefix the column name to keep the column unique.

Using the DAO

DAO (Data Access Objects) is the most direct interface to operate the database in the application. The operation of the database in the application is expressed on this object. That is to say, the application does not need to know the specific database operation method, but only needs to use DAO to complete the database operation. So this set of Dao objects also forms the core component of Room. The DAO can be an interface or an abstract class. If it is an abstract class, it can have a constructor that takes RoomDatabase as its only argument, and Room automatically generates the implementation classes for each DAO at compile time.

new

If you define a DAO method annotated with @INSERT, Room will automatically generate an implementation that inserts all arguments into the database in a single transaction. If the method has only one argument, it can return a rowId of type long. If the method arguments are arrays or collections, Then it can return long[] or List< long >:

@Dao
public interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);

    @Insert
    public void insertBothUsers(User user1, User user2);

    @Insert
    public void insertUsersAndFriends(User user, List<User> friends);
}
Copy the code

update

The @update annotated method can change a set of entities, use the matching primary key to query to change those entities, and return the number of database Update rows of type int:

@Dao
public interface MyDao {
    @Update
    public void updateUsers(User... users);
}
Copy the code

delete

The @delete annotation method can Delete a given set of entities, use the matching primary key to query to change those entities, and return the number of deleted rows in the database of type int:

@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}
Copy the code

The query

The @Query annotated method allows you to easily read and write to the database, and Room validates this method at compile time, so compile-time errors will be reported if there is a problem with the Query. Room also validates the return value of the query, and if the field name of the response to the query doesn’t match the field name of the returned object, you’ll see a warning if some fields don’t match, and an error if all fields don’t match. Here is a simple query for all users:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}
Copy the code

If you want to add a query condition, you can use the following method to obtain the parameter value:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

    @Query("SELECT * FROM user WHERE first_name LIKE :search "
           + "OR last_name LIKE :search")
    public List<User> findUserWithName(String search);
}
Copy the code

Of course, query criteria sets are also supported:

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
Copy the code

Most of the time, we don’t need to query all the fields in the table. Instead, we use only the columns used by the UI. To save resources and speed up the query, we can define a POJO containing the fields used (annotated by @embedded). Query methods can use this POJO:

public class NameTuple {
    @ColumnInfo(name="first_name")
    public String firstName;

    @ColumnInfo(name="last_name")
    public String lastName;
}
Copy the code
@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    public List<NameTuple> loadFullName(a);
}
Copy the code

Room also allows you to easily perform multi-table queries, such as querying all books that a user borrows:

@Dao
public interface MyDao {
    @Query("SELECT * FROM book "
           + "INNER JOIN loan ON loan.book_id = book.id "
           + "INNER JOIN user ON user.id = loan.user_id "
           + "WHERE user.name LIKE :userName")
   public List<Book> findBooksBorrowedByNameSync(String userName);
}
Copy the code

Pojos can also be used for multi-table queries, such as the user name and his pet name:

@Dao
public interface MyDao {
   @Query("SELECT user.name AS userName, pet.name AS petName "
          + "FROM user, pet "
          + "WHERE user.id = pet.user_id")
   public LiveData<List<UserPet>> loadUserAndPetNames();

   // You can also define this class in a separate file, as long as you add the
   // "public" access modifier.
   static class UserPet {
       public String userName;
       publicString petName; }}Copy the code

The return value of the query method can be LiveData so that you can update the UI in real time as the database updates, The return value may also be RxJava2 Publisher or Flowable (need to add an android. Arch. Persistence. Room: RxJava2 dependence), and can even Cursor (use Cursor directly API is not recommended).

Database update and migration

As the functionality of your application changes, you need to make changes to entity and database, but many times you don’t want to lose data that already exists in the database, especially if it can’t be recovered from a remote server. That is, if you do not provide the necessary migration operations, Room will rebuild the database and all data in the database will be lost. To do this, Room allows you to write Migration classes to protect user data. Each Migration class specifies a startVersion and an endVersion. At runtime, Room runs the migrate() method of each Migration class to migrate the database to the latest version in the correct order:

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

static final Migration MIGRATION_1_2 = new Migration(1.2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
                + "`name` TEXT, PRIMARY KEY(`id`))"); }};static final Migration MIGRATION_2_3 = new Migration(2.3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Book "
                + " ADD COLUMN pub_year INTEGER"); }};Copy the code

Note that to ensure that the migration logic works as expected, you should use the full query instead of referring to constants that represent the query.

After the migration process is complete, Room validates the schema to ensure that the migration is done correctly, and if Room finds a problem, it throws an exception containing mismatch information. Migrating a database is an important and unavoidable operation. If the migration goes wrong, it can cause your application to fall into a crash loop. In order to maintain the stability of your application, you must test the whole process of migrating in advance. In order to test, you need to add an android. Arch. Persistence. Room: testing relies on, and you need to export the database schema. At compile time, Room exports your database’s schema information as a JSON file. To export the schema, you need to set the annotation processor property room. SchemaLocation in the build.gradle file:

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}
Copy the code

You need to save this exported JSON file in version control, because this file represents the schema history of the database. Also you need to add the schema location as the asset folder:

android {
    ...
    sourceSets {
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}
Copy the code

The MigrationTestHelper class in the test tool can read these schema files, and it also implements JUnit4’s TestRule interface, so it can manage creating databases:

@RunWith(AndroidJUnit4.class)
public class MigrationTest {
    private static final String TEST_DB = "migration-test";

    @Rule
    public MigrationTestHelper helper;

    public MigrationTest(a) {
        helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
                MigrationDb.class.getCanonicalName(),
                new FrameworkSQLiteOpenHelperFactory());
    }

    @Test
    public void migrate1To2(a) throws IOException {
        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);

        // db has schema version 1. insert some data using SQL queries.
        // You cannot use DAO classes because they expect the latest schema.db.execSQL(...) ;// Prepare for the next version.
        db.close();

        // Re-open the database with version 2 and provide
        // MIGRATION_1_2 as the migration process.
        db = helper.runMigrationsAndValidate(TEST_DB, 2.true, MIGRATION_1_2);

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.}}Copy the code

Database testing

Write JUnit test is usually faster and more intuitive than the UI testing, using the Room. The inMemoryDatabaseBuilder structure – in the memory version of the database allows you to test more closed:

@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
    private UserDao mUserDao;
    private TestDatabase mDb;

    @Before
    public void createDb(a) {
        Context context = InstrumentationRegistry.getTargetContext();
        mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
        mUserDao = mDb.getUserDao();
    }

    @After
    public void closeDb(a) throws IOException {
        mDb.close();
    }

    @Test
    public void writeUserAndReadInList(a) throws Exception {
        User user = TestUtil.createUser(3);
        user.setName("george");
        mUserDao.insert(user);
        List<User> byName = mUserDao.findUsersByName("george");
        assertThat(byName.get(0), equalTo(user)); }}Copy the code

Advanced usage and techniques

TypeConverter

Sometimes, we need to store some custom datatypes in the database, or do some type conversions before storing them in the database. For example, we need to store datatypes as Unix timestamps in the database:

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(); }}Copy the code

Then use @typeconverters to annotate the elements that require a converter. If the Database is annotated, all DAOs and entities in the Database can use it. If the Dao is annotated, all methods in the Dao can use it. If Entity is annotated, all fields in Entity can use it. If a POJO is annotated, all fields in the POJO can use it. If an Entity field is annotated, only the Entity field can use it. If a Dao method is annotated, it can be used by all the arguments in that Dao method. If the Dao method parameter is annotated, only this parameter can be used:

@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao(a);
}
Copy the code

When querying, you can still use your custom types, just as you would use primitive types:

@Dao
public interface UserDao {...@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
    List<User> findUsersBornBetweenDates(Date from, Date to);
}
Copy the code

Creation of a Database object

Instantiating RoomDatabase is quite expensive, and it is best to inject a unique Database instance using a dependency injection tool like Dagger2, such as:

@Module(includes = ViewModelModule.class)
class AppModule {...@Singleton @Provides
    GithubDb provideDb(Application app) {
        return Room.databaseBuilder(app, GithubDb.class,"github.db").build();
    }

    @Singleton @Provides
    UserDao provideUserDao(GithubDb db) {
        return db.userDao();
    }

    @Singleton @Provides
    RepoDao provideRepoDao(GithubDb db) {
        returndb.repoDao(); }}Copy the code

Even if dependency injection is not used, a singleton should be used to create a Database:

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {

    private static volatile AppDatabase INSTANCE;

    public abstract UserDao userDao(a);

    public static AppDatabase getInstance(Context context) {
        if (INSTANCE == null) {
            synchronized (AppDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            AppDatabase.class, "sample.db") .build(); }}}returnINSTANCE; }}Copy the code

thread

Operating on the database is a very time-consuming operation, so you cannot query or change the database in the main thread (UI thread). Room does thread checks for this, and raises an exception if you operate on the database in the main thread. For convenience, Room also allows you to return Publisher and Flowable from LiveData or RxJava directly in query operations.

reference

  • Save data in a local database using Room