参考:jetpack之Room数据库
目录
- 引言
- 一、基本使用
- 1. 导入相关引用
- 2. 建表Entity
- 3. 数据库操作类Dao
- 4. 数据库RoomDatabase
- 5. 简单使用
- 二、ViewModel + LiveData + Room 的结合开发
- 1. 建表Entity
- 2. 数据库操作类Dao
- 3. 数据库RoomDatabase
- 4. 仓库Repository
- 5. ViewModel
- 6. 数据适配器
- 7. UI布局
- 8. Activity的简单运用
- 三、数据库升级
- 1. 新增字段
- 2. 删除字段
- 四. Room的源码原理了解
引言
Android Jetpack的出现统一了Android开发生态,各种三方库逐渐被官方组件所取代。
Room也同样如此,逐渐取代竞品成为最主流的数据库ORM框架。这当然不仅仅因为其官方身份,更是因为其良好的开发体验,大大降低了SQLite的使用门槛。
相对于SQLiteOpenHelper等传统方法,使用Room操作SQLite有以下优势:
- 编译器的SQL语法检查
- 开发高效,避免大量模板代码
- API设计友好,容易理解
- 可以LiveData关联,具备LiveData Lifecycle的所有魅力
Room的使用,主要涉及以下3个组件:
- Database:访问底层数据库的入口
- Entity:代表数据库中的表(table),一般用注解
- Data Access Object(DAO):数据库访问者
通过Database获取DAO,然后通过DAO查询并获取entities,最终通过entities对数据库table中数据进行读写。
Room是SQLite数据库的抽象
一、基本使用
项目整体架构层级:
1. 导入相关引用
app的build.gradle中添加如下配置:
apply plugin: 'com.android.application'
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.myroom2"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
// 导入Room相关依赖
def room_version = "2.0.0-beta01"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" // use kapt for Kotlin
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// Test helpers
testImplementation "androidx.room:room-testing:$room_version"
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
}
2. 建表Entity
Student.java
package com.example.myroom.entity;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
// @Entity注解在类之上,代表是一张数据库表,tableName属性可以指定表的名称,默认为类名
// @Entity(tableName = "Student")
@Entity
public class Student {
// 主键 autoGenerate=true 自增长 从1开始
@PrimaryKey(autoGenerate = true)
private int uid;
// 字段名称,name属性可以指定字段名称,默认为变量名
// 默认不写@ColumnInfo注解,也会在数据库根据变量名新建一个字段。
@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 +
'}';
}
}
StudentTuple.java
package com.example.myroom.entity;
import androidx.room.ColumnInfo;
// @Entity 不能加,不然加了就是一张表了
// 并且 StudentTuple类中的name属性和pwd属性必须要和Student的name属性和pwd属性一致
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 + '\'' +
'}';
}
}
- @Entity:注解在类之上,代表是一张数据库表,tableName属性可以指定表的名称,默认为类名。
- @PrimaryKey:表的主键,autoGenerate =true代表自增长。
- @ColumnInfo:字段名称,name属性可以指定字段名称,默认为变量名。
- @Ignore:该注解表示当前数据库忽略当前属性,不会创建该字段。
默认不写@ColumnInfo注解,也会在数据库根据变量名新建一个字段。
3. 数据库操作类Dao
StudentDao.java
package com.example.myroom.dao;
import com.example.myroom.entity.Student;
import com.example.myroom.entity.StudentTuple;
import java.util.List;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
// 当前类必须是interface
// @Dao注解在类之上,提供给外部操作数据库的方法
@Dao
public interface StudentDao {
// Student...等价于Student[]
@Insert
void insert(Student... students);
@Delete
void delete(Student student);
@Update
void update(Student student);
@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类接收
// StudentTuple类中的name属性和pwd属性必须要和Student的name属性和pwd属性一致
@Query("select name, pwd from Student")
StudentTuple getRecord();
}
- 当前类必须是interface
- @Dao 注解在类之上,提供给外部操作数据库的方法
- @Insert 添加,可以添加单个对象,亦可以是List、Array等集合类型。
- @Query ,可以根据sql语句进行一系列查询、更新、删除操作。
4. 数据库RoomDatabase
AppDataBase.java
package com.example.myroom.db;
import com.example.myroom.dao.StudentDao;
import com.example.myroom.entity.Student;
import androidx.room.Database;
import androidx.room.RoomDatabase;
/**
* @Dataase : 注解在类之上,抽象类,对外提供每张表的操作类
* entities : 所有的数据库表代表的类
* version : 数据库版本,升级数据库需要修改版本号,一般和Migration配合使用
* exportSchems : 存储展示数据库的结果信息,如果不设置的话,需要在database类上配置 exportSchema = false,要不然编译的时候会出现警告
*/
@Database(entities = {Student.class}, version = 1, exportSchema = false)
public abstract class AppDataBase extends RoomDatabase {
// 暴露dao
public abstract StudentDao userDao();
}
- @Database: 注解在类之上,抽象类,对外提供每张表的操作类
- entities:所有的数据库表代表的类
- version:数据库版本,升级数据库需要修改版本好,一般和Migration配合使用
- exportSchema :存储展示数据库的结构信息,如果不设置的话,需要再database类上配置exportSchema = false,要不然编译的时候会出现警告。
5. 简单使用
MainActivity.java
package com.example.myroom.ui;
import androidx.appcompat.app.AppCompatActivity;
import androidx.room.Room;
import android.os.Bundle;
import android.util.Log;
import com.example.myroom.R;
import com.example.myroom.dao.StudentDao;
import com.example.myroom.db.AppDataBase;
import com.example.myroom.entity.Student;
import com.example.myroom.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();
}
public class DBTest extends Thread {
@Override
public void run() {
// 数据库的操作都在这里
AppDataBase db = Room.databaseBuilder(getApplicationContext(),
AppDataBase.class,
"MyRoomDB")
// 可以设置强制主线程,默认是让你用子线程
//.allowMainThreadQueries()
.build();
StudentDao dao = db.userDao();
// U
dao.insert(new Student("test1", "111", 1));
dao.insert(new Student("test2", "222", 2));
dao.insert(new Student("test3", "333", 3));
dao.insert(new Student("test4", "444", 4));
dao.insert(new Student("test5", "555", 5));
// 查询全部数据
List<Student> all = dao.getAll();
Log.d("abc", "run getAll(): all.toString()=" + all.toString());
// 查询名字为test2的一条数据
Student test2 = dao.findByName("test2");
Log.d("abc", "run findByName(): test2.toString()=" + test2.toString());
// 查询uid为2, 3, 4的三条数据
List<Student> allId = dao.getAllId(new int[]{2, 3, 4});
Log.d("abc", "run getAllId(): allId.toString()=" + allId.toString());
// 查询Student包里面的数据 到 StudentTuple里面去
StudentTuple record = dao.getRecord();
Log.d("abc", "run getRecord(): record.toString()=" + record.toString());
}
}
}
log输出结果:
2023-08-31 11:52:37.856 3988-4016/com.example.myroom D/abc: run getAll(): all.toString()=[Student{uid=1, name='test1', password='111', address=1}, Student{uid=2, name='test2', password='222', address=2}, Student{uid=3, name='test3', password='333', address=3}, Student{uid=4, name='test4', password='444', address=4}, Student{uid=5, name='test5', password='555', address=5}]
2023-08-31 11:52:37.862 3988-4016/com.example.myroom D/abc: run findByName(): test2.toString()=Student{uid=2, name='test2', password='222', address=2}
2023-08-31 11:52:37.866 3988-4016/com.example.myroom D/abc: run getAllId(): allId.toString()=[Student{uid=2, name='test2', password='222', address=2}, Student{uid=3, name='test3', password='333', address=3}, Student{uid=4, name='test4', password='444', address=4}]
2023-08-31 11:52:37.882 3988-4016/com.example.myroom D/abc: run getRecord(): record.toString()=StudentTuple{name='test1', password='111'} record.toString()=StudentTuple{name='test1', password='111'}
二、ViewModel + LiveData + Room 的结合开发
项目整体框架结构:
1. 建表Entity
Student.java
package com.example.myroom2.entity;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity
public class Student {
@PrimaryKey(autoGenerate = true)
private int uid;
@ColumnInfo(name = "name")
private String name;
@ColumnInfo(name = "pwd")
private String password;
@ColumnInfo(name = "addressId")
private int addressId;
public Student(int uid, String name, String password, int addressId) {
this.uid = uid;
this.name = name;
this.password = password;
this.addressId = addressId;
}
public Student(String name, String password, int addressId) {
this.name = name;
this.password = password;
this.addressId = addressId;
}
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 getAddressId() {
return addressId;
}
public void setAddressId(int addressId) {
this.addressId = addressId;
}
@Override
public String toString() {
return "Student{" +
"uid=" + uid +
", name='" + name + '\'' +
", password='" + password + '\'' +
", addressId=" + addressId +
'}';
}
}
2. 数据库操作类Dao
StudentDao.java
package com.example.myroom2.dao;
import com.example.myroom2.entity.Student;
import java.util.List;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
@Dao
public interface StudentDao {
@Insert
void insert(Student... students);
@Delete
void delete(Student student);
@Update
void update(Student student);
@Query("select * from Student")
List<Student> getAll();
// 使用LiveData 关联Room
@Query("select * from Student order by uid")
LiveData<List<Student>> getAllLiveDataStudent();
}
3. 数据库RoomDatabase
package com.example.myroom2.db;
import android.content.Context;
import com.example.myroom2.dao.StudentDao;
import com.example.myroom2.entity.Student;
import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
// exportSchema = false 尽量写,内部需要检测,如果没有写,会抛出异常,因为内部要记录升级的所有副本
@Database(entities = {Student.class}, version = 2, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
private static AppDatabase instance;
public static synchronized AppDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class,
"MyRoomDB")
// 可以强制在主线程运行数据库操作
.allowMainThreadQueries()
.build();
}
return instance;
}
// 暴露dao
public abstract StudentDao studentDao();
}
4. 仓库Repository
StudentRepository.java
package com.example.myroom2.repository;
import android.content.Context;
import com.example.myroom2.dao.StudentDao;
import com.example.myroom2.db.AppDatabase;
import com.example.myroom2.entity.Student;
import java.util.List;
import androidx.lifecycle.LiveData;
// 仓库
public class StudentRepository {
private LiveData<List<Student>> liveDataAllStudent; // 触发 改变的 LiveData的数据
private StudentDao studentDao; // 用户操作,只面向Dao
public StudentRepository(Context context) {
AppDatabase database = AppDatabase.getInstance(context);
studentDao = database.studentDao();
if (liveDataAllStudent == null) {
liveDataAllStudent = studentDao.getAllLiveDataStudent(); // 使用LiveData关联Room
}
}
// 下面的代码是:提供一些API给ViewModel使用
// 增
public void insert(Student... students){
studentDao.insert(students);
}
// 删
public void delete(Student student) {
studentDao.delete(student);
}
// 改
public void update(Student student) {
studentDao.update(student);
}
// 查 普通的查询
public List<Student> getAll() {
return studentDao.getAll();
}
// 查 关联LiveData 暴露环节
public LiveData<List<Student>> getLiveDataAllStudent() {
return studentDao.getAllLiveDataStudent();
}
}
5. ViewModel
StudentViewModel.java
package com.example.myroom2.viewmodel;
import android.app.Application;
import com.example.myroom2.entity.Student;
import com.example.myroom2.repository.StudentRepository;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
// AndroidViewModel 保证数据的稳定性
// AndroidViewModel与ViewMOdel的区别是AndroidViewModel多了一个application环境
public class StudentViewModel extends AndroidViewModel {
private StudentRepository studentRepository;
public StudentViewModel(@NonNull Application application) {
super(application);
studentRepository = new StudentRepository(application);
}
// 增
public void insert(Student... students) {
studentRepository.insert(students);
}
// 删
public void delete(Student student) {
studentRepository.delete(student);
}
// 改
public void update(Student student) {
studentRepository.update(student);
}
// 查 普通的查
public List<Student> getAll() {
return studentRepository.getAll();
}
// 查 关联LiveData
public LiveData<List<Student>> getAllLiveDataStudent() {
return studentRepository.getLiveDataAllStudent();
}
}
6. 数据适配器
这里简单采用ListView来显示列表数据
GoodsAdapter.java
package com.example.myroom2.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.example.myroom2.R;
import com.example.myroom2.entity.Student;
import java.util.List;
// ListView 的 Adapter
public class GoodsAdapter extends BaseAdapter {
private LayoutInflater inflater;
private List<Student> students;
public GoodsAdapter(Context context, List<Student> students) {
this.inflater = LayoutInflater.from(context);
this.students = students;
}
@Override
public int getCount() {
return students.size();
}
@Override
public Object getItem(int position) {
return students.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = inflater.inflate(R.layout.item, null);
Student student = students.get(position);
TextView tv_id = view.findViewById(R.id.tv_id);
tv_id.setText("number:" + student.getUid());
TextView tv_name = view.findViewById(R.id.tv_name);
tv_name.setText("name:"+student.getName());
return view;
}
}
7. UI布局
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:ignore="MissingConstraints" />
</LinearLayout>
ListView列表中对应的item布局
item.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 配合 ListView用的 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#666"
android:textSize="28sp" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
android:textColor="#666"
android:textSize="25sp"
android:layout_marginLeft="30dp"/>
</LinearLayout>
8. Activity的简单运用
package com.example.myroom2.ui;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.widget.ListView;
import com.example.myroom2.R;
import com.example.myroom2.adapter.GoodsAdapter;
import com.example.myroom2.entity.Student;
import com.example.myroom2.repository.StudentRepository;
import com.example.myroom2.viewmodel.StudentViewModel;
import java.util.List;
public class MainActivity extends AppCompatActivity {
ListView listView;
StudentViewModel studentViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = findViewById(R.id.listView);
// 如果MainViewModel extends ViewModel
// mainViewModel = new ViewModelProvider(this,
// new ViewModelProvider.NewInstanceFactory()).get(MainViewModel.class);
// 如果MainViewModel extends AndroidViewModel
studentViewModel = new ViewModelProvider(getViewModelStore(), new ViewModelProvider.AndroidViewModelFactory(getApplication()))
.get(StudentViewModel.class);
// 观察者
studentViewModel.getAllLiveDataStudent().observe(this, new Observer<List<Student>>() {
@Override
public void onChanged(List<Student> students) {
// 更新UI
listView.setAdapter(new GoodsAdapter(MainActivity.this, students));
}
});
// 模拟 仓库
new Thread() {
@Override
public void run() {
super.run();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 默认给数据库Room增加数据
for (int i = 0; i < 50; i++) {
studentViewModel.insert(new Student("test", "123", 1));
}
}
}.start();
// 模拟仓库 数据库数据被修改了,一旦数据库被修改了,那么数据会驱动UI的发生改变
new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
studentViewModel.update(new Student(4, "test" + i, "123", 1));
}
}
}.start();
}
}
三、数据库升级
往往在实际的项目开发阶段会有这样的需求:后台服务器返回的实体字段信息一天一个样。如果我们想之前的数据信息不被覆盖,但是要把新的字段信息添加到数据库表中,就需要借助Migration了。
1. 新增字段
(1)第一步,在表中添加字段属性,比如在Student表中新增flag属性字段
// 注意:升级增加的字段
@ColumnInfo(name="flag")
private int flag;
Student.java
package com.example.myroom2.entity;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity
public class Student {
@PrimaryKey(autoGenerate = true)
private int uid;
@ColumnInfo(name = "name")
private String name;
@ColumnInfo(name = "pwd")
private String password;
@ColumnInfo(name = "addressId")
private int addressId;
// 注意:升级增加的字段
@ColumnInfo(name="flag")
private int flag;
public int getFlag() {
return flag;
}
public void setFlag(int flag) {
this.flag = flag;
}
public Student(int uid, String name, String password, int addressId) {
this.uid = uid;
this.name = name;
this.password = password;
this.addressId = addressId;
}
public Student(String name, String password, int addressId) {
this.name = name;
this.password = password;
this.addressId = addressId;
}
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 getAddressId() {
return addressId;
}
public void setAddressId(int addressId) {
this.addressId = addressId;
}
@Override
public String toString() {
return "Student{" +
"uid=" + uid +
", name='" + name + '\'' +
", password='" + password + '\'' +
", addressId=" + addressId +
'}';
}
}
(2)第二步,修改数据库版本,在RoomDatabase的实现类AppDatabase中修改version版本将之前的1改成2
// exportSchema = false 尽量写,内部需要检测,如果没有写,会抛出异常,因为内部要记录升级的所有副本
@Database(entities = {Student.class}, version = 2, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
... ...
}
(3) 第三步,新建升级类MIGRATION_1_2,重写migrate()方法,在migrate()方法中进行sql语句操作:
// 下面是稳定升级的方式
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// 这里用SQL脚本完成数据的变化
database.execSQL("alter table Student add column flag integer not null default 1");
}
};
(4)第四步,添加数据库升级配置,在Room.databaseBuilder()中进行操作
instance = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class,
"MyRoomDB")
// 可以强制在主线程运行数据库操作
.allowMainThreadQueries()
// 暴力升级 强制执行(数据会丢失 慎用)
// .fallbackToDestructiveMigration()
// 稳定的方式升级
.addMigrations(MIGRATION_1_2)
.build();
其中(2)、(3)、(4)这三个步骤都是在RoomDatabase的实现类AppDatabase中进行操作,代码修改如下:
AppDatabase.java
package com.example.myroom2.db;
import android.content.Context;
import com.example.myroom2.dao.StudentDao;
import com.example.myroom2.entity.Student;
import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
// exportSchema = false 尽量写,内部需要检测,如果没有写,会抛出异常,因为内部要记录升级的所有副本
@Database(entities = {Student.class}, version = 2, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
private static AppDatabase instance;
public static synchronized AppDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class,
"MyRoomDB")
// 可以强制在主线程运行数据库操作
.allowMainThreadQueries()
// 暴力升级 强制执行(数据会丢失 慎用)
// .fallbackToDestructiveMigration()
// 稳定的方式升级
.addMigrations(MIGRATION_1_2)
.build();
}
return instance;
}
// 暴露dao
public abstract StudentDao studentDao();
// 下面是稳定升级的方式
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// 这里用SQL脚本完成数据的变化
database.execSQL("alter table Student add column flag integer not null default 1");
}
};
}
2. 删除字段
这里要注意Room是不能降级
的,我非要删除一个字段,却要保证数据的稳定性,这个是特殊情况,需采用特殊手法进行降级。
这个相对来说,比新增字段要复杂的多。比如我在上面的基础上再把flag字段删除掉
(1)第一步,将实体类中的flag字段删掉
(2)第二步,修改数据库版本,在RoomDatabase的实现类AppDatabase中修改version版本由原来的2修改为3。
(3)第三步,新建升级类MIGRATION_2_3,重写migrate()方法,在migrate方法中进行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 integer)");
// 2.把之前表的数据插入到新表中
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");
}
};
其实这一步可以新建升级类MIGRATION_2_3继承Migration
public class MIGRATION_2_3 extends Migration {
/**
* 这个版本2要和对应的数据库版本对应
*/
public MIGRATION_2_3() {
super(2, 3);
}
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
//TODO 1、创建一个备份表
database.execSQL("create table student_temp (uid integer primary key not null, name text, pwd text, addressId integer)");
//TODO 2、把数据拷贝到备份表中
database.execSQL("insert into student_temp (uid, name, pwd, addressId) " + "select uid, name, pwd, addressid from student");
//TODO 3、删除旧表
database.execSQL("drop table student");
//TODO 4、将备份表名称改为原来的表名称
database.execSQL("alter table student_temp rename to student");
}
}
注意,构造方法中版本是从2到3,需要在migrate方法中执行4个步骤才能将age字段移除。
- 创建备份表,包含所有字段,除了age字段
- 把原始数据拷贝到新的备份表中
- 删除原始表
- 修改备份表名称为原始表名称
(4)第四步,添加数据库升级配置,在Room.databaseBuilder中进行操作
instance = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class,
"MyRoomDB")
// 可以强制在主线程运行数据库操作
.allowMainThreadQueries()
// 暴力升级 强制执行(数据会丢失 慎用)
// .fallbackToDestructiveMigration()
// 稳定的方式升级
.addMigrations(MIGRATION_2_3)
.build();
四. Room的源码原理了解
APT注解处理器生成代码
AppDataBase_Impl.java
package com.example.myroom.db;
import androidx.room.DatabaseConfiguration;
import androidx.room.InvalidationTracker;
import androidx.room.RoomOpenHelper;
import androidx.room.RoomOpenHelper.Delegate;
import androidx.room.util.TableInfo;
import androidx.room.util.TableInfo.Column;
import androidx.room.util.TableInfo.ForeignKey;
import androidx.room.util.TableInfo.Index;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.SupportSQLiteOpenHelper.Callback;
import androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration;
import com.example.myroom.dao.StudentDao;
import com.example.myroom.dao.StudentDao_Impl;
import java.lang.IllegalStateException;
import java.lang.Override;
import java.lang.String;
import java.lang.SuppressWarnings;
import java.util.HashMap;
import java.util.HashSet;
@SuppressWarnings("unchecked")
public final class AppDataBase_Impl extends AppDataBase {
private volatile StudentDao _studentDao;
@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(1) {
@Override
public void createAllTables(SupportSQLiteDatabase _db) {
_db.execSQL("CREATE TABLE IF NOT EXISTS `Student` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `pwd` TEXT, `address` INTEGER NOT NULL)");
_db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
_db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"228d349e23ba02cda67e8bc5ffa53bf0\")");
}
@Override
public void dropAllTables(SupportSQLiteDatabase _db) {
_db.execSQL("DROP TABLE IF EXISTS `Student`");
}
@Override
protected void onCreate(SupportSQLiteDatabase _db) {
if (mCallbacks != null) {
for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
mCallbacks.get(_i).onCreate(_db);
}
}
}
@Override
public void onOpen(SupportSQLiteDatabase _db) {
mDatabase = _db;
internalInitInvalidationTracker(_db);
if (mCallbacks != null) {
for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
mCallbacks.get(_i).onOpen(_db);
}
}
}
@Override
protected void validateMigration(SupportSQLiteDatabase _db) {
final HashMap<String, TableInfo.Column> _columnsStudent = new HashMap<String, TableInfo.Column>(4);
_columnsStudent.put("uid", new TableInfo.Column("uid", "INTEGER", true, 1));
_columnsStudent.put("name", new TableInfo.Column("name", "TEXT", false, 0));
_columnsStudent.put("pwd", new TableInfo.Column("pwd", "TEXT", false, 0));
_columnsStudent.put("address", new TableInfo.Column("address", "INTEGER", true, 0));
final HashSet<TableInfo.ForeignKey> _foreignKeysStudent = new HashSet<TableInfo.ForeignKey>(0);
final HashSet<TableInfo.Index> _indicesStudent = new HashSet<TableInfo.Index>(0);
final TableInfo _infoStudent = new TableInfo("Student", _columnsStudent, _foreignKeysStudent, _indicesStudent);
final TableInfo _existingStudent = TableInfo.read(_db, "Student");
if (! _infoStudent.equals(_existingStudent)) {
throw new IllegalStateException("Migration didn't properly handle Student(com.example.myroom.entity.Student).\n"
+ " Expected:\n" + _infoStudent + "\n"
+ " Found:\n" + _existingStudent);
}
}
}, "228d349e23ba02cda67e8bc5ffa53bf0", "7b5888a424cc9ab1174dfe15e25efa2c");
final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
.name(configuration.name)
.callback(_openCallback)
.build();
final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
return _helper;
}
@Override
protected InvalidationTracker createInvalidationTracker() {
return new InvalidationTracker(this, "Student");
}
@Override
public void clearAllTables() {
super.assertNotMainThread();
final SupportSQLiteDatabase _db = super.getOpenHelper().getWritableDatabase();
try {
super.beginTransaction();
_db.execSQL("DELETE FROM `Student`");
super.setTransactionSuccessful();
} finally {
super.endTransaction();
_db.query("PRAGMA wal_checkpoint(FULL)").close();
if (!_db.inTransaction()) {
_db.execSQL("VACUUM");
}
}
}
@Override
public StudentDao userDao() {
if (_studentDao != null) {
return _studentDao;
} else {
synchronized(this) {
if(_studentDao == null) {
_studentDao = new StudentDao_Impl(this);
}
return _studentDao;
}
}
}
}
StudentDao_Impl.java
package com.example.myroom.dao;
import android.database.Cursor;
import androidx.room.EntityDeletionOrUpdateAdapter;
import androidx.room.EntityInsertionAdapter;
import androidx.room.RoomDatabase;
import androidx.room.RoomSQLiteQuery;
import androidx.room.util.StringUtil;
import androidx.sqlite.db.SupportSQLiteStatement;
import com.example.myroom.entity.Student;
import com.example.myroom.entity.StudentTuple;
import java.lang.Override;
import java.lang.String;
import java.lang.StringBuilder;
import java.lang.SuppressWarnings;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("unchecked")
public final class StudentDao_Impl implements StudentDao {
private final RoomDatabase __db;
private final EntityInsertionAdapter __insertionAdapterOfStudent;
private final EntityDeletionOrUpdateAdapter __deletionAdapterOfStudent;
private final EntityDeletionOrUpdateAdapter __updateAdapterOfStudent;
public StudentDao_Impl(RoomDatabase __db) {
this.__db = __db;
this.__insertionAdapterOfStudent = new EntityInsertionAdapter<Student>(__db) {
@Override
public String createQuery() {
return "INSERT OR ABORT INTO `Student`(`uid`,`name`,`pwd`,`address`) VALUES (nullif(?, 0),?,?,?)";
}
@Override
public void bind(SupportSQLiteStatement stmt, Student value) {
stmt.bindLong(1, value.getUid());
if (value.getName() == null) {
stmt.bindNull(2);
} else {
stmt.bindString(2, value.getName());
}
if (value.getPassword() == null) {
stmt.bindNull(3);
} else {
stmt.bindString(3, value.getPassword());
}
stmt.bindLong(4, value.getAddress());
}
};
this.__deletionAdapterOfStudent = new EntityDeletionOrUpdateAdapter<Student>(__db) {
@Override
public String createQuery() {
return "DELETE FROM `Student` WHERE `uid` = ?";
}
@Override
public void bind(SupportSQLiteStatement stmt, Student value) {
stmt.bindLong(1, value.getUid());
}
};
this.__updateAdapterOfStudent = new EntityDeletionOrUpdateAdapter<Student>(__db) {
@Override
public String createQuery() {
return "UPDATE OR ABORT `Student` SET `uid` = ?,`name` = ?,`pwd` = ?,`address` = ? WHERE `uid` = ?";
}
@Override
public void bind(SupportSQLiteStatement stmt, Student value) {
stmt.bindLong(1, value.getUid());
if (value.getName() == null) {
stmt.bindNull(2);
} else {
stmt.bindString(2, value.getName());
}
if (value.getPassword() == null) {
stmt.bindNull(3);
} else {
stmt.bindString(3, value.getPassword());
}
stmt.bindLong(4, value.getAddress());
stmt.bindLong(5, value.getUid());
}
};
}
@Override
public void insert(Student... students) {
__db.beginTransaction();
try {
__insertionAdapterOfStudent.insert(students);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
@Override
public void delete(Student student) {
__db.beginTransaction();
try {
__deletionAdapterOfStudent.handle(student);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
@Override
public void update(Student student) {
__db.beginTransaction();
try {
__updateAdapterOfStudent.handle(student);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
@Override
public List<Student> getAll() {
final String _sql = "select * from Student";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
final Cursor _cursor = __db.query(_statement);
try {
final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
final int _cursorIndexOfPassword = _cursor.getColumnIndexOrThrow("pwd");
final int _cursorIndexOfAddress = _cursor.getColumnIndexOrThrow("address");
final List<Student> _result = new ArrayList<Student>(_cursor.getCount());
while(_cursor.moveToNext()) {
final Student _item;
final String _tmpName;
_tmpName = _cursor.getString(_cursorIndexOfName);
final String _tmpPassword;
_tmpPassword = _cursor.getString(_cursorIndexOfPassword);
final int _tmpAddress;
_tmpAddress = _cursor.getInt(_cursorIndexOfAddress);
_item = new Student(_tmpName,_tmpPassword,_tmpAddress);
final int _tmpUid;
_tmpUid = _cursor.getInt(_cursorIndexOfUid);
_item.setUid(_tmpUid);
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
@Override
public Student findByName(String name) {
final String _sql = "select * from Student where name like?";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
int _argIndex = 1;
if (name == null) {
_statement.bindNull(_argIndex);
} else {
_statement.bindString(_argIndex, name);
}
final Cursor _cursor = __db.query(_statement);
try {
final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
final int _cursorIndexOfPassword = _cursor.getColumnIndexOrThrow("pwd");
final int _cursorIndexOfAddress = _cursor.getColumnIndexOrThrow("address");
final Student _result;
if(_cursor.moveToFirst()) {
final String _tmpName;
_tmpName = _cursor.getString(_cursorIndexOfName);
final String _tmpPassword;
_tmpPassword = _cursor.getString(_cursorIndexOfPassword);
final int _tmpAddress;
_tmpAddress = _cursor.getInt(_cursorIndexOfAddress);
_result = new Student(_tmpName,_tmpPassword,_tmpAddress);
final int _tmpUid;
_tmpUid = _cursor.getInt(_cursorIndexOfUid);
_result.setUid(_tmpUid);
} else {
_result = null;
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
@Override
public List<Student> getAllId(int[] userIds) {
StringBuilder _stringBuilder = StringUtil.newStringBuilder();
_stringBuilder.append("select * from Student where uid in(");
final int _inputSize = userIds.length;
StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
_stringBuilder.append(")");
final String _sql = _stringBuilder.toString();
final int _argCount = 0 + _inputSize;
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, _argCount);
int _argIndex = 1;
for (int _item : userIds) {
_statement.bindLong(_argIndex, _item);
_argIndex ++;
}
final Cursor _cursor = __db.query(_statement);
try {
final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
final int _cursorIndexOfPassword = _cursor.getColumnIndexOrThrow("pwd");
final int _cursorIndexOfAddress = _cursor.getColumnIndexOrThrow("address");
final List<Student> _result = new ArrayList<Student>(_cursor.getCount());
while(_cursor.moveToNext()) {
final Student _item_1;
final String _tmpName;
_tmpName = _cursor.getString(_cursorIndexOfName);
final String _tmpPassword;
_tmpPassword = _cursor.getString(_cursorIndexOfPassword);
final int _tmpAddress;
_tmpAddress = _cursor.getInt(_cursorIndexOfAddress);
_item_1 = new Student(_tmpName,_tmpPassword,_tmpAddress);
final int _tmpUid;
_tmpUid = _cursor.getInt(_cursorIndexOfUid);
_item_1.setUid(_tmpUid);
_result.add(_item_1);
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
@Override
public StudentTuple getRecord() {
final String _sql = "select name, pwd from Student";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
final Cursor _cursor = __db.query(_statement);
try {
final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
final int _cursorIndexOfPassword = _cursor.getColumnIndexOrThrow("pwd");
final StudentTuple _result;
if(_cursor.moveToFirst()) {
final String _tmpName;
_tmpName = _cursor.getString(_cursorIndexOfName);
final String _tmpPassword;
_tmpPassword = _cursor.getString(_cursorIndexOfPassword);
_result = new StudentTuple(_tmpName,_tmpPassword);
} else {
_result = null;
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
}