Android room库简介

本文参考: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对应的类必须满足下面几个条件:

  1. 必须是abstract类而且的extends RoomDatabase。
  2. 必须在类头的注释中包含与数据库关联的实体列表(Entity对应的类)。
  3. 包含一个具有0个参数的抽象方法,并返回用@Dao注解的类。

在运行时,可以通过Room.databaseBuilder() 或者Room.inMemoryDatabaseBuilder()获取Database实例。

Entity:代表数据库中某个表的实体类。

DAO:包含用于访问数据库的方法。

1 添加依赖
    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"
2 Entity(实体)

Entity代表存放在数据库某个表中的实体类,默认情况下Room库会把Entity里面所有的字段对应到表中的每一列。Entity就是我们要向数据库中存放的数据类型。

2.1 设置Entity类
@Entity
public class User {
     
}

Entity的实体类需要添加@Entity注解。

@Entity注解包含的属性 含义
tableName 设置表名字
indices 设置索引
inheritSuperIndices 父类的索引是否会自动被当前类继承
primaryKeys 设置主键
foreignKeys 设置外键
2.2 设置表名

在默认情况下,Entity类名就是表名,但可以通过@Entity的tableName属性设置自定义表名。

@Entity(tableName = "users")
public class User {
     
	...
}
2.3 设置列名

在默认情况下,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;
}
2.4 设置主键

每个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;
}
2.5 设置索引

数据库索引可以提高数据库的访问速度。索引可以分为单列索引和组合索引,可以通过@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;
}
2.6 设置外键

因为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立即改变。
2.7 设置嵌套属性

在有些情况下,需要多个对象组合成一个对象,在对象之间有嵌套关系。在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;
}
3 Dao(方法)

这个组件代表了作为DAO的类或者接口。DAO是Room的主要组件,负责定义访问数据库的方法。Room使用过程中一般使用抽象DAO类来定义数据库的CRUD操作。DAO可以是一个接口也可以是一个抽象类。如果它是一个抽象类,它可以有一个构造函数,它将RoomDatabase作为其唯一参数。Room在编译时创建每个DAO实体。

DAO里面所有的操作都是依赖方法来实现的。

3.1 设置Dao

给一个接口或抽象类添加@Dao,则会成为Dao组件。

@Dao
public interface UserDao {
     
}

Entity类如下。

@Entity(tableName = "users")
public class User {
     

    @PrimaryKey
    private String id;

    private String name;

    private String age;
}
3.2 Query(查找)

需要在数据库中查找一些信息时,可以使用@Query注解

3.2.1 简单的查询
    @Query("select * from users")
    List<User> loadAllUsers();

这里的users是我们在@Entity中设置的tableName。

3.2.2 带参数的查询
    @Query("select * from users where id == :userId")
    User loadUserById(String userId);
3.2.3 多个参数的查询
    @Query("select * from users where age between :minAge and :maxAge")
    List<User> loadUsersBetweenAges(int minAge, int maxAge);
3.3 Insert(插入)

当我们需要给表中插入一条数据时,可以使用@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);
3.4 Update(更新)

需要更新某条数据的信息时,可以使用@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);
3.5 Delete(删除)

需要删除某条数据时,可以使用@Delete注解。

@Delete注解可以设置int返回值来表示删除了多少行。

    @Delete
    int deleteUser(User user);

我们同样可以使用@Query注解进行删除,如下所示。

    @Query("delete from users")
    int deleteUsers();
4 DateBase(数据库)

@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;
    }
}
5 使用Room库遇到的问题
5.1 RuntimeException:…_Impl does not exist

room库是使用注解生成代码的,错误原因是没有找到生成的代码,解决方法是添加下面的依赖

annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'

Android room库简介_第1张图片
这是room库一部分的依赖图,从中可以看到room库使用了JavaPoet生成代码,其原理是使用注解处理器编译和扫描注解,然后使用JavaPoet生成代码,我在之前的博客中提到过这个技术,有兴趣的可以看看。https://blog.csdn.net/Viiou/article/details/86445901

5.2 You must annotate primary keys with @NonNull. “id” is nullable. SQLite considers this a bug and Room does not allow it.

主键不能为空,所以我们给主键添上不能为空的注解即可。

@PrimaryKey
@NonNull
private String id;
5.3 Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide 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())
    }
}
5.4 Entities and Pojos must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type).

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。

6 数据层面整体设计

Android room库简介_第2张图片
我添加了几个新的类,如下所示

含义
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);
    }
}

你可能感兴趣的:(Android)