Jetpack系列之Room----入门(二)

翻译自android官网,可直接去官网观看

Jetpack系列之Room----入门(二)

  • 定义对象之间的关系
    • Create embedded objects
    • Define one-to-one relationships
    • Define one-to-many relationships
    • Define many-to-many relationships
    • Define nested relationships
  • 编写异步DAO查询
    • Language and framework options
      • Kotlin with Flow and couroutines
      • Java with RxJava
      • Java与LiveData和Guava
    • Write asynchronous one-shot queries
    • Write observable queries
  • Create views into a database
    • Create a view
    • Associate a view with your database
  • 预先填充您的Room database
    • Prepopulate from an app asset
    • Prepopulate from the file system
    • Handle migrations that include prepackaged databases
      • Example: Fallback migration with a prepackaged database
      • Example: Implemented migration with a prepackaged database
      • Example: Multi-step migration with a prepackaged database

定义对象之间的关系

由于SQLite是关系数据库,因此您可以指定实体之间的关系。即使大多数对象关系映射库都允许实体对象相互引用,但Room明确禁止这样做。要了解此决策背后的技术推理,请参阅了解Room为什么不允许对象引用。

Create embedded objects

创建嵌入式对象

有时,您希望在数据库逻辑中将实体或数据对象表示为内聚的整体,即使对象包含多个字段也是如此。在这种情况下,您可以使用 @Embedded 批注表示要分解为表中其子字段的对象。然后,您可以查询嵌入字段,就像查询其他各个列一样。

例如,你的User类可以包括Address类型的字段,它代表的命名字段组成street,city,state,和 postCode。要将组成的列分别存储在表中,请在User类中用一个@Embedded注释 Address字段,如以下代码片段所示:

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

表示该表User对象则包含以下名称的列:id,firstName,street,state,city,和post_code。

注意:嵌入字段也可以包括其他嵌入字段

如果一个实体具有多个相同类型的嵌入字段,则可以通过设置该prefix 属性使每一列保持唯一 。然后,Room将提供的值添加到嵌入对象中每个列名称的开头。

Define one-to-one relationships

定义一对一关系

一对一的关系两个实体之间是有关系的,其中父实体的每个实例正好对应子实体的一个实例,反之亦然。

例如,考虑一个音乐流应用程序,其中用户拥有他们拥有的歌曲库。每个用户只有一个库,每个库恰好对应一个用户。因此,User实体与Library实体之间应该存在一对一的关系。

首先,为您的两个实体中的每个实体创建一个类。实体之一必须包含一个变量,该变量是对另一个实体的主键的引用

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

为了查询用户列表和相应的库,必须首先对两个实体之间的一对一关系建模。为此,请创建一个新的数据类,其中每个实例都包含父实体的实例和子实体的对应实例。将@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();

Define one-to-many relationships

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

在音乐流应用程序示例中,假设用户具有将其歌曲整理到播放列表中的能力。每个用户可以根据需要创建任意数量的播放列表,但是每个播放列表仅由一个用户创建。因此,User实体与Playlist实体之间应该存在一对多的关系。

首先,为您的两个实体中的每个实体创建一个类。与前面的示例一样,子实体必须包括一个变量,该变量是对父实体主键的引用

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

Define many-to-many relationships

两个实体之间的多对多关系是指父实体的每个实例对应于子实体的零个或多个实例,反之亦然。

在音乐流应用程序示例中,请再次考虑用户定义的播放列表。每个播放列表可以包括许多歌曲,并且每首歌曲可以是许多不同播放列表的一部分。因此,Playlist实体与Song实体之间应该存在多对多关系。

首先,为您的两个实体中的每个实体创建一个类。多对多关系不同于其他关系类型,因为在子实体中通常没有引用父实体。而是,创建一个第三类来表示两个实体之间的关联实体(或交叉引用表)。交叉引用表必须具有表中表示的多对多关系中每个实体的主键列。在该示例中,交叉引用表中的每一行对应于一个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;
}

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

  • 如果要查询playlists和每个playlist对应的song的列表,请创建一个新的数据类,该类包含单独的playlist和该playlist包含的所有song对象的list 。
  • 如果要查询songs 及其对应的playlists 对象的list ,请创建一个新的数据类,该类包含 Song 对象和包含该Song的所有Playlist对象的list 。

在这两种情况下,都可以通过使用这些类中每个注释中的associateBy属性来建模实体之间的关系, @Relation以标识提供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();

注意:如果@Relation注释不符合您的特定用例,则可能需要JOIN在SQL查询中使用关键字来手动定义适当的关系。要了解有关手动查询多个表的更多信息,请阅读使用Room DAO访问数据。

Define nested relationships

定义嵌套关系

有时,您可能需要查询三个或更多彼此相关的表。在这种情况下,您将在表之间定义嵌套关系。

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

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

userWithPlaylistandSongs类间接地建模了三个实体类之间的关系:User,Playlist和Song。如图1所示。
Jetpack系列之Room----入门(二)_第1张图片
图1.音乐流应用程序示例中的关系类图。

UserWithPlaylistsAndSongs对User和PlaylistWithSongs之间的关系进行建模,而后者又对Playlist和Song之间的关系进行建模。

如果您的集合中还有其他表,则创建一个类以对其余每个表之间的关系进行建模,并为对所有先前表之间的关系进行建模的关系类进行建模。这将在您要查询的所有表之间创建一串嵌套关系。

最后,向DAO类中添加一个方法以公开您的应用程序所需的查询功能。此方法需要Room运行多个查询,因此添加 @Transaction注释以确保整个操作是原子执行的:

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

警告:查询具有嵌套关系的数据需要Room操纵大量数据并可能影响性能。在查询中使用尽可能少的嵌套关系

编写异步DAO查询

为了防止查询阻止UI,Room不允许在主线程上访问数据库。此限制意味着您必须使DAO查询异步。Room库包括与多个不同框架的集成,以提供异步查询执行。

DAO查询分为三类:

  • One-shot write queries,用于插入,更新或删除数据库中的数据。
  • One-shot read queries ,仅从数据库读取一次数据,然后返回带有数据库快照的结果。
  • Observable read queries 可观察的读取查询,每次基础数据库表发生更改时都从数据库中读取数据,并发出新值以反映这些更改。

Language and framework options

语言和框架选项

Room提供了与特定语言功能和库的互操作性的集成支持。下表根据查询类型和框架显示适用的返回类型:

查询类型 Kotlin语言功能 RxJava RxJava Jetpack Lifecycle
One-shot write Coroutines (suspend) Single, Maybe, Completable ListenableFuture N/A
One-shot read Coroutines (suspend) Single, Maybe ListenableFuture N/A
One-shot read Flow Flowable, Publisher, Observable N/A LiveData

Kotlin with Flow and couroutines

Kotlin提供的语言功能使您无需第三方框架即可编写异步查询:

  • 在2.2或更高版本的Room中,您可以使用Kotlin的 Flow 功能编写可观察的查询。
  • 在2.1或更高版本的Room中,您可以使用suspend关键字使用Kotlin协程使您的DAO查询异步。

注意:要将Kotlin Flow和协程用于Room,必须在build.gradle文件中包含room-ktx工件。有关更多信息,请参见 声明依赖项。

Java with RxJava

如果您的应用程序使用Java编程语言,则可以使用RxJava框架中的特殊返回类型来编写异步DAO方法。

  • 对于one-shot queries,Room2.1和更高版本支持 Completable, Single以及Maybe 返回类型
  • 对于observable queries,Room 支持 Publisher, Flowable以及Observable 返回类型

注意:要将RxJava与Room一起使用,必须在build.gradle文件中包含room-rxjava2工件。有关更多信息,请参见声明依赖项。

Java与LiveData和Guava

如果您的应用程序使用Java编程语言,并且您不想使用RxJava框架,则可以使用以下替代方法来编写异步查询:

  • 您可以使用Jetpack的LiveData 包装器类来编写异步observable queries。
  • 您可以使用Guava的ListenableFuture 包装器类来编写异步的one-shot queries

注意:要将Guava与Room一起使用,必须在build.gradle文件中包含room-guava工件 。有关更多信息,请参见声明依赖项。

Write asynchronous one-shot queries

编写异步一键式查询

一键式查询是仅运行一次并在执行时获取数据快照的数据库操作。以下是异步单次查询的一些示例:

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)

    @Update
    suspend fun updateUsers(vararg users: User)

    @Delete
    suspend fun deleteUsers(vararg users: User)

    @Query("SELECT * FROM user WHERE id = :id")
    suspend fun loadUserById(id: Int): User

    @Query("SELECT * from user WHERE region IN (:regions)")
    suspend fun loadUsersByRegion(regions: List<String>): List<User>
}

Write observable queries

编写可观察的查询

可观察查询是读取操作,只要查询所引用的任何表发生更改,它们都会发出新值。使用此方法的一种方法是,在插入,更新或删除基础数据库中的项目时,帮助您使显示的项目列表保持最新。以下是一些可观察查询的示例:

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flow<User>

    @Query("SELECT * from user WHERE region IN (:regions)")
    fun loadUsersByRegion(regions: List<String>): Flow<List<User>>
}

注意:Room中的可观察查询有一个重要限制:无论表中的任何行是否更新,无论该行是否在结果集中,该查询都会重新运行。通过从相应的库(Flow,RxJava或LiveData)应用distinctUntilChanged()运算符,可以确保只有在实际查询结果发生更改时才会通知UI。

Create views into a database

将视图创建到数据库中

Room Persistence库的2.1.0版和更高版本提供对SQLite数据库视图的支持,使您可以将查询封装到类中。Room将这些查询支持的类称为视图,并且在DAO中使用时,它们的行为与简单数据对象相同 。

注意:像实体一样,您可以SELECT针对视图运行 语句。但是,不能对视图运行INSERT,UPDATE或DELETE语句。

Create a view

要创建视图,请将@DatabaseView注释添加 到类中。将注释的值设置为类应表示的查询。

以下代码段提供了一个视图示例:

@DatabaseView("SELECT user.id, user.name, user.departmentId," +
              "department.name AS departmentName FROM user " +
              "INNER JOIN department ON user.departmentId = department.id")
public class UserDetail {
    public long id;
    public String name;
    public long departmentId;
    public String departmentName;
}

Associate a view with your database

将视图与数据库关联

要将此视图包含在应用程序数据库中,请将views属性包含 在应用程序的 @Database注释中:

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

预先填充您的Room database

有时,您可能希望您的应用程序从已经加载了一组特定数据的数据库开始。这称为预填充数据库。在Room 2.2.0及更高版本中,可以使用API​​方法在初始化时使用设备文件系统中预打包的数据库文件中的内容预填充Room数据库。

注意:内存中的room数据库不支持使用createFromAsset()或createFromFile()预填充数据库

Prepopulate from an app asset

从 app asset 中预填充

要从位于应用程序资产/目录中任何位置的预打包数据库文件预填充文件室数据库,请从RoomDatabase.Builder对象调用build()之前调用createFromAsset() :

Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
    .createFromAsset("database/myapp.db")
    .build();  

该createFromAsset()方法接受一个字符串参数,该参数包含从assets/目录到预打包数据库文件的相对路径。

注意:从asset进行预填充时,Room会验证数据库,以确保其架构与预打包数据库的架构匹配。创建预打包的数据库文件时,应导出数据库的架构以用作参考。

Prepopulate from the file system

从文件系统预填充

要从位于设备文件系统中除应用程序assets/目录之外的任何位置的预打包数据库文件预填充Room数据库,请在调用RoomDatabase.Builder对象中build()方法之前调用createFromFile():

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

该createFromFile()方法接受File预打包数据库文件的参数。Room会创建指定文件的副本,而不是直接打开它,因此请确保您的应用对文件具有读取权限

注意:从文件系统预填充时,Room会验证数据库,以确保其架构与预打包数据库的架构匹配。创建预打包的数据库文件时,应导出数据库的架构以用作参考。

Handle migrations that include prepackaged databases

处理包括预打包数据库的迁移

预打包的数据库文件还可以更改Room数据库处理回退迁移的方式。通常,当启用破坏性迁移且Room必须执行迁移而没有迁移路径时,Room会删除数据库中的所有表,并为目标版本创建具有指定架构的空数据库。但是,如果包含与目标版本号相同的预打包数据库文件,Room会在执行破坏性迁移后尝试使用预打包数据库文件的内容填充新创建的数据库

有关Room数据库迁移的更多信息,请参见迁移Room数据库。

以下各节提供了一些实际操作示例。

Example: Fallback migration with a prepackaged database

示例:使用预打包的数据库进行回退迁移

假设以下内容:

  • 您的应用在版本3上定义了Room数据库。
  • 设备上已安装的数据库实例为版本2。
  • 版本3上有一个预打包的数据库文件。
  • 从版本2到版本3没有实现的迁移路径。
  • 破坏性迁移已启用。
// Database class definition declaring version 3.
@Database(version = 3)
public abstract class AppDatabase extends RoomDatabase {
    ...
}

// Destructive migrations are enabled and a prepackaged database
// is provided.
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
    .createFromAsset("database/myapp.db")
    .fallbackToDestructiveMigration()
    .build();

在这种情况下会发生以下情况:

  1. 由于您的应用程序中定义的数据库为版本3,而设备上已安装的数据库实例为版本2,因此必须进行迁移。
  2. 由于没有从版本2到版本3的已实施迁移计划,因此迁移是回退迁移。
  3. 因为fallbackToDestructiveMigration()调用了builder方法,所以回退迁移具有破坏性。Room会删除设备上已安装的数据库实例
  4. 由于版本3中有一个预打包的数据库文件,因此Room会重新创建数据库并使用预打包的数据库文件的内容填充它。另一方面,如果您预打包的数据库文件位于版本2上,则Room会注意到它与目标版本不匹配,并且不会将其用作回退迁移的一部分

Example: Implemented migration with a prepackaged database

示例:使用预打包的数据库实施迁移

假设您的应用实现了从版本2到版本3的迁移路径:

// Database class definition declaring version 3.
@Database(version = 3)
public abstract class AppDatabase extends RoomDatabase {
    ...
}

// Migration path definition from version 2 to version 3.
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        ...
    }
};

// A prepackaged database is provided.
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
    .createFromAsset("database/myapp.db")
    .addMigrations(MIGRATION_2_3)
    .build();

在这种情况下会发生以下情况:

  1. 由于您的应用程序中定义的数据库为版本3,而设备上已安装的数据库为版本2,因此必须进行迁移。
  2. 由于存在从版本2到版本3的已实现迁移路径,因此Room运行定义的migrate()方法将设备上的数据库实例更新到版本3,从而保留数据库中已经存在的数据。Room不使用预打包的数据库文件,因为Room仅在回退迁移的情况下才使用预打包的数据库文件

Example: Multi-step migration with a prepackaged database

示例:使用预打包的数据库进行多步迁移

预打包的数据库文件还可能影响包含多个步骤的迁移。考虑以下情况:

  • 您的应用在版本4上定义了Room数据库。
  • 设备上已安装的数据库实例为版本2。
  • 版本3上有一个预打包的数据库文件。
  • 从版本3到版本4有一个已实现的迁移路径,但没有从版本2到版本3的迁移路径。
  • 破坏性迁移已启用。
// Database class definition declaring version 4.
@Database(version = 4)
public abstract class AppDatabase extends RoomDatabase {
    ...
}

// Migration path definition from version 3 to version 4.
static final Migration MIGRATION_3_4 = new Migration(3, 4) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        ...
    }
};

// Destructive migrations are enabled and a prepackaged database is
// provided.
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
    .createFromAsset("database/myapp.db")
    .addMigrations(MIGRATION_3_4)
    .fallbackToDestructiveMigration()
    .build();

在这种情况下会发生以下情况:

  1. 由于您的应用程序中定义的数据库为版本4,而设备上已安装的数据库实例为版本2,因此必须进行迁移。
  2. 由于没有从版本2到版本3的已实现迁移路径,因此迁移是回退迁移
  3. 因为fallbackToDestructiveMigration()调用了builder方法,所以回退迁移具有破坏性。Room将设备上的数据库实例删除。
  4. 由于版本3中有一个预打包的数据库文件,因此Room会重新创建数据库并使用预打包的数据库文件的内容填充它
  5. 设备上安装的数据库现在的版本为3。因为它仍低于应用程序中定义的版本,因此必须进行另一次迁移。
  6. 由于存在从版本3到版本4的已实现迁移路径,因此Room运行定义的migrate()方法将设备上的数据库实例更新到版本4,从而保留从版本3预打包的数据库文件复制过来的数据

你可能感兴趣的:(Jetpack系列之Room----入门(二))