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()]
            }
        }
    }
}
 
posted @ 2018-05-31 11:44  想不起来的角落  阅读(476)  评论(0编辑  收藏  举报