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(指令寄存器)中。 分析&…...
行波管(TWT)核心参数权衡:填充比、流通率与电子注效率的物理本质及工程设计
在行波管(TWT)设计中,填充比(F)、流通率(ηₜᵣₐₙₛ)与电子注效率(ηₑ)是决定器件性能的三大核心参数,三者并非独立存在,而是形成了紧密的物理…...
Self Service Password与LDAP集成实战:从部署到问题排查
1. Self Service Password与LDAP集成概述 自助密码重置功能已经成为企业IT基础设施中不可或缺的一部分。想象一下,当员工在深夜加班时忘记密码,又找不到IT支持人员,这种场景下的自助解决方案就显得尤为重要。Self Service Password࿰…...
【技术解析】SimpleNet:用极简网络架构革新工业图像异常检测
1. 工业图像异常检测的现状与挑战 工业生产线上的质检环节一直是个让人头疼的问题。想象一下,你站在一条每分钟生产上百件产品的流水线旁,需要肉眼检查每个产品表面是否有划痕、凹陷或污渍——这几乎是不可能完成的任务。传统计算机视觉方法在这个领域已…...
cv2.findContours()错误的解决办法ValueError: not enough values to unpack (expected 3, got 2)
方法一:直接去掉一个返回值就即可。 方法二:把OpenCV 安装3.X的版本 具体原因 2、解析差异: OpenCV2和OpenCV4中: findContours这个轮廓提取函数会返回两个值:①轮廓的点集(contours)②各层轮廓的索引(hierarchy) 返回…...
独立站页面结构优化的注意事项是什么_独立站 SEO 与品牌建设的关系是什么
独立站页面结构优化的注意事项是什么 在当今的数字化时代,独立站(独立网站)已经成为个人品牌和企业展示自我、推广产品和服务的重要平台。单凭一个美观的独立站,难以在竞争激烈的网络环境中脱颖而出。因此,独立站页面…...
3个步骤搞定本地OCR:让隐私保护与效率提升不再矛盾
3个步骤搞定本地OCR:让隐私保护与效率提升不再矛盾 【免费下载链接】Umi-OCR OCR software, free and offline. 开源、免费的离线OCR软件。支持截屏/批量导入图片,PDF文档识别,排除水印/页眉页脚,扫描/生成二维码。内置多国语言库…...
如何选择ComfyUI-FramePackWrapper的模型加载方案?从技术选型到场景适配全解析
如何选择ComfyUI-FramePackWrapper的模型加载方案?从技术选型到场景适配全解析 【免费下载链接】ComfyUI-FramePackWrapper 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI-FramePackWrapper 在AI视频生成工作流中,模型加载是影响效率与稳…...
实战工业测控:基于快马AI生成LabVIEW与数据库、Web集成的监控系统
今天想和大家分享一个最近用LabVIEW实现的工业测控项目实战经验。这个项目是为某制造车间设计的生产线监控系统,主要实现了设备数据采集、存储和可视化展示的全流程。下面我会分步骤详细介绍实现过程。 数据采集模块设计 这个环节需要实时获取产线上多个设备的运行…...
2026 企业AI 超级员工选型建议:告别伪智能,选对企业级智能体
2026 年,AI Agent 智能体技术全面落地商用,AI 超级员工已然成为企业数字化转型、降本增效的核心抓手,更是营销、运营等业务场景的刚需配置。但当下市场产品鱼龙混杂,定价从数千元到数十万元跨度极大,功能宣传动辄标榜 …...
Pixel Language Portal快速上手:使用Gradio前端快速验证Hunyuan-MT-7B能力
Pixel Language Portal快速上手:使用Gradio前端快速验证Hunyuan-MT-7B能力 1. 项目概览 Pixel Language Portal(像素语言跨维传送门)是一款基于腾讯Hunyuan-MT-7B大模型构建的创新翻译工具。它将传统翻译体验重构为16-bit像素冒险风格&…...
