Room是Google官方出品的ORM(Object-relational mapping) 框架。当前咱们也知道当前还有不少的ORM框架,例如GreenDao、OrmLite、Litepal等。目前并无深刻了解这些框架,没办法比较各个框架的优缺点,可是相对而言,Room比较官方出品,且可以更好的与LiveData及RxJava等框架结合使用,仍是推荐各位学习和使用Room框架做为数据存储的基础框架的。官方文档: https://developer.android.com/training/data-storage/room/java
Room由三个重要的组件组成:Database、Entity、DAO。react
Database 是数据库的持有者,是应用持久关联数据的底层链接的主要访问点。并且Database对应的类编写时必须知足下面几个条件:android
1. 必须是abstract类并且的extends RoomDatabase。数据库
2. 必须在类头的注释中包含与数据库关联的实体列表(Entity对应的类)。json
3. 包含一个具备0个参数的抽象方法,并返回用@Dao注解的类。数组
在运行时,你能够经过Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()获取Database实例。app
Entity表明数据库中某个表的实体类。框架
DAO封装用于访问数据库的方法。ide
在build.gradle中添加以下配置:函数
// add for room
implementation "android.arch.persistence.room:runtime:1.1.1"
// room 配合 RxJava
implementation "android.arch.persistence.room:rxjava2:1.1.1"
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
// RxJava
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxjava:2.1.3'
每一个Entity表明数据库中某个表的实体类。默认状况下Room会把Entity里面全部的字段对应到表上的每一列。
若是须要制定某个字段不做为表中的一列须要添加@Ignore注解,示例以下:
@Entity public class User { @PrimaryKey public int id; public String firstName; public String lastName; @Ignore Bitmap picture; }
示例中的picture字段代码由于使用了@Ignore因此该字段不会映射到User表中。
Entity的实体类都须要添加@Entity注解,并且Entity类中须要映射到表中的字段须要保证外部能访问到这些字段,这里建议把字段设置为public或者实现字段的getter和setter方法。
@Entity注解包含的属性有:
默认状况下Entity类的名字就是表的名字(不区分大小写)。可是咱们也能够经过@Entity的tableName属性来自定义表名字。
以下代码所示users表对应的实体类。
@Entity(tableName = "users") public class User { ... }
以下代码users表中first_name列对应firstName字段,last_name列对应lastName字段。
@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都须要至少一个字段设置为主键。即便这个Entity只有一个字段也须要设置为主键。Entity设置主键的方式有两种:
@Entity(primaryKeys = {"firstName", "lastName"}) public class User { public String firstName; public String lastName; }
@Entity public class User { @PrimaryKey public String firstName; @PrimaryKey public String lastName; }
若是但愿Room给entity设置一个自增的字段,能够设置@PrimaryKey的autoGenerate属性。
数据库索引用于提升数据库表的数据访问速度的。数据库里面的索引有单列索引和组合索引。Room里面能够经过@Entity的indices属性来给表格添加索引。
@Entity(indices = {@Index("firstName"), @Index(value = {"last_name", "address"})}) public class User { @PrimaryKey public int id; public String firstName; public String address; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; }
索引也是分两种的惟一索引和非惟一索引。惟一索引就想主键同样重复会报错的。能够经过的@Index的unique数学来设置是否惟一索引。
@Entity(indices = {@Index(value = {"first_name", "last_name"}, unique = true)}) public class User { @PrimaryKey public int id; @ColumnInfo(name = "first_name") public String firstName; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; }
由于SQLite是关系型数据库,表和表之间是有关系的。这也就是咱们数据库中常说的外键约束(FOREIGN KEY约束)。Room里面能够经过@Entity的foreignKeys属性来设置外键。正常状况下,数据库里面的外键约束。子表外键于父表。当父表中某条记录子表有依赖的时候父表这条记录是不能删除的,删除会报错。通常大型的项目不多会采用外键的形式。通常都会经过程序依赖业务逻辑来保证的。
下面咱们举一个具体的例子来讲明一下:
@Entity(indices = {@Index(value = {"first_name", "last_name"}, unique = true)}) public class User { @PrimaryKey public int id; @ColumnInfo(name = "first_name") public String firstName; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; } @Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "id", childColumns = "user_id")) public class Book { @PrimaryKey public int bookId; public String title; @ColumnInfo(name = "user_id") public int userId; }
上述代码中,foreignKeys修饰后的Book表中的userId来源于User表中的id。
@ForeignKey属性介绍:
entity:parent实体类(引用外键的表的实体)。
parentColumns:parent外键列(要引用的外键列)。
childColumns:child外键列(要关联的列)。
onDelete:默认NO_ACTION,当parent里面有删除操做的时候,child表能够作的Action动做有:
1. NO_ACTION:当parent中的key有变化的时候child不作任何动做。
2. RESTRICT:当parent中的key有依赖的时候禁止对parent作动做,作动做就会报错。
3. SET_NULL:当paren中的key有变化的时候child中依赖的key会设置为NULL。
4. SET_DEFAULT:当parent中的key有变化的时候child中依赖的key会设置为默认值。
5. CASCADE:当parent中的key有变化的时候child中依赖的key会跟着变化。
onUpdate:默认NO_ACTION,当parent里面有更新操做的时候,child表须要作的动做。Action动做方式和onDelete是同样的。
deferred:默认值false,在事务完成以前,是否应该推迟外键约束。这个怎么理解,当咱们启动一个事务插入不少数据的时候,事务还没完成以前。当parent引发key变化的时候。能够设置deferred为ture。让key当即改变。
public class Address { public String street; public String state; public String city; @ColumnInfo(name = "post_code") public int postCode; } @Entity public class User { @PrimaryKey public int id; public String firstName; @Embedded public Address address; }
@Embedded注解属性:
DAO是Room的主要组件,负责定义访问数据库的方法。Room使用过程当中通常使用抽象DAO类来定义数据库的CRUD操做。
DAO能够是一个接口也能够是一个抽象类。若是它是一个抽象类,它能够有一个构造函数,它将RoomDatabase做为其惟一参数。Room在编译时建立每一个DAO实例。DAO里面全部的操做都是依赖方法来实现的。
当DAO里面的某个方法添加了@Insert注解。Room会生成一个实现,将全部参数插入到数据库中的一个单个事务。
1. OnConflictStrategy.REPLACE:冲突策略是取代旧数据同时继续事务。
2. OnConflictStrategy.ROLLBACK:冲突策略是回滚事务。
3. OnConflictStrategy.ABORT:冲突策略是终止事务。
4. OnConflictStrategy.FAIL:冲突策略是事务失败。
5. OnConflictStrategy.IGNORE:冲突策略是忽略冲突。
一个简单的实例以下:
@Dao public interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertUsers(User... users); }
当DAO里面的某个方法添加了@Update注解。Room会把对应的参数信息更新到数据库里面去(会根据参数里面的primary key作更新操做)。
@Dao public interface UserDao { @Update(onConflict = OnConflictStrategy.REPLACE) int updateUsers(User... users); }
@Update注解的方法也能够返回int变量。表示更新了多少行。
当DAO里面的某个方法添加了@Delete注解。Room会把对应的参数信息指定的行删除掉(经过参数里面的primary key找到要删除的行)。
@Delete也是能够设置onConflict来代表冲突的时候的解决办法。
@Dao public interface UserDao { @Delete void deleteUsers(User... users); }
@Delete对应的方法也是能够设置int返回值来表示删除了多少行。
@Query注解是DAO类中使用的主要注释。它容许您对数据库执行读/写操做。@Query在编译的时候会验证准确性,因此若是查询出现问题在编译的时候就会报错。
Room还会验证查询的返回值,若是返回对象中的字段名称与查询响应中的相应列名称不匹配的时候,Room会经过如下两种方式之一提醒您:
@Query注解value参数:查询语句,这也是咱们查询操做最关键的部分。
查询全部的信息。
@Dao public interface UserDao { @Query("SELECT * FROM user") User[] loadAllUsers(); }
返回结果能够是数组,也能够是List
大多数状况下咱们都须要查询知足特定的查询条件的信息。
@Dao public interface UserDao { @Query("SELECT * FROM user WHERE firstName == :name") User[] loadAllUsersByFirstName(String name); }
查询须要多个参数的状况
@Dao public interface UserDao { @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") User[] loadAllUsersBetweenAges(int minAge, int maxAge); @Query("SELECT * FROM user WHERE firstName LIKE :search " + "OR lastName LIKE :search") List<User> findUserWithName(String search); }
有的时候可能指向返回某些特定的列信息。
下来的例子只查询user表中的firstName和lastName信息。
@Entity public class User { @PrimaryKey public String firstName; @PrimaryKey public String lastName; public int age; } public class NameTuple { private String firstName; private String lastName; public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } @Dao public interface UserDao { @Query("SELECT firstName, lastName FROM user") List<NameTuple> loadFullName(); }
在查询的时候您可能须要传递一组(数组或者List)参数进去。
@Dao public interface UserDao { @Query("SELECT firstName, lastName FROM user WHERE region IN (:regions)") public List<NameTuple> loadUsersFromRegions(List<String> regions); }
意思就是查询到结果的时候,UI可以自动更新。Room为了实现这一效果,查询的返回值的类型为LiveData。
@Dao public interface UserDao { @Query("SELECT firstName, lastName FROM user WHERE region IN (:regions)") LiveData<List<NameTuple>> loadUsersFromRegionsSync(List<String> regions); }
关于LiveData的具体用法,咱们这里就不作过多的讨论了,后续的文章中咱们会针对LiveData作详细的说明。
Room的查询也能够返回RxJava2的Publisher或者Flowable对象。固然了想要使用这一功能须要在build.gradle文件添加 implementation "android.arch.persistence.room:rxjava2:1.1.1" 依赖。
@Dao public interface UserDao { @Query("SELECT * from user") Flowable<List<User>> loadUser(); }
拿到Flowable<List<User>>数据以后就能够去调用
mAppDatabase.userDao() .loadUser() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<List<User>>() { @Override public void accept(List<User> entities) { } });
查询结果直接返回cursor。而后经过cursor去获取具体的结果信息。
@Dao public interface UserDao { @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5") Cursor loadRawUsersOlderThan(int minAge); }
关于如何从Cursor里去获取数据,你们确定都很是熟悉,这里就不赘述了。
有的时候可能须要经过多个表才能获取查询结果。这个就涉及到数据的多表查询语句了。
@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); }
也能够查询指定的某些列:
@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; } }
@Database注解能够用来建立数据库的持有者。该注解定义了实体列表,该类的内容定义了数据库中的DAO列表。这也是访问底层链接的主要入口点。注解类应该是抽象的而且扩展自RoomDatabase。
Database对应的对象(RoomDatabase)必须添加@Database注解,@Database包含的属性:
在运行时,你能够经过调用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()获取实例。
由于每次建立Database实例都会产生比较大的开销,因此应该将Database设计成单例的,或者直接放在Application中建立。
两种方式获取Database对象的区别:
咱们用一个简单的实例来讲明Database的建立。先定义一个abstract类AppDatabase继承RoomDatabase:
@Database(entities = {User.class, Book.class}, version = 3) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); public abstract BookDao bookDao(); }
建立RoomDatabase实例(AppDatabase)。这里咱们把RoomDatabase实例的建立放在Application里面。
public class AppApplication extends Application { private AppDatabase mAppDatabase; @Override public void onCreate() { super.onCreate(); mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries() .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build(); } public AppDatabase getAppDatabase() { return mAppDatabase; } /** * 数据库版本 1->2 user表格新增了age列 */ static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE User ADD COLUMN age integer"); } }; /** * 数据库版本 2->3 新增book表格 */ static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL( "CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)"); } }; }
建立RoomDatabase实例的时候,RoomDatabase.Builder类里面主要方法的介绍:
/** * 默认值是FrameworkSQLiteOpenHelperFactory,设置数据库的factory。好比咱们想改变数据库的存储路径能够经过这个函数来实现 */ public RoomDatabase.Builder<T> openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory); /** * 设置数据库升级(迁移)的逻辑 */ public RoomDatabase.Builder<T> addMigrations(@NonNull Migration... migrations); /** * 设置是否容许在主线程作查询操做 */ public RoomDatabase.Builder<T> allowMainThreadQueries(); /** * 设置数据库的日志模式 */ public RoomDatabase.Builder<T> setJournalMode(@NonNull JournalMode journalMode); /** * 设置迁移数据库若是发生错误,将会从新建立数据库,而不是发生崩溃 */ public RoomDatabase.Builder<T> fallbackToDestructiveMigration(); /** * 设置从某个版本开始迁移数据库若是发生错误,将会从新建立数据库,而不是发生崩溃 */ public RoomDatabase.Builder<T> fallbackToDestructiveMigrationFrom(int... startVersions); /** * 监听数据库,建立和打开的操做 */ public RoomDatabase.Builder<T> addCallback(@NonNull RoomDatabase.Callback callback);
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(); } }
大部分状况下设计的数据库在版本的迭代过程当中常常是会有变化的。好比忽然某个表须要新加一个字段,须要新增一个表。这个时候咱们又不想失去以前的数据。Room里面以Migration类的形式提供可一个简化SQLite迁移的抽象层。Migration提供了从一个版本到另外一个版本迁移的时候应该执行的操做。
当数据库里面表有变化的时候(无论你是新增了表,仍是改变了某个表)有以下几个场景。
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries() .fallbackToDestructiveMigration() .build();
因此在数据库有变化的时候,咱们任什么时候候都应该尽可能提供Migrating。Migrating让咱们能够本身去处理数据库从某个版本过渡到另外一个版本的逻辑。咱们用一个简单的实例来讲明。有这么个状况,数据库开始设计的时候咱们就一个user表(数据库版本 1),第一次变化来了咱们须要给user表增长一个age的列(数据库版本 2),过了一段时间又有变化了咱们须要新增长一个book表。三个过程版本1->2->3。
数据库版本为1的时候的代码,以下:
@Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } public class AppApplication extends Application { private AppDatabase mAppDatabase; @Override public void onCreate() { super.onCreate(); mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries().build(); } public AppDatabase getAppDatabase() { return mAppDatabase; } }
数据库版本为2的时候的代码,User增长了age列:
@Entity public class User { @PrimaryKey(autoGenerate = true) private long uid; private String name; private String address; private String phone; private Integer age; public long getUid() { return uid; } public void setUid(long uid) { this.uid = uid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } } @Database(entities = {User.class}, version = 2) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } public class AppApplication extends Application { private AppDatabase mAppDatabase; @Override public void onCreate() { super.onCreate(); mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries() .addMigrations(MIGRATION_1_2) .build(); } public AppDatabase getAppDatabase() { return mAppDatabase; } /** * 数据库版本 1->2 user表格新增了age列 */ static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE user " + " ADD COLUMN age INTEGER"); } }; }
数据库版本为3的时候的代码,新增了一个Book表:
@Entity public class Book { @PrimaryKey(autoGenerate = true) private Long uid; private String name; private Date time; private Long userId; public Long getUid() { return uid; } public void setUid(Long uid) { this.uid = uid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getTime() { return time; } public void setTime(Date time) { this.time = time; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } } @Database(entities = {User.class, Book.class}, version = 3) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); public abstract BookDao bookDao(); } public class AppApplication extends Application { private AppDatabase mAppDatabase; @Override public void onCreate() { super.onCreate(); mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries() .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build(); } public AppDatabase getAppDatabase() { return mAppDatabase; } /** * 数据库版本 1->2 user表格新增了age列 */ static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE User ADD COLUMN age integer"); } }; /** * 数据库版本 2->3 新增book表格 */ static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL( "CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)"); } }; }
Migrating使用过程当中也有碰到一些坑,这里告诫你们一个经验Entity中能用Integer的时候不用int。
Room也容许你会将你数据库的表信息导出为一个json文件。你应该在版本控制系统中保存该文件,该文件表明了你的数据库表历史记录,这样容许Room建立旧版本的数据库用于测试。
只须要在build.gradle文件中添加以下配置。编译的时候就会导出json文件。
android { ... defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } } } // 用于测试 sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } }
json文件文件会导出在工程目录下的schemas文件夹下面。里面会有各个版本数据库的信息。