Android Room数据库版本迁移的实战

2021-10-25 15:53:14 浏览数 (1)

前言

最近一直在做一款Android的新产品,所以更新的文章基本都是Android相关,主要是产品中的应用的东西及一些笔记,新产品中Android本地数据库要存放的东西还挺多的,所以这篇是专门针对Android本地数据库Room的版本迁移做的一个填坑记录。

Room数据库迁移

微卡智享

Room 持久性库支持通过 Migration 类进行增量迁移以满足此需求。每个 Migration 子类通过替换 Migration.migrate() 方法定义 startVersion 和 endVersion 之间的迁移路径。当应用更新需要升级数据库版本时,Room 会从一个或多个 Migration 子类运行 migrate() 方法,以在运行时将数据库迁移到最新版本:

代码语言:javascript复制
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, "  
                "PRIMARY KEY(`id`))")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
    }
}

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()

妥善处理缺失的迁移路径

如果 Room 无法找到将设备上的现有数据库升级到当前版本的迁移路径,就会发生 IllegalStateException。在迁移路径缺失的情况下,如果丢失现有数据可以接受,请在创建数据库时调用 fallbackToDestructiveMigration() 构建器方法:

代码语言:javascript复制
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .fallbackToDestructiveMigration()
        .build()

此方法会指示 Room 在需要执行没有定义迁移路径的增量迁移时,破坏性地重新创建应用的数据库表。

警告:在应用的数据库构建器中设置此选项意味着 Room 在尝试执行没有定义迁移路径的迁移时会从数据库表中永久删除所有数据。

如果您只想让 Room 在特定情况下回退到破坏性重新创建,可以使用 fallbackToDestructiveMigration() 的一些替代选项:

  • 如果特定版本的架构历史记录导致迁移路径出现无法解决的问题,请改用 fallbackToDestructiveMigrationFrom()。此方法表示您仅在从特定版本迁移时才希望 Room 回退到破坏性重新创建。
  • 如果您仅在从较高数据库版本迁移到较低数据库版本时才希望 Room 回退到破坏性重新创建,请改用 fallbackToDestructiveMigrationOnDowngrade()。

以上的介绍都是出自Android官方的开发者指南中,接下来就就是我自己的实践记录。

01

数据库增加新表

因为业务的升级需要增加一个名为t_Bill_TurnOverPick新表进行处理,表结构如下:

代码语言:javascript复制
package ryb.medicine.database.bean.table

import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import ryb.medicine.library_common.base.DateTimeUtils

/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:10:07
 * 功能模块说明:
 */
@Entity(
    tableName = "t_Bill_TurnOverPick",
    indices = [Index(value = ["dept_no"]), Index(value = ["bill_no"]),
        Index(value = ["drugs_code"]), Index(value = ["upload_flag"]),
        Index(value = ["bill_date"])]
)
class CBillTurnOverPick {

    //ID
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0

    //科室编码
    var dept_no: String = ""

    //科室编码
    var dept_name: String = ""

    //来源科室编码
    var source_dept_no: String = ""

    //来源科室编码
    var source_dept_name: String = ""

    //单号
    var bill_no: String = ""

    //单据时间
    var bill_date: String? = ""

    //单据类型 0-非清单补药  1-清单补药
    var bill_type: Int = 0

    //单据状态  0-未完成  1-已完成  -1--已做废
    var bill_status: Int = 0

    //行号
    var serial_no: String = ""

    //药品编号
    var drugs_code: String = ""

    //HIS药品ID
    var drugs_hisid: String? = ""

    //批号
    var batch_no: String? = ""

    //生产厂家
    var produce_name: String? = ""

    //生产日期
    var produce_date: String? = ""

    //效期
    var expire_date: String? = ""

    //单号需取数量
    var bill_qty: Int = 0

    //实取数量
    var real_qty: Int = 0

    //实取的库存ID信息
    var stock_id: Long? = -1

    //创建日期
    var create_date: String? = DateTimeUtils.getCurrectDate()

    //创建人
    var create_by = ""

    //修改日期
    var last_update_date: String? = DateTimeUtils.getCurrectDate()

    //修改人
    var last_update_by: String? = ""

    //通讯次数
    var upload_times: Int? = 0

    //通讯失败信息
    var upload_erromsg: String? = ""

    //上传标志   0-未上传  1-已上传
    var upload_flag: Int = 0

    //接口中间表的ID
    var interface_id: Long? = 0
}

从类中可以看出,表中除了正常的字段外,还增加了几个索引,所以做数据库版本迁移时除了用SQL语句创建表,还要加入创建索引的语句,所以定义的Migration中这里都要加上:

代码语言:javascript复制
    //数据库升级
    var migration1_2 = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            //创建本地的周转库取药数据表
            var turnoversql =
                " CREATE TABLE if not exists t_Bill_TurnOverPick (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, dept_no TEXT NOT NULL, dept_name TEXT NOT NULL, source_dept_no TEXT NOT NULL, source_dept_name TEXT NOT NULL, bill_no TEXT NOT NULL, bill_date TEXT, bill_type INTEGER NOT NULL, bill_status INTEGER NOT NULL, serial_no TEXT NOT NULL, drugs_code TEXT NOT NULL, drugs_hisid TEXT, batch_no TEXT, produce_name TEXT, produce_date TEXT, expire_date TEXT, bill_qty INTEGER NOT NULL, real_qty INTEGER NOT NULL, stock_id INTEGER, create_date TEXT, create_by TEXT NOT NULL, last_update_date TEXT, last_update_by TEXT, upload_times INTEGER, upload_erromsg TEXT, upload_flag INTEGER NOT NULL, interface_id INTEGER) "
            database.execSQL(turnoversql)

            turnoversql =
                " CREATE INDEX index_t_Bill_TurnOverPick_dept_no ON t_Bill_TurnOverPick (dept_no) "
            database.execSQL(turnoversql)

            turnoversql =
                " CREATE INDEX index_t_Bill_TurnOverPick_drugs_code ON t_Bill_TurnOverPick (drugs_code) "
            database.execSQL(turnoversql)

            turnoversql =
                " CREATE INDEX index_t_Bill_TurnOverPick_bill_date ON t_Bill_TurnOverPick (bill_date) "
            database.execSQL(turnoversql)

            turnoversql =
                " CREATE INDEX index_t_Bill_TurnOverPick_bill_no ON t_Bill_TurnOverPick (bill_no) "
            database.execSQL(turnoversql)

            turnoversql =
                " CREATE INDEX index_t_Bill_TurnOverPick_upload_flag ON t_Bill_TurnOverPick (upload_flag) "
            database.execSQL(turnoversql)
        }
    }

只有表中字段类型都和类中全部一致,运行时才会正常升级,否则会抛出异常

Caused by: java.lang.IllegalStateException: Migration didn't properly handle

02

修改原表中的主键

下面是t_Dev_Cfg的表,表中原来的主键是ipadr的IP地址字段,后来因为需要修改IP后,主键要改为dev_serialno这一列了,所以在类中先将原来ipadr上面的特征标签@PrimaryKey去掉后,加入到了dev_serialno下:

代码语言:javascript复制
package ryb.medicine.database.bean.table;

import androidx.room.Entity;
import androidx.room.PrimaryKey;

import org.jetbrains.annotations.NotNull;

import ryb.medicine.library_common.base.DateTimeUtils;

/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:9:57
 * 功能模块说明:
 */
@Entity(
        tableName = "t_Dev_Cfg"
)
public class CDev {
    public CDev() {
        ipadr = "192.168.0.252";
        port = 6020;
        dev_serialno = 1;
        tagnum = 35;
        start_tagid = "0001";
        end_tagid = "0035";
        grid_cfg = "555|5555";
        flag = 1;
        create_date = DateTimeUtils.getCurrectDate();
        create_by ="";
        last_update_date = DateTimeUtils.getCurrectDate();
        last_update_by = "";
        upload_flag = 0;
    }
    //IP地址
    @NotNull
    public String ipadr;

    //端口号
    public int port;

    //主键
    //柜子序号
    @PrimaryKey
    @NotNull
    public int dev_serialno;

    //标签个数
    public int tagnum;

    //起始标签
    public String start_tagid;

    //结束标签
    public String end_tagid;

    //货格字符   555|5555 其中|代表上下门分隔,每一行数字代表每行中的多少格
    public String grid_cfg;

    //启用标志  0-禁用    1-启用
    public int flag;

    //创建日期
    public String create_date;
    //创建人
    public String create_by;
    //修改日期
    public String last_update_date;
    //修改人
    public String last_update_by;
    //上传标志  0-未上传  1-已上传
    public int upload_flag;
}

类中结构改了后,接下来就是写迁移的语句,SQLite中没有支持ALTER TABLE DROP相关的语句,所以直接删除表中主键再重新创建主键是不允许的,只能通过重新创建表还实现。因为原表中已经有数据了,为了保证数据库迁移时数据不会导致数据丢失,所以需要将原来的数据先备份到临时表中,然后删除原来的表,再重新创建,接下来将临时表中的数据再插入回来,最后再将临时表删除即可。

代码语言:javascript复制
    var migration2_3 = object : Migration(2, 3) {
        override fun migrate(database: SupportSQLiteDatabase) {
            //备份设备表
            var sql = " CREATE TABLE if not exists tmp_t_Dev_Cfg as select * from t_Dev_Cfg"
            database.execSQL(sql)
            sql = " Drop Table if exists t_Dev_Cfg "
            database.execSQL(sql)
            //重新创建表
            sql =
                " CREATE TABLE t_Dev_Cfg (ipadr TEXT NOT NULL, port INTEGER NOT NULL, dev_serialno INTEGER NOT NULL, tagnum INTEGER NOT NULL, start_tagid TEXT, end_tagid TEXT, grid_cfg TEXT, flag INTEGER NOT NULL, create_date TEXT, create_by TEXT, last_update_date TEXT, last_update_by TEXT, upload_flag INTEGER NOT NULL, PRIMARY KEY(dev_serialno)) "
            database.execSQL(sql)
            //将源数据插回表新表中
            sql =
                " insert into t_Dev_Cfg(ipadr,port,dev_serialno,tagnum,start_tagid,end_tagid,grid_cfg,flag,create_date,create_by,last_update_date,last_update_by,upload_flag) "  
                        " select ipadr,port,dev_serialno,tagnum,start_tagid,end_tagid,grid_cfg,flag,create_date,create_by,last_update_date,last_update_by,upload_flag from tmp_t_Dev_Cfg"
            database.execSQL(sql)
            //删除创建的临时表
            sql = " Drop Table if exists tmp_t_Dev_Cfg"
            database.execSQL(sql)

        }
    }

03

增加新的视图

做数据查询时,因为经常要关联多表,在Room中有@Embedded和@Relation的方式,如下:

代码语言:javascript复制
package ryb.medicine.database.bean.data

import androidx.room.Embedded
import androidx.room.Relation
import ryb.medicine.database.bean.table.CDrugs
import ryb.medicine.database.bean.table.CDrugsStock

/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:10:36
 * 功能模块说明:
 */
data class DDrugswithStock(
    @Embedded val drugs: CDrugs,

    @Relation(
        parentColumn = "drugs_id",
        entityColumn = "drugs_id"
    )
    val stocks: List<CDrugsStock>
)

但是这个方式也无法解决一些复杂的关联查询,有时候就会用到视图比较方便,因为视图本身也是要在数据库中创建的,所以当数据库迁移时新建的视图也需要在迁移脚本中加入,本来这个我以为是最简单的,结果也是最花我时间才找到原因的,主要一是网上相关资料没有一个介绍视图升级的,所以只能自己测试,先说重点:

创建视图脚本中的视图名格式必须是:`视图名`

就是这原因导致测试了好多次一直升级不成功,新建一个名为VTest的视图,直接放代码:

代码语言:javascript复制
package ryb.medicine.database.bean.view

import androidx.room.DatabaseView

/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:18:04
 * 功能模块说明:
 */
@DatabaseView("select bill_type,id from t_Bill_Pick")
data class VTest(
    //业务类型  0-取药  1-补药  2-盘点  3-周转柜取药
    var bill_type: Int,

    //业务ID   每个业务对应的ID
    var id: Long
)

创建视图的迁移脚本:

代码语言:javascript复制
    /**
     * 注:如果是视图增加视图中前后必须加上`视图名`,否则升级失败。
     */
    var migration3_4 = object : Migration(3, 4) {
        override fun migrate(database: SupportSQLiteDatabase) {
            var sql = " Drop View if exists `VTest`";
            database.execSQL(sql)
            //创建视图
            sql =
                "CREATE View if not exists `VTest` AS select bill_type,id from t_Bill_Pick"
            database.execSQL(sql)
        }
    }

04

执行数据库升级

如上图中一样,把刚才设置的几个数据库升级都加到addMigrations中即可实现数据库升级了。图中把fallbackToDestructiveMigration直接屏蔽了,就是防止因为不版不同,当数据库升级时执行失败直接清空数据库重建,这样会导致所有的数据都清空了,非常的不友好。

代码语言:javascript复制
    fun getDatabase(context: Context): AppDataBase {
        if (INSTANCE == null) {
            synchronized(lock = AppDataBase::class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(
                        context.applicationContext,
                        AppDataBase::class.java, DATABASE_NAME
                    )
                        .allowMainThreadQueries()//允许在主线程查询数据
                        .addMigrations(
                            migration1_2, migration2_3, migration3_4
                        )//数据库升级时执行
                        //使用fallback下面这句,当数据库执行失败时会直接清空数据库重建
                        //.fallbackToDestructiveMigration()
                        .build()
                }
            }
        }
        return INSTANCE!!
    }

还需要注意的是@Database里面对应的版本号加修改到当前的版本号,以及前面新创建的表和新创建的视图也加入到entities和views中即可。

0 人点赞