闲来无事,总结一下前一阵子对Room数据开源库的学习。
数据库存储是安卓开发中数据存储的方式之一,并且安卓为我们提供了sqlite数据库和SQLiteOpenHelper等等强大的开发支持。而Room,则是对它们的一层封装,为的是让我们更加方便地操作sqlite数据库。
以下,为使用步骤:
implementation "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
@Entity(tableName = "users", //表名
primaryKeys = {"id", "name"},//主键
indices = {//索引
@Index(value = "id", unique = true) //唯一性
}) //实体
public class User implements Comparable {
@android.support.annotation.NonNull
@ColumnInfo(name = "id")
private long id;
@android.support.annotation.NonNull
@ColumnInfo(name = "name")
private String name;
@ColumnInfo(name = "position")
private String position;
@Embedded
private UserPerforms performs;
@Ignore
public User() {
}
public User(long id, String name, String position) {
this.id = id;
this.name = name;
this.position = position;
}
// ...get/set+toString()
@Override
public int compareTo(@NonNull Object o) {
if (o instanceof User) {
User u = (User) o;
if (this.id > u.id) {
return 1;
} else if (this.id < u.id) {
return -1;
} else if (this.id == u.id) {
return 0;
}
}
return 0;
}
}
这个类里面的东西我解释一下:
@Entity,表示这个类是数据库中的一张表(实体)
@Embedded,表示在这个表中嵌入另一个类(其实就是嵌了一张表)
@NonNull,主键必须声明非空,而且要在@ColumnInfo上面
@ColumnInfo(name = "id") 表示这个属性是数据库表的某一列,列名是id
@Ignore,让room忽略(我是标在无参构造方法上的,表示不让room用这个构造方法构造对象)
compareTo()方法,这个我是为了保证有序,覆写了一下
@Entity(tableName = "performs",
primaryKeys = "p_id", // 主键
foreignKeys = @ForeignKey(entity = User.class
, parentColumns = "id"
, childColumns = "p_id"
, onUpdate = ForeignKey.CASCADE
, onDelete = ForeignKey.CASCADE))
public class UserPerforms {
@android.support.annotation.NonNull
@ColumnInfo(name = "p_id")
private long p_id;
@ColumnInfo(name = "score")
private int score;
@ColumnInfo(name = "assist")
private int assist;
@Ignore
public UserPerforms() {
}
public UserPerforms(long p_id, int score, int assist) {
this.p_id = p_id;
this.score = score;
this.assist = assist;
}
//...get/set+toString()
}
注意里面外键的定义:
foreignKeys = @ForeignKey(entity = User.class
, parentColumns = "id"
, childColumns = "p_id"
, onUpdate = ForeignKey.CASCADE
, onDelete = ForeignKey.CASCADE)
entity:关联表名(类)
parentColumns:父列名
childColumns:子列名
onUpdate,onDelete:父列列值发生变化时,子列的处理方式(我都用的级联,省事儿)
此时要继承RoomDatabase这个类。这里,我用了下单例
@Database(entities = {User.class, UserPerforms.class}, version = 1, exportSchema = true)
public abstract class DatabaseCreator extends RoomDatabase {
private static DatabaseCreator databaseCreator;
public static DatabaseCreator getInstance(Context context) {
if (databaseCreator == null) {
synchronized (DatabaseCreator.class) {
if (databaseCreator == null) {
databaseCreator = Room.databaseBuilder(context.getApplicationContext(), DatabaseCreator.class,
"user_perfom_2018.db").addMigrations(new Migration(1, 2) {
//版本升级.从1升到2
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("create table coach(id int,name varchar(30),age int,constraint pk_coach PRIMARY_KEY(id) )");
}
//版本升级并不方便,也不提倡频繁对数据库进行组织结构的变动
}).build();
}
}
}
return databaseCreator;
}
public static void onDestroy() {
databaseCreator = null;
}
public abstract CRUDDAO getDao();
}
类的说明:
@Database,说明这是一个room的数据库类。里面的参数:entities说明数据库里有哪些表(实体),version是版本号,exportSchema是是否允许导出,默认为true(导出后的数据库什么样的,我们最后看看)
单例里面Room.databaseBuilder就是构造一个数据库构造者,参数列表是上下文,数据库类名和数据库文件名
addMigrations()是升级数据库时用的,我们在构造完数据库后,每对数据库表的一次结构变化操作,都要调用这个方法升级数据库,并在migrate()方法中执行升级的语句。所以,升级不方便,也别没事儿就改变数据库表的结构。
抽象方法getDao(),这个是获取数据库操作接口的方法,不用我们实现,编译通过后,room会帮我们实现
方才说了获取数据库操作接口,这个接口要我们自己声明,但不用实现。
这个接口,无非就是CRUD增查改删,不同的操作要用上不同的注解。我一个一个说:
声明接口,配上注解@Dao:
@Dao
public interface CRUDDAO {}
查询所有User,配上@Query注解,并给注解传入sql语句
@Query("select * from users")
List getAllUsers();
//查询方法返回值必须是cursor或arrayList
可以从方法参数列表中给sql传入参数,从参数列表传入的要加上:(冒号)以示区分
@Query("select users.id,users.name from users,performs " +
"where users.id = performs.p_id and performs.score > :score and performs.assist > :assist")
// 多表+筛选查询
List getUserWithLimits(int score, int assist);
可以看到,我的返回值List里的元素是UserSimple类型的,因为我们的sql是只查询了队员的号码和名字,而我们又在User类中固定了room能用的构造方法参数为3个(号码,名字,位置),这就出现了不匹配,room会编译时就拉闸的。
所以我又针对号码和名字建立了一个新的类UserSimple,由于这不是数据库中的表,所以不用也不能使用@Entity(用了就要addMigrate()),只需要注名属性和列名的对应关系就中。代码如下:
public class UserSimple {
@ColumnInfo(name = "id")
private String id;
@ColumnInfo(name = "name")
private String name;
//...get/set和toString()
}
配上@Insert注解和冲突政策(为了省事儿,我直接替换)
@Insert(onConflict = OnConflictStrategy.REPLACE)
//插入有冲突,就直接替换
void insert(User[] users);
由于我们定义外键时写了级联,所以这儿只配上一个@Delete就中
@Delete
int delete(User user);
//删除方法返回值必须是int或void
配上@Update注解
@Update
int update(User user);
//更新方法返回值也必须是int或void,但不要这么更新,要么用query,指定sql语句,要么直接重新插入
//实际覆盖插入也可以实现更新
正如上面的注释所说,@Update并不能生成正确的sql语句(不信你单步调试看看),所以更新最好利用@Query注解,而后传入更新的sql就中了
@Query("update users set name=:name where id = :id")
void updateName(String name, long id);
最好在Fragment中使用。另外,room默认是让数据库的IO操作必须放在子线程里,否则程序直接宕掉。
我为了方便,用了ButterKnife的注入和线程池的构造新线程的方法,但这里我们只看跟数据库有关的内容。
private ArrayList usersAll = new ArrayList();
private ArrayList performsAll = new ArrayList()
private void setInfo() {
usersAll.add(new User(11, "Jason", "中锋"));
usersAll.add(new User(17, "Mike", "前腰"));
usersAll.add(new User(2, "Kane", "中卫"));
usersAll.add(new User(8, "Handerson", "后腰"));
usersAll.add(new User(1, "Frank", "门将"));
performsAll.add(new UserPerforms(11, 32, 15));
performsAll.add(new UserPerforms(1, 0, 0));
performsAll.add(new UserPerforms(17, 20, 23));
performsAll.add(new UserPerforms(8, 6, 10));
performsAll.add(new UserPerforms(2, 3, 4));
}
为了使用方便,我把获取DAO接口的方法提取一下:
public class CRUDUtil {
public static CRUDDAO getDao(Context context) {
return DatabaseCreator.getInstance(context).getDao();
}
}
可以看到,这儿就是调用了我们自己的DatabaseCreator类的抽象方法--getDao()
构造一个模型和数据库交互的类,名曰MyModel,里面提供增查改删的方法,以供Fragment调用(为了方便,我就不用泛型了,直接重载)
public void insert(User[] data, Context context) {
CRUDUtil.getDao(context).insert(data);
}
public void insert(UserPerforms[] data, Context context) {
CRUDUtil.getDao(context).insert(data);
}
public void update(User data, Context context) {
CRUDUtil.getDao(context).updateName(data.getName(), data.getId());
}
public ArrayList getAllUsers(Context context) {
return (ArrayList) CRUDUtil.getDao(context).getAllUsers();
}
public ArrayList getAllPerforms(Context context) {
return (ArrayList) CRUDUtil.getDao(context).getAllPerforms();
}
public ArrayList getUsersWithLimits(Context context, int score, int assist) {
return (ArrayList) CRUDUtil.getDao(context).getUserWithLimits(score, assist);
}
public void delete(User u, Context context) {
CRUDUtil.getDao(context).delete(u);
}
ThreadUtil.executeRunnable(new Runnable() {
@Override
public void run() { // 插入数据
nameModel.insert(usersAll.toArray(new User[1]), getContext());
nameModel.insert(performsAll.toArray(new UserPerforms[1]), getContext());
}
});
ThreadUtil.executeRunnable(new Runnable() {
@Override
public void run() { // 更新数据
usersAll.get(1).setName("szc");
nameModel.update(usersAll.get(1), getContext());
}
});
ThreadUtil.executeRunnable(new Runnable() {
@Override
public void run() { // 删除数据
for (int i = 0; i < usersAll.size(); i++) {
nameModel.delete(usersAll.get(i), getContext());
sleep(1000);
}
}
});
ThreadUtil.executeRunnable(new Runnable() {
@Override
public void run() {
usersShow = (ArrayList) CRUDUtil.getDao(getContext()).getAllUsers();
// handler + message 遍历
}
});
ThreadUtil.executeRunnable(new Runnable() {
@Override
public void run() {
ArrayList userSimples = (ArrayList) CRUDUtil.getDao(getContext()).getUserWithLimits(10,10);
// handler + message 遍历
}
});
ThreadUtil是我自己封装的一个类,就是利用线程池执行子线程
可以看到,虽然上面已经可以方便地对数据库进行CRUD操作,但是数据库的数据每变化一次,我们就要主动调用方法获取一次,稍显麻烦。为此,我们可以用CompositeDisposable对象充当观察者,进行监听。
在CRUD接口中增加查询方法,返回值中List的元素变成Flowable对象
@Dao
public interface CRUDDAO {
....
@Query("select * from users")
Flowable> getAllUsersFlowable();
....
}
在MyModel中加入相应方法
public Flowable> getAllUsersFlowable(Context context) {
return CRUDUtil.getDao(context).getAllUsersFlowable();
}
在Fragment中合适部分(我选的是onCreate()中),创建并初始化CompositeDisposable对象
private CompositeDisposable disposable = new CompositeDisposable();
disposable.add(nameModel.getAllUsersFlowable(getContext())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer>() { // Flowable变成Disposable
@Override
public void accept(List users) throws Exception {
Collections.sort(users);
traverseList((ArrayList) users); // 遍历
}
}));
这样,数据库User表一有动静(因为我们是获取的User表的Flowable),disposable对象就会观察到,调用getAllUsersFlowable()方法,不出意外的话,accept方法就会被调用,遍历就会被进行。
最后,我们看一下数据库导出的内容(工程目录下schemas/包/json文件):
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "54e23e816a8145dfacd8880f58915619",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `position` TEXT, `p_id` INTEGER, `score` INTEGER, `assist` INTEGER, PRIMARY KEY(`id`, `name`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "performs.p_id",
"columnName": "p_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "performs.score",
"columnName": "score",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "performs.assist",
"columnName": "assist",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"name"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_users_id",
"unique": true,
"columnNames": [
"id"
],
"createSql": "CREATE UNIQUE INDEX `index_users_id` ON `${TABLE_NAME}` (`id`)"
}
],
"foreignKeys": []
},
{
"tableName": "performs",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`p_id` INTEGER NOT NULL, `score` INTEGER NOT NULL, `assist` INTEGER NOT NULL, PRIMARY KEY(`p_id`), FOREIGN KEY(`p_id`) REFERENCES `users`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "p_id",
"columnName": "p_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "score",
"columnName": "score",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "assist",
"columnName": "assist",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"p_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"p_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"54e23e816a8145dfacd8880f58915619\")"
]
}
}