本文参考:https://www.jianshu.com/p/3e358eb9ac43
Android 2017 IO大会推出了官方数据库框架:Room。Room对原生的SQLite API进行了一层封装。
Room是一个对象关系映射(ORM)库。Room抽象了SQLite的使用,可以在充分利用SQLite的同时流畅的访问数据库。
Room官方文档:https://developer.android.com/training/data-storage/room/
Room由三个重要的组件组成:Database、Entity、DAO。
Database:包含数据库持有者,并作为与应用持久关联数据的底层连接的主要访问点。
Database对应的类必须满足下面几个条件:
在运行时,可以通过Room.databaseBuilder() 或者Room.inMemoryDatabaseBuilder()获取Database实例。
Entity:代表数据库中某个表的实体类。
DAO:包含用于访问数据库的方法。
implementation "android.arch.persistence.room:runtime:1.1.1"
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
testImplementation "android.arch.persistence.room:testing:1.1.1"
Entity代表存放在数据库某个表中的实体类,默认情况下Room库会把Entity里面所有的字段对应到表中的每一列。Entity就是我们要向数据库中存放的数据类型。
@Entity
public class User {
}
Entity的实体类需要添加@Entity注解。
@Entity注解包含的属性 | 含义 |
---|---|
tableName | 设置表名字 |
indices | 设置索引 |
inheritSuperIndices | 父类的索引是否会自动被当前类继承 |
primaryKeys | 设置主键 |
foreignKeys | 设置外键 |
在默认情况下,Entity类名就是表名,但可以通过@Entity的tableName属性设置自定义表名。
@Entity(tableName = "users")
public class User {
...
}
在默认情况下,Entity类中字段名就是表中列的名称,但可以通过@ColumnInfo注解来自定义表中列的名字。比如下列代码,在 user 表中,name 字段对应的列名是 user_name ,age 字段对应的列名是 user_age。
@Entity(tableName = "user")
public class User {
@ColumnInfo(name = "user_name")
private String name;
@ColumnInfo(name = "user_age")
private String age;
}
每个Entity类都需要至少一个主键,即使这个Entity类只有一个属性。
设置主键的方法主要有两种:
1.通过@Entity注解的primaryKeys属性来设置主键,可以设置成单个主键,也可以是复合主键。
@Entity(primaryKeys = {
"name", "age"})
public class User {
private String name;
private String age;
}
2.通过@PrimaryKey注解设置主键
@Entity
public class User {
@PrimaryKey
private String name;
@PrimaryKey
private String age;
}
如果希望主键是自增的,可以设置@PrimaryKey注解中的autoGenerate属性。
@Entity
public class User {
@PrimaryKey(autoGenerate = true)
private String name;
private String age;
}
数据库索引可以提高数据库的访问速度。索引可以分为单列索引和组合索引,可以通过@Entity注解中的indices属性进行设置。
@Entity(indices = {
@Index(value = {
"id"}), @Index(value = {
"name", "age"})})
public class User {
@PrimaryKey
private int id;
private String name;
private String age;
}
索引又可以分为唯一索引和非唯一索引,可以通过@Index注解中的unique设置是否唯一索引。
@Entity(indices = {
@Index(value = {
"id"}, unique = true)})
public class User {
@PrimaryKey
private int id;
private String name;
private String age;
}
因为SQLite是关系形数据库,表和表之间是有关系的。这也就是我们数据库中常说的外键约束(FOREIGN KEY约束)。Room里面可以通过@Entity的foreignKeys属性来设置外键。我们用一个具体的例子来说明。
正常情况下,数据库里面的外键约束。子表外键于父表。当父表中某条记录子表有依赖的时候父表这条记录是不能删除的,删除会报错。一般大型的项目很少会采用外键的形式。一般都会通过程序依赖业务逻辑来保证的。
父表
@Entity(indices = {
@Index(value = {
"id"}, unique = true)})
public class User {
@PrimaryKey
private int id;
private String name;
private String age;
}
子表
@Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "id", childColumns = "userId"))
public class Book {
@PrimaryKey
private int bookId;
private String title;
private int userId;
}
子表Book的foreignKeys设置后,userId属性来源于父表User的id属性。
@Foreignkey属性 | 含义 |
---|---|
entity | parent实体类(引用外键的表的实体) |
parentColumns | parent外键列(要引用的外键列) |
childColumns | child外键列(要关联的列) |
onDelete | 默认NO_ACTION,当parent里面有删除操作的时候,child表可以做的Action动作有: 1. NO_ACTION:当parent中的key有变化的时候child不做任何动作。 2. RESTRICT:当parent中的key有依赖的时候禁止对parent做动作,做动作就会报错。 3. SET_NULL:当paren中的key有变化的时候child中依赖的key会设置为NULL。 4. SET_DEFAULT:当parent中的key有变化的时候child中依赖的key会设置为默认值。 5. CASCADE:当parent中的key有变化的时候child中依赖的key会跟着变化。 |
onUpdate | 默认NO_ACTION,当parent里面有更新操作的时候,child表需要做的动作。Action动作方式与onDelete一样 |
deferred | 默认值false,在事务完成之前,是否应该推迟外键约束。当我们启动一个事务插入很多数据的时候,事务还没完成之前。当parent引起key变化的时候。可以设置deferred为ture。让key立即改变。 |
在有些情况下,需要多个对象组合成一个对象,在对象之间有嵌套关系。在Room库中可以通过@Embedded属性进行设置。
@Entity(indices = {
@Index(value = {
"id"}, unique = true)})
public class User {
@PrimaryKey
private int id;
private String name;
private String age;
@Embedded
private Address address;
}
public class Address {
private String country;
private String province;
private String city;
}
这个组件代表了作为DAO的类或者接口。DAO是Room的主要组件,负责定义访问数据库的方法。Room使用过程中一般使用抽象DAO类来定义数据库的CRUD操作。DAO可以是一个接口也可以是一个抽象类。如果它是一个抽象类,它可以有一个构造函数,它将RoomDatabase作为其唯一参数。Room在编译时创建每个DAO实体。
DAO里面所有的操作都是依赖方法来实现的。
给一个接口或抽象类添加@Dao,则会成为Dao组件。
@Dao
public interface UserDao {
}
Entity类如下。
@Entity(tableName = "users")
public class User {
@PrimaryKey
private String id;
private String name;
private String age;
}
需要在数据库中查找一些信息时,可以使用@Query注解
@Query("select * from users")
List<User> loadAllUsers();
这里的users是我们在@Entity中设置的tableName。
@Query("select * from users where id == :userId")
User loadUserById(String userId);
@Query("select * from users where age between :minAge and :maxAge")
List<User> loadUsersBetweenAges(int minAge, int maxAge);
当我们需要给表中插入一条数据时,可以使用@Insert注解。
@Insert注解可以设置一个属性:onConflict:默认值是OnConflictStrategy.ABORT,表示当插入有冲突的时候的处理策略。其中OnConflictStrategy封装了Room解决冲突的相关策略:
参数 | 含义 |
---|---|
OnConflictStrategy.REPLACE | 冲突策略是取代旧数据同时继续事务 |
OnConflictStrategy.ROLLBACK | 冲突策略是回滚事务 |
OnConflictStrategy.ABORT | 冲突策略是终止事务 |
OnConflictStrategy.FAIL | 冲突策略是事务失败 |
OnConflictStrategy.IGNORE | 冲突策略是忽略冲突 |
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertUser(User user);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertUsers(List<User> users);
需要更新某条数据的信息时,可以使用@Update注解。
@Update与@Insert一样,设置onConflict来表明冲突的时候的解决办法。
@Update注解的方法可以返回int变量。表示更新了多少行。
@Update(onConflict = OnConflictStrategy.REPLACE)
int updateUser(User user);
我们也可以使用@Query注解进行更新,如下所示。
@Query("update users set name =:userName where id =:userId ")
int updateUserById(String userId, String userName);
需要删除某条数据时,可以使用@Delete注解。
@Delete注解可以设置int返回值来表示删除了多少行。
@Delete
int deleteUser(User user);
我们同样可以使用@Query注解进行删除,如下所示。
@Query("delete from users")
int deleteUsers();
@Database注解可以用来创建数据库的持有者。该注解定义了实体列表,该类的内容定义了数据库中的DAO列表。这也是访问底层连接的主要入口点。注解类应该是抽象的并且扩展自RoomDatabase。
Database对应的对象(RoomDatabase)必须添加@Database注解,@Database包含的属性:
entities:数据库相关的所有Entity实体类,他们会转化成数据库里面的表。
version:数据库版本。
exportSchema:默认true,也是建议传true,这样可以把Schema导出到一个文件夹里面。同时建议把这个文件夹上次到VCS。
在运行时,你可以通过调用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()获取实例。因为每次创建Database实例都会产生比较大的开销,所以应该将Database设计成单例的,或者直接放在Application中创建。
两种方式获取Database对象的区别:
Room.databaseBuilder():生成Database对象,并且创建一个存在文件系统中的数据库。
Room.inMemoryDatabaseBuilder():生成Database对象并且创建一个存在内存中的数据库。当应用退出的时候(应用进程关闭)数据库也消失。
@Database(entities = User.class, version = 1)
public abstract class UserDataBase extends RoomDatabase {
public abstract UserDao userDao();
private static volatile UserDataBase INSTANCE;
private UserDataBase() {
}
public UserDataBase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (UserDataBase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(), UserDataBase.class, "RoomTest.db")
.build();
}
}
}
return INSTANCE;
}
}
room库是使用注解生成代码的,错误原因是没有找到生成的代码,解决方法是添加下面的依赖
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
这是room库一部分的依赖图,从中可以看到room库使用了JavaPoet生成代码,其原理是使用注解处理器编译和扫描注解,然后使用JavaPoet生成代码,我在之前的博客中提到过这个技术,有兴趣的可以看看。https://blog.csdn.net/Viiou/article/details/86445901
主键不能为空,所以我们给主键添上不能为空的注解即可。
@PrimaryKey
@NonNull
private String id;
room.schemaLocation
annotation processor argument OR set exportSchema to false.@Database注解的exportSchema默认为true,我们需要添加导出schema的目录,所以可以将exportSchema的值设为false,不导出schema,自然不需要添加schema的目录。
另外一种方法是添加导出schema的目录,Room会将数据库的表信息导出为一个json文件。你应该在版本控制系统中保存该文件,该文件代表了你的数据库表历史记录,这样允许Room创建旧版本的数据库用于测试。代码如下所示:
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
// 用于测试
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
Entity类构造方法的参数必须和字段一致,即一模一样,解决方法是直接将字段复制粘贴到构造方法即可。
@Entity(tableName = "users")
public class User {
@PrimaryKey
@NonNull
private String id;
private String name;
private String age;
public User(String id, String name, String age) {
this.id = id;
this.name = name;
this.age = age;
}
}
至此,整个Room库的大体操作便差不多了,但是,实际使用的话不是很优雅,所以接下来的是对整个数据库操作的封装,可以优雅,方便的CURD。
类 | 含义 |
---|---|
UsersDataSource | 数据操作接口,定义了该做什么样的操作 |
UsersLocalDataSource | 数据操作在本地的具体实现 |
UsersRepository | Users仓库,在本地与网络之间进行选择 |
DataRepository | 数据仓库,方便使用 |
AppExecutors | 整个App的线程池,方便使用 |
DiskIOThreadExecutors | 本地操作线程池 |
MainIOThreadExecutors | 主线程操作线程池 |
UsersDataSource.java
public interface UsersDataSource {
interface LoadUsersCallback {
void onUsersLoaded(List<User> userList);
void onDataNotAvailable();
}
interface GetUserCallback {
void onUserLoaded(User user);
void onDataNotAvailable();
}
void loadUsers(LoadUsersCallback loadUsersCallback);
void getUser(String userId, GetUserCallback getUserCallback);
void saveUser(User user);
void renameUser(User user, String name);
void deleteUsers();
}
UsersLocalDataSource.java
public class UsersLocalDataSource implements UsersDataSource {
private static volatile UsersLocalDataSource INSTANCE;
private AppExecutors mAppExecutors;
private UsersDao mUsersDao;
private UsersLocalDataSource(AppExecutors appExecutors, UsersDao usersDao) {
mAppExecutors = appExecutors;
mUsersDao = usersDao;
}
public static UsersLocalDataSource getInstance(AppExecutors appExecutors, UsersDao usersDao) {
if (INSTANCE == null) {
synchronized (UsersLocalDataSource.class) {
if (INSTANCE == null) {
INSTANCE = new UsersLocalDataSource(appExecutors, usersDao);
}
}
}
return INSTANCE;
}
@Override
public void loadUsers(final LoadUsersCallback loadUsersCallback) {
Runnable runnable = new Runnable() {
@Override
public void run() {
final List<User> userList = mUsersDao.loadAllUsers();
mAppExecutors.mainIO().execute(new Runnable() {
@Override
public void run() {
if (userList.isEmpty()) {
loadUsersCallback.onDataNotAvailable();
} else {
loadUsersCallback.onUsersLoaded(userList);
}
}
});
}
};
mAppExecutors.diskIO().execute(runnable);
}
@Override
public void getUser(final String userId, final GetUserCallback getUserCallback) {
Runnable runnable = new Runnable() {
@Override
public void run() {
final User user = mUsersDao.loadUserById(userId);
mAppExecutors.mainIO().execute(new Runnable() {
@Override
public void run() {
if (user.isEmpty()) {
getUserCallback.onDataNotAvailable();
} else {
getUserCallback.onUserLoaded(user);
}
}
});
}
};
mAppExecutors.diskIO().execute(runnable);
}
@Override
public void saveUser(final User user) {
Runnable runnable = new Runnable() {
@Override
public void run() {
mUsersDao.insertUser(user);
}
};
mAppExecutors.diskIO().execute(runnable);
}
@Override
public void renameUser(final User user, final String name) {
Runnable runnable = new Runnable() {
@Override
public void run() {
mUsersDao.updateUserById(user.getId(), name);
}
};
mAppExecutors.diskIO().execute(runnable);
}
@Override
public void deleteUsers() {
Runnable runnable = new Runnable() {
@Override
public void run() {
mUsersDao.deleteUsers();
}
};
mAppExecutors.diskIO().execute(runnable);
}
}
UsersRepository.java
public class UsersRepository implements UsersDataSource {
private static volatile UsersRepository INSTANCE;
private final UsersDataSource mLocalUsersDataSource;
private UsersRepository(UsersDataSource localUsersDataSource) {
mLocalUsersDataSource = localUsersDataSource;
}
public static UsersRepository getInstance(UsersDataSource localUserDataSource) {
if (INSTANCE == null) {
synchronized (UsersRepository.class) {
if (INSTANCE == null) {
INSTANCE = new UsersRepository(localUserDataSource);
}
}
}
return INSTANCE;
}
@Override
public void loadUsers(LoadUsersCallback loadUsersCallback) {
mLocalUsersDataSource.loadUsers(loadUsersCallback);
}
@Override
public void getUser(String userId, GetUserCallback getUserCallback) {
mLocalUsersDataSource.getUser(userId,getUserCallback);
}
@Override
public void saveUser(User user) {
mLocalUsersDataSource.saveUser(user);
}
@Override
public void renameUser(User user, String name) {
mLocalUsersDataSource.renameUser(user, name);
}
@Override
public void deleteUsers() {
mLocalUsersDataSource.deleteUsers();
}
}
DataRepository.java
public class DataRepository {
public static UsersRepository users(Context context) {
UsersDataBase usersDataBase = UsersDataBase.getInstance(context);
AppExecutors appExecutors = new AppExecutors();
UsersDataSource localUserDataSource = UsersLocalDataSource.getInstance(appExecutors, usersDataBase.usersDao());
UsersRepository usersRepository = UsersRepository.getInstance(localUserDataSource);
return usersRepository;
}
}
AppExecutors.java
public class AppExecutors {
private Executor mDiskIO;
private Executor mMainIO;
public AppExecutors() {
this(new DiskIOThreadExecutors(), new MainIOThreadExecutors());
}
public AppExecutors(Executor diskIO, Executor mainIO) {
mDiskIO = diskIO;
mMainIO = mainIO;
}
public Executor mainIO() {
return mMainIO;
}
public Executor diskIO() {
return mDiskIO;
}
}
DiskIOThreadExecutors.java
public class DiskIOThreadExecutors implements Executor {
private final Executor mDiskIO;
public DiskIOThreadExecutors() {
mDiskIO = Executors.newSingleThreadExecutor();
}
@Override
public void execute(Runnable command) {
mDiskIO.execute(command);
}
}
MainIOThreadExecutors.java
public class MainIOThreadExecutors implements Executor {
private Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
@Override
public void execute(Runnable command) {
mMainThreadHandler.post(command);
}
}