写点什么

Android 从零开始搭建 MVVM 架构(4),Android 开发进阶吃透这一篇必拿 60W 年薪

用户头像
Android架构
关注
发布于: 2021 年 11 月 05 日

[Android 从零开始搭建 MVVM 架构(6)————使用玩 Android API 带你搭建 MVVM 框架(初级篇)](


)


[Android 从零开始搭建 MVVM 架构(7) ———— 使用玩 Android API 带你搭建 MVVM 框架(终极篇)](


)


还是那张图 AAC(Android Architecture Components)



这篇我们讲 Room,让我们了解和认识 Room 后,最终运用到我们的 MVVM 的项目中去。本文是自己的总结,如有错误,请指正

一、Room 介绍和简单认识

简介


Room 是 google 为了简化旧式的 SQLite 操作专门提供的一个覆盖 SQLite 抽象层框架库


作用


实现 SQLite 的增删改查(通过注解的方式实现增删改查,类似 Retrofit。)


在使用 Room,有 4 个模块:


  • Bean:实体类,表示数据库表的数据

  • Dao:数据操作类,包含用于访问数据库的方法

  • Database:数据库持有者 & 数据库版本管理者

  • Room:数据库的创建者 & 负责数据库版本更新的具体实现者


与 greendao 的区别(这里只是简单从表面看):同样基于 ORM 模式封装的数据库。而 Room 和其他 ORM 对比,具有编译时验证查询语句正常性,支持 LiveData 数据返回等优势。我们选择 room,更多是因为对 LiveData 的完美支持。同时也支持 RxJava,我们都知道数据库操作这些耗时操作都应该放在子线程里,所以配合 RxJava 和 LiveData 很完美了。因为他们都是异步的


//添加 Room 的依赖 implementation 'android.arch.persistence.room:runtime:2.1.4'annotationProcessor 'android.arch.persistence.room:compiler:2.1.4'

二、Bean:实体类,表示数据库表的数据

意思就是我们要往数据库里建表、建字段。就是使用这个 bean 对象。首先介绍下注解


  • @Entity : 数据表的实体类。

  • @PrimaryKey : 每一个实体类都需要一个唯一的标识。

  • @ColumnInfo : 数据表中字段名称。

  • @Ignore : 标注不需要添加到数据表中的属性。

  • @Embedded : 实体类中引用其他实体类。

  • @ForeignKey : 外键约束。


这里我们建一个 Person 类(为了能保存数据,使数据持久化且 Room 必须能够对它进行操作,你可以用 public 修饰属性,或者你也可以设置成 private,但必须提供 set 和 get 方法)。这里只是简单展示,后面详细讲解,觉得细节太多了


表名为 person 的表


@Entitypublic class Person {public Person(String name, int age) {this.name = name;this.age = age;}


@PrimaryKey(autoGenerate = true)@ColumnInfo(name = "uid")private int uid;


private String name;private int age;


@Ignoreprivate int money;@Embeddedprivate Address address;


//...我用的是 private,暂且去掉了 set 和 get 方法。便于读者理解}


public class Address {private String city;private String street;//...省略部分代码,便于理解}

2.1、@Entity

用了 @Entity 标注的类,表示当前类的类名作为表名,这个类里面的所有属性,作为表里的字段。这里我们先只关注 @Entity 来讲,后面又很多细节,文章接下来都以这种讲解分格。更加直击重点

2.1.1、如果不想用类名作为表名,我们可以这样

//这样的话,我们的表名就变成了 other@Entity(tableName = "other")public class Person {}

2.1.2、@Entity 里的复合主键

在 Person 里,我们用 @PrimaryKey(autoGenerate = true)标识 uid 为主键,且设置为自增长。设置为主键的字段不得为空也不允许有重复值。


复合主键:多字段主键则构成主键的多个字段的组合不得有重复(假如我们用 name 做主键,如果我们有 2 个 name 相同的数据一起插入,数据就会被覆盖掉。但是现实中真的有同名的人,是 2 条数据,这时候我们就要用 name 和出生日期作为复合主键也就是多个主键,主键都一致才会覆盖数据)


@Entity(primaryKeys = {"uid","name"})public class Person {}


直接这样设置后,运行项目。这里有几点要注意的:


  • 首先会报错:You must annotate primary keys with @NonNull. "name" is nullable。所以要加上,


@Entity(primaryKeys = {"uid","name"})public class Person {//name 字段要用 @NonNull 标注 @NonNullprivate String name;}

2.1.3、@Entity 里的索引的使用

索引的使用(有单列索引和组合索引,还有索引的唯一性)


//单列索引 @Entity(indices = {@Index(value = "name")})//单列索引唯一性 @Entity(indices = {@Index(value = "name", unique = true)})//组合索引 @Entity(indices ={@Index(value = {"name","age"})})//组合索引唯一性 @Entity(indices ={@Index(value = {"name","age"},unique = true)})//当然可以混起来用 如下:@Entity(indices ={@Index(value = "name"),@Index(value = {"name","age"},unique = true)})public class Person {


}


  • 数据库索引是用来提高数据库访问速度的,可以说单纯是优化的意思。我们加上索引后,之后的其他操作都没有变的

  • 如果加上唯一性有点类似主键,重复数据会报错,但是索引并不像主键那样,作为条件才能去覆盖数据

  • 插入数据的时候加上动作 @Insert(onConflict = OnConflictStrategy.REPLACE)加上动作,他的意思是主键相同的话,旧数据会替换新数据。但如果我们主键不同,但加了索引唯一性的话,索引相同的话,这次插入则失败。相信这么说,应该明白了

2.1.4、@Entity 里的外键约束

同样以之前的 Person 作为父类,我们再定一个衣服类 Clothes。(这里先省略 Dao,Database,Room 步骤,后面会细讲)


Clothes:


@Entity(foreignKeys = @ForeignKey(entity = Person.class,parentColumns = "uid",childColumns = "father_id"))public class Clothes {@PrimaryKey(autoGenerate = true)private int id;private String color;private int father_id;//...省略 get 和 set}


好多人不知道外键约束是什么意思,这里我们先往里面插数据,然后我们看看 db 里的数据:


第一步:我们往 Person 里面插入 2 填数据


1、(uid =1 name = 岩浆 age =18)


2、(uid =2 name = 小学生 age=10);


第二部:我们往衣服里面插入 3 条数据


1、(id = 1 color = 红色 father_id = 1)


2、(id = 2 color = 黑色 father_id = 1)


3、(id = 3 color = 红色 father_id = 2)


这里其实显而易见,可以先认为,person 岩浆有 2 件衣服,红色和黑色的衣服;person 小学生有 1 件衣服,红色的衣服。我们看看表是怎么样的。意思就是用 parentColumns = "uid"(person 的 uid 字段)作为 childColumns = "father_id"(clothes 的 father_id 字段)。这里就相当于约束到了。先不急,我们看看 2 张表。


person 表(后面会有教程,教你怎么看 db 数据库):



clothes 表



那么为什么说是外键约束呢?当然这里有操作。如下:


@Entity(foreignKeys = @ForeignKey(onDelete = CASCADE,onUpdate = CASCADE,entity = Person.class,parentColumns = "uid",childColumns = "father_id"))public class Clothes {


}


这里我加了 2 个动作,在删除和更新的时候用了 onDelete = CASCADE,onUpdate = CASCADE。这里动作有以下:


  • NO_ACTION:当 person 中的 uid 有变化的时候 clothes 的 father_id 不做任何动作

  • RESTRICT:当 person 中的 uid 在 clothes 里有依赖的时候禁止对 person 做动作,做动作就会报错。

  • SET_NULL:当 person 中的 uid 有变化的时候 clothes 的 father_id 会设置为 NULL。

  • SET_DEFAULT:当 person 中的 uid 有变化的时候 clothes 的 father_id 会设置为默认值,我这里是 int 型,那么会设置为 0

  • CASCADE:当 person 中的 uid 有变化的时候 clothes 的 father_id 跟着变化,假如我把 uid = 1 的数据删除,那么 clothes 表里,father_id = 1 的都会被删除。


现在是不是很清楚了。很多博客都带过。我也费力讲清楚了。给博主个赞把。文章 demo 没有做处理,在观察时,记得请按顺序观察。

2.2、@PrimaryKey

//省略部分代码,便于理解 public class Person {//person 当然不需要符合主键,我们可以直接这样默认 uid 为主键//想要自增长那么这样 @PrimaryKey(autoGenerate = true)@PrimaryKeyprivate int uid;}

2.3、@ColumnInfo

我们都知道,Person 里的属性值名就是表里的字段名。假如不像用属性名当字段名,可以这样


//省略部分代码,便于理解 public class Person {//那么这个时候我的主键在表里的 key 就是 uid_@ColumnInfo(name = "uid_")private int uid;}

2.4、@Ignore

如果不想要属性值作为表里的字段,那么忽略掉


//省略部分代码,便于理解 public class Person {//让我们忽略调钱,人要钱干嘛。。@Ignoreprivate int money;}

2.5、@Embedded

实体类中引用其他实体类。这样的话 Address 里属性也成为了表 person 的字段。


//省略部分代码,便于理解 public class Person {@Embeddedprivate Address address;}


我们 Address 里有 2 个字段,city,street,所以我们的表也是



这里有个特殊的地方,比如说这个人很有钱(刚刚才忽略掉钱),有 2 个家,有 2 个 Address 类,那么怎么办呢,


//@Embedded(prefix = "one"),这个是区分唯一性的,比如说一这个人可能有 2 个地址类似于 tag,那么在数据表中就会以 prefix+属性值命名 @Embedded(prefix = "one")private Address address;@Embedded(prefix = "two")private Address address;

三、Dao:数据操作类,包含用于访问数据库的方法

这里直接上代码,相关标注是:


  • @Dao : 标注数据库操作的类。

  • @Query : 包含所有 Sqlite 语句操作。

  • @Insert : 标注数据库的插入操作。

  • @Delete : 标注数据库的删除操作。

  • @Update : 标注数据库的更新操作。


@Daopublic interface PersonDao {//查询所有数据 @Query("Select * from person")List<Person> getAll();


//删除全部数据 @Query("DELETE FROM person")void deleteAll();


//一次插入单条数据 或 多条// @Insert(onConflict = OnConflictStrategy.REPLACE),这个是干嘛的呢,下面有详细教程 @Insertvoid insert(Person... persons);


//一次删除单条数据 或 多条 @Deletevoid delete(Person... persons);


//一次更新单条数据 或 多条 @Updatevoid update(Person... persons);


//根据字段去查找数据 @Query("SELECT * FROM person WHERE uid= :uid")Person getPersonByUid(int uid);


//一次查找多个数据 @Query("SELECT * FROM person WHERE uid IN (:userIds)")List<Person> loadAllByIds(List<Integer> userIds);


//多个条件查找 @Query("SELECT * FROM person WHERE name = :name AND age = :age")Person getPersonByNameage(String name, int age);}


这里唯一特殊的就是 @Insert。其有一段介绍:对数据库设计时,不允许重复数据的出现。否则,必然造成大量的冗余数据。实际上,难免会碰到这个问题:冲突。当我们像数据库插入数据时,该数据已经存在了,必然造成了冲突。该冲突该怎么处理呢?在 @Insert 注解中有 conflict 用于解决插入数据冲突的问题,其默认值为 OnConflictStrategy.ABORT。对于 OnConflictStrategy 而言,它封装了 Room 解决冲突的相关策略。


  • OnConflictStrategy.REPLACE:冲突策略是取代旧数据同时继续事务

  • OnConflictStrategy.ROLLBACK:冲突策略是回滚事务

  • OnConflictStrategy.ABORT:冲突策略是终止事务

  • OnConflictStrategy.FAIL:冲突策略是事务失败

  • OnConflictStrategy.IGNORE:冲突策略是忽略冲突


这里比如在插入的时候我们加上了 OnConflictStrategy.REPLACE,那么往已经有 uid=1 的 person 表里再插入 uid =1 的 person 数据,那么新数据会覆盖就数据。如果我们什么都不加,那么久是默认的 OnConflictStrategy.ABORT,重复上面的动作,你会发现,程序崩溃了。也就是上面说的终止事务。其他大家可以自己试试

四、Database:数据库持有者 & 数据库版本管理者

直接上代码


//注解指定了 database 的表映射实体数据以及版本等信息(后面会详细讲解版本升级)@Database(entities = {Person.class, Clothes.class}, version = 1)public abstract class AppDataBase extends RoomDatabase {public abstract PersonDao getPersonDao();


public abstract ClothesDao getClothesDao();}

五、Room:数据库的创建者 & 负责数据库版本更新的具体实现者

Room 创建我们的 AppDataBase,我们把它封装成单例,省的每次都去执行一遍,耗性能


public class DBInstance {//private static final String DB_NAME = "/sdcard/LianSou/room_test.db";private static final String DB_NAME = "room_test";public static AppDataBase appDataBase;public static AppDataBase getInstance(){if(appDataBase==null){synchronized (DBInstance.class){if(appDataBase==null){appDataBase = Room.databaseBuilder(MyApplication.getInstance(),AppDataBase.class, DB_NAME)//下面注释表示允许主线程进行数据库操作,但是不推荐这样做。//我这里是为了 Demo 展示,稍后会结束和 LiveData 和 RxJava 的使用.allowMainThreadQueries().build();}}}return appDat


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


aBase;}}


做完这一切,那么我们的准备工作就做完了。让我们来插入一条数据


Person person_ = new Person("Room", 18);DBInstance.getInstance().getPersonDao().insert(person_);

5.1、额外知识点

这里怎么查看 db 数据呢?首先我们把 db 文件存在手机内存里,记得打开存储权限,就是在上面代码里指定路径


private static final String DB_NAME = "/sdcard/LianSou/room_test.db";

插入数据后,就会在手机内存卡生成 db 文件。


拿到 db 文件,怎么办呢。用插件!!Database Navigator,[插件教程](


)

六、数据库版本升级

这里的意思比如我已经往 person 表存里数据。但是我要增加字段,或者是增加索引。如果你直接写上去,你会发现,你再使用数据库的时候,会直接崩溃。怎么办呢,用过 greendao 的人都知道,我们要升级数据库版本


@Entitypublic class Person {

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android从零开始搭建MVVM架构(4),Android开发进阶吃透这一篇必拿60W年薪