您好,欢迎来到源码搜藏网!分享精神,快乐你我!
[加入VIP] 设为首页 | 收藏本站 | 网站地图 | Sitemap | TAG标签
  • 首 页
  • 在线工具
  • jquery手册
  • 当前位置:首页 > 安卓源码 > 技术博客 >

    谷歌官方Android应用架构库——Room 持久化库

    时间:2018-04-01 22:51 来源:互联网 作者:源码搜藏 浏览:收藏 挑错 推荐 打印

    Room提供了一个SQLite之上的抽象层,使得在充分利用SQLite功能的前提下顺畅的访问数据库。 对于需要处理大量结构化数据的App来说,把这些数据做本地持久化会带来很大的好处。常见的用例是缓存重要数据块。这样当设备无法连网的时候,用户仍然可以浏览内容。

    Room提供了一个SQLite之上的抽象层,使得在充分利用SQLite功能的前提下顺畅的访问数据库。

    对于需要处理大量结构化数据的App来说,把这些数据做本地持久化会带来很大的好处。常见的用例是缓存重要数据块。这样当设备无法连网的时候,用户仍然可以浏览内容。而用户对内容做出的任何改动都在网络恢复的时候同步到服务端。

    核心framework内置了对SQL的支持。虽然这些API很强大,但是都很低级,使用起来很花时间和精力:

    • 没有编译时的SQL查询检查机制。当数据表发生改变的时候,需要手动更新受影响的SQL查询。这个过程既耗时又容易出错。

    • 需要写很多公式化的代码在SQL查询与Java对象之间转换。

    Room处理了这些相关的事情,同时提供了SQLite之上的抽象层。

    Room中有三个主要的组件:

    • Database:你可以用这个组件来创建一个database holder。注解定义实体的列表,类的内容定义从数据库中获取数据的对象(DAO)。它也是底层连接的主要入口。

    这个被注解的类是一个继承RoomDatabase的抽象类。在运行时,可以通过调用Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()来得到它的实例。

    • Entity:这个组件代表一个持有数据库的一个表的类。对每一个entity,都会创建一个表来持有这些item。你必须在Database类中的entities数组中引用这些entity类。entity中的每一个field都将被持久化到数据库,除非使用了@Ignore注解。

    注:实体可以有一个空构造函数(如果DAO类可以访问每个持久化字段),或者一个构造函数的参数包含与实体中的字段匹配的类型和名称。Romm还可以使用全部或部分构造函数,例如只接收一些字段的构造函数。

    • DAO:这个组件代表一个作为Data Access Objec的类或者接口。DAO是Room的主要组件,负责定义查询(添加或者删除等)数据库的方法。使用@Database注解的类必须包含一个0参数的,返回类型为@Dao注解过的类的抽象方法。Room会在编译时生成这个类的实现。

    注:通过DAO而不是query builders或者直接的query语句来处理数据库,可以把数据库的各个部分分离开来。而且DAO还可以让你轻松的使用假的database来测试app。

    这些组件以及它们与app其余部分之间的关系如图1:

    谷歌官方Android应用架构库——Room 持久化库

    下面是一个只有一个entity和一个DAO的数据库配置的简单例子:

    User.java

    @Entity
    public class User {
        @PrimaryKey
        private int uid;
    
        @ColumnInfo(name = "first_name")
        private String firstName;
    
        @ColumnInfo(name = "last_name")
        private String lastName;
    
        // Getters and setters are ignored for brevity,
        // but they're required for Room to work.
    }

    UserDao.java

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

    AppDatabase.java

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

    创建了上面的文件之后,可以使用下面的代码来得到database的实例了:

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

    在实例化AppDatabase对象的时候应该遵循单例模式,因为每个Database实例都是相当耗费的,而且也很少需要多个实例。

    Entities

    当一个类用@Entity注解并且被@Database注解中的entities属性所引用,Room就会在数据库中为那个entity创建一张表。

    默认Room会为entity中定义的每一个field都创建一个column。如果一个entity中有你不想持久化的field,那么你可以使用@Ignore来注释它们,如下面的代码所示:

    @Entity
    class User {
        @PrimaryKey
        public int id;
    
        public String firstName;
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }

    要持久化一个field,Room必须有获取它的渠道。你可以把field写成public,也可以为它提供一个setter和getter。如果你使用setter和getter的方式,记住它们要基于Room的Java Bean规范。

    Primary key

    每个entity必须至少定义一个field作为主键(primary key)。即使只有一个field,你也必须用@PrimaryKey注释这个field。如果你想让Room为entity设置自增ID,你可以设置@PrimaryKey的autoGenerate属性。如果你的entity有一个组合主键,你可以使用@Entity注解的primaryKeys属性,具体用法如下:

    @Entity(primaryKeys = {"firstName", "lastName"})
    class User {
        public String firstName;
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }

    Room默认把类名作为数据库的表名。如果你想用其它的名称,使用@Entity注解的tableName属性,如下:

    @Entity(tableName = "users")
    class User {
        ...
    }

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

    和tableName属性类似,Room默认把field名称作为数据库表的column名。如果你想让column有不一样的名称,为field添加@ColumnInfo属性,如下:

    @Entity(tableName = "users")
    class User {
        @PrimaryKey
        public int id;
    
        @ColumnInfo(name = "first_name")
        public String firstName;
    
        @ColumnInfo(name = "last_name")
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }

    Indices 和 uniqueness

    为了提高查询的效率,你可能想为特定的字段建立索引。要为一个entity添加索引,在@Entity注解中添加indices属性,列出你想放在索引或者组合索引中的字段。下面的代码片段演示了这个注解的过程:

    @Entity(indices = {@Index("name"), @Index("last_name", "address")})
    class User {
        @PrimaryKey
        public int id;
    
        public String firstName;
        public String address;
    
        @ColumnInfo(name = "last_name")
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }

    有时候,某个字段或者几个字段必须是唯一的。你可以通过把@Index注解的unique属性设置为true来实现唯一性。下面的代码防止了一个表中的两行数据出现firstName和lastName字段的值相同的情况:

    @Entity(indices = {@Index(value = {"first_name", "last_name"},
            unique = true)})
    class User {
        @PrimaryKey
        public int id;
    
        @ColumnInfo(name = "first_name")
        public String firstName;
    
        @ColumnInfo(name = "last_name")
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }

    关系

    因为SQLite是关系数据库,你可以指定对象之间的关联。虽然大多数ORM库允许entity对象相互引用,但是Room明确禁止了这种行为。详细情况见:Addendum: No object references between entities。

    虽然不可以使用直接的关联,Room仍然允许你定义entity之间的外键(Foreign Key)约束。

    比如,假设有另外一个entity叫做calledBook,你可以使用@ForeignKey注解定义它和User entity之间的关联,如下:

    @Entity(foreignKeys = @ForeignKey(entity = User.class,
                                      parentColumns = "id",
                                      childColumns = "user_id"))
    class Book {
        @PrimaryKey
        public int bookId;
    
        public String title;
    
        @ColumnInfo(name = "user_id")
        public int userId;
    }

    外键非常强大,因为它允许你指定当被关联的entity更新时做什么操作。例如,通过在@ForeignKey注解中包含Delete = CASCADE, 你可以告诉SQLite,如果相应的User实例被删除,那么删除这个User下的所有book。

    嵌套对象

    有时你可能想把一个entity或者一个POJOs作为一个整体看待,即使这个对象包含几个field。这种情况下,你可以使用@Embedded注解,表示你想把一个对象分解为表的子字段。然后你就可以像其它独立字段那样查询这些嵌入的字段。

    比如,我们的User类可以包含一个类型为Address的field,Address代表street,city,state, 和postCode字段的组合。为了让这些组合的字段单独存放在这个表中,对User类中的Address字段使用@Embedded注解,如下面的代码所示:

    class Address {
        public String street;
        public String state;
        public String city;
    
        @ColumnInfo(name = "post_code")
        public int postCode;
    }
    
    @Entity
    class User {
        @PrimaryKey
        public int id;
    
        public String firstName;
    
        @Embedded
        public Address address;
    }

    那么现在代表一个User对象的表就有了如下的字段:id,firstName,street,state,city,以及post_code。

    注:嵌套字段也可以包含其它的嵌套字段。

    如果一个entity有多个嵌套字段是相同类型,你可以设置prefix属性保持每个字段的唯一性。Room就会在嵌套对象中的每个字段名的前面添加上这个值。

    Data Access Objects (DAOs)

    Room中的主要组件是Dao类。DAO抽象出了一种操作数据库的简便方法。

    注:Room不允许通过主线程上访问数据库,除非您在构建器上调用allowMainThreadQueries(),因为它可能会长时间地锁定用户界面。异步查询(返回LiveData或RxJava Flowable的查询)将免除此规则,因为它们在需要时异步地在后台线程上运行查询。

    便利的方法

    DAO提供了多种简便的查询方式,本文档列出几种常见的例子。

    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。

    update

    Update是一个更新一系列entity的简便方法。它根据每个entity的主键作为更新的依据。下面的代码演示了如何定义这个方法:

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

    你可以让这个方法返回一个int类型的值,表示更新影响的行数,虽然通常并没有这个必要。

    delete

    这个API用于删除一系列entity。它使用主键找到要删除的entity。下面的代码演示了如何定义这个方法:

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

    你可以让这个方法返回一个int类型的值,表示从数据库中被删除的行数,虽然通常并没有这个必要。

    使用@Query的方法

    @Query是DAO类中主要被使用的注解。它允许你在数据库中执行读写操作。每个@Query方法都是在编译时检查,因此如果查询存在问题,将出现编译错误,而不是在运行时引起崩溃。

    Room还会检查查询的返回值,如果返回的对象的字段名和查询结果的相应字段名不匹配,Room将以下面两种方式提醒你:

    • 如果某些字段名不匹配给出警告。
    • 如果没有匹配的字段名给出错误提示。

    简单的查询

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

    这是一个非常简单的查询,加载所有的user。在编译时,Room知道它是查询user表中的所有字段。如果query有语法错误,或者user表不存在,Room将在app编译时显示恰当的错误信息。

    向query传递参数

    大多数时候,你需要向查询传递参数来执行过滤操作,比如只显示大于某个年龄的user。为此,在Room注解中使用方法参数,如下:

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

    当编译时处理到这个查询的时候,Room把:minAge用方法中的minAge匹配。Room使用参数的名称来匹配。如果有不匹配的情况,app编译的时候就会出现错误。

    你也可以传递多个参数或者在查询中多次引用它们,如下面的代码所示:

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

    返回字段的子集

    大多数时候,我们只需要一个entity的部分字段。比如,你的界面也许只需显示user的first name 和 last name,而不是用户的每个详细信息。只获取UI需要的字段可以节省可观的资源,查询也更快。

    只要结果的字段可以和返回的对象匹配,Room允许返回任何的Java对象。比如,你可以创建如下的POJO获取user的first name 和 last name:

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

    现在你可以在query方法中使用这个POJO:

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

    Room知道这个查询返回first_name和last_name字段的值,并且这些值可以被映射到NameTuple类的field中。因此Room可以生成正确的代码。如果查询返回了太多的字段,或者某个字段不在NameTuple类中,Room将显示一个警告。

    注:这些POJO也可以使用@Embedded注解。

    传入参数集合

    一些查询可能需要你传入个数是一个变量的参数,只有在运行时才知道具体的参数个数。比如,你可能想获取一个区间的用户信息。当一个参数代表一个集合的时候Room是知道的,它在运行时自动根据提供的参数个数扩展。

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

    可观察的查询

    当执行查询的时候,你通常希望app的UI能自动在数据更新的时候更新。为此,在query方法中使用LiveData类型的返回值。当数据库变化的时候,Room会生成所有的必要代码来更新LiveData。

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

    注:对于version 1.0,Room使用query中的table列表来决定是否更新LiveData对象。

    RxJava

    Room还可以让你定义的查询返回RxJava2的Publisher和Flowable对象。要使用这个功能,在Gradle dependencies中添加android.arch.persistence.room:rxjava2。然后你就可以返回RxJava2中定义的对象类型了,如下面的代码所示:

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

    Direct cursor access

    如果你的app需要获得直接返回的行,你可以在查询中返回Cursor对象,如下面的代码所示:

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

    注:不推荐使用Cursor API,因为它无法保证行是否存在或者行中有哪些值。只有在当前的代码需要一个cursor,而且你又不好重构的时候才使用这个功能。

    多表查询

    某些查询可能需要根据多个表查询出结果。Room允许你书写任何查询,因此表连接(join)也是可以的。而且如果响应是一个可观察的数据类型,比如Flowable或者LiveData,Room将观察查询中涉及到的所有表。

    下面的代码演示了如何执行一个表连接查询来查出借阅图书的user与被借出图书之间的信息。

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

    你也可以返回POJO对象。比如你可以写一个如下的查询加载user与它们的宠物名字:

    @Dao
    public interface MyDao {
       @Query("SELECT user.name AS userName, pet.name AS petName "
              + "FROM user, pet "
              + "WHERE user.id = pet.user_id")
       public LiveData<List<UserPet>> loadUserAndPetNames();
    
       // You can also define this class in a separate file, as long as you add the
       // "public" access modifier.
       static class UserPet {
           public String userName;
           public String petName;
       }
    }

    使用类型转换器

    Room内置了原始类型。但是,有时你会希望使用自定义数据类型。 要为自定义类型添加这种支持,可以提供一个TypeConverter,它将一个自定义类转换为Room保留的已知类型。

    比如,如果我们要保留Date的实例,我们可以编写以下TypeConverter来存储数据库中的等效的Unix时间戳记:

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

    上述示例定义了两个函数,一个将Date对象转换为Long对象,另一个将从Long到Date转换为执行逆转换。 由于Room已经知道如何持久化Long对象,因此可以使用此转换器来持久保存Date类型的值。

    接下来,将@TypeConverters注释添加到AppDatabase类,以便Room可以使用你为该AppDatabase中的每个实体和DAO定义的转换器:

    AppDatabase.java

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

    使用这些转换器,可以在其他查询中使用自定义类型,就像使用原始类型一样,如以下代码片段所示:

    User.java

    @Entity
    public class User {
        ...
        private Date birthday;
    }

    UserDao.java

    @Dao
    public interface UserDao {
        ...
        @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
        List<User> findUsersBornBetweenDates(Date from, Date to);
    }

    数据库迁移

    随着app功能的添加和修改,你需要修改entity类来反应这些变化。当一个用户更新了app的最新版本之后,你并不希望它们丢失所有的现有数据,尤其是当你无法通过远程服务器恢复这些数据的时候。

    Room让你可以让你写Migration类来保存用户数据。每个Migration类指定from和to版本。运行时Room运行每个Migration类的 migrate() 方法,使用正确的顺序把数据库迁移到新版本。

    注意:如果你没有提供必要的migration,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("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");
        }
    };

    注意:为了让迁移的逻辑是可预知的,请使用完整的查询而不是用引用代表查询的constant。

    当迁移过程完成之后,Room会检查schema以确保迁移正确进行。如果Room发现了问题,会抛出异常。

    测试迁移

    写迁移不是一件简单的事情,如果写法不恰当可能导致app的进入崩溃的恶性循环。为了保证app的稳定性,你应该先测试迁移。Room提供了一个testing Maven artifact来帮助你完成这个测试过程。但是要让这个artifact工作,需要导出数据库的schema。

    导出 schemas

    在编译的时候,Room将database的schema信息导出到一个JSON文件中。为此,要在build.gradle 文件中设置room.schemaLocation注解处理器属性,如下面的代码所示:

    build.gradle

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

    你应该把导出的在这个JSON文件-它代表了你的数据库的schema历史-保存到你的版本管理系统中,这样就可以让Room创建旧版本的数据库来测试。

    测试迁移需要把Room的Maven artifact android.arch.persistence.room:testing 添加到你的test dependencies,并且把schema的位置作为 asset folder添加进去,代码如下:

    build.gradle

    android {
        ...
        sourceSets {
            androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
        }
    }

    testing package提供了一个MigrationTestHelper类,它可以读出这些schema文件。它同时也是一个 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.getContext(),
                    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.
        }
    }
    谷歌官方Android应用架构库——Room 持久化库转载http://www.codesocang.com/appboke/38513.html
    标签:网站源码