Day939.如何小步安全地升级数据库框架 -系统重构实战
如何小步安全地升级数据库框架
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@Testfun `should get message list is empty when database has not data`() = runBlocking {//givenval localDataSource = LocalDataSource(ApplicationProvider.getApplicationContext())//whenval messageListFromCache = localDataSource.getMessageListFromCache()//thenassert(messageListFromCache.isEmpty())}//用例2@Testfun `should get message list success when database has data`() = runBlocking {//givenval localDataSource = LocalDataSource(ApplicationProvider.getApplicationContext())localDataSource.saveMessageToCache(getMockData())//whenval messageListFromCache = localDataSource.getMessageListFromCache()//thenval 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
) {@Ignoreval formatDate = DateUtil.getDateToString(date)@Ignoreval 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>@Insertsuspend 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 机制来迁移数据。
相关文章:
Day939.如何小步安全地升级数据库框架 -系统重构实战
如何小步安全地升级数据库框架 Hi,我是阿昌,今天学习记录的是关于如何小步安全地升级数据库框架的内容。 当消息组件的数据存储都是采用 SQL 拼写的方式来操作,这样不便于后续的扩展及维护。除此之外,相比前面的其他重构&#x…...
2023 年十大 API 管理趋势
作者郑玩星,API7.ai 技术工程师。 阅读原文 什么是 API?什么是 API 管理? 近期,AIGC(AI Generated Content,生成式人工智能)在各行业的应用日趋普及。AIGC 服务提供商通过 API 向外部提供其内…...
计算机网络微课堂1-3节
目录 1. TCP/TP协议编辑 2. 3.调制解调器 4.因特网的组成 5.电路交换 6.分组交换 重要常用 7.报文交换 8.总结电路交换 报文交换和分组交换 9. 1. TCP/TP协议 2. ISP 网络提供商 ISP的三层 国际 国家 和本地 3.调制解调器 什么是调制解调器,它存在的…...
[Eigen中文文档] Array类与元素操作
文档总目录 本文目录什么是Array类?Array类型访问Array中的值加法与减法Array乘法其他按元素操作的运算array和matrix表达式之间的转换英文原文(The Array class and coefficient-wise operations) 本页旨在提供有关如何使用Eigen的Array类的概述和说明。 什么是A…...
python学习,全球有哪些特别好的社区推荐呢?
Surfshark可以访问全球社区学习的surfshark工具使用方法教程:qptool.net/shark.html 以下是一些全球范围内比较受欢迎的 Python 学习社区: 中文社区:csdn.net 优势:本土国语社区,获得相关知识与经验便利。 Python官…...
LC-1042. 不邻接植花(四色问题(染色法))
1042. 不邻接植花 难度中等198 有 n 个花园,按从 1 到 n 标记。另有数组 paths ,其中 paths[i] [xi, yi] 描述了花园 xi 到花园 yi 的双向路径。在每个花园中,你打算种下四种花之一。 另外,所有花园 最多 有 3 条路径可以进入…...
python实战应用讲解-【numpy科学计算】scikits-learn模块(附python示例代码)
目录 Numpy 安装scikits-learn 准备工作 具体步骤 Numpy 加载范例数据集 具体步骤...
大数据开发必备面试题Spark篇01
1、Hadoop 和 Spark 的相同点和不同点? Hadoop 底层使用 MapReduce 计算架构,只有 map 和 reduce 两种操作,表达能力比较欠缺,而且在 MR 过程中会重复的读写 hdfs,造成大量的磁盘 io 读写操作,所以适合高时…...
SpringBoot整合xxl-job详细教程
SrpingBoot整合xxl-job,实现任务调度说明调度中心执行器调试整合SpringBoot说明 Xxl-Job是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。Xxl-Job有…...
【MySQL--04】数据类型
文章目录1.数据类型1.1数据类型分类1.2数值类型1.2.1tinyint类型1.2.2bit类型1.2.3小数类型1.2.3.1 float1.2.3.2 decimal1.3字符串类型1.3.1 char1.3.2 varchar1.3.3char和varchar的比较1.4日期和时间类型1.5 enum和set1.5.1 enum1.5.2 set1.5.3 示例1.数据类型 1.1数据类型分…...
git 将其它分支的文件检出到工作区
主要是使用如下命令: git checkout [-f|--ours|--theirs|-m|--conflict<style>] [<tree-ish>] [--] <pathspec>…覆盖与 pathspec 匹配的文件的内容。当没有给出<tree-ish> (通常是一个commit)时,用 index 中的内容覆盖工作树…...
人工智能的最大危险是什么?
作者:GPT(AI智学习) 链接:https://www.zhihu.com/question/592107303/answer/2966857095 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 首先:人工智能为人类带来了很多益处&…...
rk3568点亮E-ink
rk3568 Android11/12 适配 E-ink “EINK”是英语ElectronicInk的缩写。翻译成中文为“电子墨水”。电子墨水由数百万个微胶囊(Microcapsules)所构成,微胶囊的大小约等同于人类头发的直径。每个微胶囊里含有电泳粒子──带负电荷的白色以及带正电荷的黑色粒子&#…...
如何将Springboot项目通过IDEA打包成jar包,并且转换成可执行文件
首先在IDEA打开你的项目,需要确认项目可以正常运行,然后点击页面右侧的Maven,运行Lifecycle下的package, 此时在项目的target目录下就可以看到一个jar包 这个时候你可以在jar包所在目录下执行cmd窗口,运行 java -jar campus-market-0.0.1-S…...
总结:网卡
一、背景 经常听到eth0,bond0这些概念,好奇他们的区别,于是有了此篇文章记录下。 二、介绍 网卡:即网络接口板,又称网络适配器或NIC (网络接口控制器),是一块被设计用来允许计算机在计算机网络上进行通讯…...
Java这么卷,还有前景吗?
“Java很卷”、“大家不要再卷Java了”,经常听到同学这样抱怨。但同时,Java的高薪也在吸引越来越多的同学。不少同学开始疑惑:既然Java这么卷,还值得我入行吗? 首先先给你吃一颗定心丸:现在选择Java依然有…...
后端简易定时任务框架选择(Python/Go)--gocron
文章目录前言实现后语前言 在使用Python的web框架中,包括flask/Django,其中大量用到celery;celery作为异步任务使用的多,同时也会用celery来跑些定时任务,比如每晚定时跑脚本、跑数据统计等闲时任务。但随着任务量的增…...
【GStreamer学习】之GStreamer基础教程
目标 没有什么比在屏幕上打印出“Hello World”更能获得对软件库的第一印象了! 但是由于我们正在学习多媒体框架,所以我们将输出“Hello World!”改为播放视频。 不要被下面的代码量吓到:只有 4 行是真正需要的, 其…...
各类Round-Robin总结,含Verilog实现
1. Fixed Priority Arbitrary 固定优先级就是指每个req的优先级是不变的,即优先级高的先被处理,优先级低的必须是在没有更高优先级的req的时候才会被处理。所以转化为数学模型就是找出req序列中第一个为1的位置,然后将其转换为onehot。 例如: req[3:0] = 4b1100 ==> g…...
《软件设计师-知识点》
1、指令流水线 (一)一条指令的执行过程可分为三个阶段:取指、分析、执行。 取指:根据PC(程序计数器)内容访问主存储器,取出一条指令送到IR(指令寄存器)中。 分析&…...
深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建
华为云FlexusDeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色,华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型,能助力我们轻松驾驭 DeepSeek-V3/R1,本文中将分享如何…...
3-11单元格区域边界定位(End属性)学习笔记
返回一个Range 对象,只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意:它移动的位置必须是相连的有内容的单元格…...
rnn判断string中第一次出现a的下标
# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...
AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...
保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek
文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama(有网络的电脑)2.2.3 安装Ollama(无网络的电脑)2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
华为OD机试-最短木板长度-二分法(A卷,100分)
此题是一个最大化最小值的典型例题, 因为搜索范围是有界的,上界最大木板长度补充的全部木料长度,下界最小木板长度; 即left0,right10^6; 我们可以设置一个候选值x(mid),将木板的长度全部都补充到x,如果成功…...
