Android 数据存储(四)-Room

5,134 阅读6分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

一、概述

1.1 描述

上文介绍到了SQLite Api,今天咱们介绍一下 Room ( Android Jetpack 重要成员之一 )。

Room 持久性库在 SQLite 之上提供了一个抽象层,以允许流畅的数据库访问,同时利用 SQLite 的全部功能。特别是,Room 提供以下好处:

  • SQL 查询的编译时验证。
  • 方便的注释,最大限度地减少重复和容易出错的样板代码。
  • 简化的数据库迁移路径。

由于这些考虑,我们强烈建议您使用 Room 而不是直接使用 SQLite API。

1.2 主要部件

Room 包含三个主要组件:

  • 保存数据库并用作与应用程序持久数据的底层连接的主要访问点的数据库类
  • 表示应用数据库中表的数据实体
  • 数据访问对象(DAO),提供你的应用可用于在数据库中查询、更新、插入和删除数据的方法。

数据库类为您的应用程序提供与该数据库关联的 DAO 实例。反过来,应用程序可以使用 DAO 从数据库中检索数据作为关联数据实体对象的实例。该应用程序还可以使用定义的数据实体来更新相应表中的行,或创建新行以进行插入。下图说明了 Room 不同组件之间的关系。

二、创建 Room

2.1 添加依赖项

在 app/build.gradle文件中添加依赖:

dependencies {
    //Room
    def room_version = "2.4.1"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
}

2.2 创建数据实体

将每个 Room 实体定义为一个使用 @Entity 注释的类。 Room 实体包括数据库中对应表中每一列的字段,包括一个或多个构成主键的列。因此,实体类往往是不包含任何逻辑小型模型类。我们的 User 类代表数据库中数据的模型,告诉 Room 它应该基于这个类创建一个表:

@Entity
public class User {
    @PrimaryKey
    public int id;
    
    public String mName;
    public int mAge;
}

通过将 @PrimaryKey 注释添加到正确的字段来设置主键(如:id)。

注意:要保留一个字段,Room 必须有权访问它。 你其设为 public 或为其提供 getter 和 setter 方法来确保 Room 可以访问该字段。

默认情况下:

  • 数据库表名:Room 使用类名作为数据库表名。或通过 @Entity 注释的 tableName 属性来设置表的名称。

  • 列名:Room 默认使用字段名作为数据库中的列名。或通过 @ColumnInfo 注释添加到字段并设置 name 属性来修改列名。

2.2.1 设置 tableName or name 属性

@Entity(tableName = "users")
public class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "name")
    public String mName;
    @ColumnInfo(name = "age")
    public int mAge;
}

此时我们将表名设置为 users 。将 mName 和 mAge 在表中的列名分别修改为 name 和 age。

注意:SQLite 中的表名和列名不区分大小写。

2.2.2 设置主键

1、定义单主键

每个 Room 实体必须定义一个主键,用于唯一标识相应数据库表中的每一行。最直接的方法是使用 @PrimaryKey 注释单个列,如上面 User 类中的 id 属性

注意:如果你需要 Room 为实体实例分配自动 ID,请将 @PrimaryKey 的 autoGenerate 属性设置为 true

@Entity(tableName = "users")
public class User {
    @PrimaryKey(autoGenerate = true)
    public int id;
}

2、定义复合主键

如果你需要通过多个列的组合来唯一标识实体的实例,你可以通过在 @Entity 的 primaryKeys 属性中列出这些列来定义复合主键:

@Entity(tableName = "users",primaryKeys = {"mName","mAge"})
public class User {
    public String mName;
    public int mAge;
}

2.2.3 忽略字段

默认情况下,Room 为实体中定义的每个字段创建一个列。但是某些字段我们不需要保存到数据库,可以使用@Ignore 对其进行注释。

@Entity(tableName = "users")
public class User {
    @Ignore      //@Ignore 此属性不在数据库生产列
    public String nickname;
}

2.3 创建数据访问对象 (DAO)

DAO 负责定义访问数据库的方法。使用 Room,我们不需要所有与 Cursor 相关的代码,只需使用 UserDao 类中的注释定义我们的查询即可。每个 DAO 都包含提供对应用程序数据库的抽象访问的方法在编译时,Room 会自动生成您定义的 DAO 的实现

你可以将 DAO 定义为接口或抽象类。对于基本用例,通常应该使用接口。无论哪种情况,都必须使用 @Dao 注释你的 DAO。DAO 没有属性,但它们确实定义了一种或多种方法来与应用程序数据库中的数据进行交互。

@Dao //这个是必须的
public interface UserDao {
    //无需编写任何 SQL 代码即可在数据库中插入、更新和删除行的便捷方法。
    //新增单个实体
    @Insert
    void insertUser(User user);
    //新增多个实体
    @Insert
    void insertUsers(List<User> users);

    //更新数据
    @Update
    void updateUser(User user);
    //删除数据
    @Delete
    void deleteUser(User user);
    
    //编写自己的 SQL 查询(query)方法
    //查询 users 表
    @Query("SELECT * FROM users")
    List<User> getAll();

    //根据name查询 users 表,将参数集合传递给查询
    @Query("SELECT * FROM users WHERE name IN (:usernames)")
    List<User> loadAllByNames(int[] usernames);

    //将简单参数传递给查询
    @Query("SELECT * FROM users WHERE age > :minAge")
    public User[] loadAllUsersOlderThan(int minAge);
}

2.4 创建数据库

定义一个 AppDatabase 类来保存数据库。AppDatabase 定义数据库配置并充当应用程序对持久数据的主要访问点。数据库类必须满足以下条件:

  • 该类必须使用 @Database 注释进行注释,该注释包括一个实体数组,该数组列出了与数据库关联的所有数据实体。
  • 该类必须是扩展 RoomDatabase 的抽象类。
  • 对于与数据库关联的每个 DAO 类,数据库类必须定义一个具有零参数并返回 DAO 类实例的抽象方法。
import androidx.room.Database;

@Database(entities = {User.class},version = 1)
public abstract class AppDatabase {
    public abstract UserDao userDao();
}

Room 的三个主要组件已经创建好了,咱们开始使用吧。

三、使用 Room

3.1 创建数据库

数据库保存路径:/data/data/com.scc.datastorage/files/room_db

注意:数据库的操作一定要放到子线程中,切不可在主线程中操作(否则会报错),虽然可以强制开启允许这么做,但不要在主线程中操作数据库,避免遇到ANR问题。

            //创建数据库
        String dir = getFilesDir() + "/room_db";
        AppDatabase db = Room.databaseBuilder(AppGlobalUtils.getApplication(),
                AppDatabase.class, dir)
                .build();

允许在主线程中进行数据库操作:

在构建AppDatabase实例的时候,添加allowMainThreadQueries()方法,这样Room就可以在主线程中进行数据库操作了,但是这个方法只建议在测试环境下使用

        db = Room.databaseBuilder(AppGlobalUtils.getApplication(),
                AppDatabase.class, dir)
                .allowMainThreadQueries()
                .build();

3.2 添加数据

            List<User> list = new ArrayList<>();
            list.add(new User(10, "帅次次", 20, "09:00"));
            list.add(new User(12, "朱元璋", 30, "11:00"));
            list.add(new User(15, "赵匡胤", 40, "13:00"));
            list.add(new User(18, "李世民", 50, "15:00"));
            new Thread(() -> {
                userDao.insertUsers(list);
                Log.e("Room", "插入成功:" + db.userDao().queryAll().size());
            }).start();

3.3 查找数据

            new Thread(() -> {
                StringBuilder sql = new StringBuilder();

                List<User> list = userDao.queryAll();
                sql.append(list.size());
                if (list.size() > 0) {
                    for (User bean : list) {
                        sql.append("\n").append(bean.toString());
                    }
                }
                Log.e("Room", sql.toString());
            }).start();

3.4 修改数据

            new Thread(() -> {
                StringBuilder sql = new StringBuilder();
                List<Integer> id = new ArrayList<>();
                id.add(18);
                List<User> list = db.userDao().queryAllById(id);
                if (list.size() > 0) {
                    sql.append("\n 修改数据前:").append(list.get(0).toString());
                }
                User user = new User(18, "武媚娘", 32, "12:00");
                userDao.updateUser(user);
                list = db.userDao().queryAllById(id);
                if (list.size() > 0) {
                    sql.append("\n 修改数据后:").append(list.get(0).toString());
                }
                Log.e("Room", String.valueOf(sql));
            }).start();

3.5 删除数据

                new Thread(() -> {
                    StringBuilder sql = new StringBuilder();
                    List<User> list = userDao.queryAll();
                    if (list.size() > 0) {
                        for (User bean : list) {
                            sql.append("\n").append(bean.toString());
                        }
                    }
                    User user = new User();
                    user.id = 12;
                    db.userDao().deleteUser(user);
                    sql.append("\n删除后");
                    List<User> listDelete = db.userDao().queryAll();
                    Log.e(getClass().getName(), "list:" + list.size());
                    if (listDelete.size() > 0) {
                        for (User bean : listDelete) {
                            sql.append("\n").append(bean.toString());
                        }
                    }
                    Log.e("Room", sql.toString());
                }).start());

然后你会发现 Room 和 SQLite的用法基本一致。操作简单易上手,说试试就试试。

四、相关链接

Android 数据全方案处理

Android 数据存储(一)-文件存储

Android 数据存储(二)-Preferences or MMKV

Android 数据存储(三)-SQLite数据库实例