Room数据库学习记录

Room数据库

简介:
  1. Room是Google官方推出的Architecture Components中的一个持久性数据库,提供了基于SQLite的抽象层,可以在充分利用SQLite功能的前提下流畅的访问数据库。

  2. Room是google官方开发的对象关系映射(ORM)库框架,采用注解的形式。它能缓存相关数据,使用户在断网的情况下浏览相应内容,重新连接网络时,用户发起的内容更改都会同步到服务器。能将本地数据保存到数据库中,它提供了一个SQLite的抽象层(封装),能够让我们更加稳健地访问数据库,能够提升数据库的性能。

优点
  1. 使用便捷。只需简单的三步少量的代码即可实现常规的CDUR;
  2. 支持LiveData;
  3. 与Rxjava结合可以实时监听数据变化。
  4. 针对 SQL 查询的编译时验证。
  5. 可最大限度减少重复和容易出错的样板代码的方便注解。
  6. 简化了数据库迁移路径。
Room的三个主要组件
  1. Entity: 表示Room数据库内的表(Table)。
  2. Database: 数据库的持有者,是与 App 持久关联数据的底层连接的主要访问点。是一个继承 RoomDatabase 的抽象类。在类前注解中包含与数据库相关联的实体列表。类中包含一个具有 0 个参数的抽象方法,并返回用 @Dao 注解的类。 用户可以通过调用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 获取 Database 实例。
  3. Dao(Data access object): Dao是用户来使用访问数据库的主要工具,它可以是一个接口,可以是一个抽象类。如果是一个抽象类,可以有一个构造函数,其只接收一个 RoomDatabase 参数。当系统编译时,Room会自动为Dao创造具体实现。
Room的相关依赖

数据库依赖官网

dependencies {
    def room_version = "2.4.1"
	//Room数据库的依赖
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"

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

    // optional - RxJava3 support for Room
    implementation "androidx.room:room-rxjava3:$room_version"

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

    // optional - Test helpers
    testImplementation "androidx.room:room-testing:$room_version"

    // optional - Paging 3 Integration
    implementation "androidx.room:room-paging:2.4.1"
}
使用Room数据库
使用@Entity创建表
@Entity(tableName = "student")  //设置表名
public class Student {

    @NonNull    //主键不能为null,必须添加这个注解
    @PrimaryKey(autoGenerate = true)    //主键是否自动增长,默认为false
    private int id;

    @ColumnInfo(name = "firstName")     //可以通过设置name =xxx 的方式重新命名字段
    private String name;

    @Embedded
    private Grade grade;

    public Student() {
    }
    
    @Ignore		//只允许有一个主构造方法,其他构造方法要使用@Ignore设置为忽略
    public Student(int id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
    
     @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", grade=" + grade.toString() +
                '}';
    }
}

上表中的嵌套类:

public class Grade {

    private int math;
    private int english;
    private int chinese;

    @Ignore
    private int history;    //这个字段将被忽略,不会被映射到表中

    public Grade() {
    }

    @Ignore
    public Grade(int math, int english, int chinese, int history) {
        this.math = math;
        this.english = english;
        this.chinese = chinese;
        this.history = history;
    }

    public int getHistory() {
        return history;
    }

    public void setHistory(int history) {
        this.history = history;
    }

    public int getMath() {
        return math;
    }

    public void setMath(int math) {
        this.math = math;
    }

    public int getEnglish() {
        return english;
    }

    public void setEnglish(int english) {
        this.english = english;
    }

    public int getChinese() {
        return chinese;
    }

    public void setChinese(int chinese) {
        this.chinese = chinese;
    }

    @Override
    public String toString() {
        return "Grade{" +
                "math=" + math +
                ", english=" + english +
                ", chinese=" + chinese +
                ", history=" + history +
                '}';
    }
}
  1. 介绍: 上述过程即为Room数据库的表单的创建过程,通过注解我们可以在原有类上完成表的创建,与SQLite相比Room数据库去除了我们自己通过SQ语句创建表单的繁琐过程。

  2. 下面说一下类中出现的注解的意思:

    • @Entity

      在Room中每个表对应一个具有 @Entity 注解的JavaBean,也就是说一个bean对应一张表;(通过此注解标注此类为表单)

    • @PrimaryKey

      所有的CRUD操作都是根据主键进行操作的,所以每个表要求必须有一个主键,主键字段通过 @PrimaryKey 进行标识,因为主键不能为空,所以要求必须添加android.support.annotation注解库中的 @NonNull 注解 ,否则编译报错。主键默认是不自增,因为主键的类型并不限于long、int等类型,String等类型也是可以的;

    • @ColumnInfo

      bean中每一个具有@ColumnInfo 注解的字段会被映射为表中的一个字段,映射字段可以更改,默认直接使用当前字段,注意如果字段为private修饰的则必须提供对应的getter、setter方法,public字段修饰则不用;

    • @Ignore

      顾名思义,该注解是用于设置忽略的,每个表对应的JavaBean只能有一个主构造方法,若还有其它的构造方法则必须使用@Ignore注解进行标识,而且此注解还可标识字段,被标识的字段将不会被创建在表单中,在使用数据库对对象进行存储时此字段同理也将不会被存储。

    • @Embedded

      这也是一个很方便实用的注解,这个注解可以将普通JavaBan中的字段也引入到对应的表中,查询结果会自动为对应的bean。使用此注解将实现嵌套类的直接存储。

使用@Dao,创建操作数据库的接口
  1. Dao在Room组件中是一个非常重要的组成部分。在这个Dao类中,定义了许多操作数据库的简便方法。
@Dao
public interface StudentDao {

    //-------------------------插入--------------------------

    /**
     * 插入操作,若已存在相同的主键的数据则直接覆盖
     * 返回值也可以设为void
     *
     * @param students
     * @return 插入的 row id集合
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    List<Long> insert(Student... students);

    /**
     * 同上
     *
     * @param student
     * @return 插入的 row id
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    Long insert(Student student);

    /**
     * 同上
     *
     * @param list
     * @return 插入的 row id
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    List<Long> insert(List<Student> list);

    //-------------------------更新--------------------------

    /**
     * 更新操作
     *
     * @param student
     * @return 更新成功的数量
     */
    @Update
    int update(Student student);

    /**
     * 同上
     *
     * @param student
     * @return 更新成功的数量
     */
    @Update
    int update(Student... student);

    //-------------------------查询--------------------------

    /**
     * 查询指定名字的数据
     * @param name
     * @return
     */

    @Query("select * from student where firstName =:name")
    List<Student> getByName(String name);

    /**
     * 查询指定id的数据
     * @param id
     * @return
     */

    @Query("SELECT * FROM student where id = :id")
    Student getById(int id);

    /**
     * 查询所有数据
     *
     * @return
     */
    @Query("select * from student")
    List<Student> loadAll();

    /**
     * 查询所有数据
     * 与Rxjava的Flowable结合,可以进行数据变化监听
     * @return
     */
    @Query("select * from student")
    Flowable<List<Student>> getAll();

    //-------------------------删除--------------------------------------------

    /**
     * 根据主键删除
     * @param student
     * @return 删除成功的数量
     */
    @Delete
    int deleteById(Student student);
	
    /**
     * 删除所有数据
     * @return
     */
    @Query("delete from student")
    int deleteAll();

    /**
     * 删除指定名字的数据
     *
     * @param name
     * @return
     */
    @Query("delete from student where firstName = :name")
    int deleteByName(String name);
}
  1. 下面介绍上面类中各注解:

    • @Dao

      DAO是数据库访问对象,要变成一个数据库访问对象必须在类的声明上面添加 @Dao 注解,并且根据官方文档的要求,DAO必须是一个interface 或者 abstract class,然后在里面写操作数据库的相关接口,主要的是通过在方法上添加 @Insert、@Update、@Query、@Delete中的某一个注解实现对应的“插入、更新、查询、删除”操作。

      注意:DAO类中这些操作都是阻塞的,使用要在异步线程中使用。在创建Database对象时可以选择是否允许在主线程中查询数据

    • @Insert

      @Insert 方法的每个参数必须是带有 @Entity 注解的 Room 数据实体类的实例或数据实体类实例的集合。调用 @Insert 方法时,Room 会将每个传递的实体实例插入到相应的数据库表中。

      如果 @Insert 方法接收单个参数,则会返回 long 值,这是插入项的新 rowId。如果参数是数组或集合,则该方法应改为返回由 long 值组成的数组或集合,并且每个值都作为其中一个插入项的 rowId。返回值也可void;

      插入操作可能出现冲突,所以我们通过@Insert(onConflict = OnConflictStrategy.REPLACE)设置在出现冲突时的应对策略,OnConflictStrategy接口中给我们提供了以下5个策略:

      public @interface OnConflictStrategy {
          /**
           * OnConflict strategy constant to replace the old data and continue the transaction.
           * 取代旧数据同时继续事务
           */
          int REPLACE = 1;
          
          /**
           * OnConflict strategy constant to rollback the transaction.
           * 回滚事务
           */
          int ROLLBACK = 2;
      
          /**
           * OnConflict strategy constant to abort the transaction.
           * 终止事务
           */
          int ABORT = 3;
      
          /**
           * OnConflict strategy constant to fail the transaction.
           * 事务失败
           */
          int FAIL = 4;
      
          /**
           * OnConflict strategy constant to ignore the conflict.
           * 忽略冲突
           */
          int IGNORE = 5;
      }
      
    • @Update

      Room 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。

      @Update 方法可以选择性地返回 int 值,该值指示成功更新的行数。

    • @Delete

      Room 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。

      @Delete 方法可以选择性地返回 int 值,该值指示成功删除的行数。

    • @Query

      使用 @Query注解,**您可以编写 SQL 语句并将其作为 DAO 方法公开。使用这些查询方法从应用的数据库查询数据,或者需要执行更复杂的插入、更新和删除操作。**Room 会在编译时验证 SQL 查询。这意味着,如果查询出现问题,则会出现编译错误,而不是运行时失败。通过SQL语句可以实现复杂插入,更新等操作。

      • 查询字段的子集
        有时候我们不需要查询返回所有的数据,我们只需要表中的某几条数据,这是我们要创建一个类,这个类中包含我们想得到的对象,然后在Query的返回参数中设置成这个类的对象就可以了。

        // An highlighted block
        public class NameTuple {
            @ColumnInfo(name="first_name")
            public String firstName;
            @ColumnInfo(name="last_name")
            public String lastName;
        }
        
        @Dao
        public interface MyDao {
            @Query("SELECT first_name, last_name FROM user")
            public List<NameTuple> loadFullName();
        }
        
        
    • 使用Room写SQL引用查询条件的格式是“字段名称=:引用值”(字段名称会有智能提示,会有亮色等提示),比如例子中的:

      	@Query("SELECT * FROM student where id = :id")	//此冒号后的id即为此方法传入的参数
          Student getById(int id);
      
    使用@Database,创建数据库
    //entities表示要包含哪些表;version为数据库的版本,数据库升级时更改;exportSchema是否导出数据库结构,默认为true
    @Database(entities = {Student.class}, version = 1, exportSchema = true)
    public abstract class AppDatabase extends RoomDatabase {
    
        public static AppDatabase sInstance;
    
        public static AppDatabase getInstance(Context context) {
            if(sInstance == null) {
                synchronized (AppDatabase.class) {
                    if(sInstance == null) {
                        //三个参数:第一个Context对象,第二个为数据库类,第三个为数据库名字
                        sInstance = Room.databaseBuilder(context,AppDatabase.class,"data")
                                // 设置是否允许在主线程做查询操作
                                .allowMainThreadQueries()
                                // setJournalMode(@NonNull JournalMode journalMode)  设置数据库的日志模式
                                // 设置迁移数据库如果发生错误,将会重新创建数据库,而不是发生崩溃
                                // .fallbackToDestructiveMigration() 会清理表中的数据 ,不建议这样做
                                //设置从某个版本开始迁移数据库如果发生错误,将会重新创建数据库,而不是发生崩溃
                                //.fallbackToDestructiveMigrationFrom(int... startVersions);
                                /*.addCallback(new Callback() {	进行数据库的打开合创建的监听
                                    @Override
                                    public void onCreate(@NonNull SupportSQLiteDatabase db) {
                                        super.onCreate(db);
                                    }
    
                                    @Override
                                    public void onOpen(@NonNull SupportSQLiteDatabase db) {
                                        super.onOpen(db);
                                    }
                                }) */
                                .build();
                    }
                }
            }
            return sInstance;
        }
    
        public abstract StudentDao getStudentDao();
    }
    
    • @Database

      @Database用于创建数据库,需要继承RoomDatabase,若该注解后面不加“exportSchema = false”,该属性默认为true,则表示导出数据库的概要和记录,建议默认设置导出,方便查看历史更改记录,这个时候要在app/build.gradle下的defaultConfig中要增加如下配置,不然编译会报错

      javaCompileOptions {
          annotationProcessorOptions {
              //room的数据库概要、记录
              arguments = ["room.schemaLocation":
                                   "$projectDir/schemas".toString()]
          }
      }
      

    增加该配置后编译在Project视图下即可看到导出的数据库结构文件,文件是以json的格式记录。

    然后再创建一个获取StudentDao 的抽象方法getStudentDao(),这一部分就算基本完成了。
    在这里插入图片描述

    如上:

    	public abstract StudentDao getStudentDao();		//此抽象方法会返回自己的Dao对象
    
    使用:

    由于数据库查询等操作为费时操作,所以需要放在子线程进行,所以建议学习Rxjava,通过Rxjava实现查询等的异步操作。这里简单展示:

     findViewById(R.id.query).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Observable.create(new ObservableOnSubscribe<List<Student>>() {
                        @Override
                        public void subscribe(@NonNull ObservableEmitter<List<Student>> emitter) throws Exception {
                            List<Student> list = AppDatabase.getInstance(getApplicationContext()).getStudentDao().getByName("王皓炜");
                            emitter.onNext(list);
                            emitter.onComplete();
                        }
                    }).subscribeOn(Schedulers.io())	//设置被观察者在子线程进行
                            .observeOn(AndroidSchedulers.mainThread())	//设置观察者在主线程
                            .subscribe(new Observer<List<Student>>() {
                                @Override
                                public void onSubscribe(@NonNull Disposable d) {
    
                                }
    
                                @Override
                                public void onNext(@NonNull List<Student> list) {
                                    for(int i = 0; i < list.size(); i++) {
                                        Log.d("TAG", list.get(i).toString());
                                    }
                                }
    
                                @Override
                                public void onError(@NonNull Throwable e) {
                                    Log.d("TAG", "15");
    
                                }
    
                                @Override
                                public void onComplete() {
    
                                }
                            });
                }
            });
    

    我们通常可以提前创建一个管理类:

    public class DbManger {
    
        private static Context context;
    
        private DbManger(){};
    
        public static DbManger sInstance;
    
        public static DbManger getInstance(Context context) {
            DbManger.context = context;
            if (sInstance == null) {
                synchronized (DbManger.class) {
                    if (sInstance == null) {
                        sInstance = new DbManger();
                    }
                }
            }
            return sInstance;
        }
    
        /**
         * 插入一个数据
         * @param student
         * @return
         */
        public Observable<Long> insert (Student student) {
            return Observable.create(new ObservableOnSubscribe<Long>() {
                @Override
                public void subscribe(@NonNull ObservableEmitter<Long> emitter) throws Exception {
                    Long l = AppDatabase.getInstance(context).getStudentDao().insert(student);
                    emitter.onNext(l);
                }
            }).subscribeOn(Schedulers.io())
              .observeOn(AndroidSchedulers.mainThread());
        }
    }
    

    然后插入数据使用此类:

    findViewById(R.id.add).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            
            CompositeDisposable compositeDisposable = new CompositeDisposable();
            Student student = new Student();
            student.setName("王皓炜");
            student.setGrade(new Grade(59, 59, 59, 59));
            Disposable d = DbManger.getInstance(getApplicationContext()).insert(student).subscribe();
            compositeDisposable.add(d);
        }
    });
    

    这样代码将看着更加简洁。

数据库升级

Room中升级可以根据情景不同分为两种方式:

1.简单粗暴式

在创建Database对应类时设置fallbackToDestructiveMigration(),这样每次要升级时直接更改数据库版本号就可以了,不需要其它额外的操作,不过这样用户体验不大好。

sInstance = Room.databaseBuilder(context,AppDatabase.class,"data")
        .fallbackToDestructiveMigration()   //设置数据库升级的时候清除之前的所有数据
        .build();
2.优雅友好式

Room中提供了专门用于升级的Migration类,通过它可以优雅的实现数据库升级,原来的数据不会丢失。

首先在自己的数据类中添加字段:

如我想在student表中增添sport字段,则首先在对应类中添加字段:(此字段只要在student表中即可,也可添加至嵌套类,如将sport添加至grade中

@Entity(tableName = "student")  //设置表名
public class Student {

    @NonNull    //主键不能为null,必须添加这个注解
    @PrimaryKey(autoGenerate = true)    //主键是否自动增长,默认为false
    private int id;

    @ColumnInfo(name = "firstName")     //可以通过设置name =xxx 的方式重新命名字段
    private String name;

    @ColumnInfo(name = "sport")	//新添加的字段
    private Integer sport;

    @Embedded
    private Grade grade;

    public Student() {
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", sport=" + sport +
                ", grade=" + grade.toString() +
                '}';
    }

    @Ignore
    public Student(int id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public Integer getSport() {
        return sport;
    }

    public void setSport(Integer sport) {
        this.sport = sport;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
}

然后在表中创建字段:

	 sInstance = Room.databaseBuilder(context,AppDatabase.class,"data")
                            .addMigrations(MIGRATION_1_2)	//升级数据库
                            .build();

	/**
     * 数据库版本从1升至2,且在student表单中增添sport字段
     */
	static final Migration MIGRATION_1_2 = new Migration(1,2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            //向student表中添加一个sport字段
            database.execSQL("ALTER TABLE student ADD COLUMN sport INTEGER"); //这种方式不支持一次添加多个字段,可以多写几条sql语句分别执行
        }
    };

然后需要将数据库版本修改至2,如下:

    @Database(entities = {Student.class}, version = 2, exportSchema = true)	//这里的版本从1修改至2,与上面对应
    public abstract class AppDatabase extends RoomDatabase {

    }

至此数据库升级完成,对比数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQ6pkDtf-1644985406952)(C:\Users\过客\AppData\Roaming\Typora\typora-user-images\image-20220216114331762.png)]

由于此前没有sport字段,所以对于之前存储的数据sport字段为默认值,此后就为存储的数值了。

注意:

若你新添加的字段上添加有@NoNull注解,如:

    @NonNull
    @ColumnInfo(name = "sport")
    private Integer sport;

那么在创建时也要与之对应:

database.execSQL("ALTER TABLE student ADD COLUMN sport INTEGER NOT NULL DEFAULT 1");

上面两者需要对应,否则会抛异常。

3.跳跃式升级

上面的升级方式都是渐进式的一级一级往上升,但实际情况下可能会出现:用户当前app的数据库版本是1,而当前最新版app的数据库版本是3,此时用户升级就会直接从版本1升级到版本3,这种跳跃式的升级又该怎么处理呢?这种时候就要先创建新表,复制旧表数据,然后重命名新表。

/**
     * 数据库升级  version1 -> version3  物理
     */
    static final Migration MIGRATION_1_3 = new Migration(1, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            //创建新表
            database.execSQL("CREATE TABLE IF NOT EXISTS student_new(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
                                     "name TEXT,chinese INTEGER,english INTEGER,math INTEGER,sport INTEGER,physical INTEGER)");
            //备份数据
            database.execSQL("INSERT INTO student_new(id,name,chinese,english,math) SELECT id,name,chinese,english,math FROM student");
            //删除旧表
            database.execSQL("DROP TABLE student");
            //重命名表
            database.execSQL("ALTER TABLE student_new RENAME TO student");
        }
    };
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值