翻译自android官网,可直接去官网观看
由于SQLite是关系数据库,因此您可以指定实体之间的关系。即使大多数对象关系映射库都允许实体对象相互引用,但Room明确禁止这样做。要了解此决策背后的技术推理,请参阅了解Room为什么不允许对象引用。
创建嵌入式对象
有时,您希望在数据库逻辑中将实体或数据对象表示为内聚的整体,即使对象包含多个字段也是如此。在这种情况下,您可以使用 @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将提供的值添加到嵌入对象中每个列名称的开头。
定义一对一关系
一对一的关系两个实体之间是有关系的,其中父实体的每个实例正好对应子实体的一个实例,反之亦然。
例如,考虑一个音乐流应用程序,其中用户拥有他们拥有的歌曲库。每个用户只有一个库,每个库恰好对应一个用户。因此,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();
两个实体之间的一对多关系是指父实体的每个实例对应于子实体的零个或多个实例,但子实体的每个实例只能对应父实体的一个实例。
在音乐流应用程序示例中,假设用户具有将其歌曲整理到播放列表中的能力。每个用户可以根据需要创建任意数量的播放列表,但是每个播放列表仅由一个用户创建。因此,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();
两个实体之间的多对多关系是指父实体的每个实例对应于子实体的零个或多个实例,反之亦然。
在音乐流应用程序示例中,请再次考虑用户定义的播放列表。每个播放列表可以包括许多歌曲,并且每首歌曲可以是许多不同播放列表的一部分。因此,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;
}
下一步取决于您要如何查询这些相关实体。
在这两种情况下,都可以通过使用这些类中每个注释中的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类中添加一个方法以公开您的应用程序所需的查询功能。
这些方法每个都需要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访问数据。
定义嵌套关系
有时,您可能需要查询三个或更多彼此相关的表。在这种情况下,您将在表之间定义嵌套关系。
假设在音乐流应用程序示例中,您要查询所有用户,每个用户的所有播放列表以及每个用户的每个播放列表中的所有歌曲。用户与播放列表具有一对多关系,而播放列表与歌曲具有多对多关系。下面的代码示例显示了表示这些实体的类,以及播放列表和歌曲之间多对多关系的交叉引用表:
@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所示。
图1.音乐流应用程序示例中的关系类图。
UserWithPlaylistsAndSongs对User和PlaylistWithSongs之间的关系进行建模,而后者又对Playlist和Song之间的关系进行建模。
如果您的集合中还有其他表,则创建一个类以对其余每个表之间的关系进行建模,并为对所有先前表之间的关系进行建模的关系类进行建模。这将在您要查询的所有表之间创建一串嵌套关系。
最后,向DAO类中添加一个方法以公开您的应用程序所需的查询功能。此方法需要Room运行多个查询,因此添加 @Transaction注释以确保整个操作是原子执行的:
@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();
警告:查询具有嵌套关系的数据需要Room操纵大量数据并可能影响性能。在查询中使用尽可能少的嵌套关系。
为了防止查询阻止UI,Room不允许在主线程上访问数据库。此限制意味着您必须使DAO查询异步。Room库包括与多个不同框架的集成,以提供异步查询执行。
DAO查询分为三类:
语言和框架选项
Room提供了与特定语言功能和库的互操作性的集成支持。下表根据查询类型和框架显示适用的返回类型:
查询类型 | Kotlin语言功能 | RxJava | RxJava | Jetpack Lifecycle |
---|---|---|---|---|
One-shot write | Coroutines (suspend) | Single |
ListenableFuture |
N/A |
One-shot read | Coroutines (suspend) | Single |
ListenableFuture |
N/A |
One-shot read | Flow |
Flowable |
N/A | LiveData |
Kotlin提供的语言功能使您无需第三方框架即可编写异步查询:
注意:要将Kotlin Flow和协程用于Room,必须在build.gradle文件中包含room-ktx工件。有关更多信息,请参见 声明依赖项。
如果您的应用程序使用Java编程语言,则可以使用RxJava框架中的特殊返回类型来编写异步DAO方法。
对于one-shot queries,Room2.1和更高版本支持 Completable, Single以及Maybe 返回类型
。对于observable queries,Room 支持 Publisher, Flowable以及Observable 返回类型
。注意:要将RxJava与Room一起使用,必须在build.gradle文件中包含room-rxjava2工件。有关更多信息,请参见声明依赖项。
如果您的应用程序使用Java编程语言,并且您不想使用RxJava框架,则可以使用以下替代方法来编写异步查询:
注意:要将Guava与Room一起使用,必须在build.gradle文件中包含room-guava工件 。有关更多信息,请参见声明依赖项。
编写异步一键式查询
一键式查询是仅运行一次并在执行时获取数据快照的数据库操作。以下是异步单次查询的一些示例:
@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>
}
编写可观察的查询
可观察查询是读取操作,只要查询所引用的任何表发生更改,它们都会发出新值。使用此方法的一种方法是,在插入,更新或删除基础数据库中的项目时,帮助您使显示的项目列表保持最新。以下是一些可观察查询的示例:
@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。
将视图创建到数据库中
Room Persistence库的2.1.0版和更高版本提供对SQLite数据库视图的支持,使您可以将查询封装到类中。Room将这些查询支持的类称为视图,并且在DAO中使用时,它们的行为与简单数据对象相同 。
注意:像实体一样,您可以SELECT针对视图运行 语句。但是,不能对视图运行INSERT,UPDATE或DELETE语句。
要创建视图,请将@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;
}
将视图与数据库关联
要将此视图包含在应用程序数据库中,请将views属性包含 在应用程序的 @Database注释中:
@Database(entities = {User.class}, views = {UserDetail.class},
version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
有时,您可能希望您的应用程序从已经加载了一组特定数据的数据库开始。这称为预填充数据库。在Room 2.2.0及更高版本中,可以使用API方法在初始化时使用设备文件系统中预打包的数据库文件中的内容预填充Room数据库。
注意:内存中的room数据库不支持使用createFromAsset()或createFromFile()预填充数据库。
从 app asset 中预填充
要从位于应用程序资产/目录中任何位置的预打包数据库文件预填充文件室数据库,请从RoomDatabase.Builder对象调用build()之前调用createFromAsset() :
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
.createFromAsset("database/myapp.db")
.build();
该createFromAsset()方法接受一个字符串参数,该参数包含从assets/目录到预打包数据库文件的相对路径。
注意:从asset进行预填充时,Room会验证数据库,以确保其架构与预打包数据库的架构匹配。创建预打包的数据库文件时,应导出数据库的架构以用作参考。
从文件系统预填充
要从位于设备文件系统中除应用程序assets/目录之外的任何位置的预打包数据库文件预填充Room数据库,请在调用RoomDatabase.Builder对象中build()方法之前调用createFromFile():
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
.createFromFile(new File("mypath"))
.build();
该createFromFile()方法接受File预打包数据库文件的参数。Room会创建指定文件的副本,而不是直接打开它,因此请确保您的应用对文件具有读取权限。
注意:从文件系统预填充时,Room会验证数据库,以确保其架构与预打包数据库的架构匹配。创建预打包的数据库文件时,应导出数据库的架构以用作参考。
处理包括预打包数据库的迁移
预打包的数据库文件还可以更改Room数据库处理回退迁移的方式。通常,当启用破坏性迁移且Room必须执行迁移而没有迁移路径时,Room会删除数据库中的所有表,并为目标版本创建具有指定架构的空数据库。但是,如果包含与目标版本号相同的预打包数据库文件,Room会在执行破坏性迁移后尝试使用预打包数据库文件的内容填充新创建的数据库。
有关Room数据库迁移的更多信息,请参见迁移Room数据库。
以下各节提供了一些实际操作示例。
示例:使用预打包的数据库进行回退迁移
假设以下内容:
// 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();
在这种情况下会发生以下情况:
回退迁移具有破坏性
。Room会删除设备上已安装的数据库实例
。示例:使用预打包的数据库实施迁移
假设您的应用实现了从版本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();
在这种情况下会发生以下情况:
示例:使用预打包的数据库进行多步迁移
预打包的数据库文件还可能影响包含多个步骤的迁移。考虑以下情况:
// 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();
在这种情况下会发生以下情况: