文:栋栋
Room 是 Google 推出的 Android 架构组件库中的数据持久化组件库, 也可以说是在 SQLite 上实现的一套 ORM 解决方案。Room 主要包含三个部分:
其关系如下图所示:
一个简单 Entity 定义如下:
- @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
- }
注解 POJO 类,定义数据表名称;
- @Entity
- (tableName
- =
- "table_name**")
- @Entity
- (primaryKeys
- =
- {"firstName"
- ,
- "lastName"})
定义数据表中的字段名
- @ColumnInfo
- (name
- =
- "column_name
- ")
, unique=true 可以确保表中不会出现
- indices
- =
- {@Index
- (value
- =
- {"first_name"
- ,
- "last_name"},
- unique=
- true
- ),
- ...}
相同的数据。
- {
- "first_name",
- "last_name"
- }
不同于目前存在的大多数 ORM 库,Room 不支持 Entitiy 对象间的直接引用。(具体原因可以参考: Understand why Room doesn't allow object references) 但 Room 允许通过外键 (Foreign Key) 来表示 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;
- }
如上面代码所示,Book 对象与 User 对象是属于的关系。Book 中的 user_id, 对应 User 中的 id。 那么当一个 User 对象被删除时, 对应的 Book 会发生什么呢?
@ForeignKey 注解中有两个属性 onDelete 和 onUpdate, 这两个属性对应 ForeignKey 中的 onDelete() 和 onUpdate(), 通过这两个属性的值来设置当 User 对象被删除/更新时,Book 对象作出的响应。这两个属性的可选值如下:
在某些情况下, 对于一张表中的数据我们会用多个 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
- @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(onConflict=OnConflictStrategy.REPLACE)
DAO 中的增删改方法的定义都比较简单,这里不展开讨论,下面更多的聊一下查询方法。
Talk is cheap, 直接 show code:
- @Query("SELECT * FROM user")
- List < User > getAll();
Room 会在编译时校验 sql 语句,如果 @Query() 中的 sql 语句存在语法错误,或者查询的表不存在,Room 会在编译时报错。
- @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 会在编译时报错。
在实际某个业务场景中, 我们可能仅关心一个表部分字段的值,这时我仅需要查询关心的列即可。
定义子集的 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
Room 中查询操作除了返回 POJO 对象及其 List 以外, 还支持:
- @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
- public LiveData < List < User >> loadUsersFromRegionsSync(List < String > regions);
。
- android.arch.persistence.room:rxjava2
- @Query("SELECT * from user where id = :id LIMIT 1")
- public Flowable < User > loadUserById(int id);
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.
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);
- }
Room 中 DataBase 类似 SQLite API 中 SQLiteOpenHelper,是提供 DB 操作的切入点,但是除了持有 DB 外, 它还负责持有相关数据表(Entity)的数据访问对象(DAO), 所以 Room 中定义 Database 需要满足三个条件:
- @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();
在传统的 SQLite API 中,我们如果要升级数据库, 通常在
方法执行数据库升级的 sql 语句,这些 sql 语句的通常根据数据库版本以文件的方式或者用数组来管理。有人说这种方式升级数据库就像在拆炸弹,相比之下在 Room 中升级数据库简单的就像是按一个开关而已。
- SQLiteOpenHelper.onUpgrade
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 类时需要指定 startVersion 和 endVersion, 代码中 MIGRATION_1_2 和 MIGRATION_2_3 的 startVersion 和 endVersion 是递增的, Migration 其实是支持从版本 1 直接升到版本 3,只要其 migrate() 方法里执行的语句正常即可。那么 Room 是怎么实现数据库升级的呢?其实本质上还是调用
,Room 中自己实现了一个 SQLiteOpenHelper, 在 onUpgrade() 方法被调用时触发 Migration,当第一次访问数据库时,Room 做了以下几件事:
- SQLiteOpenHelper.onUpgrade
被调用,并且触发 Migration
- SQLiteOpenHelper
- .onUpgrade
这样一看, 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();
在某些场景下我们的应用可能需要存储复杂的数据类型,比如 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;
- }
在 SQLite API 方式实现数据持久化的项目中,相信都有一个任务繁重的 SQLiteOpenHelper 实现, 一堆维护表的字段的 Constant 类, 一堆代码类似的数据库访问类(DAO),访问数据库时需要做 Cursor 的遍历,构建并返回对应的 POJO 类... 相比之下,Room 作为在 SQLite 之上封装的 ORM 库确实有诸多优势,比较直观的体验是:
想要了解更多 Room 相关内容可以戳下面的链接:
技术沙龙推荐
点击下方图片即可阅读
来源: https://juejin.im/entry/5a3884c56fb9a04522079f4c