JetPack-(8)-Room 数据库

281 阅读18分钟

1.Room基本概念

Android采用Sqlite作为数据库存储。由于Sqlite代码写起来繁琐且容易出错,Android官方推出了一个ORM库(Room),Room就是为了方便Sqlite的使用而出现的,它本质上是再SQLite上面提供了一个抽象层,包括数据库的创建、升级、增删改查等操作。 优点有:

  • 在编译时校验SQL语句
  • 易用的注解减少重复和易错的模板代码
  • 简化的数据库迁移路径

2.Room介绍

Room包含三个主要组件

  1. 数据库类(RoomDatabase):包含数据库的持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入者,使用@Database注解的类应该满足以下条件:
  • 是继承RoomDatabase的抽象类
  • 在注释中添加与数据库关联的实体列表
  • 包含具有0个参数且返回使用@Dao注释的类的抽象方法
  • 在运行时,可以通过调用Room.databaseBuilder()或Room.inMemoryDatabaseBuilder()获取
  1. 数据实体类(Entity):一个Entity对应于数据库中的一张表。Entity类是Sqlite表结构对Java类的映射,在Java中可以被看做一个Model类
  2. 数据访问对象(DAO):即Data Access Objects,数据访问对象,即我们可以通过它来访问数据。 简单来说,一个Entity代表一张表,而每张表都需要一个Dao对象,用于对表进行增删改查。Room数据库在被实例化之后,我们就可以通过数据库实例得到Dao对象(Get Dao),进而通过Dao对象对表中的数据进行操作。

1710696726806.jpg

3.Room的基本使用

需求:假设要创建一个学生数据库,数据库中有一张学生表,用于保存学生的基本信息,对该学生表进行增删改查

3.1 导入依赖

在app的build.gradle中添加Room的相关依赖

//导入Room
    def room_version = "2.2.5"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-rxjava2:$room_version"
    implementation "androidx.room:room-guava:$room_version"

3.2 编写Student类

创建一个关于学生的Entity,即创建一张学生表,新建一个名为Student的Java类文件,并在类文件的上方添加@Entity标签

@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;
    }

    @Ignore
    public Student(int id) {
        this.id = id;
    }
}
  1. @Entity 标签用于将Student类与Room中的数据表对应起来。tableName属性可以为数据表设置表明,若不设置,则表明与类名相同
  2. @PrimaryKey 标签用于指定该字段作为表的主键
  3. @ColumnInfo 标签可以设置该字段存储在数据库表中的名字,并指定字段的类型。
  4. @Ignore 标签用来告诉Room忽略该字段或该方法。
  5. 保留某个字段,Room必须拥有该字段的访问权限。可以将某个字段设为公开字段,也可以为其提供getter和setter,这里就将其字段设为公开字段。

3.3 编辑Dao文件

我们需要一个Dao文件接口文件,以便对Entity进行访问,注意,在接口文件的上方,需要加入@Dao注解

@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> getStudentList();

    //查询某个学生
    @Query("SELECT * FROM student WHERE id = :id")
    Student getStudentById(int id);
}

3.4 创建数据库

@Database(entities = {Student.class}, version = 1, exportSchema = false)
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.getApplicationContext(), MyDatabase.class, DATABASE_NAME).build();
        }
        return databaseInstance;
    }

    public abstract StudentDao studentDao();
}
  1. @Database 标签用于告诉系统这是Room数据库对象。entities属性用于指定该数据库有哪些表,若需要建立多张表,则表名以逗号相隔开。version属性用于指定数据库版本号,后面数据库的升级正是依据版本号进行判断的。exportScheme属性用于是否将数据库结构保存到目录
  2. 数据库类需要继承自RoomDatabase,由于每次创建Database实例都会产生比较大的开销,所以我们使用的是单例模式。
  3. 通过Room.databaseBuilder(),生成Database对象,创建一个数据库
  4. 另外,我们之前创建的Dao对象,在此以抽象方法的形式返回。

至此,数据库的和表的创建已经完成了,接下来对项目Build一下,发现会生成以下文件。

1710698782296.jpg

那我们怎么操作呢?其实很简单

3.4.1 获取数据库对象

MyDatabase myDatabase = MyDatabase.getInstance(this);

3.4.2 插入数据

myDatabase.studentDao().insertStudent(new Student(name,age))

3.4.3 更新数据

myDatabase.studentDao().updateStudent(new Student(name,age))

3.4.4 删除数据

myDatabase.studentDao().deleteStudent(new Student(id))

3.4.5 查询所有学生

myDatabase.studentDao().getStudentList()

3.4.6 查询某个学生

myDatabase.studentDao().getStudentById(id)

这些方法都是我们之前在Dao文件中已经定义好的。需要注意的是,我们不能直接在UI线程中执行这些操作,所有操作都需要放在工作线程中进行。

3.5 RoomActivity的布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button
            android:id="@+id/addBtn"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="添加学生"/>
        <Button
            android:id="@+id/delBtn"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="删除学生"/>
        <Button
            android:id="@+id/updateBtn"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="修改学生"/>
        <Button
            android:id="@+id/queryIdBtn"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="根据ID查询学生"/>
        <Button
            android:id="@+id/queryBtn"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="查询所有学生"/>
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="删除学生的id"
            android:textSize="18sp"/>
        <EditText
            android:id="@+id/delId"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="修改学生的id"
            android:textSize="18sp"/>
        <EditText
            android:id="@+id/updateId"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="修改学生的姓名"
            android:textSize="18sp"/>
        <EditText
            android:id="@+id/updateName"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="查询学生的id"
            android:textSize="18sp"/>
        <EditText
            android:id="@+id/queryId"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

3.6 listView中布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_id"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="1"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="哈哈哈"
            android:textSize="20sp" />

        <TextView
            android:id="@+id/tv_age"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="18"
            android:textSize="20sp" />
    </LinearLayout>

</LinearLayout>

3.7 ListViewAdapter编写


public class ListAdapter extends BaseAdapter {
    private Context context;
    private List<Student> mList;

    public ListAdapter(Context context, List<Student> mList) {
        this.context = context;
        this.mList = mList;
    }

    public void setmList(List<Student> mList) {
        this.mList = mList;
        notifyDataSetChanged();
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return mList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View view, ViewGroup viewGroup) {
        ViewHolder viewHolder;
        if (view == null) {
            viewHolder = new ViewHolder();
            view = LayoutInflater.from(context).inflate(R.layout.list_item, viewGroup, false);
            viewHolder.tv_id = view.findViewById(R.id.tv_id);
            viewHolder.tv_name = view.findViewById(R.id.tv_name);
            viewHolder.tv_age = view.findViewById(R.id.tv_age);
            view.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) view.getTag();
        }
        viewHolder.tv_id.setText(mList.get(position).id + "");
        viewHolder.tv_name.setText(mList.get(position).name);
        viewHolder.tv_age.setText(mList.get(position).age);
        return view;
    }

    class ViewHolder {
        private TextView tv_id;
        private TextView tv_name;
        private TextView tv_age;
    }
}

3.8 RoomActivity代码编写

public class RoomActivity extends AppCompatActivity implements View.OnClickListener {
    private Button addBtn,delBtn,updateBtn,queryIdBtn,queryBtn;
    private EditText delId,updateId,updateName,queryId;
    private ListView mListView;
    //添加学生
    private  static  final  int ADD_STUDENT = 1001;
    //Handler
    private  Handler handler = new Handler();
    //定义一个遍历
    private  int count  = 1;
    //存放学生基本信息集合
    private List<Student> mList = new ArrayList<>();
    //数据库对象
    private  MyDatabase myDatabase;
    //ListView的Adapter
    private  listAdapter adapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_room);
        initView();
    }
    //初始化控件
    private void initView() {

        //初始化数据库
        myDatabase = MyDatabase.getInstance(this);

        addBtn = findViewById(R.id.addBtn);
        delBtn = findViewById(R.id.delBtn);
        updateBtn = findViewById(R.id.updateBtn);
        queryIdBtn = findViewById(R.id.queryIdBtn);
        queryBtn = findViewById(R.id.queryBtn);

        delId = findViewById(R.id.delId);
        updateId = findViewById(R.id.updateId);
        updateName = findViewById(R.id.updateName);
        queryId = findViewById(R.id.queryId);

        //默认有一条数据防止报错
        mList.add(new Student("xiaoxin","20"));
        mListView = findViewById(R.id.listView);
        adapter = new listAdapter(this,mList);
        mListView.setAdapter(adapter);

        //添加点击事件
        addBtn.setOnClickListener(this);
        delBtn.setOnClickListener(this);
        updateBtn.setOnClickListener(this);
        queryIdBtn.setOnClickListener(this);
        queryBtn.setOnClickListener(this);

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            //添加学生信息
            case R.id.addBtn:
                addStudent();
                break;
            //删除学生信息
            case R.id.delBtn:
                delStudent();
                break;
            //修改学生信息
            case R.id.updateBtn:
                updateStudent();
                break;
            //根据ID查询学旬信息
            case R.id.queryIdBtn:
                queryStudentById();
                break;
            //查询所有学生信息
            case R.id.queryBtn:
                queryStudentList();
                break;
        }
    }

    //根据ID查询好友
    private void queryStudentById() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String id = queryId.getText().toString().trim();
                if(TextUtils.isEmpty(id)){
                    return;
                }
                Student student = myDatabase.studentDao().getStudentById(Integer.valueOf(id));
                //查询之后,通知Adapter进行更新
                //如果之前的mList中有数据,先清空,毕竟我们只显示通过id查询到的数据
                if(mList.size()>0){
                    mList.clear();
                }
                mList.add(student);
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        //通知Adapter刷新
                        adapter.setmList(mList);
                    }
                });
            }
        }).start();
    }

    //修改学生
    private void updateStudent() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String id = updateId.getText().toString().trim();
                String name = updateName.getText().toString().trim();
                if(TextUtils.isEmpty(id)){
                    return;
                }
                if(TextUtils.isEmpty(name)){
                    return;
                }

                myDatabase.studentDao().updateStudent(new Student(Integer.valueOf(id),name,20+""));
                //删除之后,在对数据进行查询一次
                queryStudentList();
            }
        }).start();
    }

    //删除学生
    private void delStudent() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String id = delId.getText().toString().trim();
                if(TextUtils.isEmpty(id)){
                    return;
                }

                myDatabase.studentDao().deleteStudent(new Student(Integer.valueOf(id)));
                //删除之后,在对数据进行查询一次
                queryStudentList();
            }
        }).start();
    }

    //查询所有学生
    private void queryStudentList() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                mList = myDatabase.studentDao().getStudentList();
                //Adapter的更新需到主线程进行更新
                //用Handler发送到主线程进行更新
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        //通知Adapter刷新
                        adapter.setmList(mList);
                    }
                });
            }
        }).start();
    }

    //添加学生信息
    private void addStudent() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //使用count,为了区分添加的学生重复
                myDatabase.studentDao().insertStudent(new Student("xiaoxin"+count,20+count+""));
                count++;
                queryStudentList();
            }
        }).start();
    }
}

运行结果如下:

1710699173127.jpg

好了,Room的基本使用就到这里了。但是你有没有发现,每次对数据库操作的时候都需要在工作线程中进行,这意味着,每次当数据库中的数据发生变化的时候,都需要开启一个工作线程进行查询。那么,能否在数据发生变化时,自动接收数据呢?通过LiveData就可以解决这个问题

4. Room与LiveData,ViewModel结合使用

我们希望当Room数据库中的数据发生变化时,能够通过LiveData组件通知View层,实现数据的自动更新,如下图所示:

1710699332277.jpg

我们知道,LiveData通常与ViewModel一起使用,ViewModel是用于存放数据的,因此我们可以将数据库放在ViewModel中进行实例化。但数据库的实例化需要用到Context,而ViewModel中最好不要传入Context,因此,我们应该使用它的子类AndroidViewModel,它的构造器含有Application对象,正好可以作为Context的子类,对数据库进行实例化

4.1 案例优化

4.1.1 修改学生表Dao文件

我们希望当学生表中的数据发生变化时,能够收到实时通知。这里需要使用LiveData将getStudentList()方法的返回对象List包装起来

@Dao
public interface StudentDao {
    //添加学生信息
    @Insert
    void insertStudent(Student student);

    //删除学生
    @Delete
    void deleteStudent(Student student);

    //修改学生
    @Update
    void updateStudent(Student student);

    //查询所有学生
    @Query("SELECT * FROM student")
    LiveData<List<Student>> getStudentList();

    //查询某个学生
    @Query("SELECT * FROM student WHERE id = :id")
    Student getStudentById(int id);
}

4.1.2 创建StudentViewModel类

public class StudentViewModel extends AndroidViewModel {
    private MyDatabase myDatabase;
    private LiveData<List<Student>> listLiveData;

    public StudentViewModel(@NonNull Application application) {
        super(application);
        //对数据库进行初始化
        myDatabase = MyDatabase.getInstance(application);
        //获取学生数据
        listLiveData = myDatabase.studentDao().getStudentList();
    }

    public LiveData<List<Student>> getListLiveData() {
        return listLiveData;
    }
}

4.1.3 Activity中实例化StudentViewModel

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_room);
        initView();

        StudentViewModel studentViewModel = new ViewModelProvider(this).get(StudentViewModel.class);
        studentViewModel.getListLiveData().observe(this, new Observer<List<Student>>() {
            @Override
            public void onChanged(List<Student> students) {
                if(mList.size()>0){
                    mList.clear();;
                }
                mList.addAll(students);
                adapter.setmList(mList);
            }
        });
    }

现在我们就可以将增加、删除或修改代码中这一行代码queryStudentList()给删除了。

运行程序,当对数据库进行增加、删除或修改操作时,public void onChanged(List students)方法会被自动调用,只需要在该方法中通知Adapter刷新数据即可。与之前的每次修改数据库之后,都要开启一个工作线程手动查询一次数据库相比,LiveData无疑可以大大减轻工作量。

5. Room数据库升级

使用Migration升级数据库

随着业务的变化,数据库可能也需要做一些调整。例如,数据表可能需要增加一个新字段。Android提供了一个名为Migration的类,来对Room进行升级

public Migration(int startVersion,int endVersion)

Migration有两个参数,startVersion和endVersion。startVersion表示当前数据库版本(设备上安装的版本),endVersion表示将要升级到的版本。

若设备中的应用程序数据库版本为1,那么以下Migation会将你的数据库版本从1升级到2

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
       //执行与升级相关的操作
    }
};

以此类推,若数据库版本需要从2升级到3,则也需要写一个这样的Migration

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
       //执行与升级相关的操作
    }
};

在Migration编写完升级方案后,还需要通过addMigrations()方法,将升级方案添加到Room

Room.databaseBuilder(
                    context.getApplicationContext(),
                    MyDatabase.class,
                    DATABASE_NAME)
                    .addMigrations(MIGRATION_1_2,MIGRATION_2_3)
                    .build();

若用户设备上的应用程序数据库版本为1,而当前要安装的应用程序数据库版本为3,那该怎么办呢?

在这种情况下,Room会先判断当前有没有直接从1到3的升级方案,如果有,就直接执行从1到3的升级方案,如果没有,那么Room会按照顺序先后执行Migration(1,2),Migration(2,3)以完成升级。

比如说,我在Student类中新增加了一个sex字段,那么我需要对数据库进行升级,代码如下:

//创建数据库
@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.getApplicationContext(),
                    MyDatabase.class,
                    DATABASE_NAME)
                    .addMigrations(MIGRATION_1_2)
                    .build();
        }
        return  databaseInstance;
    }
    public  abstract  StudentDao studentDao();
}
 /**
     * 数据库版本 1->2 student表格新增了sex列
     */
    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE User ADD COLUMN sex integer");
        }
    };

异常处理

假设我们将数据库版本升级到4,却没有为此写相应的Migration,则会出现一个illegalStateException异常。这是因为Room在升级过程中没有匹配到相应的Migration,为了防止出现升级失败导致应用程序崩溃的情况,我们可以在创建数据库时加入 fallbackToDestructiveMigration()方法,该方法能够在出现升级异常时,重新创建数据表。需要注意的是,虽然应用程序不会崩溃,但是由于数据表被重新创建,所有的数据也将会丢失。

Room.databaseBuilder(
                    context.getApplicationContext(),
                    MyDatabase.class,
                    DATABASE_NAME)
                    .fallbackToDestructiveMigration()
                    .addMigrations(MIGRATION_1_2)
                    .build();

在升级过程中,该如何修改Room数据库的版本号呢?直接通过@Database标签中的version属性进行修改就可以了。 @Database(entities={Student.class},version=1)

6. Entity实体

Entity实体就相当于数据库中的一张表,而Entity里面的字段就相当于表中的每一列。在Room的基本使用中,我们已经说明了如何定义Entity:

 @Entity
    public class User {
        @PrimaryKey
        public int id;

        public String firstName;
        public String lastName;
    }

@Entity注解包含的属性有:

  1. tableName:设置表名字,默认是类名字。
  2. indices:设置索引
  3. inheritSuperIndices:父类的索引是否会被当前类继承
  4. primaryKeys:设置主键
  5. foreign Keys:设置外键

6.1 使用主键

每个实体必须将至少1个字段定义为主键。即使只有1个字段,仍需要为该字段添加@PriamryKey主键,如果想让主键实现自增长,则可以设置@PrimaryKey的autoenerate属性,如果实体具有复合主键,可以使用@Entity注释的primaryKes属性,如以下代码段所示:

   @Entity(primaryKeys = {"firstName", "lastName"})
    public class User {
        public String firstName;
        public String lastName;
    }

也可以通过@primaryKey主键来设置主键

@Entity
    public class User {
    	@PrimaryKey
        public String firstName;
        @PrimaryKey
        public String lastName;
    }

如果实体继承了父实体的字段,则使用@Entity属性的ignroedColumns属性:

@Entity(ignoredColumns = "picture")
    public class RemoteUser extends User {
        @PrimaryKey
        public int id;

        public boolean hasVpn;
    }

6.2 忽略字段

默认情况下,Room会为实体中定义的每个字段创建一个列。如果某个实体中有您不想保留的字段,则可以使用@Ignore为这些字段添加注释,添加@Ignore注解的字段,Room就不会为其创建一个列:

 @Entity
    public class User {
        @PrimaryKey
        public int id;

        public String firstName;
        public String lastName;

        @Ignore
        Bitmap picture;
    }

7. 定义对象之间的关系

7.1 创建嵌套对象

有时,你需要将某个实体或数据对象合并成一个紧密的整体。可以使用Embedded注解表示要嵌入。然后,您可以像查询其他各个列一样查询嵌套字段

例如,你的User类可以包含一个Address类型的字段,它表示名为street、city、state、和postCode的字段的组合。那么我们可以在User类中添加Address字段(带有@Embedded注解)代码如下:

public class Address {
        public String street;
        public String state;
        public String city;

        @ColumnInfo(name = "post_code") public int postCode;
    }

    @Entity
    public class User {
        @PrimaryKey public int id;

        public String firstName;

        @Embedded 
        public Address address;
    }

注意,嵌套字段还可以包含其他嵌套字段。也就是Address类也可以嵌套其他字段。

@Embedded有个prefix属性,当某个实体具有相同类型的多个嵌套字段,可以通过设置prefix属性确保每个列的唯一性。Room会将提供的值加到嵌套对象中每个列名称的开头。

7.2 定义一对一关系

两个实体之间的一对一关系是指:父实体的每个实例都恰好对应于子实体的一个实例。

例如:假设有一个音乐在线播放应用,用于在该应用中具有一个属于自己的歌曲库。每个用户只有一个库,每个库恰好对应于一个用户。因此,User实体和Libray实体之间就存在一种一对一的关系

1.首先,为您的两个实体分别创建一个类,其中一个实体必须包含一个变量,且该变量是对另外一个实体的主键的引用

 @Entity
    public class User {
        @PrimaryKey public long userId;
        public String name;
        public int age;
    }

    @Entity
    public class Library {
        @PrimaryKey public long libraryId;
        public long userOwnerId;
    }

userOwnerId字段就是对另一个实体的主键的引用

如需查询用户列表和对应的库,你必须先在两个实体之间建立一对一关系。因此,我们现需要重新创建一个数据类,其中每个实例都包含父实体的一个实例与之对应的子实体实例。将@Relation主键添加到子实体的实例上,同时键parentColumn设置为父实体主键列的名称,并键entityColumn设置为引用父实体主键的子实体列的名称。

 public class UserAndLibrary {
        @Embedded 
        public User user;
        @Relation(
             parentColumn = "userId",
             entityColumn = "userOwnerId"
        )
        public Library library;
    }

最后,向DAO类添加一个方法,用于返回键父实体与子实体配对的数据类的所有实例。该方法需要Room运行两次查询,因此应向该方法添加@Transaction注释,以确保整个操作以原子方式执行。

@Transaction
    @Query("SELECT * FROM User")
    public List<UserAndLibrary> getUsersAndLibraries();

7.3 定义一对多关系

两个实体之间的一对多关系是指:父实体的每个实例对应于子实体的零个或多个实例,但子实体的每个实例只能恰好对应于父实体的一个实例。

在音乐在线播放应用示例中,假设用户可以将其歌曲整理到播放列表中。每个用户可以创建任意数量的播放列表,但每个播放列表只能由一个用户创建。因此,User实体和PlayList实体之间存在一对多关系。

1.首先,创建两个实体类。与上个示例中一样,子实体必须包含一个变量,且该变量是对父实体的主键的引用。

@Entity
    public class User {
        @PrimaryKey 
        public long userId;
        public String name;
        public int age;
    }

    @Entity
    public class Playlist {
        @PrimaryKey 
        public long playlistId;
        //这个变量是对父实体的主键的引用
        public long userCreatorId;
        public String playlistName;
    }

为了查询用户列表和对应的播放列表,必须在两个实体之间建立一对多关系。重新创建一个实体类。实体中包含父实体的一个实例和与之对应的所有子实体实例的列表。将@Relation注释添加到子实体的实例,同时将parentColumn设置为父实体主键列的名称,并将entityColumn设置为引用父实体主键的子实体列的名称。

 public class UserWithPlaylists {
        @Embedded public User user;
        @Relation(
             parentColumn = "userId",
             entityColumn = "userCreatorId"
        )
        public List<Playlist> playlists;
    }

最后,向DAO类添加一个方法,用于返回将父实体与子实体配对的数据类的所有实例。该方法需要Room运行两次查询,因此应向该方法添加@Transaction注释,以确保整个操作以原子方式执行。

7.4 定义多对多关系

多对多关系是指:父实体的每个实例对应于子实体的零个或多个实例。

在音乐在线播放应用示例中,每个播放列表都可以包含多首歌曲,每首歌曲都可以包含在多个不同的播放列表中。因此,Playlist实体和Song实体之间存在多对多的关系。

1.首先创建两个实体类。多对多关系与其他关系类型不同的一点在于,子实体中通常不存在对父实体的引用。因此,需要创建第三个类来表示两个实体之间的关联实体(即交叉引用表)。交叉引用表中必须包含多对多关系中每个实体的主键列。在本例中,交叉引用表中的每一行都对应于Playlist实例和Song实例的配对,其中引用的歌曲包含在引用的播放列表中:

 @Entity
    public class Playlist {
        @PrimaryKey 
        public long playlistId;
        public String playlistName;
    }

    @Entity
    public class Song {
        @PrimaryKey 
        public long songId;
        public String songName;
        public String artist;
    }

    @Entity(primaryKeys = {"playlistId", "songId"})
    public class PlaylistSongCrossRef {
        public long playlistId;
        public long songId;
    }

下一步取决于您想如何查询这些相关实体。

  • 如果您想查询播放列表和每个播放列表所含歌曲的列表,则应创建一个新的数据类,其中包含单个Playlist对象,以及该播放列表所包含的所有Song对象的列表。
  • 如果您想查询歌曲和每首歌曲所在播放列表的列表,则应该创建一个新的数据类,其中包含单个Song对象,以及包含该歌曲的所有Playlist对象的列表

在这两种情况下,都可以通过以下方法在实体之间建立关系:在上述每个类中的@Relation注释中使用associateBy属性来确定提供Playlist实体于Song实体之间关系的交叉引用实体。

 public class PlaylistWithSongs {
        @Embedded public Playlist playlist;
        @Relation(
             parentColumn = "playlistId",
             entityColumn = "songId",
             associateBy = @Junction(PlaylistSongCrossref.class)
        )
        public List<Song> songs;
    }

    public class SongWithPlaylists {
        @Embedded public Song song;
        @Relation(
             parentColumn = "songId",
             entityColumn = "playlistId",
             associateBy = @Junction(PlaylistSongCrossref.class)
        )
        public List<Playlist> playlists;
    }

最后,向DAO类添加一个方法,用于提供您的引用所需的查询功能。

  • getPlaylistsWithSongs:该方法会查询数据库并返回查询到的所有PlaylistWithSongs对象。
  • getSongsWithPlaylists:该方法会查询数据库并返回查询到的所有SongWithPlaylists对象。

这两个方法都需要Room运行两次查询,因此应该为这两个方法添加@Transaction注释,以确保整个操作以原子方式执行。

@Transaction
    @Query("SELECT * FROM Playlist")
    public List<PlaylistWithSongs> getPlaylistsWithSongs();

    @Transaction
    @Query("SELECT * FROM Song")
    public List<SongWithPlaylists> getSongsWithPlaylists();

7.5 定义嵌套关系

有时,您可能需要查询包含三个或更多表格的集合,这些表格之间互相关联。在这种情况下,您需要定义各个表之间的嵌套关系。

在音乐在线播放应用实例中,假设您想要查询所有用户、每个用户的所有播放列表以及每个用户的各个播放列表中包含的所有歌曲。用户与播放列表之间存在一对多关系,而播放列表与歌曲之间存在多对多关系。以下代码示例显示了代表这些实体以及播放列表与歌曲之间多对多关系的交叉引用表的类:

@Entity
    public class User {
        @PrimaryKey 
        public long userId;
        public String name;
        public int age;
    }

    @Entity
    public class Playlist {
        @PrimaryKey 
        public long playlistId;
        public long userCreatorId;
        public String playlistName;
    }
    @Entity
    public class Song {
        @PrimaryKey
        public long songId;
        public String songName;
        public String artist;
    }

    @Entity(primaryKeys = {"playlistId", "songId"})
    public class PlaylistSongCrossRef {
        public long playlistId;
        public long songId;
    }

首先,按照常规方法使用数据类和 @Relation 注释在集合中的两个表格之间建立关系。以下示例展示了一个 PlaylistWithSongs 类,该类可在 Playlist 实体类和 Song 实体类之间建立多对多关系:

public class PlaylistWithSongs {
        @Embedded public Playlist playlist;
        @Relation(
             parentColumn = "playlistId",
             entityColumn = "songId",
             associateBy = @Junction(PlaylistSongCrossRef.class)
        )
        public List<Song> songs;
    }

定义表示此关系的数据类后,请创建另一个数据类,用于在集合中的另一个表与第一个关系类之间建立关系,并将现有关系嵌套到新关系中。以下示例展示了一个 UserWithPlaylistsAndSongs 类,该类可在 User 实体类和 PlaylistWithSongs 关系类之间建立一对多关系:

 public class UserWithPlaylistsAndSongs {
        @Embedded public User user;
        @Relation(
            entity = Playlist.class,
            parentColumn = "userId",
            entityColumn = "userCreatorId"
        )
        public List<PlaylistWithSongs> playlists;
    }

UserWithPlaylistsAndSongs 类间接地在以下三个实体类之间建立了关系:User、Playlist 和 Song

1710779773123.jpg 最后,向 DAO 类添加一个方法,用于提供您的应用所需的查询功能。该方法需要 Room 运行多次查询,因此应添加 @Transaction 注释,以便确保整个操作以原子方式执行。

 @Transaction
    @Query("SELECT * FROM User")
    public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();

注意:使用嵌套关系查询数据需要 Room 处理大量数据,可能会影响性能。因此,请在查询中尽量少用嵌套关系。

8. 创建数据库

@Database(entities = {User.class,Library .class,Playlist.class,Song.class}, version = 1)
    public abstract class AppDatabase extends RoomDatabase {
	private  static  AppDatabase databaseInstance;

    public  static  synchronized  MyDatabase getInstance(Context context){
        if(databaseInstance == null){
            databaseInstance = Room.databaseBuilder(
                    context.getApplicationContext(),
                    AppDatabase .class,
                    DATABASE_NAME)
                    .build();
        }
        return  databaseInstance;
    }
        public abstract UserDao userDao();
        public abstract LibraryDao libraryDao();
        public abstract SongDao songDao();
        public abstract PlaylistDao playlistDao();
    }

  • @Database标签用于告诉系统这是Room数据库对象。entities属性用于指定该数据库有哪些表,若需要建立多张表,则表名以逗号相隔开。version属性用于指定数据库版本号,后面数据库的升级正是依据版本号进行判断的
  • 数据库类需要继承自RoomDatabase,由于每次创建Database实例都会产生比较大的开销,所以可以使用单例模式进行创建。
  • 通过Room.databaseBuilder():生成Database对象,创建一个数据库
  • 抽象方法userDao,libraryDao等是为了获取操作当前实体的Dao文件

9. 创建Dao文件

如需使用 Room 持久性库访问应用的数据,您可以使用数据访问对象 (DAO)。这些 Dao 对象构成了 Room 的主要组件,因为每个 DAO 都包含一些方法,这些方法提供对应用数据库的抽象访问权限。 DAO 既可以是接口,也可以是抽象类。如果是抽象类,则该 DAO 可以选择有一个以 RoomDatabase 为唯一参数的构造函数。Room 会在编译时创建每个 DAO 实现。

9.1 插入数据

当您创建 DAO 方法并使用 @Insert 对其进行注释时,Room 会生成一个实现,该实现在单个事务中将所有参数插入数据库中。

 @Dao
    public interface MyDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        //插入多条数据
        public void insertUsers(User... users);
		
		//插入2条数据
        @Insert
        public void insertBothUsers(User user1, User user2);
		
		//一对多关系 插入数据
        @Insert
        public void insertUsersAndFriends(User user, List<User> friends);
        //多对多插入数据
         public void insertplaylistAndSong(PlaylistSongCrossRef playlistSongCrossRef );
    }

@Insert有一个属性为onConflict,取值有如下:

  1. OnConflictStrategy.ABORT(默认):在发生冲突时回滚事务
  2. OnConflictStrategy.REPLACE:键将现有行替换为新行
  3. OnConflictStrategy.IGNORE:以保持现有行,忽略冲突
  4. FAIL和ROLLBACK官方不建议使用此常熟,这里就不介绍了。

9.2 更新数据

Updatae便捷方法会修改数据库中以参数形式给出的一组实体。它使用与每个实体的主键匹配的查询。

	@Dao
    public interface MyDao {
        @Update
        public void updateUsers(User... users);
    }

9.3 删除数据

Delete 便捷方法会从数据库中删除一组以参数形式给出的实体。它使用主键查找要删除的实体。

 @Dao
    public interface MyDao {
        @Delete
        public void deleteUsers(User... users);
    }

9.4 查询数据

@Query 是 DAO 类中使用的主要注释。它允许您对数据库执行读/写操作。

9.4.1 简单查询

	@Dao
    public interface MyDao {
        @Query("SELECT * FROM user")
        public User[] loadAllUsers();
    }

这是一个极其简单的查询,可加载所有用户。在编译时,Room 知道它在查询用户表中的所有列。如果查询包含语法错误,或者数据库中没有用户表格,则 Room 会在您的应用编译时显示包含相应消息的错误。

9.4.2 将参数传递给查询

在大多数情况下,您需要将参数传递给查询以执行过滤操作,例如仅显示某个年龄以上的用户。要完成此任务,请在 Room 注释中使用方法参数,如以下代码段所示:

	@Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge")
        public User[] loadAllUsersOlderThan(int minAge);
    }

在编译时处理此查询时,Room 会将 :minAge 绑定参数与 minAge 方法参数进行匹配。Room 通过参数名称进行匹配。如果有不匹配的情况,则应用编译时会出现错误。

您还可以在查询中传递多个参数或多次引用这些参数,如以下代码段所示:

	@Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
        public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

        @Query("SELECT * FROM user WHERE first_name LIKE :search " +
               "OR last_name LIKE :search")
        public List<User> findUserWithName(String search);
    }

9.4.3 返回列的子集

大多数情况下,您只需获取实体的几个字段。例如,您的界面可能仅显示用户的名字和姓氏,而不是用户的每一条详细信息。通过仅提取应用界面中显示的列,您可以节省宝贵的资源,并且您的查询也能更快完成。

借助 Room,您可以从查询中返回任何基于 Java 的对象,前提是结果列集合会映射到返回的对象。例如,您可以创建以下基于 Java 的普通对象 (POJO) 来获取用户的名字和姓氏:

public class NameTuple {
        @ColumnInfo(name = "first_name")
        public String firstName;

        @ColumnInfo(name = "last_name")
        @NonNull
        public String lastName;
    }

@Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user")
        public List<NameTuple> loadFullName();
    }

Room 知道该查询会返回 first_name 和 last_name 列的值,并且这些值会映射到 NameTuple 类的字段中。因此,Room 可以生成正确的代码。如果查询返回的列过多,或者返回 NameTuple 类中不存在的列,则 Room 会显示一条警告。

注意:这些POJO也可以使用@Embedde注解

9.4.4 传递参数的集合

部分查询可能要求您传入数量不定的参数,参数的确切数量要到运行时才知道。例如,您可能希望从部分区域中检索所有用户的相关信息

	@Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        public List<NameTuple> loadUsersFromRegions(List<String> regions);
    }

9.4.5 直接光标访问

如果应用的逻辑要求直接访问返回行,您可以从查询中返回 Cursor 对象,如以下代码段所示:

@Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
        public Cursor loadRawUsersOlderThan(int minAge);
    }

9.4.6 查询多个表格

以下代码段展示了如何执行表格联接以整合以下两个表格的信息:一个表格包含当前借阅图书的用户,另一个表格包含当前处于已被借阅状态的图书的数据

 	@Dao
    public interface MyDao {
        @Query("SELECT * FROM book " +
               "INNER JOIN loan ON loan.book_id = book.id " +
               "INNER JOIN user ON user.id = loan.user_id " +
               "WHERE user.name LIKE :userName")
       public List<Book> findBooksBorrowedByNameSync(String userName);
    }

您还可以从这些查询中返回 POJO。例如,您可以编写一条加载某位用户及其宠物名字的查询,如下所示:

	@Dao
    public interface MyDao {
       @Query("SELECT user.name AS userName, pet.name AS petName " +
              "FROM user, pet " +
              "WHERE user.id = pet.user_id")
       public LiveData<List<UserPet>> loadUserAndPetNames();

       static class UserPet {
           public String userName;
           public String petName;
       }
    }

注意: 查询的列需要用别名来代替,别名需和UserPet中的字段名字相同。

10. 预填充数据库

从Room2.2版本开始,Room加入了两个新的API,用于在给定已填充数据库文件的基础上创建Room数据库。基于createFromAsset() API和createFromFiIe() API,开发者可以基于特定的预打包好的数据库文件来创建Room数据库 例如,假设你的应用程序需要用一个Room数据库,以存储世界各地的城市信息。那么你可以在应用程序发布时,将cities.db文件放到assets目录下,在用户首次打开应用程序时,使用createFromAsset()方法,基于cities.db文件创建你的Room数据库。如果你担心将数据库文件打包进assets目录会增加应用程序的大小,还可以考虑在用户首次打开应用程序时,通过网络连接将数据库文件下载至SD卡,接着通过createFromFile()方法来创建Room数据库。

createFromAsset()API的使用方法

首先需要创建一个数据库文件students.db. 创建的方式有很多,这里使用的是SQLite Expert Personal软件 点击File ->new Database

1710780530985.jpg

2.接着创建数据库表student,其中的字段与Student模型类中的字段保持一致

1710780554536.jpg 3. 接着,往数据库中添加数据,模拟数据的预填充

1710780579646.jpg 4.保存数据库,并将数据库文件放到项目的assets/databases目录下

1710780607851.jpg 5.在创建数据库时,调用createFromAssest()方法,基于assets/database/student.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.getApplicationContext(),
                    MyDatabase.class,
                    DATABASE_NAME)
                    //从assest/databases 目录下读取student.db
                    .createFromAsset("databases/student.db")
                    .build();
        }
        return  databaseInstance;
    }
    public  abstract  StudentDao studentDao();
}

从文件系统预填充

从位于设备文件系统任意位置(应用的 assets/ 目录除外)的预封装数据库文件预填充 Room 数据库,可以使用createFromFileAPI,这里就不详细说明了。

Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
        .createFromFile(new File("mypath"))
        .build();