Android Room 1 - chuwuwang/ReadingNote GitHub Wiki
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
Room 具有以下注释处理器选项:
-
room.schemaLocation:配置并启用将数据库架构导出到给定目录中的JSON文件的功能。
-
room.incremental:启用Gradle增量注释处理器。
-
room.expandProjection:配置Room以重新编写查询,使其顶部星形投影在展开后仅包含DAO方法返回类型中定义的列。
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"true"]
}
}
}
}
Room持久性库在SQLite的基础上提供了一个抽象层,允许流利的数据库访问,同时利用的SQLite的全部功能。
Room主要有以下三个部分组成:
-
Database:标有 @Database注解的类需要具备以下特征:
-
继承RoomDatabase的抽象类
-
在注释中包括与数据库关联的实体列表( @Database(entities = { } ) )
-
包含一个无参的抽象方法并返回一个使用@Dao注解的类
-
-
Entity:对应数据库中的表
-
DAO:包含访问数据库的方法
-
定义一个抽象类继承RoomDatabase。
-
使用@Database注解这个抽象类,同时使用entities属性配置表,version配置版本号。
-
定义一系列的Dao层的抽象方法。
-
定义一个接口或抽象类,并使用@Dao注解这个类。
-
定义各种操作表的抽象方法,并使用@Query等注解对应的抽象方法。
以上各部分的依赖关系如下图所示:
默认情况下,Room为实体中定义的每个字段创建一个列。如果实体有不想持久的字段,则可以使用@Ignore来注解它们。必须通过Database类中的entities数组引用实体类。
每个实体必须定义至少1个字段作为主键。即使只有1个字段,仍然需要用@PrimaryKey注解字段。
此外,如果您想Room自动分配IDs给实体,则可以设置@PrimaryKey的autoGenerate属性。如果实体具有复合主键,则可以使用@Entity注解的primaryKeys属性。如下面的代码片段所示:
@Entity( primaryKeys = {"firstName", "lastName"} )
public class User {
public String firstName;
public String lastName;
}
默认情况下,Room使用类名作为数据库表名。如果希望表具有不同的名称,请设置@Entity注解的tableName属性。SQLite中的表名不区分大小写。
与tableName属性类似,Room使用字段名称作为数据库中的列名。如果希望列具有不同的名称,请将@ColumnInfo注解添加到字段中,如下面的代码片段所示:
@Entity(tableName = "users")
public class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
}
注意: Entity可以有一个空的构造函数(如果DAO类可以访问每个持久化字段),或者一个构造函数其参数包含与实体类中的字段匹配的类型和名字。Room还可以使用全部或部分构造函数,比如只接收部分字段的构造函数。
根据访问数据的方式,你可能希望对数据库中的某些字段进行索引,以加快查询速度。要向实体添加索引,请在@Entity注解中包含indices属性,列出要包含在索引或组合索引中的列的名字。
有时,数据库中的某些字段或字段组合必须是唯一的。你可以通过设置@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;
}
有时,你希望将一个实体或POJO表达作为数据库逻辑中的一个整体,即使对象包含了多个字段。在这种情况下,你可以使用@Embeded注解来表示要在表中分为为子字段的对象。然后,你可以像其他单独的列一样查询嵌入的字段。
例如,我们的User类可以包含一个类型为Address的字段,其表示了一个字段组合,包含street、city、state和postCode。为了将这些组合列单独的存放到表中,将Address字段加上@Embedde注解,如下代码片段所示:
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属性来保持每列的唯一性。Room然后将提供的值添加到嵌入对象的每个列名的开头。
当一个类中嵌套多个类,并且这些类具有相同的字段,则需要调用@Embedded的属性prefix 添加一个前缀,生成的列名为前缀+列名。
public class User {
@PrimaryKey(autoGenerate = true) public int id;
public String firstName;
public String lastName;
@Embedded(prefix = "first")
Address address;
@Embedded(prefix = "second")
AddressTwo addressTwo;
}
该例中将会创建firstStreet,firstState...secondStreet,secondState...等列。
Room的主要组件是Dao类。DAO以简洁的方式抽象了对于数据库的访问。
DAO既可以是接口,也可以是抽象类。如果是抽象类,它可以有一个构造函数,它把RoomDatabase作为唯一的参数。Room在编译时创建每个DAO实现。
注意: Room不允许在主线程中访问数据库,除非你可以builder上调用allowMainThreadQueries(),因为它可能会长时间锁住UI。异步查询(返回LiveData或RxJava Flowable的查询)则不受此影响,因为它们在有需要时异步运行在后台线程上。
当您创建一个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方法只接收1个参数,则可以返回一个Long型的值,这是插入项的新rowId。如果参数是数组或集合,则应该返回long[]或者List类型的值。有时插入数据和更新数据会产生冲突,所以就有了冲突之后要怎么解决,SQLite对于事务冲突定义了5个方案。
OnConflictStrategy
-
REPLACE:见名知意,替换,违反的记录被删除,以新记录代替之
-
IGNORE:违反的记录保持原貌,其它记录继续执行
-
FAIL:终止命令,违反之前执行的操作得到保存
-
ABORT:终止命令,恢复违反之前执行的修改
-
ROLLBACK:终止命令和事务,回滚整个事务
@Update注解在数据库中用于修改一组实体的字段。它使用每个实体的主键来匹配查询。
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
通常不是必须的,但你可以让此方法返回一个int值,指示数据库中更新的行数。
用于从数据库中删除给定参数的一系列实体,它使用主键匹配数据库中相应的行。
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
@Query是DAO类中使用的主要注解。它允许您在数据库上执行读/写操作。每个@Query方法在编译时被验证,因此,如果存在查询问题,则会发生编译错误而不是运行时故障。
Room还验证查询的返回值,这样如果返回对象中字段的名称与查询响应中的相应列名不匹配,则Room将以以下两种方式之一提醒您:
-
如果只有一些字段名匹配,则发出警告。
-
如果没有字段名匹配,则会出错。
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
Observable查询
当执行查询时,您经常希望应用程序的UI在数据更改时自动更新。要实现这一点,请在查询方法描述中使用类型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);
}
RXJava的响应式查询
Room还可以从您定义的查询中返回RXJava2 Publisher和Flowable对象。若要使用此功能,请将androidx.room:room-rxjava2库添加到gradle的依赖关系中。
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
// Emits the number of users added to the database.
@Insert
public Maybe<Integer> insertLargeNumberOfUsers(List<User> users);
// Makes sure that the operation finishes successfully.
@Insert
public Completable insertLargeNumberOfUsers(User... users);
/* Emits the number of users removed from the database. Always emits at
least one user. */
@Delete
public Single<Integer> deleteUsers(List<User> users);
}
Room允许你从查询中返回任意的java对象,只要结果列集能被映射到返回的对象。比如,你可以创建下面的POJO来拉取用户的first name和last name。
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<NameTuple> loadFullName();
}
Room理解这个查询是要返回first_name和last_name列的值,并且这些值可以映射成NameTuple类的字段。因此,Room可以生成正确的代码。如果查询返回太多列,或者有列不存在NameTuple类,Room则显示一个警告。
**注意:**这些POJO也可以使用@Embedded注解。
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();
}
}
@Database(entities = {User.java}, version = 1)
@TypeConverters( { Converter.class } )
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
@Entity
public class User {
...
private Date birthday;
}
上面的例子中定义了两个函数,一个转换Date对象到Long对象,另一个执行逆变换,从Long到Date。由于Room是知道如何持久化Long对象的,因此它可以使用此转换器来持久保存Date类型的值。接下来,添加@TypeConverters注释到AppDatabase类,这样Room就可以在AppDatabase中的实体和Dao上使用上面的定义的类型转换器。还可以限制@TypeConverters到不同的范围,包括单个实体,DAO和DAO方法。
当你添加和更改App功能时,你需要修改实体类来反映这些更改。当用户更新到你的应用最新版本时,你不想要他们丢失所有存在的数据,尤其是你无法从远端服务器恢复数据时。
Room允许你编写Migration类来保留用户数据。每个Migration类指明一个startVersion和endVersion。在运行时,Room运行每个Migration类的migrate()方法,使用正确的顺序来迁移数据库到最新版本。
警告: 如果你没有提供需要的迁移类,Room将会重建数据库,也就意味着你会丢掉数据库中的所有数据。
默认情况下,如果没有匹配到升级策略,则app直接crash。
为了防止crash,可添加fallbackToDestructiveMigration方法配置,直接删除所有的表,重新创建表。
同理如果没有匹配到降级规则,默认也会crash。
可以通过fallbackToDestructiveMigrationOnDowngrade方法配置删表重建,但不能指定version删表重建。
Repository类用于访问多个数据源。Repository并不是架构组件库的一部分,而是代码解耦和架构比较推荐的方法。Repository类处理数据操作。它为应用程序提供了一个整洁的API。
Repository管理数据的查询线程,同时,可以使用多个后端。在常规情况下,Repository主要实现从服务端拉取数据还是从本地数据库拉取数据的逻辑。
ViewModel 主要扮演为UI提供数据的角色,同时,在配置更改后可以继续存在。ViewModel可以看做Repository和UI的通信中心。也可以使用ViewModel共享数据。ViewModel是lifecycle library的一部分。
ViewModel将UI数据和Activity和Fragment类进行分离,更符合单一职责原则:Activity和Fragment负责展示UI,而ViewModel负责持有并处理UI所需要的所有数据。
在ViewModel 中,使用LiveData更新UI的数据。因为LiveData有以下几个优点:
-
可以监听数据,只有当数据更改时才会更新UI。
-
通过ViewModel可以将Repository和UI完全隔离。在ViewModel中不会直接进行数据库调用,这使得代码更方便进行测试。
ViewModel是UI与Repository的通信中心。即UI更新数据是通过ViewModel进行的。为了显示当前数据的内容,在ViewModel中添加一个观察者,用以监听LiveData的更改。
在MainActivity的onCreate()方法中创建ViewModel实例,并监听数据库数据的更新。
注意: 实例化AppDatabase对象时,应该遵循单例模式,因为每个RoomDatabase实例都相当昂贵,而且很少需要访问多个实例。
-
当一个类由@Entity注解,并且由@Database注解的entities属性引用,Room将在数据库中为其创建一张数据库表。
-
Room不允许在主线程中访问数据库,除非在buid的时候使用allowMainThreadQueries()方法。
Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "user.db")
.allowMainThreadQueries()
.build();
https://medium.com/jastzeonic/kotlin-room-%E4%BD%BF%E7%94%A8%E5%88%9D%E9%AB%94%E9%A9%97-f3af4f8ddc80