继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Android Room使用详解

30秒到达战场
关注TA
已关注
手记 415
粉丝 95
获赞 569

使用Room将数据保存在本地数据库


Room提供了SQLite之上的一层抽象, 既允许流畅地访问数据库, 也充分利用了SQLite.

处理大量结构化数据的应用, 能从在本地持久化数据中极大受益. 最常见的用例是缓存有关联的数据碎片. 以这种方式, 在设备不能访问网络的时候, 用户依然能够浏览离线内容. 任何用户发起的改变, 都应该在设备重新在线之后同步到服务器.

因为Room为你充分消除了这些顾虑, 使用Room而非SQLite是高度推荐的.

 

添加依赖

 

Room的依赖添加方式如下:

复制代码

 1 dependencies { 2     def room_version = "1.1.1" 3  4     implementation "android.arch.persistence.room:runtime:$room_version" 5     annotationProcessor "android.arch.persistence.room:compiler:$room_version" 6  7     // optional - RxJava support for Room 8     implementation "android.arch.persistence.room:rxjava2:$room_version" 9 10     // optional - Guava support for Room, including Optional and ListenableFuture11     implementation "android.arch.persistence.room:guava:$room_version"12 13     // Test helpers14     testImplementation "android.arch.persistence.room:testing:$room_version"15 }

复制代码

 

Room有3个主要构件:

  • Database: 包含了数据库持有者, 并对于连接应用上持久化的相关数据, 作为一个主要的访问点, 来服务. 注解了@Database的类应该满足以下条件:

  1. 继承了RoomDatabase的抽象类;

  2. 包含实体列表, 而这些实体与该注解之下数据库关联;

  3. 包含一个抽象方法, 无参且返回一个注解了@Dao的类;

在运行时, 你可以通过调用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()方法请求Database实例.

  • Entity: 表示数据库内的表.

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

这些构件, 以及它们与app余下内容的关系, 如下图:

https://img3.mukewang.com/5b3f4b620001b4ec11881084.jpg
下面的代码片断, 包含了一个数据库配置示例, 有一个实体和一个DAO:
User.java

复制代码

 1 @Entity 2 public class User { 3     @PrimaryKey 4     private int uid; 5  6     @ColumnInfo(name = "first_name") 7     private String firstName; 8  9     @ColumnInfo(name = "last_name")10     private String lastName;11 12     // Getters and setters are ignored for brevity,13     // but they're required for Room to work.14 }

复制代码

 

UserDao.java

复制代码

 1 @Dao 2 public interface UserDao { 3     @Query("SELECT * FROM user") 4     List<User> getAll(); 5  6     @Query("SELECT * FROM user WHERE uid IN (:userIds)") 7     List<User> loadAllByIds(int[] userIds); 8  9     @Query("SELECT * FROM user WHERE first_name LIKE :first AND "10            + "last_name LIKE :last LIMIT 1")11     User findByName(String first, String last);12 13     @Insert14     void insertAll(User... users);15 16     @Delete17     void delete(User user);18 }

复制代码

 

AppDatabase.java

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

 

在创建了以上文件之后, 你能够使用以下代码来创建一个database实例:

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

 

备注: 在实例化AppDatabase对象的时候, 你应该使用单例模式, 因为每一个RoomDatabase实例都是非常耗时的, 而且你也应该很少访问多个实例.

 

使用Room实体定义数据

 

在使用Room持久化库的时候, 把相关联的域的集合定义为实体. 对于每一个实体, 数据库都会创建一个表, 该表来持有数据项.

默认情况下, Room会为实体中定义的每个域创建一个列. 如果实体中有你不想持久化的域, 可以使用@Ignore来注解掉. 在Database类中, 你必须通过entities数据来引用实体类.

下面的代码片断展示了如何定义一个实体:

复制代码

 1 @Entity 2 public class User { 3     @PrimaryKey 4     public int id; 5  6     public String firstName; 7     public String lastName; 8  9     @Ignore10     Bitmap picture;11 }

复制代码


在持久化一个域, Room必须能够访问它. 你可以将域设置为public, 或者你可以提供该的getter/setter. 如果你使用了getter/setter的方式, 一定要记住: 在Room里面, 它们是基于JavaBeans转换的.

备注: 实体要么有个空的构造器(如果相应的DAO类能够访问每一个持久化域的话), 要有构造器里面的参数, 数据类型和名字跟实体里面定义的域相匹配. Room也能够使用包含全部或者部分域的构造器, 例如, 一个构造器只能获取所有域中的几个.

 

使用主键

 

每一个实体必须定义至少1个主键. 即使只有一个域, 你依然需要使用@PrimaryKey来注解它. 而且, 如果你想Room分配自动ID给实体的话, 你需要设置@PrimaryKey的autoGenerate属性. 如果实体有一个复合主键的话, 你需要使用注解@Entity的primaryKeys属性, 示例代码如下:

复制代码

1 @Entity(primaryKeys = {"firstName", "lastName"})2 public class User {3     public String firstName;4     public String lastName;5 6     @Ignore7     Bitmap picture;8 }

复制代码

 

默认情况下, Room使用实体类的名字作为数据库表的名字. 如果你想要表拥有一个不同的名字, 设置@Entity注解的tableName属性, 示例代码如下:

 1 @Entity(tableName = "users") 2 public class User { 3  ... 4 } 


注意: SQLite中表名是大小写敏感的.

跟tableName属性相似的是, Room使用域的名字作为数据库中列的名字. 如果你想要列有一个不同的名字的话, 给域添加@ColumnInfo注解, 示例代码如下:

复制代码

 1 @Entity(tableName = "users") 2 public class User { 3     @PrimaryKey 4     public int id; 5  6     @ColumnInfo(name = "first_name") 7     public String firstName; 8  9     @ColumnInfo(name = "last_name")10     public String lastName;11 12     @Ignore13     Bitmap picture;14 }

复制代码

 

 

注解索引和唯一性

 

依赖于你如何访问数据, 你也许想要在数据库中建立某些域的索引, 以加速查询速度. 要给实体添加索引, 需要在@Entity中引入indices属性, 并列出你想要在索引或者复合索引中引入的列的名字. 下列代码说明了注解的处理过程:

复制代码

 1 @Entity(indices = {@Index("name"), 2         @Index(value = {"last_name", "address"})}) 3 public class User { 4     @PrimaryKey 5     public int id; 6  7     public String firstName; 8     public String address; 9 10     @ColumnInfo(name = "last_name")11     public String lastName;12 13     @Ignore14     Bitmap picture;15 }

复制代码

有些时候, 数据库中的某些域或几组域必须是唯一的. 你可以通过将注解@Index的unique属性设置为true, 强制完成唯一的属性.
下面的代码示例防止表有两行数据在列firstName和lastName拥有相同值:

复制代码

 1 @Entity(indices = {@Index(value = {"first_name", "last_name"}, 2         unique = true)}) 3 public class User { 4     @PrimaryKey 5     public int id; 6  7     @ColumnInfo(name = "first_name") 8     public String firstName; 9 10     @ColumnInfo(name = "last_name")11     public String lastName;12 13     @Ignore14     Bitmap picture;15 }

复制代码


定义对象之间的关系

 

因为SQLite是关系型数据库, 你可以指定对象之间的关系. 尽管大多数对象关系的映射允许实体对象引用彼此, 而Room却显式地禁止了这个特性. 要想了解这个讨论背后的原因, 请查看这篇文章. //todo

尽管你不能使用直接的对象关系, Room仍然允许你在实体之间定义外键约束.

比如, 如果有一个实体类Book, 你可以使用@ForeignKey注解定义它和实体User的关系, 示例代码如下:

复制代码

 1 @Entity(foreignKeys = @ForeignKey(entity = User.class, 2                                   parentColumns = "id", 3                                   childColumns = "user_id")) 4 public class Book { 5     @PrimaryKey 6     public int bookId; 7  8     public String title; 9 10     @ColumnInfo(name = "user_id")11     public int userId;12 }

复制代码

外键非常强大, 因为它允许你指定做什么操作, 在引用实体更新的时候. 比如, 你可以告诉SQLite为用户删除所有的书, 在相应的User实例被删除时, 而该User被Book通过在@ForeignKey注解里面声明onDelete = CASCADE而关联.

 

备注: SQLite将@Insert(onConflict = REPLACE)作为REMOVE和REPLACE的集合来操作, 而非单独的UPDATE操作. 这个取代冲突值的方法能够影响你的外键约束.

 

创建嵌套对象

 

有些时候, 在数据库逻辑中, 你想将一个实体或者POJO表示为一个紧密联系的整体, 即使这个对象包含几个域. 在这些情况下, 你能够使用@Embedded注解来表示一个对象, 而你想将这个对象分解为表内的子域. 然后你可以查询这些嵌套域, 就像你查询其它的独立列一样.

举个例子, User类包含一个Address类的域, 这个域表示的是street, city, state, postCode这几个域的复合. 为了在表中单独存储复合的列, 在User类里面, 引入一个注解了@Embedded的Address域, 就像如下代码片断展示的一样:

复制代码

 1 public class Address { 2     public String street; 3     public String state; 4     public String city; 5  6     @ColumnInfo(name = "post_code") 7     public int postCode; 8 } 9 10 @Entity11 public class User {12     @PrimaryKey13     public int id;14 15     public String firstName;16 17     @Embedded18     public Address address;19 }

复制代码

这个表表示User对象包含如下几列: id, firstName, street, state, city和post_code.

 

备注: 嵌套的域同样可以包含其它的嵌套域.

 

如果实体拥有多个相同类型的嵌套域, 你可以通过设置prefix属性保留每一列唯一. 然后Room给嵌套对象的每一个列名的起始处添加prefix设置的给定值.

 

通过Room DAO访问数据

 

要通过Room持久化库访问应用的数据, 你需要使用数据访问对象(data access objects, 即DAOs). Dao对象集形成了Room的主要构成, 因为每一个DAO对象都引入了提供了抽象访问数据库的方法.

使用DAO对象而非查询构造器或者直接查询来访问数据库, 你可以分开不同的数据库架构组成. 此外, DAO允许你轻易地模拟数据库访问.

DAO要么是接口, 要么是抽象类. 如果DAO是抽象类的话, 它可以随意地拥有一个将RoomDatabase作为唯一参数的构造器. Room在运行时创建DAO的实现.

备注: Room并不支持在主线程访问数据库, 除非在Builder调用allowMainThreadQueries()方法, 因为它很可能将UI锁上较长一段时间. 但是, 异步查询--返回LiveData/Flowable实例的查询--则从此规则中免除, 因为它们在需要的时候会在后台线程异步地运行查询.

 

方便地定义方法

 

使用DAO类, 可以非常方便地表示查询.

 

插入

 

当你创建了一个DAO方法并注解了@Insert的时候, Room生成了一个实现, 在单个事务中将所有的参数插入数据库.
下面的代码片断展示了几个示例查询:

复制代码

 1 @Dao 2 public interface MyDao { 3     @Insert(onConflict = OnConflictStrategy.REPLACE) 4     public void insertUsers(User... users); 5  6     @Insert 7     public void insertBothUsers(User user1, User user2); 8  9     @Insert10     public void insertUsersAndFriends(User user, List<User> friends);11 }

复制代码

如果@Insert方法只接收了一个参数, 它可以返回一个long, 表示新插入项的rowId; 如果参数是数组或者集合, 同时地, 它应该返回long[]或者List<Long>.

 

更新

 

按照惯例, 在数据库中, Update方法修改了作为参数传递的实体集合. 它使用查询来匹配每一个实体的主键.
下面的代码片断展示了如何定义这个方法:

1 @Dao2 public interface MyDao {3     @Update4     public void updateUsers(User... users);5 }

尽管通常情况下并不需要, 但是依然可以将这个方法返回int值, 表示在数据库中被修改的行数.

 

删除

 

按照惯例, Delete方法从数据库中删除了作为参数传递的实体集合. 它使用主键找到要删除的实体.
下面的代码片断展示了如何定义这个方法:

1 @Dao2 public interface MyDao {3     @Delete4     public void deleteUsers(User... users);5 }

尽管通常情况下并不需要, 但是依然可以将这个方法返回int值, 表示从数据库中删除的行数.

 

查询

 

@Query是在DAO类中使用的主要的注解. 它允许你在数据库中执行读写操作. 每一个@Query方法都在编译时被证实, 因为, 如果查询有问题出现的话, 会出现编译错误而非运行失败.
Room也证实查询的返回值, 以确定返回对象的域的名字是否跟查询响应中对应列的名字匹配, Room使用如下两种方式提醒你:

  • 如果只有一些域匹配, 它会给予警告;

  • 如果没有域匹配, 它会给予错误;

 

简单查询

 

1 @Dao2 public interface MyDao {3     @Query("SELECT * FROM user")4     public User[] loadAllUsers();5 }

这是一个非常简单的查询, 加载了所有User. 在编译时, Room知晓这是在查询user表中所有列.

如果查询语句包含语法错误, 或者user表在数据库中并不存在, Room会在编译时展示恰当的错误信息.

 

查询语句中传参

 

大多数时候, 你需要向查询语句中传参, 以执行过滤操作, 比如, 只展示大于某个年龄的user.

要完成这个任务, 在Room注解中使用方法参数, 如下所示:

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

当这个查询在编译时处理的时候, Room匹配到 :minAge, 并将它跟方法参数minAge绑定. Room使用参数名来执行匹配操作. 如果不匹配的话, app编译时会发生错误.

你也可以在查询中传递多个参数, 或者将参数引用多次, 如下所示:

复制代码

1 @Dao2 public interface MyDao {3     @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")4     public User[] loadAllUsersBetweenAges(int minAge, int maxAge);5 6     @Query("SELECT * FROM user WHERE first_name LIKE :search "7            + "OR last_name LIKE :search")8     public List<User> findUserWithName(String search);9 }

复制代码

 

返回列的子集

 

大多数情况下, 你只需要实体中的几个域. 比如, UI中只需要展示用户的姓和名, 而非用户的每一个细节. 通过只查询UI中展示的列, 将节省宝贵的资源, 查询也更快.

Room允许从查询中返回基于Java的对象, 只要结果列集合能够映射成返回对象. 比如, 你创建了一个POJO来获取用户的名和姓:

复制代码

1 public class NameTuple {2     @ColumnInfo(name="first_name")3     public String firstName;4 5     @ColumnInfo(name="last_name")6     public String lastName;7 }

复制代码

现在, 你可以在查询方法中使用这个POJO了:

1 @Dao2 public interface MyDao {3     @Query("SELECT first_name, last_name FROM user")4     public List<NameTuple> loadFullName();5 }

Room明白: 查询返回了列first_name和last_name, 这些值能够映射到NameTuple为的域中.

由此, Room能够产生适当的代码. 如果查询返回了太多列, 或者返回了NameTuple类中并不存在的列, Room将展示警告信息.
备注: POJO也可以使用@Embedded注解.

 

传递参数集

 

一些查询可能要求你传入可变数目的参数, 直到运行时才知道精确的参数数量.

比如, 你可能想要搜索地区子集下的所有用户. Room明白参数表示集合的时机, 并在运行时自动地基于提供了参数数目展开它.

1 @Dao2 public interface MyDao {3     @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")4     public List<NameTuple> loadUsersFromRegions(List<String> regions);5 }

 

可观察查询

 

在执行查询的时候, 经常想要在数据发生改变的时候自动更新UI. 要达到这个目的, 需要在查询方法描述中返回LiveData类型的值. 在数据库更新的时候, Room生成所有必要的代码以更新LiveData.

复制代码

1 @Dao2 public interface MyDao {3     @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")4     public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);5 }

复制代码

备注: 在1.0版本的时候, Room使用查询中访问的表的列表来决定是否更新LiveData实例.

 

RxJava响应式查询

 

Room也可以从定义的查询中返回RxJava2中的Publisher和Flowable.

要使用这个功能, 在build.gradle文件中添加依赖: android.arch.persistence.room:rxjava2. 之后, 你可以返回在RxJava2中定义的数据类型, 如下所示:

1 @Dao2 public interface MyDao {3     @Query("SELECT * from user where id = :id LIMIT 1")4     public Flowable<User> loadUserById(int id);5 }


游标直接访问

 

如果你的应用逻辑要求直接访问返回的行, 你可以从查询中返回Cursor对象, 如下所示:

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

注意: 十分不推荐使用Cursor API. 因为它并不保证行是否存在以及行包含什么值.

除非你有需要Cursor的代码并且并不轻易的修改它的时候, 你才可以使用这个功能.

 

查询多表

 

有些查询可能要求访问多个表以计算结果. Room允许你写任何查询, 所以你也可以联接表. 此外, 如果响应是可观测数据类型, 诸如Flowable/LiveData, Room观察并证实查询中引用的所有表.

下面的代码片段展示了如何执行表联接, 以合并包含借书用户的表和包含在借书数据的表的信息:

复制代码

1 @Dao2 public interface MyDao {3     @Query("SELECT * FROM book "4            + "INNER JOIN loan ON loan.book_id = book.id "5            + "INNER JOIN user ON user.id = loan.user_id "6            + "WHERE user.name LIKE :userName")7    public List<Book> findBooksBorrowedByNameSync(String userName);8 }

复制代码

你也可以从这些查询中返回POJO. 比如, 你可以写查询加载用户和它的宠物名:

复制代码

 1 @Dao 2 public interface MyDao { 3    @Query("SELECT user.name AS userName, pet.name AS petName " 4           + "FROM user, pet " 5           + "WHERE user.id = pet.user_id") 6    public LiveData<List<UserPet>> loadUserAndPetNames(); 7  8  9    // You can also define this class in a separate file, as long as you add the10    // "public" access modifier.11    static class UserPet {12        public String userName;13        public String petName;14    }15 }

复制代码

 

迁移Room数据库

 

当应用中添加或者改变特性的时候, 需要修改实体类以反映出这些改变. 当用户升级到最新版本的时候, 你不想用户失去所有数据, 尤其是如果你还不能从远程服务器恢复这些数据的时候.

Room持久化库允许写Migration类来保留用户数据. 每一个Migration类指定了startVersion和endVersion. 在运行时, Room运行每一个Migration类的migrate()方法, 使用正确的顺序迁移数据库到最新版本.

注意: 如果你不提供必要的迁移, Room会重建数据库, 这意味着你会失去原有数据库中的所有数据.

复制代码

 1 Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") 2         .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build(); 3  4 static final Migration MIGRATION_1_2 = new Migration(1, 2) { 5     @Override 6     public void migrate(SupportSQLiteDatabase database) { 7         database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, " 8                 + "`name` TEXT, PRIMARY KEY(`id`))"); 9     }10 };11 12 static final Migration MIGRATION_2_3 = new Migration(2, 3) {13     @Override14     public void migrate(SupportSQLiteDatabase database) {15         database.execSQL("ALTER TABLE Book "16                 + " ADD COLUMN pub_year INTEGER");17     }18 };

复制代码

注意: 要保证迁移逻辑按照预期进行, 需要使用全查询而非引用表示查询的常量.
在迁移完成之后, Room会证实这个计划, 以确保迁移正确在发生了. 如果Room发现了问题, 它会抛出包含不匹配信息的异常.

 

迁移测试

 

写Migration并不是没有价值的, 不能恰当的写Migration会在应用中引起崩溃. 在保持应用的稳定性, 你应该事先测试Migration. Room提供了一个Maven测试工具. 但是, 如果要使这个工具工作, 你需要导出数据库schema.

 

导出schema

 

在编译的时候, Room会导出数据库schem信息, 形成一个Json文件. 要导出schema, 需要在build.gradle文件中设置room.schemaLocation注解处理器属性, 如下所示:
build.gradle:

复制代码

 1 android { 2     ... 3     defaultConfig { 4         ... 5         javaCompileOptions { 6             annotationProcessorOptions { 7                 arguments = ["room.schemaLocation": 8                              "$projectDir/schemas".toString()] 9             }10         }11     }12 }

复制代码

你应该保存导出的Json文件--这些文件表示了数据库schema的历史--在你的版本控制体系中, 因为它允许Room创建老版本数据库用于测试.

要测试这些Migration, 需要在测试需要的依赖中添加 anroid.arch.persistence.room:testing , 并在资产文件夹下添加schema地址, 如下所示:
build.gradle:

复制代码

1 android {2     ...3     sourceSets {4         androidTest.assets.srcDirs += files("$projectDir/schemas".toString())5     }6 }

复制代码

测试包提供了MigrationTestHelper类, 它能够读取这些schema文件. 它也实现了JUnit4 TestRule接口, 所有它能够管理已创建的数据库.

示例Migration测试如下:

复制代码

 1 @RunWith(AndroidJUnit4.class) 2 public class MigrationTest { 3     private static final String TEST_DB = "migration-test"; 4  5     @Rule 6     public MigrationTestHelper helper; 7  8     public MigrationTest() { 9         helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),10                 MigrationDb.class.getCanonicalName(),11                 new FrameworkSQLiteOpenHelperFactory());12     }13 14     @Test15     public void migrate1To2() throws IOException {16         SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);17 18         // db has schema version 1. insert some data using SQL queries.19         // You cannot use DAO classes because they expect the latest schema.20         db.execSQL(...);21 22         // Prepare for the next version.23         db.close();24 25         // Re-open the database with version 2 and provide26         // MIGRATION_1_2 as the migration process.27         db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);28 29         // MigrationTestHelper automatically verifies the schema changes,30         // but you need to validate that the data was migrated properly.31     }32 }

复制代码


测试数据库

 

在使用Room持久化库创建数据库的时候, 证实应用数据库和用户数据的稳定性非常重要.

有两种方式测试你的数据库:

  • 在真机上;

  • 在虚拟机上(不推荐);

备注: 在运行应用的测试的时候, Room允许你创建模拟DAO类的实例. 使用这种方式的话, 如果不是在测试数据库本身的话, 你不必创建完成的数据库. 这个功能是可能的, 因为DAO并不泄露任何数据库细节.

 

真机测试

 

测试数据库实现的推荐途径是在真机上运行JUnit测试. 因为这些测试并不创建Activity, 它们应该比UI测试执行地更快.

在设置测试的时候, 你应该创建内存版本数据库, 以确保测试更加地密封. 如下所示:

复制代码

 1 @RunWith(AndroidJUnit4.class) 2 public class SimpleEntityReadWriteTest { 3     private UserDao mUserDao; 4     private TestDatabase mDb; 5  6     @Before 7     public void createDb() { 8         Context context = InstrumentationRegistry.getTargetContext(); 9         mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();10         mUserDao = mDb.getUserDao();11     }12 13     @After14     public void closeDb() throws IOException {15         mDb.close();16     }17 18     @Test19     public void writeUserAndReadInList() throws Exception {20         User user = TestUtil.createUser(3);21         user.setName("george");22         mUserDao.insert(user);23         List<User> byName = mUserDao.findUsersByName("george");24         assertThat(byName.get(0), equalTo(user));25     }26 }

复制代码

 

虚拟机测试

 

Room使用了SQLite支持库, 后者提供了在Android Framework类里面匹配的接口. 这个支持允许你传递自定义的支持库实现来测试数据库查询.
备注: 尽管这个设置允许测试运行地很快, 但它并不是值得推荐的, 因为运行在自己以及用户真机上面的SQLite版本, 可能并不匹配你的虚拟机上面的SQLite版本.

 

使用Room引用复杂数据

 

Room提供了功能支持基数数据类型和包装类型之间的转变, 但是并不允许实体间的对象引用.

 

使用类型转换器

 

有时候, 应用需要使用自定义数据类型, 该数据类型的值将保存在数据库列中. 要添加这种自定义类型的支持, 你需要提供TypeConverter, 用来将自定义类型跟Room能够持久化的已知类型相互转换.

比如, 如果我们想要持久化Date类型, 我们需要写下面的TypeConverter来在数据库中保存等价的Unix时间戳:

复制代码

 1 public class Converters { 2     @TypeConverter 3     public static Date fromTimestamp(Long value) { 4         return value == null ? null : new Date(value); 5     } 6  7     @TypeConverter 8     public static Long dateToTimestamp(Date date) { 9         return date == null ? null : date.getTime();10     }11 }

复制代码

上述示例定义了2个方法, 一个把Date转变成Long, 一个把Long转变成Date. 因为Room已经知道如何持久化Long对象, 它将使用这个转换器持久化Date类型的值.

接下来, 添加@TypeConverters注解到AppDatabbase类上, 之后Room就能够在AppDatabase中定义的每一个实体和DAO上使用这个转换器.
AppDatabase.java

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

使用这些转换器, 你之后就能够在其它的查询中使用自定义的类型, 就像你使用基本数据类型一样, 如下所示:
User.java

1 @Entity2 public class User {3     ...4     private Date birthday;5 }

UserDao.java

1 @Dao2 public interface UserDao {3     ...4     @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")5     List findUsersBornBetweenDates(Date from, Date to);6 }

你也可以限制@TypeConverters的使用范围, 包括单个实体, DAO和DAO方法.

 

理解为什么Room不允许对象引用

 

要点: Room不允许实体类间的对象引用. 相反, 你必须显式地请求应用需要的数据.

从数据库到对应对象模型的映射关系是通用最佳实践, 在服务器端也运行良好. 即使是在程序加载它们正在访问的域的时候, 服务器依然执行良好.

然而在客户端, 这种类型的懒加载并不可行, 因为它通常发生在UI线程, 而在UI线程止查询硬盘信息产生了显著的性能问题. UI线程只有16ms计算和绘制Activity更新的布局, 所以, 即使查询花费了仅仅5ms, 看起来依然是应用绘制超时, 引起显著的视觉差错. 如果有另外的事件并行运行, 或者, 设备正在运行其它的硬盘密集型任务, 查询要完成就要花费更多的时间. 然而, 如果不使用懒加载, 应用获取超过需要的数据, 也会引起内存消耗问题.

对象关系型映射通常将这个决定留给开发者, 让他们做出应用用例最佳的选择. 开发者通常决定在应用和UI之间共享模型. 然后, 这个解决方案并不权衡地很好, 因为UI随着时间改变, 共享模型会产生对于开发者而言难以参与和debug的问题.

比如, UI加载Book对象列表, 同时每一本书有个Author对象. 最初你可能设计查询使用懒加载, 之后Book对象使用getAuthor()方法返回作者. getAuthor()方法的首次调用查询了数据库. 之后一段时间, 你发现同样需要展示作者姓名. 你轻易地添加如下这样的方法调用:

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

 

然后, 这个貌似无辜的改变引起Author表在主线程被查询.

如果你提前查询作者信息, 而在你不再需要这个数据之后, 将很难改变加载的方式. 比如, UI不再需要展示Author信息, 而应用依然高效地加载不同展示的数据, 浪费了宝贵的内存空间. 应用的效率将会降级, 如果Author类引用了其它的表, 如Books.

要使用Room同时引用多个实体, 需要创建包含每个实体的POJO类, 之后写联接了相应表的查询语句. 这个结构良好的模型, 结合了Room鲁棒的查询证实能力, 允许应用在加载资源时消耗更少的资源, 提升了应用的性能和用户体验.

原文出处

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP