Android架构组件—Room

概述

android系统中的数据库SQLite使用起来并不方便,早期学习的时候一直很讨厌使用,后来出现了GreenDao、OrmLite、Realm极大的方便了android开发中的数据持久化。去年google推出了架构组件,其中room就是一款orm框架。

添加Room依赖库

详细查看room配置

1.添加google的maven库,在project的gradle文件:

allprojects {
    repositories {
        jcenter()
        google() // 添加谷歌maven库
    }
}

2.添加架构组件依赖库,在module的gradle文件:

dependencies {
    // Room (use 1.1.0-alpha2 for latest alpha)
    implementation "android.arch.persistence.room:runtime:1.0.0"
    annotationProcessor "android.arch.persistence.room:compiler:1.0.0"

    // Test helpers for Room
    testImplementation "android.arch.persistence.room:testing:1.0.0"
}

1.以上为gradle插件3.0
2.如果是kotlin项目,确保用kapt代替annotationProcessor,同时也要添加kotlin-kapt插件

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'

3.为room添加rxjava支持库

dependencies {
    // RxJava support for Room (use 1.1.0-alpha1 for latest alpha)
    implementation "android.arch.persistence.room:rxjava2:1.0.0"
}

4.Room @Dao查询中添加对Guava的Optional和ListenableFuture类型的支持。

dependencies {
    // Guava support for Room
    implementation "android.arch.persistence.room:guava:1.1.0-alpha2"
}

5.和LiveData一起使用

 // ReactiveStreams support for LiveData
 implementation "android.arch.lifecycle:reactivestreams:1.0.0"

Room三大组件

Room中有三个主要组件。

  • Database: 用这个组件创建一个数据库。注解定义了一系列entities,并且类中提供一系列Dao的抽象方法,也是下层主要连接的访问点。注解的类应该是一个继承 RoomDatabase的抽象类。在运行时,你能通过调用Room.databaseBuilder()或者 Room.inMemoryDatabaseBuilder()获得一个实例
  • Entity: 用这个组件创建表,Database类中的entities数组通过引用这些entity类创建数据库表。每个entity中的字段都会被持久化到数据库中,除非用@Ignore注解
  • DAO: 这个组件代表了一个用来操作表增删改查的dao。Dao 是Room中的主要组件,负责定义访问数据库的方法。被注解@Database的类必须包含一个没有参数的且返回注解为@Dao的类的抽象方法。在编译时,Room创建一个这个类的实现。

    Entity类能够有一个空的构造函数(如果dao类能够访问每个持久化的字段)或者一个参数带有匹配entity中的字段的类型和名称的构造函数

    如下代码片段包含一个简单的三大组件使用例子:
    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 getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List 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();
}

通过以上文件,可以使用如下代码创建一个数据库实例:

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

生成数据库实例的具体操作:

Room.databaseBuilder(getApplicationContext(),
                        RoomDemoDatabase.class, "database_name")
                        .addCallback(new RoomDatabase.Callback() {
                            //第一次创建数据库时调用,但是在创建所有表之后调用的
                            @Override
                            public void onCreate(@NonNull SupportSQLiteDatabase db) {
                                super.onCreate(db);
                            }

                            //当数据库被打开时调用
                            @Override
                            public void onOpen(@NonNull SupportSQLiteDatabase db) {
                                super.onOpen(db);
                            }
                        })
                        .allowMainThreadQueries()//允许在主线程查询数据
                        .addMigrations()//迁移数据库使用
                        .fallbackToDestructiveMigration()//迁移数据库如果发生错误,将会重新创建数据库,而不是发生崩溃
                        .build();

注意:初始化AppDatabase对象时必须遵守单例模式。因为每个RoomDatabase实例是相当昂贵的,并且几乎不需要访问多个实例。

Entity相关

当一个类被注解为@Entity并且引用到带有@Database 注解的entities属性,Room为这个数据库引用的entity创建一个数据表。
默认情况下,Room为每个定义在entity中的字段创建一个列。如果一个entity的一些字段不想持久化,可以使用@Ignore注解它们,像如下展示的代码片段:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

Entity的字段必须为public或提供setter或者getter。

Primary Key(主键)

每个entity必须定义至少一个字段作为主键。即使这里只有一个字段,仍然需要使用@PrimaryKey注解这个字段。并且,如果想Room动态给entity分配自增主键,可以设置@PrimaryKey的autoGenerate属性为true。如果entity有个组合的主键,你可以使用@Entity注解的primaryKeys属性,正如如下片段展示的那样:

@Entity
class User {
    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
    // 自增主键
    @PrimaryKey(autoGenerate = true)
    public int id;
}

// 组合主键
@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使用字段名称作为列名称。如果你希望一个列有不同的名称,为字段增加@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 and uniqueness(索引和唯一性)

public @interface Index {
  //定义需要添加索引的字段
  String[] value();
  //定义索引的名称
  String name() default "";
  //true-设置唯一键,标识value数组中的索引字段必须是唯一的,不可重复
  boolean unique() default false;
}

数据库索引可以加速数据库查询,@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;
}

有时,表中的某个字段或字段组合需要确保唯一性,可以设置@Entity的@Index注解的unique属性为true。如下代码:

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

Relationships

SQLite是个关系型数据库,能够指明两个对象的关系。大多数ORM库支持entity对象引用其他的。Room明确的禁止这样。更多细节请参考Understand why Room doesn’t allow object references

public @interface ForeignKey {
  //引用外键的表的实体
  Class entity();
  //要引用的外键列
  String[] parentColumns();
  //要关联的列
  String[] childColumns();
  //当父类实体(关联的外键表)从数据库中删除时执行的操作
  @Action int onDelete() default NO_ACTION;
  //当父类实体(关联的外键表)更新时执行的操作
  @Action int onUpdate() default NO_ACTION;
  //在事务完成之前,是否应该推迟外键约束
  boolean deferred() default false;
  //给onDelete,onUpdate定义的操作
  int NO_ACTION = 1;
  int RESTRICT = 2;
  int SET_NULL = 3;
  int SET_DEFAULT = 4;
  int CASCADE = 5;
  @IntDef({NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE})
  @interface Action {
    }
}

Room允许定义外键约束在两个entities。
例如:如果有一个entity叫book,你可以定义它和user的关系通过使用 @ForeignKey
注解,如下所示:

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

1.外键是十分强大的,因为它们允许你指明当引用的entity被更新后做什么。例如,如果相应的user实例被删除了,你可以通过包含@ForeignKey注解的onDelete=CASCADE属性让SQLite为这个user删除所有的书籍。
2.SQLite处理@Insert(OnConflict=REPLACE) 作为一个REMOVE和REPLACE操作而不是单独的UPDATE操作。这个替换冲突值的方法能够影响你的外键约束。

Nested objects
有时,希望entity中包含一个具有多个字段的对象作为字段。在这种情况下,可以使用@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。

注意:嵌入式字段还可以包含其他嵌入式字段

如果一个实体具有相同类型的多个内嵌字段,则可以通过设置前缀属性(prefix)使每个列保持惟一。然后将所提供的值添加到嵌入对象中每个列名的开头

 @Embedded(prefix = "foo_")
 Coordinates coordinates;

Data Access Objects (DAOs)相关

Room的三大组件之一Dao(interface),以一种干净的方式去访问数据库。

注意: Room不允许在主线程中访问数据库。除非在建造器中调用allowMainThreadQueries(),可能会造成长时间的锁住UI。异步查询(返回LiveData或者RxJava流的查询)是从这个规则中豁免,因为它们异步的在后台线程中进行查询。

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

如果@Insert方法接收只有一个参数,它可以返回一个插入item的新rowId 的long值,如果参数是一个集合的数组,它应该返回long[]或者List

Update
@Update 是更新一系列entities集合、给定参数的惯例方法。它使用query来匹配每个entity的主键。如下代码说明如何定义这个方法:

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

尽管通常不是必须的,你能够拥有这个方法返回int值指示数据库中更新的数量。

@Insert, @Update都可以执行事务操作,定义在OnConflictStrategy注解类中

public @interface OnConflictStrategy {
    //策略冲突就替换旧数据
    int REPLACE = 1;
    //策略冲突就回滚事务
    int ROLLBACK = 2;
    //策略冲突就退出事务
    int ABORT = 3;
    //策略冲突就使事务失败 
    int FAIL = 4;
    //忽略冲突
    int IGNORE = 5;
}

Delete
@Delete是一个从数据库中删除一系列给定参数的entities的惯例方法。它使用主键找到要删除的entities。如下所示:

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

尽管通常不是必须的,你能够拥有这个方法返回int值指示数据库中删除的数量。

Query
@Query 是DAO类中使用的主要注解,它允许你执行读/写操作在数据库中。每个@Query方法在编译时被校验,所以如果查询出了问题,将在编译时出现而不是运行时。

  • 如果仅有一些字段匹配会警告
  • 如果没有字段匹配会报错

查询示例:

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

在编译时,Room知道这是查询user表中的所有列。如果查询包含语法错误,或者如果用户表不存在,Room在app编译时会报出合适的错误消息。

往查询中传入参数:

大多数时间,你需要传入参数到查询中去过滤操作,例如只展示比一个特定年龄大的用户,为了完成这个任务,在你的Room注解中使用方法参数,如下所示:

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

当这个查询在编译器被处理,Room匹配: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 findUserWithName(String search);
}

Returning subsets of columns(返回列中的子集)
多数时候,我们仅需要获取一个entity中的部分字段。例如,你的UI可能只展示user第一个和最后一个名称,而不是所有关于用户的细节。通过获取展示在UI的有效数据列可以使查询完成的更快。
只要查询中列结果集能够被映射到返回的对象中,Room允许你返回任何java对象。例如:
创建如下POJO通过拿取用户的姓和名。

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

Room理解查询返回first_name和last_name的列值被映射到NameTuple类中。因此,Room能够生成合适的代码。如果查询返回太多columns,或者一个列不存在,Room将会报警。

Passing a collection of arguments
部分查询可能需要传入可变数量的参数,确切数量的参数直到运行时才知道。例如,想提取来自某个地区所有用户的信息。如下:

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

Observable queries
使用返回值类型为LiveData实现数据库更新时ui数据自动更新。

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

RxJava
Room也能返回RxJava2 Publisher和Flowable对象,需添加android.arch.persistence.room:rxjava2 依赖。使用方法如下所示:

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

Direct cursor access(直接游标访问)
如果你的应用逻辑直接访问返回的行,你可以返回一个Cursor对象从你的查询当中,如下所示:

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

注意:非常不建议使用Cursor API 因为它不能保证行是否存在或者行包含什么值。使用这个功能仅仅是因为你已经有期望返回一个cursor的代码并且你不能轻易的重构。

Querying multiple tables(联表查询)
一些查询可能访问多个表去查询结果。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 findBooksBorrowedByNameSync(String userName);
}

从这些查询当中也能返回POJOs,例如,可以写一个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> 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;
   }
}

Using type converters (使用类型转换)
Room为原始类型和可选的装箱类型提供嵌入支持。然而,有时你可能使用一个单独存入数据库的自定义数据类型。为了添加这种类型的支持,你可以提供一个把自定义类转化为一个Room能够持久化的已知类型的TypeConverter。

例如:如果我们想持久化日期的实例,我们可以写如下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类
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 findUsersBornBetweenDates(Date from, Date to);
}

还可以将@TypeConverter限制在不同的范围内,包含单独的entity,Dao和Dao中的 methods,具体查看官方文档。

Database migration(数据库迁移)

当添加或修改app后需要升级版本,中间可能修改了entity类。Room允许使用Migration类保留用户数据。每个Migration类在运行时指明一个开始版本和一个结束版本,Room执行每个Migration类的migrate()方法,使用正确的顺序去迁移数据库到一个最近版本。

注意:如果不提供必需的migrations类,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");
    }
};

输出模式
在编译时,将数据库的模式信息导出到JSON文件中,这样可有利于我们更好的调试和排错(DataBase的exportSchema = true)

module中的build.gradle

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

数据库迁移以及修改需要经过测试才能放心更新,具体测试方法请参考官方文档

你可能感兴趣的:(框架,Android)