Android Jetpack架构组件(六)之Room

1、Room简介

在Android应用开发中,持久化数据的方式有不少,常见的有Shared Preferences、Internal Storage、External Storage、SQLite Databases和Network Connection五种。其中,SQLite使用数据库方式进行存储,适合用来存储数据量比较大的场景。html

不过,因为SQLite写起来比较繁琐且容易出错,所以,社区出现了各类ORM(Object Relational Mapping)库,如ORMLite、Realm、LiteOrm和GreenDao等,这些第三方库有一个共同的目的,那就是为方便开发者方便使用ORM而出现,简化的操做包括建立、升级、CRUD等功能。
在这里插入图片描述java

为了简化SQLite操做,Jetpack库提供了Room组件,用来帮助开发者简化开发者对数据库操做。Room 持久库提供了一个SQLite抽象层,让开发者访问数据库更加稳健,数据库操做的性能也获得提高。android

2、Room使用

2.1 Room相关概念

Room组件库包含 3 个重要的概念,分布是Entity、Dao和Database。sql

  • Entity:实体类,对应的是数据库的一张表结构,须要使用注解 @Entity 进行标记。
  • Dao:包含访问一系列访问数据库的方法,须要使用注解 @Dao 进行标记。
  • Database:数据库持有者,是应用持久化相关数据的底层链接的主要接入点,须要使用注解 @Database 进行标记。

使用@Database注解需知足如下条件:shell

  • 定义的类必须是一个继承于RoomDatabase的抽象类。
  • 在注解中须要定义与数据库相关联的实体类列表。
  • 包含一个没有参数的抽象方法而且返回一个带有注解的 @Dao。

简单来讲,应用使用 Room 数据库来获取与该数据库关联的数据访问对象 (DAO)。而后应用使用每一个 DAO从数据库中获取实体,再将对这些实体的全部更改保存回数据库中。 最后应用使用实体来获取和设置与数据库中的表列相对应的值。数据库

下面是使用Entity、Dao、Database三者和应用的对应架构示意图,以下所示。
在这里插入图片描述windows

2.2 基本使用

2.2.1 添加依赖

首先,在app的build.gradle中增长如下配脚本。api

dependencies {
    
    def room_version = "2.2.5"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
}

2.2.2 Entity

Room的使用和传统的Sqlite数据库的使用流程是差很少的。首先,使用 @Entity注解定义一个实体类,类会被映射为数据库中的一张表,默认实体类的类名为表名,字段名为表名,以下所示。架构

@Entity
public class User {
    @PrimaryKey(autoGenerate = true)
    public int uid;

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

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

    @Ignore
    public boolean sex;
}

其中,@PrimaryKey注解用来标注表的主键,而且使用autoGenerate = true 来指定了主键自增加。@ColumnInfo注解用来标注表对应的列的信息好比表名、默认值等等。@Ignore 注解用来标示忽略这个字段,使用了这个注解的字段将不会在数据库中生成对应的列信息。app

2.2.3 Dao

Dao类是一个接口,主要用于定义一系列操做数据库的方法,即一般咱们所说的增删改查。为了方便开发者操做数据库,Room提供了@Insert、@Delete、@Update 和 @Query等注解。

@query注解
@Query 是一个查询注解,它的参数时String类型,咱们直接写SQL语句进行执行。好比,咱们根据ID查询某个用户的信息。

@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);

@Insert注解
@Insert注解用于向表中插入一条数据,咱们定义一个方法而后使用 @Insert注解标注便可,以下所示。

@Insert
void insertAll(User... users);

其中,@Insert注解有个onConflict参数,表示的是当插入的数据已经存在时候的处理逻辑,有三种操做逻辑,分布是REPLACE、ABORT和IGNORE。

@Delete注解
@Delete注解用于删除表的数据,以下所示。

@Delete
void delete(User user);

@Update注解
@Update注解用于修改某一条数据 ,和@Delete同样也是根据主键来查找要删除的实体。

@Update
void update(User user);

接下来,咱们新建一个UserDao类,并添加以下代码。

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);

    @Query("DELETE  FROM user WHERE uid = :uid ")
    void deleteUserById(int uid);

    @Query("UPDATE  user SET first_name = :firstName where uid =  :uid")
    void updateUserById(int uid, String firstName);

    @Update
    void update(User user);
}

2.2.4 Database

首先,定义一个继承RoomDatabase的抽象类,而且使用 @Database 注解进行标识,以下所示。

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

    private static  AppDatabase instance = null;

    public static synchronized AppDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(
                    context.getApplicationContext(),
                    AppDatabase.class,
                    "user.db" //数据库名称
                ).allowMainThreadQueries().build();
        }
        return instance;
    }
}

完成上述操做以后,使用如下代码得到建立数据库的实例。

AppDatabase db = AppDatabase.getInstance(this);
        UserDao dao = db.userDao();
        User user=new User();
        user.firstName="ma";
        user.lastName="jack";
        dao.insertAll(user);

2.2.5 综合示例

接下来,咱们经过一个简单的综合练习来讲说Room的基本使用方法。首先,咱们在activity_main.xml布局文件中新增4个按钮,分别用来增删改查。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_insert"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:textSize="24dp"
        android:text="插入数据" />

    <Button
        android:id="@+id/btn_delete"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textSize="24dp"
        android:text="删除数据" />

    <Button
        android:id="@+id/btn_query"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textSize="24dp"
        android:text="查询数据" />

    <Button
        android:id="@+id/btn_update"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textSize="24dp"
        android:text="更新数据" />

</LinearLayout>

而后咱们编写代码实现相关的功能,以下所示。

public class MainActivity extends AppCompatActivity {

    AppDatabase db=null;
    UserDao dao=null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
       db = AppDatabase.getInstance(this);
       dao = db.userDao();
       insert();
       query();
       update();
    }


    private void insert() {
        findViewById(R.id.btn_insert).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (int i=0;i<10;i++) {
                    User user=new User("张三"+i,"100"+i);
                    dao.insertAll(user);
                }
            }
        });
    }

    private void query() {
        findViewById(R.id.btn_query).setOnClickListener(new View.OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public void onClick(View v) {
                dao.getAll().forEach(new Consumer<User>() {
                    @Override
                    public void accept(User user) {
                        Log.d("Room", user.firstName+","+user.lastName);
                    }
                });
            }
        });
    }

    private void update() {
        findViewById(R.id.btn_update).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dao.updateUserById(2, "李四");
                User updateUser = dao.loadUserById(2);
                Log.e("Room", "update${user.firstName},${user.lastName}");
            }
        });
    }
}

接下来,运行代码,执行插入操做,生成的数据库位于data/data/packageName/databases目录下。而后,再执行查询功能,控制台输出内容以下。

com.xzh.jetpack D/Room: 张三0,1000
com.xzh.jetpack D/Room: 张三1,1001
com.xzh.jetpack D/Room: 张三2,1002
com.xzh.jetpack D/Room: 张三3,1003
com.xzh.jetpack D/Room: 张三4,1004
com.xzh.jetpack D/Room: 张三5,1005
com.xzh.jetpack D/Room: 张三6,1006
com.xzh.jetpack D/Room: 张三7,1007
com.xzh.jetpack D/Room: 张三8,1008
com.xzh.jetpack D/Room: 张三9,1009

须要说明的是,全部对数据库的操做都不能够在主线程中进行,除非在数据库的Builder上调用了allowMainThreadQueries()或者全部的操做都在子线程中完成,不然程序会崩溃报并报以下错误。

java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

3、 预填充数据库

有时候,咱们但愿在应用启动时数据库中就已经加载了一组特定的数据,咱们将这种行为称为预填充数据库。在 Room 2.2.0 及更高版本中,开发者可使用 API 方法在初始化时用设备文件系统中预封装的数据库文件中的内容预填充 Room 数据库。

3.1 从应用资源预填充

预填充指的是从位于应用 assets/ 目录中的任意位置的装数据库文件预填充 Room 数据库,使用的时候调用createFromAsset() 方法,而后再调用 build()方法便可,以下所示。

Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
        .createFromAsset("database/myapp.db")
        .build();

createFromAsset() 方法接受一个包含assets/ 目录的相对路径的字符串参数。

3.2 从文件系统预填充

除了将数据内置到应用的 assets/ 目录除外, 咱们还能够 从位于设备文件系统任意位置读取预封装数据库文件来预填充 Room 数据库,使用时须要调用createFromFile() 方法,而后再调用 build(),以下所示。

Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
        .createFromFile(new File("mypath"))
        .build();

createFromFile() 方法接受表明预封装数据库文件的绝对路径的 File 参数,Room 会建立指定文件的副本,而不是直接打开它,而且使用时请确保应用具备该文件的读取权限。

4、迁移数据库

4.1 基本使用

在使用数据库的时候就避免不了须要对数据库进行升级。例如,随着业务的变化,须要在数据表中新增一个字段,此时就须要对数据表进行升级。

在Room中, 数据库的升级或者降级须要用到Migration 类。每一个 Migration 子类经过替换 Migration.migrate() 方法定义 startVersion 和 endVersion 之间的迁移路径。当应用更新须要升级数据库版本时,Room 会从一个或多个 Migration 子类运行 migrate() 方法,以在运行时将数据库迁移到最新版本。

例如,当前设备中应用的数据库版本为1,若是要将数据库的版本从1升级到2,那么代码以下。

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`))");
    }
};

其中,Migration方法须要startVersion和endVersion两个参数,startVersion表示的是升级开始的版本,endVersion表示要升级到的版本,同时须要将@Database注解中的version的值修改成和endVersion相同。

以此类推,若是当前应用的数据库版本为2,想要升级到到版本3,那么代码以下。

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");
    }
};

在Migration编写完升级方案后,还须要使用addMigrations()方法将升级的方案添加到Room中,以下所示。

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("");执行sql语句
        }
    };

    static final Migration MIGRATION_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
//            database.execSQL("");执行sql语句
        }
    };
    
Room.databaseBuilder(app,AppDatabase.class, DB_NAME)
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
    .build();

而后,在Android Studio的工具栏上依次点击【View】->【Tool windows】->【Device File Explorer】打开数据表便可查看。

数据库降级使用和升级的步骤差很少,也是使用addMigrations只是startVersion > endVersion 。当在升级或者降级的过程当中出现版本未匹配到的状况的时候,默认状况下会直接抛异常出来。

固然咱们也能够处理异常。升级的时候能够添加fallbackToDestructiveMigration方法,当未匹配到版本的时候就会直接删除表而后从新建立。降级的时候添加fallbackToDestructiveMigrationOnDowngrade方法,当未匹配到版本的时候就会直接删除表而后从新建立。

4.2 迁移测试

迁移一般十分复杂,而且数据库迁移错误可能会致使应用崩溃。为了保持应用的稳定性,须要开发者对迁移进行测试。为此,Room 提供了一个 room-testing 来协助完成此测试过程。

4.2.1 导出架构

Room 能够在编译时将数据库的架构信息导出为 JSON 文件。如需导出架构,请在 app/build.gradle 文件中设置 room.schemaLocation 注释处理器属性,以下所示。

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

导出的 JSON 文件表明数据库的架构历史记录。您应将这些文件存储在版本控制系统中,由于此系统容许 Room 出于测试目的建立较旧版本的数据库。

4.2.2 测试单次迁移

测试迁移以前,须要先添加测试依赖androidx.room:room-testing,并将导出的架构的位置添加为资源目录,以下所示。

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets.
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

dependencies {
    ...
      testImplementation "androidx.room:room-testing:2.2.5"
}

测试软件包提供了可读取导出的架构文件的 MigrationTestHelper 类。该软件包还实现了 JUnit4 TestRule 接口,所以能够管理建立的数据库。例如,下面是单次迁移的测试的示例代码。

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

    @Rule
    public MigrationTestHelper helper;

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

    @Test
    public void migrate1To2() 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.
    }
}

4.2.3 测试全部迁移

虽然能够测试单次增量迁移,但建议您添加一个测试,涵盖为应用的数据库定义的全部迁移。这可确保最近建立的数据库实例与遵循定义的迁移路径的旧实例之间不存在差别。下面的示例演示了迁移全部测试。

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

    @Rule
    public MigrationTestHelper helper;

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

    @Test
    public void migrateAll() throws IOException {
        // Create earliest version of the database.
        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
        db.close();

        // Open latest version of the database. Room will validate the schema
        // once all migrations execute.
        AppDatabase appDb = Room.databaseBuilder(
                InstrumentationRegistry.getInstrumentation().getTargetContext(),
                AppDatabase.class,
                TEST_DB)
                .addMigrations(ALL_MIGRATIONS).build();
        appDb.getOpenHelper().getWritableDatabase();
        appDb.close();
    }

    // Array of all migrations
    private static final Migration[] ALL_MIGRATIONS = new Migration[]{
            MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4};
}

4.3 迁移异常处理

迁移数据库的过程当中,不可避免的会出现一些异常,若是 Room 没法找到将设备上的现有数据库升级到当前版本的迁移路径,会提示IllegalStateException错误。在迁移路径缺失的状况下,若是丢失现有数据能够接受,那么在建立数据库时能够调用 fallbackToDestructiveMigration() 构建器方法。

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .fallbackToDestructiveMigration()
        .build();

fallbackToDestructiveMigration()方法会指示 Room 在须要执行没有定义迁移路径的增量迁移时,破坏性地从新建立应用的数据库表。若是只想让 Room 在特定状况下回退到破坏性从新建立,可使用 fallbackToDestructiveMigration() 的一些替代选项,以下所示。

  • fallbackToDestructiveMigrationFrom():若是特定版本的架构历史记录致使迁移路径出现没法解决的问题可使用此方法。
  • fallbackToDestructiveMigrationOnDowngrade():若是仅在从较高数据库版本迁移到较低数据库版本时才但愿 Room 回退到破坏性从新建立可使用此方法。

5、测试和调试数据库

5.1 测试数据库

为了测试咱们建立的数据库,有时候须要在Activity中编写一些测试代码。在Android中测试数据库有两种方式。

  • 在 Android 设备上测试。
  • 在主机开发计算机上测试(不推荐)。

5.1.1 在 Android 设备上测试数据库

如需测试数据库实现,推荐的方法是编写在 Android 设备上运行的 JUnit 测试,因为执行这些测试不须要建立 Activity,所以它们的执行速度应该比界面测试速度更快。以下是一个JUnit 测试的示例。

@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
    private UserDao userDao;
    private TestDatabase db;

    @Before
    public void createDb() {
        Context context = ApplicationProvider.getApplicationContext();
        db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
        userDao = db.getUserDao();
    }

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

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

5.1.2 在主机上测试数据库

Room 使用 SQLite 支持库,该支持库提供了与 Android 框架类中的接口相对应的接口。经过此项支持,开发者能够传递该支持库的自定义实现来测试数据库查询。

5.2 调试数据库

Android SDK 包含一个 sqlite3 数据库工具,可用于检查应用的数据库。它包含用于输出表格内容的 .dump 以及用于输出现有表格的 SQL CREATE 语句的 .schema 等命令。咱们能够在命令行执行 SQLite 命令,以下所示。

adb -s emulator-5554 shell
sqlite3 /data/data/your-app-package/databases/rssitems.db

更多的sqlite3命令行能够参考SQLite 网站上提供的sqlite3命令行文档

相关文章
相关标签/搜索