Android Room 使用指南

  本内容主要介绍 Android 中使用 Room 保存数据到本地数据库的方法。

  以下是 Android Room 的官方介绍文档:

标题网址
Room Persistence Library
(Room 库的简单介绍)
https://developer.android.com/topic/libraries/architecture/room
Save data in a local database using Room
(Room 的使用指南)
https://developer.android.com/training/data-storage/room/
Android Room with a View - Java
(Room 的使用实例)
https://codelabs.developers.google.com/codelabs/android-room-with-a-view/#0

一、简介

  Room 是一个对象关系映射(ORM)库。可以很容易将 SQLite 表数据转换为 Java 对象。Room 在编译时检查 SQLite 语句。

  Room 为 SQLite 提供一个抽象层,以便在充分利用 SQLite 的同时,可以流畅地进行数据库访问。

1.1 添加依赖

  如果想使用 Room,需要你的 APP 或者 module 的 build.gradle 中添加以下依赖:

dependencies {
    def room_version = "1.1.1"

    // Room components
    implementation "android.arch.persistence.room:runtime:$room_version"
    // For Kotlin use kapt instead of annotationProcessor
    annotationProcessor "android.arch.persistence.room:compiler:$room_version"
    
    // optional - RxJava support for Room
    implementation "androidx.room:room-rxjava2:$room_version"

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"

    // optional - Coroutines support for Room
    implementation "androidx.room:room-coroutines:$room_version"

    // Test helpers
    testImplementation "androidx.room:room-testing:$room_version"
}

  前面的两句是必须的,后面的部分为可选的。点击 这里 可以查看最新依赖版本号和依赖声明方法。

1.2 Room 组件

  Room 有 3 个主要的组件:

  • Database:包含数据库持有者,并作为与 App 持久关联数据的底层连接的主要访问点。

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

    • 是一个继承至 RoomDatabase 的抽象类。
    • 在注解中包含与数据库相关联的实体列表。
    • 包含一个具有 0 个参数的抽象方法,并返回用 @Dao 注解的类。

    在运行时,您可以通过调用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 获取 Database 实例。

  • Entity:表示数据库内的表(Table)。

  • DAO:包含用于访问数据库的方法。

1.3 Room 各组件间关系

  Room 的大致使用方法如下:

  • App 通过 Room 的 Database 获取与数据库相关的数据库访问对象(DAO)。
  • 然后,App 使用 DAO 从数据库中获取 Entity,并且将 Entity 的变化保存到数据库中。
  • 最后,APP 使用 Entity 获取和设置数据库中表的数据。

  Room 中各组件之间的关系如图-1 所示:

图-1 Room 框架图

二、Entity(实体)

  在使用 Room 持久化库(Room persistence library)时,需要将相关字段集定义为 Entity。对于每一个 Entity,在与其相关的 Database 对象中会创建一个表(Table)。必须通过 Database 类的 entities 数组引用这个 Entity 类。

  下面的代码片段展示如何定义 Entity:

@Entity
public class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;
}

  要持久化一个字段(Field),Room 必须能够使用它。可以将字段设置为 public,也可以为它提供 getter 和 setter 方法。在提供 getter 和 setter 方法时,需要遵守 Room 中的 JavaBeans 协议。

2.1 设置 Table 名称

  Room 默认使用类名作为数据库的 Table 名称。可以通过 @Entity 的 tableName 属性设置 Table 的名称。(注意:在 SQLite 中,Table 名称是不区分大小写的。)

@Entity(tableName = "users")
public class User {
    // ...
}
2.2 设置列名

  Room 使用字段(Filed)名称作为在数据库中的默认列名。可以通过给 Filed 添加 @ColumnInfo 注解设置列名。

@Entity(tableName = "users")
public class User {
    @PrimaryKey
    public int id;

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

    @ColumnInfo(name = "last_name")
    public String lastName;
}
2.3 设置主键

  每个 Entity 必须设置至少一个 Field 作为主键(primary key)。即使只有 1 个 Field,也需要将其设置为主键。有两种方法设置主键:

  • 使用注解 @PrimaryKey,可以用来设置单个主键。
@Entity
public class User {
    @PrimaryKey(autoGenerate = true)
    @NonNull
    public String firstName;

    public String lastName;
}

如果需要 Room 自动分配 IDs 给 Entity,可以设置 @PrimaryKey 的 autoGenerate 属性。

  • 使用注解 @Entity 的 primaryKeys 属性,可以用来设置单个主键和复合主键。
@Entity(primaryKeys = {"firstName", "lastName"})
public class User {
    @NonNull
    public String firstName;

    @NonNull
    public String lastName;
}
2.4 设置忽略字段(Ignore fields)

  默认情况下,Room 为 Entity 中每个 Field 创建一列。如果在 Entity 中存在不需要持久化的 Field,可以给它们添加 @Ignore 注解。

@Entity
public class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

  如果子类不需要持久化父类中的 Field,使用 @Entity 的 ignoredColumns 属性更为方便。

@Entity(ignoredColumns = "picture")
public class RemoteUser extends User {
    @PrimaryKey
    public int id;

    public boolean hasVpn;
}

三、DAO(Data access object)

  在 Room 持久化库中,使用数据访问对象(data access objects, DAOs)访问 App 的数据。Dao 对象集合是 Room 的主要组件,因为每个 DAO 提供访问 App 的数据库的抽象方法。

  通过使用 DAO 访问数据库,而不是通过查询构造器或直接查询,可以分离数据库架构的不同组件。此外,在测试应用时,DAOs 可以轻松模拟数据库访问。

  DAO 可以是接口(interface),也可以是抽象类(abstract class)。如果是一个抽象类,可以有一个构造函数,其只接收一个 RoomDatabase 参数。在编译时,Room 为每个 DAO 创建具体实现。

注意:除非在构造器上调用 allowMainThreadQueries(),否则 Room 不支持在主线程上进行数据库访问,因为它可能会长时间锁定 UI。不过异步查询(返回 LiveData 或 Flowable 实例的查询)不受此规则约束,因为它们在需要时会在后台线程进行异步查询。

3.1 插入(Insert)

  当创建 DAO 方法并使用 @Insert 对其进行注解时,Room 将生成一个实现,在单个事务中将所有参数插入数据库中。

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

  当使用 @Insert 注解的方法仅仅只有一个参数时,可以返回一个 long 类型的值,其表示插入项的 rowId。如果参数是一个数组或集合,则返回 long[]List<Long> 类型的值。

3.2 更新(Update)

  更新方法修改数据库中的一组 Entity(由参数提供)。使用每个 Entity 的主键进行匹配查询。

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

  更新方法可以返回一个 int 类型的值,其表示数据库中更新的行数,不过通常是不需要的。

3.3 删除(Delete)

  删除函数移除数据库中的一组 Entity(由参数提供)。使用实体的主键进行匹配。

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

  和更新方法一样,删除方法也可以返回一个 int 类型的值,其表示数据库中删除的行数,通常也是不需要的。

3.4 查询(Query)

  @Query 是 DAO 类中的重要注解。它允许在数据库上执行读写操作。每个 @Query 方法都是在编译时验证的;因此,如果存在查询问题,将出现编译错误而不是运行时错误。

  在编译时,Room 还验证查询的返回值,如果返回对象中的字段名称与查询中的相应列名称不匹配,将通过以下两种方式之一告知:(在下面 3.4.3 返回列的子集 会提到)

  • 如果仅仅部分 Field 名称匹配,将显示 Warning。
  • 如果没有 Field 名称匹配,将显示 Error。
3.4.1 简单查询

  下面是一个简单的查询,获取所有 User。

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

  在编译时,Room 知道查询 user 表中的所有列。如果这个查询存在语法错误,或者数据库中不存在 user 表,Room 将显示相应的错误。

3.4.2 带参数的查询

  大多数情况下,需要将参数传递到查询中以执行筛选操作,例如仅需要显示大于某一年龄的 User。这时,我们可以使用方法参数。

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    public User[] loadAllUsersOlderThan(int minAge);
}

  在编译时,Room 使用 minAge 方法参数匹配 :minAge 绑定参数。如果存在匹配错误,将出现编译错误。

  还可以在查询中传递多个参数或者多次引用它们。

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

  在查询时,传递的参数还可以是一个集合。Room 知道参数何时是一个集合,并根据提供的参数数量在运行时自动展开。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
3.4.3 返回列的子集

  大多数情况下,我们可能只需要获取一个 Entity 中的几个 Field。这样可以节省宝贵的资源,并且可以更快速地完成查询。

  只要结果列集合可以映射到返回的对象中,Room 允许返回任何基于 Java 的对象。例如,可以创建以下普通的 Java 对象(plain old Java-based object, POJO)来获取用户的 first name 和 last name:

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

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

  如果查询结果返回太多列,或者一列在 NameTuple 中不存在,Room 将显示一个警告。

注意:POJO 也可是使用 @Embedded 注解。

3.4.4 可观察的查询

  如果希望 App 的 UI 在数据发生变化时自动更新 UI,可以在查询方法中返回一个 LiveData 类型的值。Room 会产生所有必须的代码,用于在数据库发生变化时更新这个 LivaData 对象。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}
3.4.5 RxJava 的响应式查询

  Room 支持返回一下 RxJava2 类型的值:

  • @Query 方法:支持返回 PublisherFlowableObservable 类型的值。
  • @Insert@Update@Delete 方法:Room 2.1.0 及以上版本支持返回 CompletableSingle<T>Maybe<T> 类型的值。

  需要在 App 的 build.gradle 文件中添加对最新 rxjava2 版本的依赖:

dependencies {
    implementation 'androidx.room:room-rxjava2:2.1.0-alpha02'
}

  点击 这里 查看更详细的信息。

3.4.6 直接 Cursor 访问

  查询的返回值可以是 Cursor 对象。

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    public Cursor loadRawUsersOlderThan(int minAge);
}

注意:强烈建议不要使用这种方式。

3.4.7 多表查询

  Room 允许进行多表查询。如果返回的是可观察的数据类型(例如 FlowableLivaData),Room 将监控所有在查询中引用的表,用于刷新数据。

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

四、Database

  在 Room 持久化库中,通过 @Database 类访问数据库。

4.1 定义 Database

  下面的代码片段展示如何定义 Database:

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

  如果直接按照上面的写法,会出现以下错误信息:

警告: Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide `room.schemaLocation` annotation processor argument OR set exportSchema to false.

  上面的错误信息已经提供了两种解决方法:

  • 给 RoomDatabase 设置 exportSchema = false。

    @Database(entities = {User.class}, version = 1, exportSchema = false)
    public abstract class AppDatabase extends RoomDatabase {
        public abstract UserDao userDao();
    }
    
  • 在你的 APP 或者 module 的 build.gradle 中添加以下注解信息:

    android {
        ...
        defaultConfig {
            ...
            //指定room.schemaLocation生成的文件路径
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = ["room.schemaLocation":
                                 "$projectDir/schemas".toString()]
                }
            }
        }
    
    }
    
4.2 获取数据库实例

  可以通过以下方法获取创建的数据库的实例:

AppDatabase db = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

  其中 database-name 为你自己定义的数据库名称,比如 RoomSample.db。因为其会占用较多的资源,所以一般建议使用单例模式。

五、Room 数据库迁移

  在 Room 持久化库中通过使用 Migration 类保存用户数据。每个 Migration 类指定起始版本和结束版本。在运行时,Room 运行每个 Migration 类的 migrate() 方法,使用正确的顺序将数据库迁移到后面的版本。

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

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

注意:为了使迁移逻辑正常执行,请使用完整查询,不要使用表示查询的常量。

5.1 优雅地处理丢失的迁移路径

  在更新数据库的模式(schema)后,一些设备上的数据库可能仍然是旧的模式版本。如果 Room 无法找到将设备的数据库从旧版本升级到当前版本的迁移规则,将出现 IllegalStateException

  为了防止这种情况发生时应用崩溃,在创建数据库时调用 fallbackToDestructiveMigration() 方法,这样 Room 将会重建应用的数据库表(将直接删除原数据库表中的所有数据)。

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

  这种破坏性的恢复逻辑包括几个额外的选项:

  • 仅当数据库从问题版本进行迁移时使用回退逻辑,使用 fallbackToDestructiveMigrationFrom()
  • 仅当尝试模式降级时执行破坏性重建,使用 fallbackToDestructiveMigrationOnDowngrade()

六、测试数据库

  在使用 Room 持久化库创建数据库时,验证应用的数据库和用户数据的稳定性是有必要的。

  测试数据库有两种方法:

  • 在 Android 设备上。
  • 在开发机器上(不推荐)。
6.1 在 Android 设备上测试

  测试数据库实现的推荐方法是编写一个在 Android 设备上运行的 JUnit 测试。因为这些测试不需要创建一个 Activity,所以它们应该比 UI 测试更快执行。

  在编写测试时,应创建数据库的 in-memory 版本,以便使测试更加封闭。

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

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

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

    @Test
    public void writeUserAndReadInList() 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));
    }
}
6.2 在开发机器上测试

  不推荐使用这种方法,因为您的设备或者用户设备上的 SQLite 版本可能与开发机器上的版本不匹配。

七、使用 Room 引用复杂数据

  Room 提供了在基本类型和盒式类型之间转换的功能,但不允许 Entity 之间的对象引用。

7.1 使用类型转换器

  有时,希望将自定义的数据类型的值存储在数据库的单个列中。为了支持自定义类型,需要提供一个 TypeConverter,它将自定义类型转换为 Room 能够持久化的已知类型。

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

  由于 Room 已经知道如何持久化 Long 对象,所以它能使用这个转换器来持久化 Data 类型的数据。

  接下来,为 AppDatabase 添加 @TypeConverters 注解,以便 Room 能使用为 Entity 和 DAO 定义的转换器。

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

  使用这些转换器后,在查询中可以像使用基本类型一样使用自定义类型。

@Entity
public class User {
    private Date birthday;
}
@Dao
public interface UserDao {
    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
    List<User> findUsersBornBetweenDates(Date from, Date to);
}
7.2 理解为什么 Room 不允许对象引用

  重要信息:Room 不允许 Entity 类之间的对象引用。而应该显式地请求 App 需要的数据。

  将数据库与相应对象模型建议映射关系是一种常见的做法,在服务器端非常有效。即使程序在访问 Field 时加载它们,服务器端仍然表现良好。

  但是,在客户端,这种类型的延迟加载是不可行的。因为通常这一过程发生在 UI 线程,在 UI 线程上查询磁盘上的信息会造成严重的性能问题。UI 线程通常有大约 16ms 的时间来计算和绘制 Activity 的更新布局,即使一个查询只需要 5ms,App 可能仍然不够时间绘制帧,从而导致明显的视觉延迟。如果有一个单独的事务并行运行,或者设备正在运行其他磁盘密集型任务,那么查询操作可能花费更多时间。然而,如果不使用延迟加载,App 将获取比它实际需要更多的数据,从而出现内存消耗问题。

  对象关系映射通过让开发人员做这个决定,这样他们能够为 App 用户事例做出最好的选择。开发人员通常选择在 App 和 UI 之间共享模型。然而,这种方案的扩展性很差;因为当 UI 发生变化时,这种共享模型将出现难以预料和调试的问题。

  例如,考虑一个加载 Book 对象列表的 UI,每一个 book 持有一个 Author 对象。最初,可能会使用延迟加载进行查询,以便让 Book 实例检索 author。第一次检索 author 时,进行查询数据库操作。后来,需要在 App 的 UI 中显示 author 名称。可以很容易地访问这个名称,如下面的代码片段所示:

authorNameTextView.setText(book.getAuthor().getName());

  然后,这种看似无害的更改会导致在主线程上查询 Author 表。

  如果你提前查询 author 信息,则在不再需要该数据时,很难更改数据的加载方式。例如,如果 App 的 UI 不再需要显示 Author 信息,那么 App 会加载不再显示的数据,从而浪费宝贵的内存空间。如果 Author 类引用其他表(比如 Books),App 的效率会进一步降低。

  要使用 Room 同时引用多个 Entity,需要创建一个包含每个 Entity 的 POJO,然后编写一个连接相应表的查询。这种结构良好的模型与 Room 强大的查询功能相结合,可让 App 在加载数据时消耗更少的资源,从而提高 App 的性能和用户体验。

八、Room 的优势

  在 Android 中,如果直接通过 SQLite API 实现数据持久化,需要实现以下操作:

  • 需要创建一个任务繁重的 SQLiteOpenHelper 类,用于创建数据库以及数据库的升降级等。
  • 需要创建维护表的字段的 Constant 类。
  • 需要为数据库 CRUD 操作(create、read、update 和 delete)编写各种函数。
  • 访问数据时需要对 Cursor 进行遍历操作。
  • 其他。

  相比之下,Room 作为在 SQLite 之上封装的 ORM 库,具备以下优势:

  • 比 SQLite API 更简单的使用方式。
  • 省略了许多重复代码。
  • 能在编译时校验 SQL 语句的正确性。
  • 数据库相关的代码分为 Entity,DAO,Database三个部分,结构清晰。
  • 其他。

参考:

[1] https://developer.android.com/topic/libraries/architecture/room
[2] https://developer.android.com/training/data-storage/room/
[3] https://blog.csdn.net/u011897062/article/details/82107709
[4] https://www.jianshu.com/p/3e358eb9ac43
[5] https://www.jianshu.com/p/654d883e6ed0

附录

  持久化(Persistence)就是把内存中的数据保存到可永久保存的存储设备中。Android 提供了三种方式用于数据持久化:文件存储、SharedPreference 存储和 SQLite 数据库存储。

  持久层(Persistence Layer)就是专注于实现数据持久化应用领域的某个特定系统的一个逻辑层面,将数据使用和数据实体相关联。

  • 5
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值