【Android Jetpack】Room——基本使用篇

1. 前言

这是一篇之前的文章,记录在看云文档中。还是决定将其整理到Jetpack这个专栏中,构成一套。下面的文章也是根据之前的开发记录来的,做了一个简单的事件记录条目的保存,下面开始正题。


当然,我们所使用的就是之前使用过的SQLite数据库。可以简单回顾一下在java中是如何操作数据库的:

  • 继承自SQLiteOpenHelper类,复写对应的方法,可以得到一个Helper实例;
  • 通过SQLiteOpenHelper的实例的getWritableDatabase()来得到一个数据库实例;
  • 然后就可以通过这个数据库实例进行CRUD操作;

简单回顾一下在Java中的流程:

// 构建一个子类Helper
public class MySQLiteOpenHelper extends SQLiteOpenHelper {
    
    
    private Context context;
    private String name;
    private String bookSql = "create table Book (id integer primary key autoincrement, " +
            "name text, pages integer)";
    private String userSql = "create table User (name text, age integer)";

    public MySQLiteOpenHelper(@Nullable Context context,
                              @Nullable String name,
                              @Nullable SQLiteDatabase.CursorFactory factory,
                              int version) {
    
    
        super(context, name, factory, version);
        this.context = context;
        this.name = name;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    
    
        // 创建数据库表
        db.execSQL(bookSql);
        db.execSQL(userSql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists User");
        onCreate(db); // 重新执行一下onCreate方法
    }
}

// 获取db对象
mySQLiteOpenHelper = new MySQLiteOpenHelper(getApplicationContext(),
                "BookDatabase.db", null, 3);
SQLiteDatabase db= mySQLiteOpenHelper.getWritableDatabase();



// CRUD
db.insert("Book", null, values);

2. Kotlin中的数据库操作

虽然在Kotlin中也可以像上面的那种方式一样来进行数据库的操作,但是Google推出了一款数据库框架,即:Room。下面就使用这个框架进行完成操作。

2.1 依赖

首先需要添加依赖:

// Room
def room_version = "2.2.6"
implementation "androidx.room:room-runtime:$room_version"
// For Kotlin use kapt instead of annotationProcessor (注意这个注释)
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
testImplementation "androidx.room:room-testing:$room_version"

当然,这里在kotlin中使用kapt,我们需要导入这个插件:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}

注释:如果项目是使用Kotlin语言来开发的,在添加room-compiler的时候使用kapt关键字,java语言开发的就使用annotationProcessor关键字。

2.2 基础概念

要想使用Room,必须要了解最基础的三个概念:

  • Entity:实体类,对应的是数据库的一张表结构。需要使用注解 @Entity 标记。默认实体类的类名为表名,字段名为数据库中的字段。

  • Dao:包含访问一系列访问数据库的方法。需要使用注解 @Dao 标记。

  • Database:数据库持有者,作为与应用持久化相关数据的底层连接的主要接入点。需要使用注解 @Database 标记。

使用@Database注解需满足以下条件:

  • 定义的类必须是一个继承于RoomDatabase的抽象类。

  • 在注解中需要定义与数据库相关联的实体类列表。

  • 包含一个没有参数的抽象方法并且返回一个带有注解的 @Dao

注释:以上基础概念摘自:Jetpack架构组件 — Room入坑详解

2.2 1 @Entity

从前面我们知道,@Entity作用在类上,该类对应数据库中的一个数据表。属性对应数据库中的字段,那么类似的我们可以指定主键和注释。同样也是使用注解:

  • @PrimaryKey注解用来标注表的主键,可以使用autoGenerate = true 来指定了主键自增长;
  • @ColumnInfo注解用来标注表对应的列的信息比如表名、默认值等等。
  • @Ignore 注解顾名思义就是忽略这个字段,使用了这个注解的字段将不会在数据库中生成对应的列信息。

2.2.2 @Dao

Dao类是一个接口,其中定义了一系列的操作数据库的方法。Room也为我们的提供了相关的注解,有@Insert@Delete@Update@Query

比如:

@Query("select * from user where userId = :id") 
fun getUserById(id: Long): User 

2.2.3 @Database

首先需要定义一个类,继承自RoomDatabase,并添加注解 @Database 来标识。

2.3 实战

这里我们需要一个数据库来存储用户记事本的数据,大致包括如下内容:

字段 说明
title 标题
content 数据内容
firstSubmit 首次提交时间
lastModifiy 最后一次修改时间
type 类型,普通记事或者待办
label 标签,支持多个,使用#号分割
category 分类,工作/学习/生活/…
groupId 组号,默认为1,表示单个记录;如果为多个,表示前端显示为重叠

那么首先我们需要使用@Entity注解来生成逻辑的表(MFNote):

@Entity
class MFNote {
    
    
    @PrimaryKey(autoGenerate = true)
    var noteId: Int = 0

    @ColumnInfo(defaultValue = "无标题")
    lateinit var title: String

    @ColumnInfo(defaultValue = "")
    lateinit var content: String

    @ColumnInfo(name = "first_submit")
    var submit: String? = null

    @ColumnInfo(name = "last_modify")
    var modify: String? = null

    var type: Int = 0

    @ColumnInfo(defaultValue = "默认")
    lateinit var label: String


    @ColumnInfo(defaultValue = "默认")
    lateinit var category: String

    @ColumnInfo(name = "group_id")
    var groupId: Int = 1
}

然后构建一个访问MFNote表的DAO接口(MFDao):

@Dao
interface MFDao {
    
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(mfNote: MFNote?)

    @Delete
    fun delete(mfNote: MFNote): Int

    @Query("select * from MFNote")
    fun getAllNotes(): List<MFNote>

    @Query("select * from MFNote where type = :type")
    fun getNotesByType(type: Int): MFNote

    @Update
    fun updateNoteByNote(mfNote: MFNote)
}

参数onConflict,表示的是当插入的数据已经存在时候的处理逻辑,有三种操作逻辑:REPLACEABORTIGNORE。如果不指定则默认为ABORT终止插入数据。这里我们将其指定为REPLACE替换原有数据。

最后需要构建Room使用的入口RoomDatabase

@Database(entities = [MFNote::class], version = 1)
abstract class MFNoteDataBase : RoomDatabase() {
    
    

    abstract fun mfDao(): MFDao

    companion object {
    
    
        @Volatile
        private var mInstance: MFNoteDataBase? = null
        private const val DATABASE_NAME = "MFNote.db"

        @JvmStatic
        fun getInstance(context: Context): MFNoteDataBase? {
    
    
            if (mInstance == null) {
    
    
                synchronized(MFNoteDataBase::class.java) {
    
    
                    if (mInstance == null) {
    
    
                        mInstance = createInstance(context)
                    }
                }
            }
            return mInstance
        }

        private fun createInstance(context: Context): MFNoteDataBase {
    
    
            mInstance = Room.databaseBuilder(
                context.applicationContext,
                MFNoteDataBase::class.java,
                DATABASE_NAME
            ).build()
            return mInstance as MFNoteDataBase
        }
    }
}

在这里我们只需要对一个数据库表进行操作,所以就定义了一个抽象接口。如果需要定义多个,比如下面的写法:

@Database(entities = [User::class, Course::class, Teacher::class, UserJoinCourse::class, IDCard::class], version = 1)
abstract class AppDataBase : RoomDatabase() {
    
    
    abstract fun userDao(): UserDao
    abstract fun teacherDao(): TeacherDao
    abstract fun courseDao(): CourseDao
    abstract fun userJoinCourseDao(): UserJoinCourseDao
    abstract fun idCardDao(): IDCardDao
}
  • @Database 表示继承自RoomDatabase的抽象类,entities指定表的实现类列表,version指定了DB版本

  • 必须提供获取DAO接口的抽象方法,比如上面定义的movieDao()Room将通过这个方法实例化DAO接口

接下来就是调用了:

class TestActivity : AppCompatActivity() {
    
    

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)

        // 调用Room数据库
        val mfDao = MFNoteDataBase.getInstance(this)?.mfDao()
        mfDao?.insert(MFNote())

        mfDao?.getAllNotes()?.forEach {
    
    
            Log.e("TAG", "onCreate: ${
      
      it.title}, ${
      
      it.category}" )
        }
    }
}

结果:
在这里插入图片描述
最终我在Dao层添加了如下方法:

@Dao
interface MFDao {
    
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(mfNote: MFNote?)

    @Delete
    fun delete(mfNote: MFNote): Int

    @Query("select * from MFNote")
    fun getAllNotes(): List<MFNote>

    @Query("select * from MFNote where noteId = :id")
    fun getNoteByNoteId(id: Int): MFNote

    @Query("select * from MFNote where type = :type")
    fun getNotesByType(type: Int): List<MFNote>

    @Query("select * from MFNote where label like '%' || :label || '%'")
    fun getNotesByLabel(label: String): List<MFNote>

    @Query("select * from MFNote where category like '%' || :category || '%'")
    fun getNotesByCategory(category: String): List<MFNote>

    @Query("select * from MFNote where group_id = :groupId")
    fun getNotesByGroupId(groupId: Int): List<MFNote>

    @Query("select * from MFNote where first_submit >= :beginTime and first_submit <= :endTime")
    fun getNotesBySubmitTime(beginTime: String, endTime: String): List<MFNote>

    @Query("select * from MFNote where first_submit >= :beginTime")
    fun getNotesByStartSubmitTime(beginTime: String): List<MFNote>

    @Query("select * from MFNote where first_submit <= :endTime")
    fun getNotesByEndSubmitTime(endTime: String): List<MFNote>

    @Query("select * from MFNote where last_modify >= :beginTime and last_modify <= :endTime")
    fun getNotesByModifyTime(beginTime: String, endTime: String): List<MFNote>

    @Query("select * from MFNote where last_modify >= :beginTime")
    fun getNotesByStartModifyTime(beginTime: String): List<MFNote>

    @Query("select * from MFNote where last_modify <= :endTime")
    fun getNotesByEndModifyTime(endTime: String): List<MFNote>

    @Query("select * from MFNote where (title like '%' || :words || '%') or (content like '%' || :words || '%') or (first_submit like '%' || :words || '%') or (last_modify like '%' || :words || '%') or (label like '%' || :words || '%') or (category like '%' || :words || '%')")
    fun getNodesByKeyWords(words: String): List<MFNote>

    @Update
    fun updateNoteByNote(mfNote: MFNote)
}

测试代码:

class TestActivity : AppCompatActivity() {
    
    

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)

        // 调用Room数据库
        val mfDao = MFNoteDataBase.getInstance(this)?.mfDao()
        val mfNote = MFNote()
        mfNote.title = "测试"
        mfNote.content = "第一条测试"
        mfDao?.insert(mfNote)
        mfDao?.getAllNotes()?.forEach {
    
    
            Log.e("TAG", "onCreate: ${
      
      it.title}, ${
      
      it.category}" )
        }

        val notesByType = mfDao?.getNotesByType(0)
        notesByType?.forEach {
    
    
            Log.e("TAG", "onCreate: ${
      
      it.title}, ${
      
      it.category}" )
        }

        val entity = mfDao?.getNoteByNoteId(2)

        Log.e("TAG", "onCreate: ${
      
      entity?.title}, ${
      
      entity?.firstSubmit}" )

        entity?.title = "厉害"
        mfDao?.updateNoteByNote(entity!!)
    }
}

以上代码均测试通过。

3. 数据库版本升级(Migration)

3.1 正常升级

数据库版本升级,也就是在原来的表中加入一些字段内容。在Room中提供了比较便捷的数据库升级方式。直接使用Migration来可以完成。甚至可以完成跳版本的升级,比如当前用户版本为1,而最新的版本为3,那么Room可以依次执行1-2-3的过程。比如此时我有一个表User,如下:
在这里插入图片描述

当然,对应的Room的使用这里不再介绍。此时的需求为在表中新增一个字段:nickName。那么可以在创建数据库的时候进行添加Migration

private fun createInstance(context: Context): UserDataBase {
    
    
    mInstance = Room.databaseBuilder(
        context.applicationContext,
        UserDataBase::class.java,
        DATABASE_NAME
    ).addMigrations(Migration_1_2)  // 数据库版本升级
        .build()
    return mInstance as UserDataBase
}

// Migration(int startVersion, int endVersion)
private val Migration_1_2 = object : Migration(1, 2) {
    
    
    override fun migrate(database: SupportSQLiteDatabase) {
    
    
        database.execSQL("alter TABLE User add column nickName TEXT default '123' not null")
    }
}

对应的,需要在@Entity注释的类中添加对应的字段:

@ColumnInfo(name = "nickName", typeAffinity = ColumnInfo.TEXT)
var nickname = ""

然后将之前的@Database注释的版本改为目标版本2

@Database(entities = arrayOf(User::class), version = 2, exportSchema = false)
abstract class UserDataBase : RoomDatabase()

最后,就可以查看下数据库:
在这里插入图片描述

数据更新后的表:
在这里插入图片描述

当然,如果需要从版本1升级到版本3,就可以对应的定义:

private val Migration_1_2 = object : Migration(1, 2)
private val Migration_2_3 = object : Migration(2, 3)

以及在表中的字段定义好,然后在创建数据库的时候进行使用:

mInstance = Room.databaseBuilder(
        context.applicationContext,
        UserDataBase::class.java,
        DATABASE_NAME
    ).addMigrations(Migration_1_2, Migration_2_3)  // 数据库版本升级
        .build()

3.2 异常升级

参考文档:手动迁移
值得注意的是,有些时候可能敲错了,在@Database注解的参数中指定了一个不存在的版本,会导致程序的异常退出。比如此时我这里没有写2到3版本的升级Migration和添加对应的字段,当指定3后程序异常退出,报错为:

Caused by: java.lang.IllegalStateException: A migration from 2 to 3 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration …) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

根据提示说明,知道要么添加一个新的Migration来指定添加的表字段,要么指定fallbackToDestructiveMigration方法。这里就添加一个fallbackToDestructiveMigration方法,即:

mInstance = Room.databaseBuilder(
    context.applicationContext,
    UserDataBase::class.java,
    DATABASE_NAME
).addMigrations(Migration_1_2)  // 数据库版本升级
    .fallbackToDestructiveMigration()
    .build()

最后,程序可以正常运行,但是用户数据库文件中的数据均会清空(破坏性地重新创建应用的数据库表,Room 在尝试执行没有定义迁移路径的迁移时会从数据库表中永久删除所有数据)。

3.3 Scheme文件

再次回顾一下@Database的注解:

@Database(entities = arrayOf(User::class), version = 3, exportSchema = false)
abstract class UserDataBase : RoomDatabase()

使用的时候, exportSchema指定为false,也就是不需要Schema文件。这里可以设置为true,然后在配置文件中指定一下生成的路径:

android {
    
    
    ...
    defaultConfig {
    
    
        ...
        javaCompileOptions {
    
    
            annotationProcessorOptions {
    
    
                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
            }
        }
    }
}

在项目路径下就可以看见生成的json文件:
在这里插入图片描述

导出的 JSON 文件代表数据库的架构历史记录。您应将这些文件存储在版本控制系统中,因为此系统允许 Room 出于测试目的创建较旧版本的数据库。

猜你喜欢

转载自blog.csdn.net/qq_26460841/article/details/124305084