Day939.如何小步安全地升级数据库框架 -系统重构实战

news2025/2/26 2:56:54

如何小步安全地升级数据库框架

Hi,我是阿昌,今天学习记录的是关于如何小步安全地升级数据库框架的内容。

当消息组件的数据存储都是采用 SQL 拼写的方式来操作,这样不便于后续的扩展及维护。除此之外,相比前面的其他重构,升级数据框架需要考虑的场景会更多,例如升级框架以后用户的重要数据不能丢失

以 Sharing 项目为例,把项目中原先采用 SQL 拼写的方式替换为使用 Room 框架来统一管理缓存数据。在这个过程中你分享如何小步安全重构,分阶段完成数据库框架的升级。为了确保重构完的代码不会破坏原有功能,还有用户的关键数据不丢失,并如何给数据操作相关功能做自动化测试覆盖,以及如何实现更安全的数据迁移。


一、代码分析

消息组件中创建数据库表的相关操作,核心代码是后面这样。

//数据库表的创建
class DataBaseHelper(context: Context?) : SQLiteOpenHelper(context, "message.db", null, 1) {
    override fun onCreate(db: SQLiteDatabase) {
        createTable(db)
    }
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
    fun createTable(db: SQLiteDatabase) {
        val createTableSql = """CREATE TABLE IF NOT EXISTS $message_info(
   $id INTEGER PRIMARY KEY AUTOINCREMENT,
   $content VARCHAR(1024) ,
   $fileName VARCHAR(1024) ,
   $date LONG 
)"""
        try {
            db.execSQL(createTableSql)
        } catch (e: Exception) {
            Log.d("Task:Sql", e.message!!)
        }
    }
    companion object {
        var message_info = "message_info"
        var id = "id"
        var content = "content"
        var fileName = "fileName"
        var date = "date"
    }
}

上述核心代码,可以看出 Sharing 项目主要通过 SQLite 提供的 SQLiteDatabase 以及 SQLiteOpenHelper 来创建数据表

目前 Sharing 项目仅有一个表以及简单的几个字段,通过 SQL 拼写的方式看起来也还好维护,但是如果现在面临的是几十个表以及几百个字段,那么管理和维护这些拼写的 SQL 字符串就会非常困难,当有修改的时候也非常容易出错。


来看数据的缓存以及读取操作。

//进行信息缓存以及读取的代码
class LocalDataSource constructor( private var mContext: Context) : IDataSource {
    override fun getMessageListFromCache(): MutableList<Message> {
        val messageList: MutableList<Message> = ArrayList()
        val dataBaseHelper = DataBaseHelper(mContext)
        val c = dataBaseHelper.writableDatabase.query(
            DataBaseHelper.Companion.message_info, null,null, null, null, null,null)
        if (c.moveToFirst()) { 
            for (i in 0 until c.count) {
                c.move(i) //移动到指定记录
                val id = c.getInt(c.getColumnIndex(DataBaseHelper.Companion.id))
                val content = c.getString(c.getColumnIndex(DataBaseHelper.Companion.content))
                val fileName = c.getString(c.getColumnIndex(DataBaseHelper.Companion.fileName))
                val date = c.getLong(c.getColumnIndex(DataBaseHelper.Companion.date))
                messageList.add(Message(id, content, fileName, date))
            }
        }
        return messageList
    }
    override fun saveMessageToCache(messageList: List<Message>) {
        val dataBaseHelper = DataBaseHelper(mContext)
        if (messageList.isNotEmpty()) {
            dataBaseHelper.writableDatabase.delete(
                DataBaseHelper.Companion.message_info, null,null)
            for (message in messageList) {
                val cv = ContentValues()
                cv.put(DataBaseHelper.Companion.id, message.id)
                cv.put(DataBaseHelper.Companion.content, message.content)
                cv.put(DataBaseHelper.Companion.date, message.date)
                cv.put(DataBaseHelper.Companion.fileName, message.fileName)
                dataBaseHelper.writableDatabase.insert(
                    DataBaseHelper.Companion.message_info,null,cv)
            }
        }
    }
}

通过上述代码可以看到,减少虽然 SQLite 提供了 query 以及 delete 等操作方法,可以减少编写 SQL 字符串,但是仍然需要去编写大量的对象转换代码。其实这些代码都是前面提到的非业务的模板代码,这会大大增加我们维护代码的成本。

为了解决这些问题,官方也提供了新的数据库框架 Room。官方文档强烈建议使用 Room,而不是直接使用 SQLite API。


二、补充自动化守护测试

首先第一步还是需要先做基本的自动化测试覆盖,作为后续重构的安全守护网。

这里主要针对 LocalDataSource 类来做测试,保证基本的数据缓存以及读取功能是正确的。用例设计是这样的。

  • 测试用例 1:当 message 数据表没有缓存数据时,获取的缓存数据为空。
  • 测试用例 2:当 message 数据表中有缓存数据时,能够成功获取缓存数据。读取的缓存数据内容需要与保持的缓存数据内容一致。

现在,需要将测试用例转换成自动化测试用例。

class LocalDataSourceTest {
    //用例1
    @Test
    fun `should get message list is empty when database has not data`() = runBlocking {
        //given
        val localDataSource = LocalDataSource(ApplicationProvider.getApplicationContext())
        //when
        val messageListFromCache = localDataSource.getMessageListFromCache()
        //then
        assert(messageListFromCache.isEmpty())
    }
    //用例2
    @Test
    fun `should get message list success when database has data`() = runBlocking {
        //given
        val localDataSource = LocalDataSource(ApplicationProvider.getApplicationContext())
        localDataSource.saveMessageToCache(getMockData())
        //when
        val messageListFromCache = localDataSource.getMessageListFromCache()
        //then
        val messageOne = messageListFromCache[0]
        Truth.assertThat(messageOne.id).isEqualTo(1)
        Truth.assertThat(messageOne.content).isEqualTo("张三共享文件到消息中...")
        Truth.assertThat(messageOne.fileName).isEqualTo("大型Android遗留系统重构.pdf")
        Truth.assertThat(messageOne.formatDate).isEqualTo("2021-03-17 14:47:55")
        val messageTwo = messageListFromCache[1]
        Truth.assertThat(messageTwo.id).isEqualTo(2)
        Truth.assertThat(messageTwo.content).isEqualTo("李四共享视频到消息中……")
        Truth.assertThat(messageTwo.fileName).isEqualTo("修改代码的艺术.pdf")
        Truth.assertThat(messageTwo.formatDate).isEqualTo("2021-03-17 14:48:08")
    }
}

后面是执行测试用例的结果。

在这里插入图片描述


三、小步安全重构

在过程中每当完成一步重构后,都可以频繁运行测试来验证是否有破坏原有的逻辑。

拆分重构过程的要求是,每一小步的重构都不能破坏之前的功能,而且全部步骤都完成之后即可完成整体的重构。这里我们结合 Room 框架的设计,把整个重构分成下面这 5 个步骤。

  • 第一步是使用 Room 注解更新实体。
  • 第二步是使用 Room 的 SupportSQLiteOpenHelper 进行 SQL 操作。这 2 个步骤完成后,不会修改原有的查询及删除操作代码。
  • 第三步,进阶使用 Room 的 Dao 注解方式来管理数据的增删改查,替换掉原有的查询及删除代码。
  • 第四步是优化操作,使用协程来优化 IO 的异步操作。最后是第五步,迁移旧数据。

1、使用 Room 注解更新实体

来看第一步,这一步相对比较简单,使用 Room 的注解标记新的实体代码就可以了。


@Entity(tableName = "message_info")
class Message(
    @PrimaryKey @ColumnInfo(name = "id") var id: Int,
    @ColumnInfo(name = "content") var content: String,
    @ColumnInfo(name = "fileName") var fileName: String,
    @ColumnInfo(name = "date") var date: Long
) {
    @Ignore
    val formatDate = DateUtil.getDateToString(date)
    @Ignore
    val downloadCount = ARouter.getInstance().navigation(IFileStatistics::class.java)
        ?.getDownloadCount(id.toString())
}

2、使用 Room 的 SupportSQLiteOpenHelper 进行 SQL 操作

Room 提供了 SupportSQLiteOpenHelper 类,可以用它替换 SQLite 中的 SQLiteOpenHelper,将原本使用 SQLiteOpenHelper 的地方替换为使用 Room 的 SupportSQLiteOpenHelper

@Database(entities = [Message::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
}

class LocalDataSource constructor(
    private var mContext: Context
) : IDataSource {
    val db = Room.databaseBuilder(
        mContext,
        AppDatabase::class.java, "message.db"
    ).build()
    
   override fun getMessageListFromCache(): MutableList<Message> {
    val messageList: MutableList<Message> = ArrayList()
    val dataBaseHelper = db.openHelper
    //... ...
    return messageList
  }

  override fun saveMessageToCache(messageList: List<Message>) {
    val dataBaseHelper = db.openHelper
     //... ...
    }
}

3、使用 Dao 注解方式来管理数据的增删改查

选择将 query 及 delete 等的操作,使用 Room 的 Dao 注解做替换。


@Dao
interface MessageDao {
    @Query("SELECT * FROM message_info")
    suspend fun getAll(): List<Message>
    @Insert
    suspend fun insertAll(vararg message: Message)

    @Query("DELETE FROM message_info")
    suspend fun deleteAll()
}

完成后,就可以将 LocalDataSource 的实现替换为使用 MessageDao 进行操作了。

class LocalDataSource constructor(
    private var mContext: Context
) : IDataSource {
    val db = Room.databaseBuilder(
        mContext,
        AppDatabase::class.java, "message.db"
    ).build()
    override suspend fun getMessageListFromCache(): MutableList<Message> {
        return db.messageDao().getAll().toMutableList()
    }
    override suspend fun saveMessageToCache(messageList: List<Message>) {
        messageList.let {
            db.messageDao().deleteAll()
            db.messageDao().insertAll(*it.toTypedArray())
        }
    }
}

至此完成了 Room 框架的升级,可以对比一下 LocalDataSource 改造前后的代码,使用 Room 框架大大帮我们减少了模板代码的编写,代码更加容易维护


4、数据迁移

如果升级数据库框架时,调整过表结构的话,这时就可以用 Room 提供的 Migration 机制升级数据。

例如 Sharing 项目在升级 Room 框架时,新增了一个 count 字段用于缓存文件的下载数量,但是在旧的数据表中并没有这个字段,这时就可以使用 Migration 机制来迁移升级数据。


private val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE message_info RENAME TO message_info_back_up")
        database.execSQL("CREATE TABLE message_info ( id  INTEGER PRIMARY KEY NOT NULL, content TEXT NOT NULL,date INTEGER NOT NULL, count INTEGER NOT NUL)")
        database.execSQL("INSERT INTO message_info (id, content,date,0) SELECT id, content,date FROM message_info_back_up")
    }
}
private val db = Room.databaseBuilder(
    mContext,
    AppDatabase::class.java, "message.db"
).addMigrations(MIGRATION_1_2).build()

在实际的项目中,需要根据数据对用户的重要性,来决定是否要做数据的迁移。

例如一些缓存数据只是提高用户的体验,哪怕这部分数据没有了,从网络获取它也很方便,就不必迁移数据。但如果对用户来说是关键的数据,就必须迁移和做专项的测试。

例如一个短信息的 APP(信息只缓存在本地),当升级框架后,迁移这些短信息就非常重要,因为这部分数据丢失的话,对用户来说是非常糟糕的体验。

更多迁移数据的方法,可以参考官网的说明。


四、集成验收

跟之前的组件内分层架构重构一样,完成重构后我们需要完成最后的集成验收。

验收有三个标准:

  • 第一是编译通过,能够打包出安装包
  • 第二是架构守护用例执行通过
  • 第三是验收自动化测试执行通过

改造后相关的自动化测试运行结果。

基本冒烟及架构守护用例自动化测试报告如下:

在这里插入图片描述

消息组件自动化测试报告如下:

在这里插入图片描述

至此,我们完成了对 Sharing 项目的数据库框架升级重构。


五、总结

改造前 Sharing 项目使用了 SQLite 来管理数据库,这个方式主要存在 2 个问题。

  • 第一个是使用拼写 SQL 方式来管理表创建,不便于扩展;
  • 第二个是存在大量的对象转换重复代码,不便于维护。

根据官方的建议,使用 Room 框架来帮我们完成这些重复的工作,让可以更聚焦在业务开发上。


Room 框架的升级可以分 2 个阶段完成。

  • 第一个阶段是先引入 Room 框架,将原本使用 SQLiteOpenHelper 操作数据库的方式,调整为使用 Room 提供的 SupportSQLiteOpenHelper 来进行管理,此时不会修改原有的查询及删除操作代码。
  • 第二个阶段可以使用 Room 提供的 Dao 注解方式,替换掉原来的 insert、query 等方法,完成后可以减少大量的增删改查模板代码。此时就可以充分感受到使用框架带来的收益。同样完成一个功能,可以少写很多模板的代码。

特别需要注意的是,如果在改造过程中,如果数据表结构有变化,需要采用 Room 框架提供的 Migration 机制来迁移数据


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/413104.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

线性代数 --- Gram-Schmidt, 格拉姆-施密特正交化(上)

在求解最小二乘的问题时&#xff0c;已经介绍了类似于Gram-Schmidt的一些想法。在这里要继续介绍这些想法&#xff0c;那就是如何“改写”矩阵A中的列向量&#xff0c;使得最小二乘解的计算越来越简单&#xff0c;甚至可以直接写出答案。 标准正交基(Orthonormal Bases) 上一篇…

3.1 多维度随机变量及其分布

思维导图&#xff1a; 学习目标&#xff1a; 要学习二维随机变量及联合分布&#xff0c;我会按照以下步骤进行&#xff1a; 了解基本概念&#xff1a;首先要了解二维随机变量的概念&#xff0c;即同时包含两个随机变量的变量。还要了解二维随机变量的取值范围以及联合概率密…

【CSS】定位 ④ ( 绝对定位特点 | 相对定位不脱标示例 | 绝对定位脱标示例 )

文章目录一、绝对定位特点二、相对定位不脱标示例三、绝对定位脱标示例一、绝对定位特点 绝对定位 以 带有定位的 父级元素 为基准 , 通过 边偏移 移动位置 ; 如果 绝对定位 的元素 的 父级元素 没有定位 , 那么会 一直向上查找有定位的父级元素 , 直到浏览器 ; 绝对定位 元素…

【Linux】组管理和权限管理

目录1 Linux组的基本介绍2 文件/目录所有者2.1 查看文件的所有者2.2 修改文件所有者3 组的创建3.1 基本指令3.2 应用实例4 文件/目录 所在组4.1 查看文件/目录所在组4.2修改文件/目录所在的组5 其他组6 改变用户所在组6.1 改变用户所在的组6.2 应用实例7 权限介绍8 rwx权限详解…

多线程(八):常见锁策略

目录 前言 1. 乐观锁 VS 悲观锁 乐观锁 悲观锁 2. 轻量级锁 VS 重量级锁 轻量级锁 3. 自旋锁 VS 挂起等待锁 自旋锁 挂起等待锁 4. 读写锁 VS 互斥锁 5. 可重入锁 vs 不可重入锁 死锁 发生死锁的情况 死锁产生的四个必要条件如下&#xff1a; 6. 公平锁和非公平锁…

Hibernate多表关联——(一对多关系)

Hibernate多表关联——&#xff08;一对多关系&#xff09; 文章目录Hibernate多表关联——&#xff08;一对多关系&#xff09;1.分别在类中添加属性&#xff1a;2.hibernate建表3.使用测试类在表中添加数据hibernate是连接数据库使得更容易操作数据库数据的一个框架&#xff…

ERROR:org.apache.hadoop.hbase.PleaseHoldException: Master is initializing错误

一、问题 重新安装hbase后&#xff0c;在hbase shell中查看所有命名空间时&#xff0c;出现了ERROR:org.apache.hadoop.hbase.PleaseHoldException: Master is initializing错误。 二、方法 1、root用户下&#xff0c;关闭hbase stop-hbase.sh 2、执行以下命令删除HDFS下的hb…

go进阶篇gin框架系列三

一、模板引擎的语法 {{.}} 模板语法都包含在{{和}}中间&#xff0c;其中{{.}}中的点表示当前对象。 当我们传入一个结构体对象时&#xff0c;我们可以根据.来访问结构体的对应字段。 pipeline pipeline是指产生数据的操作。比如{{.}}、{{.Name}}等。Go的模板语法中支持使用管道…

JavaWeb开发 —— Element组件

目录 一、什么是Element&#xff1f; 二、快速入门 三、常见组件 一、什么是Element&#xff1f; ① Element&#xff1a;是饿了么团队研发的&#xff0c;一套为开发者、设计时和产品经理准备的基于Vue 2.0 的桌面端组件库。 ② 组件&#xff1a;组成网页的部件&#xff0…

FIFO的工作原理及其设计

1.简介 FIFO( First Input First Output)简单说就是指先进先出。FIFO存储器是一个先入先出的双口缓冲器&#xff0c;即第一个进入其内的数据第一个被移出&#xff0c;其中一个口是存储器的输入口&#xff0c;另一个口是存储器的输出口。 对于单片FIFO来说&#xff0c;主要有两种…

文字转语音软件的优缺点及如何选择最适合的工具

随着科技的进步&#xff0c;文字转语音技术已经越来越成熟&#xff0c;越来越多的人开始使用文字转语音软件来转换文本为语音。这种技术可以帮助人们在许多方面&#xff0c;例如改善阅读体验、方便学习、提高生产效率等。然而&#xff0c;文字转语音软件有其优缺点&#xff0c;…

一文读懂推荐系统用户画像

1.推荐系统用户画像 用户画像这个词具有广泛性。 它被应用于推荐&#xff0c;广告&#xff0c;搜索&#xff0c;个性化营销等各个领域。任何时候&#xff0c;不管出于什么目的&#xff0c;我们想描述我们的用户是谁的时候&#xff0c;大家都会用到用户画像这个词。 比如&…

VUE_学习笔记

一、 xx 二、模板语法 1.模板语法之差值语法 &#xff1a;{{ }} 主要研究&#xff1a;{{ 这里可以写什么}} 在data中声明的变量、函数等都可以。常量只要是合法的javascript表达式&#xff0c;都可以。模板表达式都被放在沙盒中&#xff0c;只能访问全局变量的一个白名单&a…

精准关键词获取-行业搜索词分析

SEO关键词的收集通常可以通过以下几种方法&#xff1a; 根据市场价值、搜索词竞争性和企业实际产品特征进行筛选&#xff1a;确定您的关键词列表之前&#xff0c;建议先进行市场分析&#xff0c;了解您的竞争对手、行业状况和目标受众等信息&#xff0c;以更好的了解所需的特定…

MySQL日志

MySQL日志 错误日志 错误日志是MySQL中最重要的日志之一&#xff0c;它记录了当mysqld启动和停止时&#xff0c;以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时&#xff0c;建议首先查看此日志。 使用如下命令&#xff0c;可…

ChatGPT宝藏插件丨装上之后,上网、语音聊天、一键分享对话……简直让你爽到起飞!

今天分享4个让你的 ChatGPT 功能更强大的浏览器插件&#xff0c;装上就能用&#xff0c;每一个都是精挑细选。 1. WebChatGPT 很多小伙伴在用 ChatGPT查阅信息时&#xff0c;发现它有一个致命的问题&#xff1a; ChatGPT的知识库全部截止到 2021年9月&#xff0c;正常情况下…

航拍构图方法

目录 简介 对比构图 重复对比 明暗对比 颜色对比 空间错位 点构图 中心点构图 九宫格构图 线构图 二分线&#xff08;水平、对称&#xff09;构图 三分线构图 平行线构图 对角线构图 引导线构图 面构图 前景构图 简化构图 总结 简介 最近接触了无人机&#xf…

MySQL数据库备份(导出导入)的命令详解mysqldump

![在这里插入图片描述](https://img-blog.csdnimg.cn/174ee9e5170445b7acab94c5d97684ea.png [options] 参数参数说明-A, --all-databases导出所有数据库-B, --databases导出指定数据库-h, --hostname指定ip&#xff0c;默认本机-P, --port#指定端口&#xff0c;默认3306-u, …

【Java面试八股文宝典之MySQL篇】备战2023 查缺补漏 你越早准备 越早成功!!!——Day21

大家好&#xff0c;我是陶然同学&#xff0c;软件工程大三即将实习。认识我的朋友们知道&#xff0c;我是科班出身&#xff0c;学的还行&#xff0c;但是对面试掌握不够&#xff0c;所以我将用这100多天更新Java面试题&#x1f643;&#x1f643;。 不敢苟同&#xff0c;相信大…

JavaEE——文件操作与IO操作

目录 文件路径 文件类型 File类 文件的创建 文件流 File类文件读写操作 通过PrintWriter写或Scanner读 PrintWriter写 Scanner读 文件路径 计算机中的文件是以树形结构进行存储的。要找一个文件&#xff0c;需要通过根目录一层一层找&#xff0c;直到找到&#xff0c;…