2022.1.3 Room数据库
-
安卓常见数据库
-
SQLIte(原生):sqlite.c执行 sql生成文件
-
Room:JetPack组件库,其实就是对SQLIte进行了二次封装
- Room+LiveData+LifeCycle
-
ORM
-
greenDao:华为的,比较成熟
-
-
查看SQLIte数据库具体信息(插件):Database Navigator
Room:
-
概述:
-
Android Jetpack的出现统一了Android开发生态,各种三方库逐渐被官方组件所取代。
Room也同样如此,逐渐取代竞品成为最主流的数据库ORM框架。
这当然不仅仅因为其官方身份,更是因为其良好的开发体验,大大降低了SQLite的使用门槛。
-
使用APT技术(编译期检查)对SQLIte进行二次封装
-
-
优势
-
相对于SQLiteOpenHelper等传统方法,使用Room操作SQLite有以下优势:
1.编译期的SQL语法检查
2.开发高效,避免大量模板代码
3.API设计友好,容易理解
4.可以LiveData关联,具备LiveData Lifecycle 的所有魅力
-
-
注意要先导包:
-
示意图:
-
-
使用:
-
Room的使用,主要涉及以下3个组件
Database: 访问底层数据库的入口
Entity: 代表数据库中的表(table),一般用注解
Data Access Object (DAO): 数据库访问者
这三个组件的概念也出现在其他ORM框架中,有过使用经验的同学理解起来并不困难:
通过Database获取DAO,然后通过DAO查询并获取entities,
最终通过entities对数据库table中数据进行读写
-
-
设计理念:
-
实体加上注解--->数据表;将Dao暴露给用户,用户实现增删改查即可
-
示意图:
-
-
三大角色(APT注解处理器扫描注解,自动生成代码,这种比反射生成代码快得多)
-
角色:
- Student(Entity)
- StudentDao:让用户面向Dao层
- StudentDatabase
-
示意图:
-
ROOM数据库使用
-
导包:
-
示意图:注意java与Kotlin是不一样的
-
-
构造数据表:@Entity
- 概述:使用注解进行字段标注,注意要生成对应的get/set方法
-
代码示意
package com.example.roomdemo01.simple1.entity; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; //面向注解进行开发,完成数据表的建立 @Entity public class Student { // 主键 SQL 唯一的 autoGenerate自增长 @PrimaryKey(autoGenerate = true) private int uid; @ColumnInfo(name = "name")//以注解中的名字为例 private String name; @ColumnInfo(name = "pwd") private String password; @ColumnInfo(name = "address") private int address; //主键是自增的。所以在构建函数中不要传进来 public Student(String name, String password, int address) { this.name = name; this.password = password; this.address = address; } public int getUid() { return uid; } public void setUid(int uid) { this.uid = uid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getAddress() { return address; } public void setAddress(int address) { this.address = address; } @Override public String toString() { return "Student{" + "uid=" + uid + ", name='" + name + ''' + ", password='" + password + ''' + ", address='" + address + ''' + '}'; } } -
构造Dao层:对数据表进行增删改查
-
概述:
- 使用注解将增删改查操作封装在Dao层,待会儿用户直接调用就行了
-
代码示意:
package com.example.roomdemo01.simple1.dao; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; import androidx.room.Update; import com.example.roomdemo01.simple1.entity.Student; import com.example.roomdemo01.simple1.entity.StudentTuple; import java.util.List; @Dao public interface StudentDao { @Insert void insert(Student... students);//相当于一个数组 @Delete void delete(Student student);//删除是根据特定条件删除的,更新同理 @Update void update(Student student); //SQL语句才是最灵活的 @Query("select * from Student") List<Student> getAll(); // 查询一条记录 @Query("select * from Student where name like:name") Student findByName(String name); // 数组查询 多个记录 @Query("select * from Student where uid in(:userIds)") List<Student> getAllId(int[] userIds); // 就是查询 name pwd 给 StudentTuple 类接收 @Query("select name,pwd from Student") StudentTuple getRecord(); }
-
细节:
- 这是一个接口:会使用APT技术去生成子类完成功能
-
后端框架历史
- HliXXX:这个灵活
- Mybatis:更加灵活
-
-
构造DatabaseStudent:暴露出Dao给用户
-
概述:
-
注解说明:
- 指定具体表,
- 指定数据库版本(不要写-1,按道理这个是不能降级,只能升级)
- 写好导出模式
-
-
代码示意:
package com.example.roomdemo01.simple1.db; import androidx.room.Database; import androidx.room.RoomDatabase; import com.example.roomdemo01.simple1.dao.StudentDao; import com.example.roomdemo01.simple1.entity.Student; // 为了养成好习惯 规则 ,要写 exportSchema = false 导出模式 // 特色说法降级 @Database(entities = {Student.class}, version = 1, exportSchema = false) public abstract class AppDataBase extends RoomDatabase { // 暴露dao public abstract StudentDao userDao(); }
-
-
用户:只需要面向Dao进行操作
-
代码示意:
package com.example.roomdemo01.simple1.ui; import android.os.Bundle; import android.util.Log; import androidx.appcompat.app.AppCompatActivity; import androidx.room.Room; import com.example.roomdemo01.R; import com.example.roomdemo01.simple1.dao.StudentDao; import com.example.roomdemo01.simple1.db.AppDataBase; import com.example.roomdemo01.simple1.entity.Student; import com.example.roomdemo01.simple1.entity.StudentTuple; import java.util.List; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 数据库的操作应该是在子线程 DbTest t = new DbTest(); t.start(); } // 测试我们刚刚写的 三个角色Room数据库 增删改查 public class DbTest extends Thread { @Override public void run() { // 数据库的操作都在这里 AppDataBase DerryDB = Room.databaseBuilder(getApplicationContext(),//建造者设计模式 AppDataBase.class, "DerryDB")//数据库名字 // 可以设置强制主线程,默认是让你用子线程,用子线程不会引起阻塞(耗时操作都放到子线程) // .allowMainThreadQueries() .build(); StudentDao dao = DerryDB.userDao(); dao.insert(new Student("Derry0", "123", 1)); dao.insert(new Student("Derry1", "456", 2)); dao.insert(new Student("Derry2", "789", 3)); dao.insert(new Student("Derry3", "111", 4)); // 查询全部数据,调用相应的函数 List<Student> all = dao.getAll(); Log.d("Derry", "run: all.toString():" + all.toString()); Log.i("Derry", "--------------------------"); // 查询名字为 Derry3 的一条数据 Student Derry3 = dao.findByName("Derry3"); Log.d("Derry", "run: Derry3.toString():" + Derry3.toString()); Log.i("Derry", "--------------------------"); // 查询 2 3 4 uid的三条数据 List<Student> allID = dao.getAllId(new int[]{2, 3, 4}); Log.d("Derry", "run: allID.toString():" + allID.toString()); Log.i("Derry", "--------------------------"); // 查询student表里面的数据 到 StudentTuple里面去 StudentTuple record = dao.getRecord(); Log.d("Derry", "run: record.toString():" + record.toString()); } } }
-
-
运行结果
-
运行截图:
-
-
ROOM缺点:
-
推荐是使用通配符 *,但是只是想去查某几个字段
-
解决办法:再次创建一个新的类(不能加Entitiy,加了就是一张表了),只保存相差的那几个字段,然后用这个类去接收查询返回的值;
-
就是克隆出一个小点的容器
-
工程结构:
-
代码示意:
package com.example.roomdemo01.simple1.entity; import androidx.room.ColumnInfo; // @Entity 不能加,加了就是一张表了 public class StudentTuple { @ColumnInfo(name = "name") public String name; @ColumnInfo(name="pwd") public String password; public StudentTuple(String name, String password) { this.name = name; this.password = password; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "StudentTuple{" + "name='" + name + ''' + ", password='" + password + ''' + '}'; } }
-
使用插件插看SQLIte数据库
-
插件安装:
-
搜索插件
-
安装成功后,侧边栏会出现Data Browser
-
-
运行上述代码:向SQLIte数据库添加内容
-
导出数据库文件
-
点击右侧侧边栏中的Device Explorer
-
找到此时AS打开的项目:data/data/项目包名
把这三个文件右键另存为:建立放在桌面上,待会儿好找,还要用的
-
点击DB Browser--->绿色 +
-
选择SQLIte
-
将另存为的那三个文件中的DB文件添加进来:(先点击绿色,红色才出来,点红色,找DB)
就是找这个蓝色框的文件,是找它另存为后的副本,因为原文件备份并且原文件隐藏的太深
-
找到了点击OK
-
测试链接+apply
-
这个时候,数据库表就出来了
-
打开SQL命令行
-
结果展示:
-
Google 标准架构模式
-
概述:MVVM+JetPack全家桶
-
MVVM:
-
View层(UI层):Activity、Fragment
-
VM层:保证View层稳定
- LiveData
-
Model层:模型数据
-
仓库层:数据仓库(这个是很麻烦的)
-
有很多个数据源
- 暴露Dao层
-
服务器:
- 暴露网络接口
-
-
示意图:
-
LiveData如何进行关联
-
使用APT注解处理器系统生成完整的SQL语句
-
进入源码:
-
进入invalidate函数:
-
进入mInvalidationRunnable线程:判断被观察者此时是否存活
-
进入mRefreshRunnable线程:当存活时将value进行post;value就是LiveData<>中的泛型类
- 调用postValue(value):回调到MainActivity中,实现数据驱动UI
ROOM是怎么会观察数据的
-
概述:
- ROOM监听数据改变(invalidate函数调用时机),判断被观察者是否存活,然后在刷新UI
- 当增加了LiveData,就会自动加一个Observer
- 当界面不可见的时候,是不会干活的
- 整合了LiveData+LifeCycle
-
实在要纠结就看源码,这里面是存在弱引用的
架构模型:多状态开发模式
-
概述
- 存在十多个ViewModel,和一个DataBinding
- 存在一个StateViewModel,这个东西来跟DataBinding进行双向绑定;
- 其余的ViewModel(RequestViewModel等)拿到model层数据,交给StateViewModel;然后在通信
-
为什么StateViewModel只有一个
-
因为界面就只有一个,界面是拿给DataBinding进行管理的
-
数据库升级:
-
细节:
-
参数设置:
-
-
概述:
- 数据库升级:表结构改变(增删字段)
- 代码位置:
-
暴力升级:数据会丢失,慎用(删库跑路)
-
增加字段:增加类属性,增加相应的set/get方法
-
直接升级:
-
-
稳定升级:写个方法+里面整个SQL脚本
-
改变表结构:注意要加上注解
-
修改数据库version:从1到2(就是加个1)
-
设计具体调用函数:使用SQL脚本完成数据变化
-
在建造者(build()函数中稳定升级):链式调用,数据库版本从1升级到2
-
测试数据库是否升级成功
-
使用插件查看数据库导出文件
- 导出数据库文件
- 插件建立连接(同上)
- 查看成功--->升级成功
-
细节:
-
凡是修改了数据表结构(新增了字段)就要进行数据库升级
- 必须保证数据库表与数据库版本相同
- 数据库一般情况是不能降级的
-
数据库降级
-
非要删除一个字段,但是要保证数据库的稳定性,这个时候就需要特殊手法了(SQL四步法)
-
SQL四步法
- 建立临时表
- 将之前的旧表复制到临时表
- 删除旧表
- 将临时表名改成旧表
-
代码:
static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { // SQL 四步法 // 1.先建立临时表 // database.execSQL("create table student_temp (uid integer primary key not null,name text,pwd text,addressId)"); // 2.把之前表的数据(SQL语句的细节,同学们可以上网查询下) // database.execSQL("insert into student_temp (uid,name,pwd,addressid) " + " select uid,name,pwd,addressid from student"); // 3.删除student 旧表 // database.execSQL("drop table student"); // 4.修改 临时表 为 新表 student // database.execSQL("alter table student_temp rename to student"); } };
-
数据库跨级:
- 写成2--->9是可以的,但是写了2--->9,就不能写2--->8了
ROOM源码
-
APT技术
-
导入注解处理器:注意所用的语言版本
-
生成两个类
原始工程结构
-
单类分析:AppDataBase_Impl
-
createOpenHelper 调用时机
-
源代码
-
MainActivity中的build()函数
-
build()中的init()
-
调用createOpenHelper 函数
-
再次点击:
-
跳转到build文件夹里面了
-
细节:跳转到build文件夹里面了
- 编译调用build函数后使用APT技术,扫描所有注解,自动生成代码继承程序猿编写的AppDataBase类
-
单类分析:StudentDao_Impl
-
这是Dao层使用APT技术生成的代码:
- 编译后扫描所有注解,生成代码,拼接生成SQL语句
ROOM数据库细节
-
优势:
- 由于架构模式决定其可以搭配LiveData进行操作
- APT技术:自动生成代码,减少SQL语句的编写(会补充上事务)
-
缺点:
- API比较少,很多功能还不完善,还是需要写SQL语句