乐趣区

关于android:Android-Jetpack架构组件六之Room

一、Room 简介

在 Android 利用开发中,长久化数据的形式有很多,常见的有 Shared Preferences、Internal Storage、External Storage、SQLite Databases 和 Network Connection 五种。其中,SQLite 应用数据库形式进行存储,适宜用来存储数据量比拟大的场景。

不过,因为 SQLite 写起来比拟繁琐且容易出错,因而,社区呈现了各种 ORM(Object Relational Mapping)库,如 ORMLite、Realm、LiteOrm 和 GreenDao 等,这些第三方库有一个独特的目标,那就是为不便开发者方便使用 ORM 而呈现,简化的操作包含创立、降级、CRUD 等性能。

为了简化 SQLite 操作,Jetpack 库提供了 Room 组件,用来帮忙开发者简化开发者对数据库操作。Room 长久库提供了一个 SQLite 形象层,让开发者拜访数据库更加持重,数据库操作的性能也失去晋升。

二、Room 应用

2.1 Room 相干概念

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

  • Entity:实体类,对应的是数据库的一张表构造,须要应用注解 @Entity 进行标记。
  • Dao:蕴含拜访一系列拜访数据库的办法,须要应用注解 @Dao 进行标记。
  • Database:数据库持有者,是利用长久化相干数据的底层连贯的次要接入点,须要应用注解 @Database 进行标记。

应用 @Database 注解需满足以下条件:

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

简略来说,利用应用 Room 数据库来获取与该数据库关联的数据拜访对象 (DAO)。而后利用应用每个 DAO 从数据库中获取实体,再将对这些实体的所有更改保留回数据库中。最初利用应用实体来获取和设置与数据库中的表列绝对应的值。

上面是应用 Entity、Dao、Database 三者和利用的对应架构示意图,如下所示。

2.2 根本应用

2.2.1 增加依赖

首先,在 app 的 build.gradle 中减少以下配脚本。

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 注解用来标示疏忽这个字段,应用了这个注解的字段将不会在数据库中生成对应的列信息。

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.

三、预填充数据库

有时候,咱们心愿在利用启动时数据库中就曾经加载了一组特定的数据,咱们将这种行为称为预填充数据库。在 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.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.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 命令行文档。

退出移动版