Room数据库入门你所关心的都在这里

Room数据库入门你所关心的都在这里

前言

首先还是介绍下Room是什么,Room 是基于Sqlite的官方数据库框架,它可以让你以最简单,最合理的代码来操作你的数据库数据。
它是google 架构组件的一部分(Architecture Components)。

很多童鞋会问为什么市面上那么多数据框框架,像XUtils、greenDao、DBFlow等,你为什么要选择Room呢?

其实理由很简单,Room是官方推出的数据库框架,无论在上手难度和执行效率都是最优秀的,再加上有官方的维护和更新就再也不怕开源方”跑路了”。

当然,处于对新东西吸引力来说,难道大家对它就一点也不敢兴趣吗?

Room概览

Room主要有三部分组成:

1、由Room注解的实体类
2、由Room注解的抽象类Dao层提供数据
3、由Room注解的的database层

其中在实体类处我们需要使用@Entity来标记实体类,在Dao层使用@Dao来标记到层,在database层使用@Database来标记数据库操作层

官方room关系图

(官方room关系图)

下面我们还是看下如何使用Rom吧

配置Room

想要使用Room首先你需要在app/build.gradle中添加以下依赖

implementation "android.arch.persistence.room:runtime:?"
annotationProcessor "android.arch.persistence.room:compiler:?"

当然,如你需要支持Rxjava 和测试功能也可以分别再引入下面的引用

implementation "android.arch.persistence.room:rxjava2:?"
androidTestImplementation "android.arch.persistence.room:testing:?"

引用中的版本号“?”请以最新版本号为准。

我们先在我们的项目中配置最上面的两个引用,关于测试和Rxjava的使用会在后面提到

使用Room

我们还是按照前面说的三部来创建看下如何使用Room

创建实体Bean

@Entity
public class User {
    @PrimaryKey
    public int id;

    @ColumnInfo
    public String name;

    @ColumnInfo
    public int age;

    @Ignore
    String picture;
}

首先我们创建User类具有4个属性,分别是id,name,age和picture

我们使用Entity来标记User类,这样Room就会根据这个注解在相应的时候生成表,表名默认为类名,当然你也可以指定为自己想要的表名。

@Entity(tableName = "user_table")

当然,我们也可以加上其他的注解参数啊

还是来看下这个注解的定义吧

@Entity

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Entity {     

    String tableName() default ""; //表名
    Index[] indices() default {};  //引索
    boolean inheritSuperIndices() default false;//是否继承父类引索
    String[] primaryKeys() default {};//主键
    ForeignKey[] foreignKeys() default {};//外键

}

所以,这些参数都是可以根据自己的需要添加的哦。

在属性字段上,id上有一个主键的注解,标记id为表的主键

@PrimaryKey

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface PrimaryKey {

       boolean autoGenerate() default false;//是否自动增长
}

所以,我们可以在注解处加上autoGenerate属性来控制id是否自动增长

@PrimaryKey(autoGenerate = true)

我们在age和name属性上加上了@ColumnInfo注解,标识它是一个字段,默认字段名为属性名

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface ColumnInfo {
    String name() default INHERIT_FIELD_NAME;//字段名
    @SuppressWarnings("unused") @android.arch.persistence.room.ColumnInfo.SQLiteTypeAffinity int typeAffinity() default UNDEFINED;//指定字段在sql中类型
    boolean index() default false;//引索
    android.arch.persistence.room.ColumnInfo.Collate int collate() default UNSPECIFIED;//参数校验
}

我们在picture字段上使用了@Ignore注解标识该字段并不会在表中生成字段。

当然,Room允许你直接把一个对象当做参数进行注解,并直接使用该对象内部的所有参数作为表的字段。

那就是使用@ColumnInfo来注解属性。

如:

@Entity
public class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name="user_name")
    public String name;

    @ColumnInfo(name="user_age")
    public int age;

    @Embedded 
    public Work work;

    @Ignore
    String picture;
}

public class Work{
    public float salary;

    public String company_address;
}

这样一来这个user表中就会有 id,user_name,user_age,salary和company_address 5个字段了。

创建Dao

@Dao
public interface UserDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
 void insertUsers(User... users);

@Insert
 void insertBothUsers(User user1, User user2);

@Insert
 void insertUsersAndFriends(User user, List<User> friends);

@Update
void updateUsers(User... users);

@Delete
 void deleteUsers(User... users);

@Query("SELECT * FROM user")
 User[] loadAllUsers();

@Query("SELECT * FROM user WHERE age > :minAge")
User[] loadAllUsersOlderThan(int minAge);

@Query("SELECT * FROM user WHERE name IN (:names)")
 List<User> loadUsersFromRegions(List<String> names);

}

使用注解Dao标记的接口类将被作为Dao层。

使用@Insert注解标记的方法将会编译为插入语句来使用,在Room中支持一次插入一个、多个参数,支持插入对象和数组。

使用onConflict参数可以来决定插入冲突时的解决方案

使用@Update注解标记的方法将会编译为插入语句来使用,同样的支持更新多个条目

使用@Delete注解标记的方法将会翻译为删除语句。

上面三个标记可以不要返回值,但是你也可以让它返回一个Int值,代表这次操作更新了多少个条目。

@Query注解就比较厉害了,使用Query标记的方法会被编译成查询语句,查询语句中可以使用“:”来引用注解处方法的参数。这样一样就再也不用直接拼接或者传入占位符了,很方便有没有。

创建DataBase

@Database(entities = {User.class}, version = 1)
public abstract class UserDataBase extends RoomDatabase {
public abstract UserDao userDao();
private static UserDataBase INSTANCE;
public static UserDataBase getDatabase(final Context context) {
    if (INSTANCE == null) {
        synchronized (UserDataBase.class) {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                        UserDataBase.class, "user")
                        .build();

            }
        }
    }
    return INSTANCE;
    }
}

首先我们创建类并使用@Database来标记类,entities设置为刚才我们创建的实体类,版本号设置为1
,当然这里我们可以设置多个实体类,就对应多张表。

然后我们使用Room.databaseBuilder()来获得DataBase数据库连接并指定数据库的名称,注意是数据库的名称,不是你表的名称。

这样一来我们就可以使用我们刚刚创建的数据库了,哈哈。

测试Room使用

在使用的地方初始化Database并获得Dao对象来调用相应的方法即可。

UserDataBase userDataBase=UserDataBase.getDatabase(getApplicationContext());
UserDao userDao = userDataBase.userDao();
User flyou = new User("flyou", 25, "www.flyou.ren/icon");
userDao.insertUsers();
List<User> users = userDao.loadAllUsers();
User[] users1 = userDao.loadAllUsersOlderThan(25);
userDao.deleteUsers(flyou);
userDao.updateUsers(flyou);

但是呢,操作数据库毕竟是耗时操作,所以请你不要在主线程操作这些东西哦。

数据库升级

在Room中升级脚本的编写也是非常的简单,我们只需要创建一个Migration对象在初始化数据的时候传递给Room即可。

看下下面的代码:

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
    database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
            + "`name` TEXT, PRIMARY KEY(`id`))");
}
};

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
    database.execSQL("ALTER TABLE user ADD 'address' Text");
}
};

然后在创建数据库的时候传入这两个Migration即可

 public static UserDataBase getDatabase(final Context context) {
    if (INSTANCE == null) {
        synchronized (UserDataBase.class) {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                        UserDataBase.class, "user")
                        .addMigrations(MIGRATION_1_2,MIGRATION_2_3)
                        .build();

            }
        }
    }
    return INSTANCE;
}

这样一来当用户本地数据库版本号是1的时候就会走MIGRATION_1_2方法,当版本号为2的时候就会走MIGRATION_2_3方法来执行数据库升级操作。

但是,大家可能会发现,如果有多个数据库版本需要升级的话,就需要连续走多个流程1-2,2-3,3-4,4-5……这样一来效率是真的低,在Room中同样支持多版本合并升级的呢。

只需要在Migration传入当前版本到最大版本即可,如:

static final Migration MIGRATION_1_3 = new Migration(1, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
                + "`name` TEXT, PRIMARY KEY(`id`))");
        database.execSQL("ALTER TABLE user ADD 'address' Text");
    }
};

同样的,我们依然需要把这个1-3的升级脚本传递给database。

经过这样的操作,版本号为1的用户就会直接使用MIGRATION_1_3这个升级脚本来进行升级了。

备注:

这里需要注意的一点是Database注解中的entities只有在第一次生成时添加有效,以后的数据库升级,你必要写升级脚本,在entities添加剂要生成的表示无效的。

@Database(entities = {User.class})

使用Rxjava

在上面的例子中我们提到,我们不能直接在主线程来操作数据库,这个时候大家肯定会第一时间想到使用RXJAVA啊,毕竟RxJAVA的线程调度是真的好用,再加上友好的流式结构,Room怎么能不支持RXJAVA呢?
还是上面说到的,在build.gradle中添加以下依赖

implementation "android.arch.persistence.room:rxjava2:?"

或许你会用到

MayBe

  1. 当数据库中没有用户且查询没有返回任何行时,Maybe将完成。
  2. 当数据库中有用户时,Maybe将触发onSuccess并完成。
  3. 如果用户在Maybe完成后更新,则没有任何反应。

还是根据上面查询数据的操作,改造如下

@Query("SELECT * FROM user")
Maybe<List<User>> loadAllUsers();

Single

  1. 当数据库中没有用户且查询没有返回任何行时,Single将触发onError(EmptyResultSetException.class)
  2. 当数据库中有用户时,Single将触发onSuccess。
  3. 如果用户在Single.onComplete被调用后更新,则没有任何反应,因为流已完成。

    @Query(“SELECT * FROM user”)
    Single> loadAllUsers();

Flowable

  1. 当有数据库中没有用户和查询不返回任何行,则Flowable不会散发,既没有onNext,也不是onError。
  2. 当数据库中有用户时,Flowable将触发onNext。
  3. 每次更新用户数据时,Flowable对象都将自动发出,允许您根据最新数据更新UI。

    @Query(“SELECT * FROM user”)
    Flowable> loadAllUsers();

在调用的地方

UserDataBase userDataBase=UserDataBase.getDatabase(getApplicationContext());
 UserDao userDao = userDataBase.userDao();

 userDao.loadAllUsers().
         subscribeOn(Schedulers.io())
         .observeOn(AndroidSchedulers.mainThread())
         .subscribe(new Consumer<List<User>>() {
             @Override
             public void accept(List<User> users) {
                 Log.d("flyou",users.toString());
             }
         });

这样一来我们操作起来就方便多了,啊哈。

使用LiveData

liveData 是google爸爸提供支持数据观察模式的框架,使用观察者模式实现,当数据源发生变更时被监听的数据处会自动获取到数据源的变化并更新。

Room做为官方架构组件的一部分是天生就支持LiveData的,可以直接在Dao层操作

  @Query("SELECT * FROM user")
Single<liveData<List<User>>> loadAllUsers();

在调用的地方:

userDao.loadAllUsers().
        subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Consumer<LiveData<List<User>>>() {
            @Override
            public void accept(LiveData<List<User>> users) {
                users.observe(SplashActivity.this, new Observer<List<User>>() {
                    @Override
                    public void onChanged(@Nullable List<User> users) {
                        recycleview.setData(users);
                        recycleviewAdapter.notifyDataStChanged();
                    }
                });
            }
        });

每当数据库人员有更新,都会触发onChanged从而触发recycleview更新数据列表。

当然,这个监听需要我们在不使用的时候去除监听,当然你也可以借助于ViewModel来管理LiveData的,这里就不再做具体的介绍了。

数据库测试

当然,大多数时候我们写的数据库操作代码不能直接来使用,所以测试代码还是要写的。

同样的在build.gradle中添加以下依赖

androidTestImplementation "android.arch.persistence.room:testing:?"

然后编写测试代码:

@RunWith(AndroidJUnit4.class)
public class UserDaoTest {

private UserDataBase mDatabase;

@Before
public void initDb() throws Exception {
    mDatabase = Room.inMemoryDatabaseBuilder(//使用内存数据库
            InstrumentationRegistry.getContext(),
            UserDataBase.class)
            // 允许主线程操作
            .allowMainThreadQueries()
            .build();
}

@After
public void closeDb() throws Exception {
    mDatabase.close();
}


@Test
public void insertAndGetUserById() {
    // Given that we have a user in the data source
    mDatabase.userDao().insertUsers();
    // When subscribing to the emissions of user
    mDatabase.userDao()
            .loadAllUsers()
            .test();

    }
}

当然,大家可以根据自己的需要定制自己的测试代码,就不在具体赘述了。

一些Note

  1. 在使用DataBase时会默认让你生成Schema(表结构,及创建语句),如果你不想生成可以设置为false
    @Database(entities = {User.class}, version = 1,exportSchema = false)
    如果想要这个Schema就需要在build.gradle配置
    android {
        defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                    includeCompileClasspath = true
                     arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]

        }
    }
}
  1. 如果你想要在编写数据bean时可以注解自定义类,你需要借助于TypeConverter
  2. Room数据库存储有缓存机制,并不会立马写入到正式数据表中,所以你直接从安装包目录拷贝出来的数据库文件有可能不是全部的数据
  3. Room在Dao成查询是可以直接返回Cursor的,但是不推荐这样使用,因为你并不知道Cursor有无数据或者包含哪些列。

嗯,就先这些吧。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器