本内容主要介绍 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 的同时,可以流畅地进行数据库访问。
如果想使用 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" }
前面的两句是必须的,后面的部分为可选的。点击 这里 可以查看最新依赖版本号和依赖声明方法。
Room 有 3 个主要的组件:
Database:包含数据库持有者,并作为与 App 持久关联数据的底层连接的主要访问点。
用 @Database 注解的类应满足以下条件:
在运行时,您可以通过调用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 获取 Database 实例。
Entity:表示数据库内的表(Table)。
DAO:包含用于访问数据库的方法。
Room 的大致使用方法如下:
Room 中各组件之间的关系如图-1 所示:
图-1 Room 框架图在使用 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 协议。
Room 默认使用类名作为数据库的 Table 名称。可以通过@Entity的 tableName 属性设置 Table 的名称。(注意:在 SQLite 中,Table 名称是不区分大小写的。)
@Entity(tableName = "users") public class User { // ... }
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; }
每个 Entity 必须设置至少一个 Field 作为主键(primary key)。即使只有 1 个 Field,也需要将其设置为主键。有两种方法设置主键:
@Entity public class User { @PrimaryKey(autoGenerate = true) @NonNull public String firstName; public String lastName; }
如果需要 Room 自动分配 IDs 给 Entity,可以设置@PrimaryKey的 autoGenerate 属性。
@Entity(primaryKeys = { "firstName", "lastName"}) public class User { @NonNull public String firstName; @NonNull public String lastName; }
默认情况下,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; }
在 Room 持久化库中,使用数据访问对象(data access objects, DAOs)访问 App 的数据。Dao 对象集合是 Room 的主要组件,因为每个 DAO 提供访问 App 的数据库的抽象方法。
通过使用 DAO 访问数据库,而不是通过查询构造器或直接查询,可以分离数据库架构的不同组件。此外,在测试应用时,DAOs 可以轻松模拟数据库访问。
DAO 可以是接口(interface),也可以是抽象类(abstract class)。如果是一个抽象类,可以有一个构造函数,其只接收一个 RoomDatabase 参数。在编译时,Room 为每个 DAO 创建具体实现。
注意:除非在构造器上调用 allowMainThreadQueries(),否则 Room 不支持在主线程上进行数据库访问,因为它可能会长时间锁定 UI。不过异步查询(返回 LiveData 或 Flowable 实例的查询)不受此规则约束,因为它们在需要时会在后台线程进行异步查询。
当创建 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>类型的值。
更新方法修改数据库中的一组 Entity(由参数提供)。使用每个 Entity 的主键进行匹配查询。
@Dao public interface MyDao { @Update public void updateUsers(User... users); }
更新方法可以返回一个int类型的值,其表示数据库中更新的行数,不过通常是不需要的。
删除函数移除数据库中的一组 Entity(由参数提供)。使用实体的主键进行匹配。
@Dao public interface MyDao { @Delete public void deleteUsers(User... users); }
和更新方法一样,删除方法也可以返回一个int类型的值,其表示数据库中删除的行数,通常也是不需要的。
@Query是 DAO 类中的重要注解。它允许在数据库上执行读写操作。每个@Query方法都是在编译时验证的;因此,如果存在查询问题,将出现编译错误而不是运行时错误。
在编译时,Room 还验证查询的返回值,如果返回对象中的字段名称与查询中的相应列名称不匹配,将通过以下两种方式之一告知:(在下面3.4.3 返回列的子集会提到)
下面是一个简单的查询,获取所有 User。
@Dao public interface MyDao { @Query("SELECT * FROM user") public User[] loadAllUsers(); }
在编译时,Room 知道查询 user 表中的所有列。如果这个查询存在语法错误,或者数据库中不存在 user 表,Room 将显示相应的错误。
大多数情况下,需要将参数传递到查询中以执行筛选操作,例如仅需要显示大于某一年龄的 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); }
大多数情况下,我们可能只需要获取一个 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注解。
如果希望 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); }
Room 支持返回一下 RxJava2 类型的值:
需要在 App 的build.gradle文件中添加对最新 rxjava2 版本的依赖:
dependencies { implementation 'androidx.room:room-rxjava2:2.1.0-alpha02' }
点击 这里 查看更详细的信息。
查询的返回值可以是Cursor对象。
@Dao public interface MyDao { @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5") public Cursor loadRawUsersOlderThan(int minAge); }
注意:强烈建议不要使用这种方式。
Room 允许进行多表查询。如果返回的是可观察的数据类型(例如Flowable或LivaData),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); }
在 Room 持久化库中,通过@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()] } } } }
可以通过以下方法获取创建的数据库的实例:
AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name").build();
其中database-name为你自己定义的数据库名称,比如RoomSample.db。因为其会占用较多的资源,所以一般建议使用单例模式。
在 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();
注意:为了使迁移逻辑正常执行,请使用完整查询,不要使用表示查询的常量。
在更新数据库的模式(schema)后,一些设备上的数据库可能仍然是旧的模式版本。如果 Room 无法找到将设备的数据库从旧版本升级到当前版本的迁移规则,将出现IllegalStateException。
为了防止这种情况发生时应用崩溃,在创建数据库时调用fallbackToDestructiveMigration()方法,这样 Room 将会重建应用的数据库表(将直接删除原数据库表中的所有数据)。
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
这种破坏性的恢复逻辑包括几个额外的选项:
在使用 Room 持久化库创建数据库时,验证应用的数据库和用户数据的稳定性是有必要的。
测试数据库有两种方法:
测试数据库实现的推荐方法是编写一个在 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)); } }
不推荐使用这种方法,因为您的设备或者用户设备上的 SQLite 版本可能与开发机器上的版本不匹配。
Room 提供了在基本类型和盒式类型之间转换的功能,但不允许 Entity 之间的对象引用。
有时,希望将自定义的数据类型的值存储在数据库的单个列中。为了支持自定义类型,需要提供一个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); }
重要信息: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 的性能和用户体验。
在 Android 中,如果直接通过 SQLite API 实现数据持久化,需要实现以下操作:
相比之下,Room 作为在 SQLite 之上封装的 ORM 库,具备以下优势:
[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)就是专注于实现数据持久化应用领域的某个特定系统的一个逻辑层面,将数据使用和数据实体相关联。