最新公告
  • 欢迎您光临AA分享网,一个高级程序员的学习、分享的分享平台!立即加入我们
  • Room Persistence Library(官网文档翻译)

    Room是谷歌官方的数据库ORM框架,使用起来非常方便。以下是官网文档的译文:

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

    Room 

    对于需要处理大量结构化数据的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都将被持久化到数据库,[email protected]

    DAO:这个组件代表一个作为Data Access Objec的类或者接口。DAO是Room的主要组件,负责定义查询(添加或者删除等)[email protected]0参数的,[email protected]译时生成这个类的实现。

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

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

    room_architecture.png

    下面是一个只有一个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

    [email protected]@Database注解中的entities属性所引用,Room就会在数据库中为那个entity创建一张表。

    默认Room会为entity中定义的每一个field都创建一个column。如果一个entity中有你不想持久化的field,[email protected],如下面的代码所示:

    @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,[email protected]为entity设置自增ID,[email protected]

    @Entity(tableName = "user")
    public class User {
        @PrimaryKey(autoGenerate = true)
         private  Integer id;
         ...
    }

    如果你的entity有一个组合主键,[email protected],具体用法如下:

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

    Room默认把类名作为数据库的表名。如果你想用其它的名称,[email protected],如下:

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

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

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

    @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 and uniqueness

    为了提高查询的效率,你可能想为特定的字段建立索引。要为一个entity添加索引,[email protected][email protected]代码片段演示了这个注解的过程:

    @Entity(indices = [email protected]("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;
    }

    有时候,某个字段或者几个字段必须是唯一的。你可以通过[email protected][email protected]lastName字段的值相同的情况:

    @Entity(indices = [email protected](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,[email protected] 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更新时做什么操作。例如,[email protected] = CASCADE, 你可以告诉SQLite,如果相应的User实例被删除,那么删除这个User下的所有book。

    Note: SQLite handles @Insert(onConflict=REPLACE) as a set of REMOVE and REPLACE operations instead of a single UPDATE operation. This method of replacing conflicting values could affect your foreign key constraints. For more details, see the SQLite documentation for theON_CONFLICT clause.

    嵌套对象

    有时你可能想把一个entity或者一个POJOs作为一个整体看待,即使这个对象包含几个field。这种情况下,[email protected][email protected]可以像其它独立字段那样查询这些嵌入的字段。

    译注:可能不是很好理解,通俗点的说法就是,让带有多个成员的类的每个变量都作为表中的字段。

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

    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抽象出了一种操作数据库的简便方法。

    便利的方法

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

    Insert

    [email protected],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);
    }

    [email protected],它可以返回一个long,代表新插入元素的rowId,如果参数是一个数组或者集合,那么应该返回long[]或者

    List<long>。

    [email protected],以及SQLite documentation for rowid tables。

    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类型的值,表示从数据库中被删除的行数,虽然通常并没有这个必要。

    [email protected]

    @[email protected][email protected]查,因此如果查询存在问题,将出现编译错误,而不是在运行时引起崩溃。

    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将显示一个警告。

    注:[email protected]

    传入参数集合

    一些查询可能需要你传入个数是一个变量的参数,只有在运行时才知道具体的参数个数。比如,你可能想获取一个区间的用户信息。当一个参数代表一个集合的时候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);
    }

    注:添加android.arch.persistence.room:rxjava2的具体做法是在模块的dependencies中添加:

    compile 'android.arch.persistence.room:rxjava2:1.0.0-alpha1'

    其实就是要加个版本号而已。

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

    使用类型转换器

    类型转换器是一款高质量的ORM必不可少的部分之一。

    Room对Java基本数据类型以及其装箱类型都提供了支持,但是有时候你可能使用了一个自定义的数据类型,并且你想将此类型的数据存储到数据库表中的字段里。

    为了实现这个功能,你需要提供一个TypeConverter,把自定义的数据类型,转换成Room能够持久化的数据类型。

    For example, if we want to persist instances of Date, we can write the following TypeConverter to store the equivalent Unix timestamp in the database:

    比如,如果你想持久化一个Date的实例,我们可以编写如下的 TypeConverter 来存储与之等价的 Unix timestamp。

    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就可以把你定义的这个converter用到entity和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);
    }

    [email protected],包括单个实体,DAO和DAO方法。有关更多详细信息,[email protected] 注释的参考文档。

    数据库迁移

    随着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提供了一个Maven测试插件来帮助你完成这个测试过程。但是要让此插件正常工作,需要导出数据库的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.
        }
    }

    原文:https://developer.android.com/topic/libraries/architecture/room.html 

    AA分享网一个高级程序员的学习、分享的IT资源分享平台
    AA分享网-企业网站源码-PHP源码-网站模板-视频教程-IT技术教程 » Room Persistence Library(官网文档翻译)
    • 262会员总数(位)
    • 5946资源总数(个)
    • 4本周发布(个)
    • 0 今日发布(个)
    • 570稳定运行(天)

    提供最优质的资源集合

    立即查看 了解详情