本文是翻译的Room的官方文档 Save data in a local database using Room
若翻译理解有误请跳转原文链接。
Overview
Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.
Room是在SQLite之上的一个抽象层,使开发者可以流畅的驾驭SQLite的全部功能。
主要分为三个部分:
-
Database
A database holder, main acces point for connection to your raltional data.
At runtime, you can acquire an instance of Database by calling Room.databaseBuilder().
它是被@Database标记的类,并满足以下的条件:
- 包含与table对应的所有实体类。
- 是一个继承RoomDatabse的抽象类。
- 包含没有参数并且返回DAO对象的抽象方法。
-
Entity
代表database中的table。
-
DAO
包含可以访问database的方法。
Room architecture diagram, like this :
sample code snippet:
@Entity
public class User {
@PrimaryKey
public int uid;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
}
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
Defining data using Room entities
For each entity, a table is created within the associated Database object to hold the items
🌟Note:
- Room must have access to entity field, public or provide getter/setter.
- Entities can have either an empty constructor or not , but constructor parameters belong to the entity fields.
Use a primary key
-
每个Entity都必须有至少一个primary key,使用@PrimaryKey annotation。
@Entity public class User { @PrimaryKey public int id; public String firstName; public String lastName; }
-
默认情况下,Room使用Entity的类名作为table的名,但是也可以使用tableName属性设置不同的名,field对应table的column,使用@ColumnInfo的name属性。
@Entity(tableName = "users") public class User { @ColumnInfo(name = "first_name") public String firstName; }
-
分配自动IDs, 使用 @PrimaryKey’s autoGenerate property.
@Entity public class User { @PrimaryKey(autoGenerate = true) public int id; public String firstName; public String lastName; }
-
复合 primary key, 使用@Entit‘s primaryKeys property.
@Entity(primaryKeys = {"firstName", "lastName"}) public class User { public String firstName; public String lastName; }
Ignore fileds
-
默认情况下,entity中的field和table中的column对应,如果不希望某个field在table中保存,可以使用@Ignore。
@Entity public class User { @PrimaryKey public int id; public String firstName; public String lastName; @Ignore Bitmap picture; }
如果field继承自父类,使用@Entity’s ignoreColumns property
@Entity(ignoredColumns = "picture") public class RemoteUser extends User { @PrimaryKey public int id; public boolean hasVpn; }
Fields be unique
by setting the unique property of an @Index annotation to true.
@Entity(indices = {@Index(value = {"first_name", "last_name"},
unique = true)})
public class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
Define relationships between objects
Most object-relational mapping libraries allow entity objects to reference each other, but Room explicitly forbids this.
like this :
@Entity
public class User {
String name;
Book book;//Room明确禁止这样做.
}
🌟Note :
- Room虽然Room不能使用直接的引用关系,但是允许你定义实体之间外键约束。
Define one-to-many relationships
Zero or more instances of Book can be linked to a single instance of User through the user_id foreign key.
Sample code snippet:
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
public class Book {
@PrimaryKey public int bookId;
public String title;
@ColumnInfo(name = "user_id") public int userId;
}
Foreign keys功能非常强大,因为它在更新referenced entity的时候还可以指定database做些什么。
一个例子,使用 @ForeignKey 's property onDelete = CASCADE,你可以告诉database在删除一个user的时候删除user对应的所有books。
Create nested objects
🌟Note: Is not reference objects.
在database table包含多个column,但是你希望拥有在database logic中的内聚整体,也就是domain model。
总之就是,domain model is some subfields within a table。
Sample code snippet:
public class Task {//domain model
public String name;
public long date;
}
@Entity(name = "task")
public class TaskEntity {
@PrimaryKey public int id;
@Embedded public Task task;
}
🌟Note:
- @Embedded 's property prefix can keep table column unique.
Define many-to-many relationships
where each entity can be linked to zero or more instances of the other.
一个例子,音乐App中,每个song可以添加到多个playlist,每个playlist也可以有很多song。
To model this relationship, you will need to create three objects:
- An entity class for the playlists.
- An entity class for the songs.
- An intermediate class to hold the information about which songs are in each playlist.
Sample code snippet:
@Entity
public class Playlist {
@PrimaryKey public int id;
public String name;
public String description;
}
@Entity
public class Song {
@PrimaryKey public int id;
public String songName;
public String artistName;
}
@Entity(tableName = "playlist_song_join",
primaryKeys = { "playlistId", "songId" },
foreignKeys = {
@ForeignKey(entity = Playlist.class,
parentColumns = "id",
childColumns = "playlistId"),
@ForeignKey(entity = Song.class,
parentColumns = "id",
childColumns = "songId")
})
public class PlaylistSongJoin {
public int playlistId;
public int songId;
}
@Dao
public interface PlaylistSongJoinDao {
@Insert
void insert(PlaylistSongJoin playlistSongJoin);
@Query("SELECT * FROM playlist " +
"INNER JOIN playlist_song_join " +
"ON playlist.id=playlist_song_join.playlistId " +
"WHERE playlist_song_join.songId=:songId")
List<Playlist> getPlaylistsForSong(final int songId);
@Query("SELECT * FROM song " +
"INNER JOIN playlist_song_join " +
"ON song.id=playlist_song_join.songId " +
"WHERE playlist_song_join.playlistId=:playlistId")
List<Song> getSongsForPlaylist(final int playlistId);
}
Create views into a database
在version 2.1.0或者更高版本,Room提供了SQLite database views。
Create a database view
Add the @DatabaseView annotation to a class. Set the annotation’s value to the query that the class should represent.
@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
@Database(entities = {User.class}, views = {UserDetail.class},
version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
Accessing data using Room DAOs
Data access objects, or DAOs,是Room Architecture中最要的组成部分。
Room会在编译时为每一个Dao创建实现类Dao_impl。
Define methods for convenience
常见的方法使用方式。
Insert
Room会生成向database table新增所有的entities的单个事务。
@Dao
public interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);
@Insert
public void insertBothUsers(User user1, User user2);
@Insert
public void insertUsersAndFriends(User user, List<User> friends);
}
被@Insert标记的方法也可以有long类型的返回值,代表新增的行的rowID,如果方法参数是array / collection,则可以返回long[ ] / List<Long>。
Update
被@Update标记的方法,在database中是使用查询匹配每一个参数的primary key,然后update。
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
方法也可以有int类型的返回值,代表有多少行被影响。
Delete
被@Delete标记的方法,在database中是使用查询匹配每一个参数的primary key,然后delete。
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
方法也可以有int类型的返回值,代表有多少行被影响。
Query for information
@Query是Dao class中比较特殊的注解,它允许你基于一个database进行read/write操作。
每一个@Query标记的方法都会在编译时被检查,如果@Query中包含语法错误,编译将报错并被出错误信息。
Sample code snippet:
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
Passing parameters into the query
将方法的参数传递到query语句中。
At compile,Room matches the :minAge bind parameter with the minAge method parameter.
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
Returning subsets of columns
查询返回table column的子集。
大多数情况下,你只需要查询部分的fields,比如只用于UI显示,Room可以满足这样的场景。
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();
}
Passing a collection of arguments
传递集合参数到查询语句中
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
Observable queries
执行查询时,如果你希望你的应用程序的UI在数据更改时自动更新。
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}
Room会生成必要的代码以便在database数据被更新的时候触发LiveData。
Reactive queries with RxJava
Room也支持返回RxJava2的相关类型。
Configure build.gradle to support rxjava2
dependencies {
def room_version = "2.1.0"
implementation 'androidx.room:room-rxjava2:$room_version'
}
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
// Emits the number of users added to the database.
@Insert
public Maybe<Integer> insertLargeNumberOfUsers(List<User> users);
// Makes sure that the operation finishes successfully.
@Insert
public Completable insertLargeNumberOfUsers(User... users);
/* Emits the number of users removed from the database. Always emits at
least one user. */
@Delete
public Single<Integer> deleteUsers(List<User> users);
}
Querying multiple tables
多表查询。
@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();
// You can also define this class in a separate file, as long as you add the
// "public" access modifier.
static class UserPet {
public String userName;
public String petName;
}
}
Referencing complex data using Room
引用复杂数据。
Room允许在基本数据类型和装箱类型之间的转换,但是不允许reference。
Use type converters
使用类型转换器 @TypeConverter。
例如,如果要保留Date类型的实例,可以编写以下代码,以将等效的Unix时间戳存储在数据库中。
-
定义TypeConverter class:
public class Converters { @TypeConverter public static Date fromTimestamp(Long value) { return value == null ? null : new Date(value); } @TypeConverter public static Long dateToTimestamp(Date date) { return date == null ? null : date.getTime(); } }
-
接下来,将@TypeConverter注解添加到抽象的Database类上,这样Room就可以使用定义好的类型转换器:
@Database(entities = {User.class}, version = 1) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); }
- 然后,你就可以像使用基本数据类型一样使用自定义的类型了:
@Entity public class User { private Date birthday; }
@Dao public interface UserDao { @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to") List<User> findUsersBornBetweenDates(Date from, Date to); }
Understand why Room doesn’t allow object references
理解为什么Room禁止Object-relational mappings。
- Object-relational mappings通常是延迟加载,它发生在主线程上,但是客户端的主线程非常敏感,然而如果你不使用延迟加载,可能会获取更多不必须的数据,从而造成更多内存消耗问题。
- Object-relational mappings通常由开发人员来决定程序和UI之间的共享模型,但是这个共享模型不能很好的扩展,因为随着UI的变化,共享数据变得很难预料和调试。
总之,Room觉得,要达到多个对象之间的关系映射,可以使用这样的方式更好:
- 创建一个包含所需要的Field的POJO
- 然后编写一个连接相应表的多表查询(Querying multiple tables)
这种方式和Room强大的查询验证功能结合起来,可以使你的应用程序在加载数据的时候消耗更少的资源,从而改善应用程序的性能和用户体验。