Android Architecture Components系列之Room数据持久化方案

上一篇说完了Android组件架构中的ViewModel和LiveData,地址:ViewModel和LiveData,两者配合使用确实比传统的app架构更加优雅,更加职责分离。而除了这种app架构,google还推出了一个关于数据持久化方案的组件Room。

何为Room?

Room是一套基于Sqlite数据库封装的一套框架组件,旨在简化Android中对数据库的操作。我们都知道Android原生的数据库是Sqlite,但是有一定的缺陷:

  1. api虽然很多,但是使用起来相对麻烦。
  2. 没有编译时的SQL检查机制,当表发生改变的时候,需要手动更新受影响的查询,耗费时间且容易出错。
  3. 对查询结果没有很好的封装,需要手动把查询结果转换成相应的对象。

Room可以大大简化我们对数据库的操作。类似的框架有GreenDao、OrmLite、Litepal等等,也都比较优秀,但Room毕竟是官方提供的框架,所以还是有必要学习一下的。

Room组件主要分为以下三个部分。

  1. @Entity注解,用来标注实体类,对应数据库中的表,实体类的字段对应数据库中的列,可以设置列名、索引、主键外键等等。
  2. @Dao注解,用来标注对数据库提供增删改查方法的接口,但不用提供实现,编译时会采用apt技术动态生成该接口的实现类。
  3. @Database注解,用来标注提供操作数据库对象的类。

说说具体使用,先跑起来,然后再深入,需要两张表。

  1. 商品分类表(category):包含的字段有:cid(分类id)、cname(分类名称)、cstatus(分类状态)。
  2. 商品表(product):包含的字段有:pid(商品id),pname(商品名称),pno(商品所属分类,依赖于分类表中的cid字段)

使用之前需要先添加依赖:

dependencies {
    ...

    // 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'

}

1.新建实体类:

category:

@Entity(tableName = "category")
public class Category {

    @ColumnInfo(name = "cid")
    @PrimaryKey
    public int cid;

    @ColumnInfo(name = "cname")
    public String cname;

    @ColumnInfo(name = "cstatus")
    public int cstatus;

}

product:

@Entity(tableName = "product")
public class Product {
    @PrimaryKey
    private int pid;
    private String pname;
    private int pno;

    public int getPid() {
        return pid;
    }

    public void setPid(int pid) {
        this.pid = pid;
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public int getPno() {
        return pno;
    }

    public void setPno(int pno) {
        this.pno = pno;
    }
}

需要注意两点:

  1. 用@Entity标注实体类的时候,如果不指定表名,则默认为类名,sqlite中数据库大小写敏感;属性如果不指定列名,则列名是属性名,也可以在该属性上通过@ColumnInfo注解指定该属性对应的列名。
  2. 实体类的属性要么是public,要么是private,是private时需要提供get和set方法。
  3. 每个实体类至少指定一个主键。如果不指定主键,编译会报错。用@PrimaryKey标记属性即可。

2.新建Dao:

CategoryDao:

@Dao
public interface CategoryDao {
    //插入
    @Insert
    void insert(Category... categories);

    //删除
    @Delete
    void delete(Category category);

    //修改
    @Update
    int update(Category... categories);

    //查询
    @Query("select * from category where cid = :cid")
    List<Category> Category(int cid);
}

ProductDao:

@Dao
public interface ProductDao {
    //插入
    @Insert
    void insert(Product... categories);

    //删除
    @Delete
    void delete(Product category);


    //修改
    @Update
    void update(Product... categories);

    //查询
    @Query("select * from product where pname = :pname")
    List<Product> Category(int pname);
}

有以下几点需要注意:

1. insert的时候,返回值可以是void,也可以是long[]或者List,表示插入行的rowId数组,如果只插入一条,返回值还可以是Long。参数也不固定,可以是List,也可以是Category[]、可变参数、或者是一个Category对象,只要至少包含一个Category对象即可。
2. delete的时候,如果传入的参数是一个对象,则会根据该对象的主键去删除该行。
3. update时,Room会把对应的参数信息更新到数据库里面去(会根据参数里面的primary key做更新操作),返回修改的行数。
4. @Query不仅仅可以查询,同样可以实现增加、删除、修改,只不过用@Query标记方法时,需要提供sql语句。比如:delete from category where cid =:cid,cid是需要传入的参数。返回值的定义可参考以上三点。

3.新建DataBase:

新建xxDataBase类,该类需要继承自RoomDatabase,且必须提供一个0参数,返回值为xxDao的方法,该类必须是抽象的。

@Database(entities = {Category.class,Product.class},version = 1,exportSchema = false)
public abstract  class MyDataBase extends RoomDatabase {

    //返回CategoryDao
    public abstract CategoryDao getCategoryDao();

    //返回CategoryDao
    public abstract ProductDao getProductDao();
}

需要用@Database注解注解该类,并指定其所拥有的表对应的实体类的Class数组,必须version指定数据库的版本号。exportSchema 先指定为false,否则会报错。

如何使用:
向category表中插入两条数据,product中插入四条数据:

public class MainActivity extends AppCompatActivity {
    private MyDataBase myDataBase;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                roomDemo();
            }
        }).start();


    }

    private void roomDemo() {
        myDataBase = Room.databaseBuilder(this, MyDataBase.class, "MyDataBase").build();

        Category category1 = new Category();
        category1.cid = 1;
        category1.cname = "手机数码";
        category1.cstatus = 0;

        Category category2 = new Category();
        category2.cid = 2;
        category2.cname = "家电家居";
        category2.cstatus = 0;

        Category category3 = new Category();
        category3.cid = 3;
        category3.cname = "服饰箱包";
        category3.cstatus = 0;

        myDataBase.getCategoryDao().insert(category1, category2,category3 );


    }
}

用SQLiteStudio查看数据:


在这里插入图片描述

emmmmm~


在这里插入图片描述

好了,这只是简单的使用,下面来深入讲一讲各个注解的使用。需要注意的是,访问数据库需要在子线程中执行,在主线程中执行会提示如下错误:


在这里插入图片描述

4.深入@Entity注解。

看下@Entity的源码,看下该注解可以设置哪些值:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Entity {
   
    String tableName() default "";

    Index[] indices() default {};

    boolean inheritSuperIndices() default false;

    String[] primaryKeys() default {};

    ForeignKey[] foreignKeys() default {};
}

这里删除了所有注释。tableName是设置表名,前面已经说过,下面说说剩下的值的作用。

1. indices
用于设置索引,具体用法,假设给Category的cid和cname设置索引。则:

@Entity(tableName = "category", indices = {@Index(value = {"cid", "cname"})})
public class Category {

    @ColumnInfo(name = "cid")
    @PrimaryKey
    public int cid;

    @ColumnInfo(name = "cname")
    public String cname;

    @ColumnInfo(name = "cstatus")
    public int cstatus;

}

indices 是一个索引数组,所以可以设置不止一个索引。而索引用@Index标记。看下@Index:

@Target({})
@Retention(RetentionPolicy.CLASS)
public @interface Index {
    String[] value();

    String name() default "";

    boolean unique() default false;
}

value用于指定索引的列名数组,name是指定索引的名称,unique是指定索引是唯一索引还是非唯一索引,唯一索引数据不允许出现相同的两列,就像主键一样,默认是非唯一索引。

  1. inheritSuperIndices
    是否自动继承父类的索引。

3.primaryKeys
用于指定主键,上面我们在属性上方用@PrimaryKey去指定该属性为主键,而primaryKeys是@Entity注解中的值,同样可以用来指定主键,可以指定一个主键,也可以指定多个主键:

@Entity(tableName = "category",primaryKeys = {"cid","cname"})
public class Category {

    @ColumnInfo(name = "cid")
    @PrimaryKey
    public int cid;

    @ColumnInfo(name = "cname")
    public String cname;

    @ColumnInfo(name = "cstatus")
    public int cstatus;

}

这里就指定了cid和cname为主键。

  1. foreignKeys
    用于指定数据表的外键。用@ForeignKey定义外键,假设product的pno字段来源于category的cid字段,则cid字段就是product中pno的外键。则外键的定义如下:
@Entity(tableName = "category")
public class Category {

    @ColumnInfo(name = "cid")
    @PrimaryKey
    public int cid;

    @ColumnInfo(name = "cname")
    public String cname;

    @ColumnInfo(name = "cstatus")
    public int cstatus;

}



@Entity(tableName = "product", foreignKeys = @ForeignKey(entity = Category.class,
                                                         parentColumns = "cid", 
                                                         childColumns = "pno"))
public class Product {
    @PrimaryKey
    private int pid;
    private String pname;
    private int pno;

    public int getPid() {
        return pid;
    }

    public void setPid(int pid) {
        this.pid = pid;
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public int getPno() {
        return pno;
    }

    public void setPno(int pno) {
        this.pno = pno;
    }
}

entity指定外键字段所在的表,parentColumns 指定外键字段,是个字符串数组,只有一个时可以省略大括号。childColumns 指定需要定义外键的字段。还有两个值onDelete和onUpdate分别指定当parent表(category)删除和更新的时候,child表(product)可以做的操作:有以下几个值:

    int NO_ACTION = 1;//当parent中的key有依赖的时候禁止对parent做动作,做动作就会报错。

    int RESTRICT = 2;//当parent中的key有依赖的时候禁止对parent做动作,做动作就会报错。

    int SET_NULL = 3;//如果parent表中有更新或删除操作,child表中对应的字段置为null

    int SET_DEFAULT = 4;//如果parent表中有更新或删除操作,child表中对应的字段置为默认值

    int CASCADE = 5;如果parent表中有更新或删除操作,child表中会删除对应的记录。

默认是NO_ACTION。会去先检查child表和parent的外键约束。RESTRICT 和NO_ACTION 效果一样。这里验证下CASCADE ,category删除cid=2的记录,看看结果是报错还是product中pno=2的所有记录被删除:


在这里插入图片描述

程序没有报错,而且product中pno=2的所有记录都被删除了,说明是成立的。至于SET_NULL 和SET_DEFAULT 大家有兴趣可以自行验证。onUpdate和onDelete一样,这里就不再赘述了。

5.深入@Insert和@Update:

@Insert和@update是用来标注xxDao中增删改查的方法的。当然还有@Query和@Delete,但是这两个还有未挖掘的属性,所以单独拎出来说一下:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface Insert {
    /**
     * What to do if a conflict happens.
     * @see <a href="https://sqlite.org/lang_conflict.html">SQLite conflict documentation</a>
     *
     * @return How to handle conflicts. Defaults to {@link OnConflictStrategy#ABORT}.
     */
    @OnConflictStrategy
    int onConflict() default OnConflictStrategy.ABORT;
}


public @interface Update {
    /**
     * What to do if a conflict happens.
     * @see <a href="https://sqlite.org/lang_conflict.html">SQLite conflict documentation</a>
     *
     * @return How to handle conflicts. Defaults to {@link OnConflictStrategy#ABORT}.
     */
    @OnConflictStrategy
    int onConflict() default OnConflictStrategy.ABORT;
}

可以看到,这两个注解都可以设置一个onConflict属性。表示插入或更新出现冲突时,比如category新增了一行已经存在的分类,数据库所采取的策略。有以下几种模式:


    int REPLACE = 1;//冲突时取代旧数据同时继续事务。
   
    int ROLLBACK = 2;//冲突时回滚事务。
 
    int ABORT = 3;//冲突时终止事务。
    
    int FAIL = 4;//冲突时事务失败。
  
    int IGNORE = 5;//冲突时忽略冲突。

默认都是ABORT,回滚成冲突之前的样子。现在我们往category插入一天和cid=3一样的记录,结果程序虽然没有插入该条记录,但是也不会报错,而是把冲突忽略掉了。如果是默认的ABORT ,插入时程序则会报错。设置为REPLACE 时,发生冲突时,会把旧数据取代,解决冲突并继续事务,程序也不会报错。

6.深入@Query注解:

@Query不仅能用来查询,还能用来删除和修改,只需要写一个sql语句,并把所需要的参数传给方法即可。
查询所有分类信息:

 @Query("select * from category")
 List<Category> query(int cid);

根据cid查询:

@Query("select * from category where cid = :cid")
List<Category> query(int cid);

可以携带多个条件:

@Query("select * from category where cid = :cid and cname=:cname")
List<Category> query(int cid,String cname);

查询结果可以是List,也可以是Category[],甚至你确定只会有一条记录,还可以返回一个Category对象。

利用@Query注解更新数据:

 @Query("update category set cname=:cname where cid=:cid")
 int update(int cid,String cname);

update结果可以返回void,也可以返回int,表示受影响的行数。

还可以配合LiveData一起使用,如果对LiveDate还不了解的朋友,可以看看这一篇博客:LiveData和ViewModel使用。这里不再深究:

@Query("select * from category where cid = :cid")
LiveData<List<Category>> query(int cid);

还可以配合大名鼎鼎的RxJava使用:

@Query("select * from category where cid = :cid")
 Flowable<List<Category>> query(int cid);

拿到 Flowable<List>对象后就可以调用RxJava的各种api了:

myDataBase.getCategoryDao()
           .query(1)
           .subscribeOn(Schedulers.io())
           .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Consumer<List<Category>>() {
                    @Override
                    public void accept(List<Category> entities) {
                    //TODO
                 }
           });

7.深入@Database注解

@Database注解包含的属性:

  1. entities:数据库相关的所有Entity实体类,他们会映射成数据库中的表。
  2. version:数据库版本。
  3. exportSchema:默认true,也是建议传true,这样可以把Schema导出到一个文件夹里面。

在运行时,可以通过调用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()两种方式获取RoomDatabase实例。但两者有些区别:

  1. Room.databaseBuilder():生成Database对象,并且创建一个存在文件系统中的数据库。
  2. Room.inMemoryDatabaseBuilder():生成Database对象并且创建一个存在内存中的数据库。当应用退出的时候(应用进程关闭)数据库也消失。

生成数据库对象需要比较大的开销,建议写成单例的形式或者在Application中进行初始化。

采用Room.databaseBuilder()生成数据库对象时,还有以下api可以调用:

  /**
     * 默认值是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);

8.数据库升级和数据迁移

数据库升级和数据的迁移需要用到Migration这个类,这个类中定义了我们我们版本升级时需要做的操作,然后再把它给数据库对象,由数据库对象来替我们完成版本的升级。举个例子?现在要在category表中新增一列描述信息列(cdesc)。假设数据库版本从1升级到2。
首先定义数据库升级所需要做的操作:

/**
     * 数据库版本 1->2 category表新增了cdesc列
     */
    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE User ADD COLUMN age varchar(100)");
        }
    };

定义完成后,需要在数据库初始化的时候将其配置给数据库对象:

myDataBase = Room.databaseBuilder(this, MyDataBase.class, "MyDataBase")
                //配置数据库升级所要做的操作
                .addMigrations(MIGRATION_1_2)
                .build();

注意: Entity中能用Integer的时候不用int。表中的结构发生变化或者新增表时,版本号都需要增加,否则会直接cracsh;增加版本号的同时,也需要提供Migration对象,否则也会崩溃。

最开始我们说过,@Database的属性exportSchema,我们设置了false,但这是不被推荐的。其默认值是true,建议传true,但之前为了不报错,我们设置成了false。现在让他默认true。这样可以把Schema导出到一个文件夹里面。 表的信息会以json文件的形式存在,该文件代表了你的数据库表历史记录,这样允许Room创建旧版本的数据库用于测试。只需要在app的build.gradle中简单的配置即可:

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
    // 用于测试
    sourceSets {
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

执行项目后,json文件就会被存储到如下路径:


在这里插入图片描述

关于Room的部分先写到这里,有未尽的欢迎各位指正,我会找时间加上,就酱,end~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值