没什么复杂的,就 3 个注解类
@Entity:一个 Entity 对应于数据库中的一张表。
@Dao:操作类
@Insert:增
@Delete:删
@Update:改
@Query(" … "):增/删/改/查
@Database:数据库相关
假设要创建一个学生数据库,数据库中有一张学生表,用于保存学生的基本信息。
implementation "androidx.room:room-runtime:2.2.5"
annotationProcessor "androidx.room:room-compiler:2.2.5"
@Entity(tableName = "student")
public class Student {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
public int id;
@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
public String name;
@ColumnInfo(name = "age", typeAffinity = ColumnInfo.TEXT)
public String age;
/**
* Room 默认会使用这个构造器操作数据
*/
public Student(int id, String name, String age) {
this.id = id;
this.name = name;
this.age = age;
}
/**
* 由于 Room 只能识别和使用一个构造器,如果希望定义多个构造器可以使用 Ignore 标签,让 Room 忽略这个构造器
* 不仅如此,@Ignore 标签还可用于字段
* Room 不会持久化被 @Ignore 标签标记过的字段的数据
*/
@Ignore
public Student(String name, String age) {
this.name = name;
this.age = age;
}
}
存储集合类型的数据(待更新。。。)参考博客
@Dao
public interface StudentDao {
@Insert
void insertStudent(Student... student);
@Delete
void deleteStudent(Student... student);
@Update
void updateStudent(Student... student);
@Query("SELECT * FROM student")
List<Student> getStudent();
@Query("SELECT * FROM student WHERE id=:id")
Student getStudentById(int id);
//在 MVVM+JetPack 架构中,返回数据可用 LiveData 包装可观察数据变化
// @Query("SELECT * FROM student WHERE id=:id")
// LiveData getStudentById(int id);
//
// @Query("SELECT * FROM student")
// LiveData> getStudent();
}
@Database(entities = {Student.class}, version = 1, exportSchema = true) //exportSchema
public abstract class MyDatabase extends RoomDatabase {
private static final String DATABASE_NAME = "student_database";
private static MyDatabaseINSTANCE;
public static synchronized StudentDatabase getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(), MyDatabase.class, DATABASE_NAME)
//.allowMainThreadQueries() //允许在 main 线程运行(慎用,不推荐,可做单元测试使用)
.build();
}
return INSTANCE;
}
//对外暴露
public abstract StudentDao studentDao();
}
数据库和表的创建工作就完成后。下面来看看如何对数据库进行增/删/改/查。这些对数据库的操作方法都是我们之前在 Dao 文件中已经定义好的。需要注意的是,不能直接在 UI 线程中执行这些操作,所有操作都需要放在工作线程中进行。
public class RoomActivity extends AppCompatActivity {
private MyDatabase myDatabase;
private List<Student> mStudentList;
private RoomAdapter mRoomAdapter;
private EditText etName;
private EditText etAge;
private EditText etId;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_room);
myDatabase = MyDatabase.getInstance(this);
initRecyclerView();
etName = findViewById(R.id.etName);
etAge = findViewById(R.id.etAge);
etId = findViewById(R.id.etId);
findViewById(R.id.addStudent).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (TextUtils.isEmpty(etName.getText().toString()) || TextUtils.isEmpty(etAge.getText().toString())) {
Toast.makeText(RoomActivity.this, "不能为空!", Toast.LENGTH_SHORT).show();
} else {
new InsertStudentTask(etName.getText().toString(), etAge.getText().toString()).execute();
}
}
});
findViewById(R.id.queryStudentList).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new QueryStudentListTask().execute();
}
});
findViewById(R.id.queryStudentById).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (TextUtils.isEmpty(etId.getText().toString())) {
Toast.makeText(RoomActivity.this, "不能为空!", Toast.LENGTH_SHORT).show();
} else {
int id = Integer.parseInt(etId.getText().toString());
new QueryStudentByIdTask(id).execute();
}
}
});
}
private void initRecyclerView() {
RecyclerView recyclerView = findViewById(R.id.recyclerView);
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(RecyclerView.VERTICAL);
recyclerView.setLayoutManager(manager);
mStudentList = new ArrayList<>();
mRoomAdapter = new RoomAdapter(this, mStudentList);
recyclerView.setAdapter(mRoomAdapter);
mRoomAdapter.setOnClickListener(new RoomAdapter.OnClickListener() {
@Override
public void onDeleteClick(Student student) {
new DeleteStudentTask(student).execute();
}
@Override
public void onUpdateClick(int id, String name, String age) {
new UpdateStudentTask(id, name, age).execute();
}
});
}
/**
* 增
*/
private class InsertStudentTask extends AsyncTask<Void, Void, Void> {
private String name;
private String age;
public InsertStudentTask(String name, String age) {
this.name = name;
this.age = age;
}
@Override
protected Void doInBackground(Void... voids) {
// 插入数据
myDatabase.studentDao().insertStudent(new Student(name, age));
return null;
}
}
/**
* 删
*/
private class DeleteStudentTask extends AsyncTask<Void, Void, Void> {
private Student student;
public DeleteStudentTask(Student student) {
this.student = student;
}
@Override
protected Void doInBackground(Void... voids) {
// 删除数据
myDatabase.studentDao().deleteStudent(student);
return null;
}
}
/**
* 改
*/
private class UpdateStudentTask extends AsyncTask<Void, Void, Void> {
private int id;
private String name;
private String age;
public UpdateStudentTask(int id, String name, String age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
protected Void doInBackground(Void... voids) {
// 更新数据
myDatabase.studentDao().updateStudent(new Student(id, name, age));
return null;
}
}
/**
* 查询所有
*/
private class QueryStudentListTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
// 查询所有学生
List<Student> studentList = myDatabase.studentDao().getStudentList();
mStudentList.clear();
mStudentList.addAll(studentList);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
mRoomAdapter.notifyDataSetChanged();
}
}
/**
* 根据 Id 查询
*/
private class QueryStudentByIdTask extends AsyncTask<Void, Void, Void> {
private int id;
public QueryStudentByIdTask(int id) {
this.id = id;
}
@Override
protected Void doInBackground(Void... voids) {
// 查询某个学生
Student student = myDatabase.studentDao().getStudentById(id);
mStudentList.clear();
if (student != null) {
mStudentList.add(student);
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
mRoomAdapter.notifyDataSetChanged();
}
}
}
public class RoomAdapter extends RecyclerView.Adapter<RoomAdapter.RoomHolder> {
private Context mContext;
private List<Student> mStudentList;
public RoomAdapter(Context mContext, List<Student> studentList) {
this.mContext = mContext;
this.mStudentList = studentList;
}
@NonNull
@Override
public RoomHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_room, parent, false);
return new RoomHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final RoomHolder holder, final int position) {
if (mStudentList == null || mStudentList.size() == 0
|| mStudentList.get(position) == null) return;
holder.tvId.setText(mStudentList.get(position).id + "");
holder.tvName.setText(mStudentList.get(position).name);
holder.tvAge.setText(mStudentList.get(position).age);
holder.btnDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onClickListener != null) {
onClickListener.onDeleteClick(mStudentList.get(position));
}
}
});
holder.btnUpdate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onClickListener != null) {
int id = mStudentList.get(position).id;
String name = holder.etName.getText().toString();
String age = holder.etAge.getText().toString();
onClickListener.onUpdateClick(id, name, age);
}
}
});
}
@Override
public int getItemCount() {
return mStudentList.size();
}
class RoomHolder extends RecyclerView.ViewHolder {
private TextView tvId, tvName, tvAge;
private EditText etName, etAge;
private Button btnDelete, btnUpdate;
public RoomHolder(@NonNull View itemView) {
super(itemView);
tvId = itemView.findViewById(R.id.tvId);
tvName = itemView.findViewById(R.id.tvName);
tvAge = itemView.findViewById(R.id.tvAge);
etName = itemView.findViewById(R.id.etName);
etAge = itemView.findViewById(R.id.etAge);
btnDelete = itemView.findViewById(R.id.btnDelete);
btnUpdate = itemView.findViewById(R.id.btnUpdate);
}
}
private OnClickListener onClickListener;
public void setOnClickListener(OnClickListener onClickListener) {
this.onClickListener = onClickListener;
}
public interface OnClickListener {
void onDeleteClick(Student student);
void onUpdateClick(int id, String name, String age);
}
}
如下所示。数据库的增/删/改/查已全部实现。上面展示的都是核心代码。布局文件等相关的代码需要自行完善。
随着业务的变化,数据库可能也需要做一些调整。例如,数据表可能需要增加一个新字段。Android 提供了一个名为 Migration 的类,来对 Room 进行升级。
public Migration(int startVersion, int endVersion)
Migration 有两个参数,startVersion 和 endVersion。startVersion 表示当前数据库版本(设备上安装的版本),endVersion 表示将要升级到的版本。若设备中的应用程序数据库版本为 1,那么以下 Migration 会将你的数据库版本从 1 升级到 2。
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// 执行与升级相关的操作
}
};
以此类推,若数据库版本从 2 升级到 3,则需要写一个这样的 Migration。
private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// 执行与升级相关的操作
}
};
若用户设备上的应用程序数据库版本为 1,而当前要安装的应用程序数据库版本为 3,那么该怎么办呢?在这种情况下,Room 会先判断当前有没有直接从 1 到 3 的升级方案,如果有,就直接执行 1 到 3 的升级方案;如果没有,那么 Room 会按照顺序先后执行 Migration(1,2)、Migration(2,3) 以完成升级。
在 Migration 中编写升级方案后,还需要通过 addMigration() 方法,将升级方案添加到 Room。
public static synchronized MyDatabase upgradeSQLite(Context context) {
if (databaseInstance == null) {
databaseInstance = Room.databaseBuilder(context.getApplicationContext(), MyDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3)
.build();
}
return databaseInstance;
}
在升级过程,该如何修改 Room 数据库的版本号呢?直接通过 @Database 标签中的 version 属性进行修改就可以了。
@Database(entities = {Student.class}, version = 1)
假设我们将数据库版本升级到 4,却没有为此写相应的 Migration,则会出现一个 IllegalStateException 异常。
这是因为 Room 在升级过程中没有匹配到相应的 Migration。为了防止出现升级失败导致应用程序崩溃的情况,我们可以在创建数据库时加入 fallbackToDestructiveMigration() 方法。该方法能够在出现升级异常时,重新创建版本号为 4 的数据表。需要注意的是,虽然应用程序不会崩溃,但由于数据表被重新创建,所有的数据也将会丢失。
public static synchronized MyDatabase upgradeSQLite(Context context) {
if (databaseInstance == null) {
databaseInstance = Room.databaseBuilder(context.getApplicationContext(), MyDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3,MIGRATION_1_3)
.fallbackToDestructiveMigration()
.build();
}
return databaseInstance;
}
在没有 Room 组件之前,若想要验证数据的修改是否符合预期,我们需要找到这个数据库文件,接着使用第三方 Sqlite 查看工具,对其进行查看和验证。另外,如果我们希望查看数据库的历次升级情况,只能通过代码版本控制工具,根据源代码的历次修改情况进行推测。这无疑是耗时耗力的。为此,Room 提供了一项功能,在每次数据库的升级过程中,它都会为你导出一个 Schema 文件,这是一个 json 格式的文件,其中包含了数据库的所有基本信息。有了该文件,开发者便能清楚地知道数据库的历次变更情况,这极大地方便了开发者排查问题。Schema 文件默认是导出的,你只需指定它的导出位置即可。
例如,我们可以在 app 的 build.gradle 文件中指定 Schema 文件的导出位置。
android {
defaultConfig {
//指定room.schemaLocation生成的文件路径
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
}
当数据库生成或修改成功后,便可以在项目的根目录下看见对应的 Schema 文件了。数字代表数据库的版本号,如下图所示。
Room 默认导出 Schema 文件,如果你不想导出这些文件,那么可以在数据库标签 @Database 中指定 exportSchema = false。但我们建议导出这些文件。如果你使用版本控制系统(如 Git)来管理代码,那么可以将这些文件一并提交到代码仓库。
@Database(entities = {Student.class}, exportSchema = false, version = 1)
在 Sqlite 中修改表结构比较麻烦。例如,我们想将 Student 表中的 age 字段类型从 TEXT 改为 INTEGER。
面对此类需求,最好的方式是采用销毁与重建策略,该策略大致分为以下几个步骤。
修改数据库的版本号为 4,并将 Student 中的 age 类型改为 int。Migration 的代码如下所示。
private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE temp_Student (id INTEGER PRIMARY KEY NOT NULL,name TEXT,age INTEGER)");
database.execSQL("INSERT INTO temp_Student (id,name ,age) SELECT id,name,age FROM Student");
database.execSQL("DROP TABLE Student");
database.execSQL("ALTER TABLE teamp_Student RENAME TO Student");
}
};
执行上方代码后,通过查看 Schema 升级文件,我们可以看到,age 字段的类型已经被修改为 INTEGER,如下图所示
从 Room 2.2 版本开始,Room 加入了两个新的 API,用于在给定已填充数据库文件的基础上创建 Room 数据库。基于这两个 API,开发者可以基于特定的预打包好的数据库文件来创建 Room 数据库。
例如,假设你的应用程序需要用一个 Room 数据库,以存储世界各地的城市信息。那么你可以在应用程序发布时,将 cities.db 文件放到 assets 目录下,在用户首次打开应用程序时,使用 createFromAsset() 方法,基于 cities.db 文件创建你的 Room 数据库。如果你担心将数据库文件打包进 assets 目录会增加应用程序的大小,还可以考虑在用户首次打开应用程序时,通过网络连接将数据库文件下载至 SD 卡,接着通过 createFromFile() 方法来创建 Room 数据库。
1、 首先需要创建一个数据库文件 students.db。创建的方式有很多,这里使用的是 DB Browser for SQLite 软件。安装该软件后,单击 “新建数据库”,新建一个名为 students 的数据库。如下图所示。
2、接着创建数据表 student,其中的字段与 Student 模型类中的字段保持一致。如下图所示。
3、接着,往数据表中添加数据,模拟数据的预填充。如下图所示,我们添加了 5 条数据。
4、保存数据库,并将数据库文件放到项目的 assets/databases 目录下,如下图所示。
5、接来下是需要关注的重点。在创建数据库时,调用 createFromAsset() 方法,基于 assets/database/students.db 文件创建 Room 数据库。
@Database(entities = {Student.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
private static final String DATABASE_NAME = "my_db";
private static MyDatabase databaseInstance;
/**
* 创建
*/
public static synchronized MyDatabase getInstance(Context context) {
if (databaseInstance == null) {
databaseInstance = Room.databaseBuilder(context, MyDatabase.class, DATABASE_NAME)
// 从 assets/database 目录下读取 students.db
.createFromAsset("databases/students.db")
.build();
}
return databaseInstance;
}
public abstract StudentDao studentDao();
}
6、 student 类中,数据类型也需要改成和数据库相对应的。我尝试把 age 字段改成 INTEGER,age属性改成 int,包括数据库中的 age 的类型也是 INTEGER,但是会报数据库结构的错误。
@Entity(tableName = "student")
public class Student {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
public int id;
@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
public String name;
@ColumnInfo(name = "age", typeAffinity = ColumnInfo.TEXT)
public String age;
/**
* Room 默认会使用这个构造器操作数据
*/
public Student(int id, String name, String age) {
this.id = id;
this.name = name;
this.age = age;
}
/**
* 由于 Room 只能识别和使用一个构造器,如果希望定义多个构造器可以使用 Ignore 标签,让 Room 忽略这个构造器
* 不仅如此,@Ignore 标签还可用于字段
* Room 不会持久化被 @Ignore 标签标记过的字段的数据
*/
@Ignore
public Student(String name, String age) {
this.name = name;
this.age = age;
}
}
7、运行应用程序,可以看到,students.db 文件中的数据已经被写入 Room 数据库中了,如下图所示。