Android Architecture Components 系列(五)Room

Room定义
    Room是一个持久化工具,和ORMLite greenDao相似。在开发中利用Room来操做SQLite数据库,在SQLite上提供了一个方便访问的抽象层。
 
传统SQLite的缺陷:
  •     没有编译时SQL语句的检查。当数据库发生变化时,须要手动的更新相关代码,会花费至关多的时间而且容易出错。
  •     编写大量SQL语句和Java对象之间相互转换的代码。
 
针对以上的缺陷 Room的组成由:
    
  •     Database  建立数据库
        使用注解申明一个类 ,注解中包含若干个Entity类,这个Database类主要负责建立数据库以及获取数据对象
 
  •     Entitles  数据库表中对应的Java对象
        表示每一个数据库的总的一个表结构,一样也是使用注解表示,类中的每一个字断都对应表中的一列
 
  •     DAO 访问数据库
        Data Access Object的缩写 ,表示从代码中直接访问数据库,屏蔽掉Sql语句
 
官方 Room结构图:
 
下面以存储User信息未为实例 :
 
User.java 实体domain类
@Entity
publicclass User { 
    @PrimaryKey 
    privateintuid; 
 
    @ColumnInfo(name ="first_name”) 
    privateString firstName; 
    
    @ColumnInfo(name ="last_name”) 
    privateString lastName; 
    // Getters and setters are ignored for brevity,    
    // but they're required for Room to work.}
    Getters和setters为了简单起见就省略了,可是对Room来讲是必须的 }
 
UserDao数据库类
@Dao
publicinterface 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 
    voidinsertAll(User... users); 
 
    @Delete
    voiddelete(User user); }
 
// AppDatabase.java
@Database(entities = {User.class}, version =1) 
    publicabstractclass AppDatabase extends RoomDatabase { 
        publicabstractUserDaouserDao(); 
}
 
代码中的建立数据库:
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
                         AppDatabase.class,"database-name").build();
ps:注意: Database最好设计成单利模式,不然对象太多会有性能的影响。
 
Entities
    用来注解一个实体类,对应数据库一张表。Room为实体中定义的每一个成员变量在数据库中建立对应的字断,若是不想保存某个字段到数据库表中,可使用 @Ignore 注解该字断 好比:
@Entity
class User { 
    @PrimaryKey 
    publicintid; 
 
    @ColumnInfo(name ="first_name”) 
    publicString firstName; 
 
    @ColumnInfo(name ="last_name”)
    publicString lastName; 
    
    @Ignore 
    Bitmap picture; 
}
ps:为了保存每个字段,这个字段须要有能够访问的getter/setter方法或是public 属性 
 

Primary Key 主键

一个Entity必须定义一个field为主键 ,即便该表只有一个成员变量或是说字段。
 
自动生成primary key :
    @primaryKey 的autoGenerate属性
 
多个Field 复合Key:
@Entity(primaryKeys = {"firstName","lastName”})
class User { 
    publicString firstName;
    publicString lastName; 
    @IgnoreBitmap picture; 
}
 
Entity的参数 指定表名:
在默认状况下 Room使用类名做为数据库的表名。若是想要自定义表名,在@Entity后使用tableName参数来指定表名
@Entity( tableName ="users”)
class User { }
 
Entity的参数 指定表的列名:
@ColumnInfo注解是改变成员变量对应的数据库的字段名称。
经过@ColumnInfo(name = "first_name")设置
Entity的参数 indices
indices的参数值是@Index 的数组,根据访问数据库的方式,对特定的fiedl创建索引来在某些状况下加快 查询速度 ,能够须要加入索引 
@Entity( indices = { @Index("name"),@Index("last_name","address" )}
    class User { 
    @PrimaryKey 
    publicintid; 
    
    publicString firstName; 
    publicString address; 
    
    @ColumnInfo(name ="last_name”)
    publicString lastName; 
    
    @IgnoreBitmap picture; 
}
Entity的参数 unique
    有时候某些字段或字段组必须是惟一的,经过将@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; 
}
Entity的参数 foreignKeys(外键)
由于SQLite是一种关系型数据库,能够指定对象之间的关系。尽管大多数ORM库容许实体对象的相互引用, 可是Room明确禁止!实体之间没有对象引用!详细的缘由,能够参考这里。
因为不能使用直接关系,因此就要用到foreignKeys(外键)
例如: 一个Pet类 须要和User类 创建关系
@Entity(foreignKeys = @ForeignKey(entity = User.class, 
                                                         parentColumns = "id”, 
                                                         childColumns = "user_id”) )
    class Pet { 
        @PrimaryKey 
        public int petId; 
        public String name; 
        @ColumnInfo(name = "user_id”) 
        public int userId; 
    }
     外键容许指定运用实体更新时发生的操做,好比你能够定义当删除User时对应的Pet类也被删除,也能够在@ForeignKey 中 添加 onDelete = CASCADE 实现 
同理的添加操做:
    @Insert(OnConflict = REPLACE)
定义里REMOVE 和REPLACE 而不是简单的UPDATE操做。这样产生的后果会影响外键定义的约束行为,详细的信息能够参考 SQLite documentation。
 
Entity的参数 关联Entity
Entity之间可能也有一对多之间的关系,好比一个User有多个Pet,经过一次查询获取多个关联的Pet。
public class UserAndAllPets {
    @Embedded
    public User user;//父类Entity
    @Relation (parentColumn = “id” , entityColumn = “user_id")
    public List<Pet>pets ;//空list
}
    @Dao
 public interface UserPetDao {
    @Query (“SELECT * from User")
    public List<UserAndAllPets> loadUserAndPets();
}
ps:注意
    使用@Relation 注解的field 必须是一个List 或是 一个Set 。Entity的类型是从返回类型中推断出来的,能够经过定义entity()来定义特定的返回类型。
    用@Relation 注解的field 必须是public 或者有public的setter。这是由于加载数据是分为两个步骤的:
        a、父Entity被查询
        b、触发用@Relation注解的entity的查询
    因此在上面的UserAndAllPets类中,首先User所在的数据库被查询,而后才触发查询Pets的查询。
便是Room首先建立一个空的对象,而后设置父Entity 和一个 空的list ,在第二次查询的时候Room将会填充这个list。
 
Entity的参数 对象嵌套对象
    有时候须要在类里面把另外一个类做为field ,这时候就须要使用@Embedded,这样就能够像查询其余列同样查询这个field类了。
    好比user类包含了一个field Address类,表明user的地址包括所在的街道,城市,州和邮编 。
 
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;
}
 
 
Data Access Objects DAO类:
数据库访问的抽象层
        Dao 能够是一个接口 也能够是一个抽象类 。若是是一个抽象类,那么它能够
接受一个RoomDatabase 做为构造器的惟一参数。
 
        Room不容许再煮现吃中访问数据库,除非在builder里面调用 allowMainThreadQueries().
由于访问数据库是耗时操做,在主线程中进行操做可能会阻塞线程,引发UI卡顿或是ANR。
 
Dao @Insert 
    @Insert 注解的方法,Room将会生成插入的代码
 
@Dao
publicinterface MyDao { 
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    publicvoidinsertUsers(User... users);
    
    @InsertpublicvoidinsertBothUsers(User user1, User user2); 
    @InsertpublicvoidinsertUsersAndFriends(User user, List<User> friends); 
}
ps:若是@Insert 只接受一个参数,那么将返回一个long,对应着插入的rowId;
        若是接受多个参数,或是数组、集合,那么就会返回一个long的数组或是list
 
Dao @Update、@Delete操做
 
@Dao
publicinterface MyDao { 
    @Update 
    publicvoidupdateUsers(User... users); 
    @Delete
    publicvoiddeleteUsers(User... users); 
}
可让update、delete方法返回一个int类型的整数,表明被update、delete的行号
 
Dao @Query操做的方法
    @Query注解的方法在编译时候就会被检查到,若是有任何查询的问题,都会抛出编译异常,而不是等到运行后才触发异常。
    Room也会检查查询返回值的类型,若是返回类型的字段和数据路列表名存在不一致,会收到警告,若是二者彻底不一致会报错。
 
  • 一个简单查询示例
@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}
 
  • 带参数的查询操做
@Dao
publicinterface MyDao { 
    @Query("SELECT * FROM userWHERE age > :minAge”)
    publicUser[]loadAllUsersOlderThan(intminAge);
}
ps:在编译时作类型检查,若是表中没有age这个列或是说字段,那么就会抛出错误。
  • 带多个参数
@Dao
    publicinterface MyDao { 
    
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge”) 
    publicUser[]loadAllUsersBetweenAges(intminAge,intmaxAge); 
   
 @Query("SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search”)
    publicList<User>findUserWithName(String search); 
}
 
支持返回列的子集
    有时候只须要Entity的几个field,例如只须要获取User的姓名就好了。那么经过只获取这两个列的数据不只可以节省宝贵的资源,还能加快查询速度!
    好比上面的User,我只须要firstName和lastName,首先定义一个子集,而后结果改为对应子集便可
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(); //定义一个子集
}
 
支持返回集合的子集
    查询两个地区的全部用户,直接用sql中的in便可,但若是这个地区是程序指定的,个数不肯定呢?
@Dao
publicinterface MyDao { 
    
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)”)
    publicList<NameTuple>loadUsersFromRegions(List<String> regions);
}
 
支持Observable 可被观察的查询
    经过LiveData的配合使用,就能够实现当数据库内容发生变化时自动收到变化后的数据的功能;能够异步的获取数据,那么咱们的Room也是支持异步查询的。
@Dao
publicinterface MyDao { 
  @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)”) 
  publicLiveData<List<User>> 
    loadUsersFromRegionsSync(List<String> regions); 
}
 
支持RxJava
Room也能够返回RxJava2中的Publisher 和 Flowable 格式的数据。RxJava是另一个异步操做库,一样也是支持的。
ps:在Gradle中添加android.arch.persistence.room:rxjava2
详细的信息能够参考 Room and RxJava这篇文章。
@Dao
publicinterface MyDao { 
    @Query("SELECT * from user where id = :id LIMIT 1”) 
    publicFlowable<User>loadUserById(intid);
}
 
支持直接获取cursor
原始的Android系统查询结果是经过Cursor来获取的,一样也支持。
@Dao
publicinterface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5”)
    publicCursorloadRawUsersOlderThan(intminAge);
}
 
支持多表查询
    有时候数据库存在范式相关,数据拆到了多个表中,那么就须要关联多个表进行查询,若是结果只是一个表的数据,那么很简单,直接用Entity定义的类型便可。
 
下面这段代码演示了如何从一个包含借阅用户信息的表和一个包含已经被借阅的书的表中获取信息:
@Dao
publicinterface 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”) 
publicList<Book>findBooksBorrowedByNameSync(String userName);
}
 
    固然也能够从查询中返回的POJO(Domain实体)类。可是须要单独定义一个POJO类,来接受数据。
// You can also define this class in a separate file, as long as you add the
   // "public" access modifier.
publicclass UserPet { 
    publicString userName; 
    publicString petName;
@Dao
publicinterface MyDao {
    @Query("SELECT user.name AS userName, pet.name AS petName “ 
        +"FROM user, pet “ 
        +"WHERE user.id = pet.user_id”) 
    publicLiveData<List<UserPet>>loadUserAndPetNames(); 
}
 
支持类型转换器
      有时候Java定义的数据类型和数据库中存储的数据类型是不一致的,Room提供类型转换,这样在操做数据库的时候能够自动转换类型。
    好比在Java中,时间用Date表示,可是在数据库中类型倒是long,这样有利于存储
publicclass Converters { 
    @TypeConverter 
    publicstaticDatefromTimestamp(Long value) { 
        returnvalue ==null?null:newDate(value); 
    }     
    @TypeConverter 
    publicstaticLongdateToTimestamp(Date date) { 
        returndate ==null?null: date.getTime(); 
    } 
}
ps:也能够存储等价的Unix时间戳。经过  TypeConverter 能够很方便的作到这一点
 
定义数据库时候须要指定类型转换,同时定义号Entity和Dao类
将@TypeConverters添加到AppDatabase中,这样Room就能自动识别这种转换:
@Database(entities = {User.java}, version =1) 
@TypeConverters({Converter.class}) 
publicabstractclass AppDatabase extends RoomDatabase { 
        publicabstractUserDaouserDao(); 
}
@Entity
publicclass User { 
            … 
    privateDate birthday; 
@Dao
publicinterface UserDao {
        ... 
    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
     List<User> findUsersBornBetweenDates(Date from, Date to);
}
关于更多 @TypeConverters的用法,能够参考这里
最后来讲一下数据库升级,或是叫数据库迁移及输出模式
    随着版本迭代,不可避免的会遇到数据库升级的问题,Room也为咱们提供了数据库升级的处理接口。
     Room使用 Migration 来实现数据库的迁移。每一个 Migration 都指定了startVersion 和 endVersion 。在运行的时候Room运行每一个  Migration  的migrate()方法 ,按正确的顺序来迁移数据库到下一个版本。若是没有提供足够的迁移信息,Room会从新建立数据库。
ps:这意味着失去原来保存的信息!!!
Room.databaseBuilder(getApplicationContext(),
             MyDb.class,"database-name").addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
    staticfinalMigration MIGRATION_1_2 =newMigration(1,2) { 
        @Override 
        publicvoidmigrate(SupportSQLiteDatabase database) { 
         database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))”);
        } 
    }; 
   staticfinalMigration MIGRATION_2_3 =newMigration(2,3) { 
        @Override
        publicvoidmigrate(SupportSQLiteDatabase database) { 
         database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER"); 
        } 
    };
    迁移过程结束后,Room将验证架构以确保迁移正确发生。若是Room发现问题,则抛出包含不匹配信息的异常。
再次警告: 若是不提供必要的迁移,Room会从新构建数据库,这意味着将丢失数据库中的全部数据。
输出模式
能够在gradle中设置开启输出模式,便于咱们调试,查看数据库表状况,以及作数据库迁移。
android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}
 
相关文章
相关标签/搜索