Android架构之Room组件(二)

Jetpack组件系列文章
Android架构之LifeCycle组件
Android架构之Navigation组件(一)
Android架构之Navigation组件(二)
Android架构之Navigation组件(三)
Android架构之Navigation组件(四)
Android架构之ViewModel组件
Android架构之LiveData组件
Android架构之Room组件(一)
Android架构之Room组件(二)
Android架构之WorkManager组件
Android架构之DataBinding(一)
Android架构之DataBinding(二)
Android架构之Paging组件(一)
Android架构之Paging组件(二)
Jetpack与MVVM架构

上一节中,我们对Room基本使用,以及Room和LivaData搭配使用,和数据库升级做出了详细的介绍,这章我们将深入学习Room的使用。

Android架构之Room组件(一)

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. foreignKeys: 设置外键

使用主键

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

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

忽略字段

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

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

        public String firstName;
        public String lastName;

        @Ignore
        Bitmap picture;
    }

定义对象之间的关系

创建嵌套对象

有时,你需要将某个实体或数据对象合并成一个紧密的整体。可以使用@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会将提供的值添加到嵌套对象中每个列名称的开头。

定义一对一关系

两个实体之间的一对一关系是指: 父实体的每个实例都恰好对应于子实体的一个实例。
例如: 假设有一个音乐在线播放应用,用户在该应用中具有一个属于自己的歌曲库。每个用户只有一个库,每个库恰好对应于一个用户。因此,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();

定义一对多关系

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

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

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 注释,以确保整个操作以原子方式执行。

@Transaction
    @Query("SELECT * FROM User")
    public List<UserWithPlaylists> getUsersWithPlaylists();

定义多对多关系

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

在音乐在线播放应用示例中,每个播放列表都可以包含多首歌曲,每首歌曲都可以包含在多个不同的播放列表中。因此,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();

定义嵌套关系

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

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

@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
Android架构之Room组件(二)_第1张图片
最后,向 DAO 类添加一个方法,用于提供您的应用所需的查询功能。该方法需要 Room 运行多次查询,因此应添加 @Transaction 注释,以便确保整个操作以原子方式执行。

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

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

创建数据库

@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文件

创建Dao文件

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

插入数据

当您创建 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 官方不建议使用此常数,这里就不介绍了。

更新数据

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

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

删除数据

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

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

查询数据

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

简单查询

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

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

将参数传递给查询

在大多数情况下,您需要将参数传递给查询以执行过滤操作,例如仅显示某个年龄以上的用户。要完成此任务,请在 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);
    }

返回列的子集

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

借助 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 也可以使用 @Embedded 注释。

传递参数的集合

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

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

直接光标访问

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

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

查询多个表格

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

 	@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中的字段名字相同。

预填充数据库

从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
Android架构之Room组件(二)_第2张图片

2.接着创建数据库表student,其中的字段与Student模型类中的字段保持一致
Android架构之Room组件(二)_第3张图片
3. 接着,往数据库中添加数据,模拟数据的预填充
Android架构之Room组件(二)_第4张图片
4.保存数据库,并将数据库文件放到项目的assets/databases目录下
Android架构之Room组件(二)_第5张图片
4. 在创建数据库时,调用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();

好了,Room就说到这里了,不足之处,望大家指出来,共同进步谢谢。

更加详细的Room使用,大家可以去官方文档瞧瞧
官方文档

你可能感兴趣的:(Android,Jetpack)