探索Android架构组件Room

 

1、简介

Room是Google推出的Android架构组件库中的数据持久化组件库, 也能够说是在SQLite上实现的一套ORM解决方案。Room主要包含三个部分:html

  • Database : 持有DB和DAO
  • Entity : 定义POJO类,即数据表结构
  • DAO(Data Access Objects) : 定义访问数据(增删改查)的接口

其关系以下图所示:java

Room Architecture Diagramandroid

2、基本使用

1. 建立Entity

1.1 一个简单的Entitiy

一个简单Entity定义以下:git

@Entity(tableName = "user" 
          indices = {@Index(value = {"first_name", "last_name"})})
public class User {

    @PrimaryKey
    private int uid;

    @ColumnInfo(name = "first_name")
    private String firstName;

    @ColumnInfo(name = "last_name")
    private String lastName;

    @Ignore
    public User(String firstName, String lastName) {
        this.uid = UUID.randomUUID().toString();
        this.firstName = firstName;
        this. lastName = lastName;
    }

    public User(String id, String firstName, String lastName) {
        this.uid = id;
        this.firstName = userName;
        this. lastName = userName;
    }
    
    // Getters and setters
}
  • @Entity(tableName = "table_name**") 注解POJO类,定义数据表名称;
  • @PrimaryKey 定义主键,若是一个Entity使用的是复合主键,能够经过@Entity注解的primaryKeys 属性定义复合主键:@Entity(primaryKeys = {"firstName", "lastName"})
  • @ColumnInfo(name = “column_name”) 定义数据表中的字段名
  • @Ignore 用于告诉Room须要忽略的字段或方法
  • 创建索引:在@Entity注解的indices属性中添加索引字段。例如:indices = {@Index(value = {"first_name", "last_name"}, unique = true), ...}, unique = true能够确保表中不会出现{"first_name", "last_name"} 相同的数据。

1.2 Entitiy间的关系

不一样于目前存在的大多数ORM库,Room不支持Entitiy对象间的直接引用。(具体缘由能够参考: Understand why Room doesn't allow object references
但Room容许经过外键(Foreign Key)来表示Entity之间的关系。github

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

如上面代码所示,Book对象与User对象是属于的关系。Book中的user_id,对应User中的id。 那么当一个User对象被删除时, 对应的Book会发生什么呢?sql

@ForeignKey注解中有两个属性onDeleteonUpdate, 这两个属性对应ForeignKey中的onDelete()onUpdate(), 经过这两个属性的值来设置当User对象被删除/更新时,Book对象做出的响应。这两个属性的可选值以下:数据库

  • CASCADE:User删除时对应Book一同删除; 更新时,关联的字段一同更新
  • NO_ACTION:User删除时不作任何响应
  • RESTRICT:禁止User的删除/更新。当User删除或更新时,Sqlite会立马报错。
  • SET_NULL:当User删除时, Book中的userId会设为NULL
  • SET_DEFAULT:与SET_NULL相似,当User删除时,Book中的userId会设为默认值

1.3 对象嵌套

在某些状况下, 对于一张表中的数据咱们会用多个POJO类来表示,在这种状况下能够用@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表中,Column 为id, firstName, street, state, city, post_code安全

2. 建立数据访问对象(DAO)

@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(List<User> users);
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);

    @Delete
    void delete(User user);
    
    @Update
    public void updateUsers(List<User> users);
}

DAO 能够是一个接口,也能够是一个抽象类, Room会在编译时建立DAO的实现。架构

Tips:

  • @Insert方法也能够定义返回值, 当传入参数仅有一个时返回long, 传入多个时返回long[]List<Long>, Room在实现insert方法的实现时会在一个事务进行全部参数的插入。
  • @Insert的参数存在冲突时, 能够设置onConflict属性的值来定义冲突的解决策略, 好比代码中定义的是@Insert(onConflict = OnConflictStrategy.REPLACE), 即发生冲突时替换原有数据
  • @Update@Delete 能够定义int类型返回值,指更新/删除的函数

DAO中的增删改方法的定义都比较简单,这里不展开讨论,下面更多的聊一下查询方法。

2.1 简单的查询

Talk is cheap, 直接show code:

@Query("SELECT * FROM user")
List<User> getAll();

Room会在编译时校验sql语句,若是@Query() 中的sql语句存在语法错误,或者查询的表不存在,Room会在编译时报错。

2.2 查询参数传递

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

看代码应该比较好理解, 方法中传递参数arg, 在sql语句中用:arg便可。编译时Room会匹配对应的参数。

若是在传参中没有匹配到:arg对应的参数, Room会在编译时报错。

2.3 查询表中部分字段的信息

在实际某个业务场景中, 咱们可能仅关心一个表部分字段的值,这时我仅须要查询关心的列便可。

定义子集的POJO类:

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

    @ColumnInfo(name="last_name")
    public String lastName;
}

在DAO中添加查询方法:

@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();

这里定义的POJO也支持使用@Embedded

2.3 查询结果的返回类型

Room中查询操做除了返回POJO对象及其List之外, 还支持:

  • LiveData<T>:
    LiveData是架构组件库中提供的另外一个组件,能够很好知足数据变化驱动UI刷新的需求。Room会实现更新LiveData的代码。
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)") 
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
  • Flowablbe<T> Maybe<T> Single<T>:
    Room 支持返回RxJava2 的Flowablbe, MaybeSingle对象,对于使用RxJava的项目能够很好的衔接, 但须要在gradle添加该依赖:android.arch.persistence.room:rxjava2
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
  • Cursor:
    返回Cursor是为了支持现有项目中使用Cursor的场景,官方不建议直接返回Cursor.

Caution: It's highly discouraged to work with the Cursor API because it doesn't guarantee whether the rows exist or what values the rows contain. Use this functionality only if you already have code that expects a cursor and that you can't refactor easily.

2.4 联表查询

Room支持联表查询,接口定义上与其余查询差异不大, 主要仍是sql语句的差异。

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

3. 建立数据库

Room中DataBase相似SQLite API中SQLiteOpenHelper,是提供DB操做的切入点,可是除了持有DB外, 它还负责持有相关数据表(Entity)的数据访问对象(DAO), 因此Room中定义Database须要知足三个条件:

  • 继承RoomDataBase,而且是一个抽象类
  • 用@Database 注解,并定义相关的entity对象, 固然还有必不可少的数据库版本信息
  • 定义返回DAO对象的抽象方法
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

建立好以上Room的三大组件后, 在代码中就能够经过如下代码建立Database实例。

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

3、数据库迁移

3.1 Room数据库升级

在传统的SQLite API中,咱们若是要升级数据库, 一般在SQLiteOpenHelper.onUpgrade方法执行数据库升级的sql语句,这些sql语句的一般根据数据库版本以文件的方式或者用数组来管理。有人说这种方式升级数据库就像在拆炸弹,相比之下在Room中升级数据库简单的就像是按一个开关而已。

Room提供了Migration类来实现数据库的升级:

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

在建立Migration类时须要指定startVersionendVersion, 代码中MIGRATION_1_2MIGRATION_2_3的startVersion和endVersion是递增的, Migration实际上是支持从版本1直接升到版本3,只要其migrate()方法里执行的语句正常便可。那么Room是怎么实现数据库升级的呢?其实本质上仍是调用SQLiteOpenHelper.onUpgrade,Room中本身实现了一个SQLiteOpenHelper, 在onUpgrade()方法被调用时触发Migration,当第一次访问数据库时,Room作了如下几件事:

  • 建立Room Database实例
  • SQLiteOpenHelper.onUpgrade被调用,而且触发Migration
  • 打开数据库

这样一看, Room中处理数据库升级确实很像是加一个开关。

3.2 原有SQLite数据库迁移至Room

由于Room使用的也是SQLite, 因此能够很好的支持原有Sqlite数据库迁移到Room。

假设原有一个版本号为1的数据库有一张表User, 如今要迁移到Room, 咱们须要定义好Entity, DAO, Database, 而后建立Database时添加一个空实现的Migraton便可。须要注意的是,即便对数据库没有任何升级操做,也须要升级版本, 不然会抛异常IllegalStateException.

@Database(entities = {User.class}, version = 2)
public abstract class UsersDatabase extends RoomDatabase {
…
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        // Since we didn't alter the table, there's nothing else to do here.
    }
};
…
database =  Room.databaseBuilder(context.getApplicationContext(),
        UsersDatabase.class, "Sample.db")
        .addMigrations(MIGRATION_1_2)
        .build();

4、复杂数据的处理

在某些场景下咱们的应用可能须要存储复杂的数据类型,好比Date,可是Room的Entity仅支持基本数据类型和其装箱类之间的转换,不支持其它的对象引用。因此Room提供了TypeConverter给使用者本身实现对应的转换。

一个Date类型的转换以下:

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

定义好转换方法后,指定到对应的Database上便可, 这样就能够在对应的POJO(User)中使用Date类了。

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

5、总结

在SQLite API方式实现数据持久化的项目中,相信都有一个任务繁重的SQLiteOpenHelper实现, 一堆维护表的字段的Constant类, 一堆代码相似的数据库访问类(DAO),访问数据库时须要作Cursor的遍历,构建并返回对应的POJO类...相比之下,Room做为在SQLite之上封装的ORM库确实有诸多优点,比较直观的体验是:

  • 比SQLite API更简单的使用方式
  • 省略了许多重复代码
  • 能在编译时校验sql语句的正确性
  • 数据库相关的代码分为Entity, DAO, Database三个部分,结构清晰
  • 简单安全的数据库升级方案

想要了解更多Room相关内容能够戳下面的连接:

做者:JellyJoe_943 连接:https://www.jianshu.com/p/654d883e6ed0 來源:简书 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

相关文章
相关标签/搜索