Jetpacak之Room

前言

google官方的数据库框架,可以无缝结合LiveData以及Rxjava结合使用。

文档:Romm doc

api详细:Room summary

基础使用步骤

引入依赖,开启androidx哦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dependencies {
def room_version = "2.2.0-alpha01" // 2.1.0 for latest stable version

implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"

// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"

// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"

// Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
  • 定义实体类 - Entity
  • 定义Dao类 - Dao
  • 定义抽象数据库类 - Database
  • 使用

定义实体

实体类对应着一张表

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class User {
@PrimaryKey
public int uid;

@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;
}

定义Dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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);

@Updata
void update(User user);
}

定义room数据库

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

使用

1
2
3
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
db.userDao().insert(new User());

定义实体

基础注解

  • @Entity 标注为实体
  • @PrimaryKey标注为主键
  • @ColumnInfo附加字段信息,比如字段名
  • @Ignore 标注为忽略属性

使用@Entity标注实体类,属性皆为对应表字段,前提是要JavaBean,要么属性public,要么提供set/get方法。

使用@PrimaryKey标注主键,autoGenerate值设置自增

1
2
3
4
5
6
7
8
@Entity
public class User {
@PrimaryKey
public int id;

public String firstName;
public String lastName;
}

可以在@Entity中设置主键

1
2
3
4
5
@Entity(primaryKeys = {"firstName", "lastName"})
public class User {
public String firstName;
public String lastName;
}

默认是以类名为表名,可以自定义表名,

1
2
3
4
@Entity(tableName = "users")
public class User {
// ...
}

SQLite中表名忽略大小写的

自定属性对应字段名,使用@ColumnInfo

1
2
3
4
5
6
7
8
9
10
11
@Entity(tableName = "users")
public class User {
@PrimaryKey
public int id;

@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;
}

忽略属性,从而不对应表字段,使用@Ignore

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class User {
@PrimaryKey
public int id;

public String firstName;
public String lastName;

@Ignore
Bitmap picture;
}

同样可以在@Entity声明

1
2
3
4
5
6
7
@Entity(ignoredColumns = "picture")
public class RemoteUser extends User {
@PrimaryKey
public int id;

public boolean hasVpn;
}

查询支持

FTS支持

FTS:Full Text Search 全文检索

要求:sdk大于16,Room2.1以上可以Fts3,Fts4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Use `@Fts3` only if your app has strict disk space requirements or if you
// require compatibility with an older SQLite version.
@Fts4
@Entity(tableName = "users")
public class User {
// Specifying a primary key for an FTS-table-backed entity is optional, but
// if you include one, it must use this type and column name.
@PrimaryKey
@ColumnInfo(name = "rowid")//FTS开启后必须int主键名为rowid
public int id;

@ColumnInfo(name = "first_name")
public String firstName;
}

使用languageId来指定字段来存储多语言信息。

1
2
3
4
5
6
7
8
@Fts4(languageId = "lid")
@Entity(tableName = "users")
public class User {
// ...

@ColumnInfo(name = "lid")
int languageId;
}

索引支持

如果app的sdk版本不支持FTS,为了加快查询速度,可以考虑使用索引,使用@Index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity(indices = {@Index("name"),@Index(value = {"last_name", "address"})})
public class User {
@PrimaryKey
public int id;

public String firstName;
public String address;

@ColumnInfo(name = "last_name")
public String lastName;

@Ignore
Bitmap picture;
}

索引唯一,添加unique属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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;
}

支持AutoValue对象

啥是AutoValue?

AutoValue说明,我看了下文档,大概意思就是说书写JavaBean类,需要写很多get/set,equals等大量模板代码,通过使用@AutoValue注解一个抽象类,书写关键代码,模板代码交由注解处理器生成。

1
2
3
4
5
6
7
8
9
10
11
import com.google.auto.value.AutoValue;

@AutoValue
abstract class Animal {
static Animal create(String name, int numberOfLegs) {
return new AutoValue_Animal(name, numberOfLegs);
}

abstract String name();
abstract int numberOfLegs();
}

通过apt注解处理器帮你生成大量的模板代码。诶,这不是和kotlin的Data类挺像的吗。hia~~

而Room使用AutoValue也很简单,某些标注了注解的地方,一定要加上@CopyAnnotations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@AutoValue
@Entity
public abstract class User {
// Supported annotations must include `@CopyAnnotations`.
@CopyAnnotations
@PrimaryKey
public abstract long getId();

public abstract String getFirstName();
public abstract String getLastName();

// Room uses this factory method to create User objects.
public static User create(long id, String firstName, String lastName) {
return new AutoValue_User(id, firstName, lastName);
}
}

定义对象关系

Room不允许对象引用

一对多

一对多的话就是通过外键制约。比如说一个人可以有多本书,一本书只能属于一个人。

通过@ForeignKey定义外键,外键作用在于能够限定两条数据的级联操作

onUpdate

onDelete

有这么几种值,也是数据库的知识,提下就行了

1
2
3
4
5
6
7
8
9
int NO_ACTION = 1;

int RESTRICT = 2;

int SET_NULL = 3;

int SET_DEFAULT = 4;

int CASCADE = 5;

具体

1
2
3
4
5
6
7
8
9
10
@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;
}

多对多

即比如一个歌单有n首歌,一首歌可以属于n个歌单

定义实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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;
}

多对多关联需要一个中间实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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;
}

通过中间实体来映射多对多操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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);
}

内嵌对象

试着想下这么一个实体,比如人的信息,可以在次分割成小的对象表示,比如通过邮编,街道,街牌标注地址对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

这样生成表结构也是一样的,更加解耦,更加好理解,以及使用了。

创键视图

sql 里面视图是用户查询使用的,实际生成的表结构也是通过查询的出来,实际上不存在。

使用@DatabaseView创键一个视图实体,只能用于查询

1
2
3
4
5
6
7
8
9
@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;
}

在数据库中关联

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

使用Dao

@Dao可以注解接口和抽象类,取决于是否需要构造。默认dao的查询不能再主线程中,但是可以通过allowMainThreadQueries()再构建器中构建,或者异步的查询考虑返回LiveData或者Flowable

insert

使用@Insert来标注插入操作

1
2
3
4
5
6
7
8
9
10
11
@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);
}

插入方法可以返回long的值,这个值就是插入Id,如果是插入集合的话,返回的也就是long[]或则List<Long>

update

根据主键去匹配行,可以返回int表示所影响的行数

1
2
3
4
5
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}

delete

通过主键去匹配行,可以返回int表示所影响的行数

1
2
3
4
5
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}

Query

简单查询

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

带参

使用:minAge占位参数

1
2
3
4
5
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}

多个占位参数

1
2
3
4
5
6
7
8
9
@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);
}

查询的部分字段

有时候你不需要获得全部字段,只需要部分字段

定义部分字段POJO,不需要用@Entity

1
2
3
4
5
6
7
8
public class NameTuple {
@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
@NonNull
public String lastName;
}

使用

1
2
3
4
5
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}

传入集合

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

可观察数据

返回LiveData

1
2
3
4
5
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}

RxJava支持

首先需要加入RxJava支持依赖

1
2
3
4
dependencies {
def room_version = "2.1.0"
implementation 'androidx.room:room-rxjava2:$room_version'
}

可以返回被压FlowableMaybeCompleteableSingle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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);
}

详情见 Room and RxJava

返回Cursor

当然了,对原来的Cursor游标也是支持返回的

非常不提倡使用Cursor,因为游标不保证能查询的行存在,以及行内元素存在,除非你的项目期待使用游标,或者重构困难才考虑返回游标

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

多表联合查询

允许多表联合查询,同时允许返回LiveDataFlowable等可观察数据

1
2
3
4
5
6
7
8
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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;
}
}

使用kotlin协程

suspend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Dao
interface MyDao {
@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")
suspend fun loadAllUsers(): Array<User>
}

声明事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Dao
abstract class UsersDao {
@Transaction
open suspend fun setLoggedInUser(loggedInUser: User) {
deleteUser(loggedInUser)
insertUser(loggedInUser)
}

@Query("DELETE FROM users")
abstract fun deleteUser(user: User)

@Insert
abstract suspend fun insertUser(user: User)
}

类型转换器

定义类型转化器

1
2
3
4
5
6
7
8
9
10
11
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();
}
}

使用

1
2
3
4
5
@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}

自动将Java的Date和数据库中的时间戳互相转化

数据库迁移

待补充

数据库测试

待补充