Android Room 数据库备份的2种方案

最近在写一个自用的轻量级记录 App,效果图如下:

所有的笔记和图片数据都存在手机里,因为是自己用,所以不会有服务器,但是自己辛辛苦苦记录的数据还是需要有一份保险,那就需要一个数据的本地备份和还原功能。

需求

整理需求如下:

  • 可以把所有的笔记数据(包含图片等附件)都备份到手机sd卡上。
  • 可以将备份的数据进加密。
  • 其他的手机通过压缩包把数据进行还原,数据格式不能变。

由于使用的是 Jetpact Room 数据库,其实就是 Room 数据库的备份和还原,结合查看了网上的资料,总结出2种方案。

存储路径

在最新的 Android 13 上,app 的私有存储路径如下:

  1. /data/data/com.ldlywt.note/
  2. /sdcard/Android/data/com.ldlywt.note/

打开 Android Studio 查看对应的目录结构。

针对 /data/data/com.ldlywt.note/

这个目录是app的私有目录,不在sd卡上,使用手机文件管理器是查看不到的

从上图可知,room数据库在 database 文件夹目录下,图片和附件之类的存在 files 目录下,其他的可以不需要管。

针对 /sdcard/Android/data/com.ldlywt.note/

这个路径是sd卡的根目录,使用手机上的文件管理器可以查看到

第一个目录是放一些缓存,图片资源放在第二个 files 文件夹下面。

Json 导出

由于kotin的强大,将room数据库里面的内容导出为Json非常的容易。

kotlin 复制代码
fun exportJson(context: Context, uri: Uri): Result<Unit> {
    val json = Json.encodeToString(tagNoteRepo.queryAllNoteShowBeanList().toSet())
    return runCatching {
        BufferedOutputStream(context.contentResolver.openOutputStream(uri)).use { out: BufferedOutputStream ->
            out.write(json.toByteArray())
        }
    }
}

如上所示,几行代码就可以把 Room 数据库的数据转换成 Json,然后输出到sd卡上。

导出格式如下:

处理 Json 数据就很容易了,但是有一个问题,attachments附件字段里面的path是绝对路径,换了手机后这里肯定不能写死,所以大量的图片数据要单独处理才行。

第一种方案

第一种方案的思路如下:

  • 将 Room 数据库里面的数据导出为 Json
  • 图片数据单独特殊处理
  • 将 Json 数据和图片打包成zip压缩包

图片单独处理

每一个图片的名字是唯一的,那是不是把 attachments 里面的 path 直接替换成 filename 文件名字就行了,然后恢复的时候再将 filename 转变成 手机上的path 路径即可。

这是转换后的 json 数据

这里的具体代码有点多,见国外大佬的 GitHub:

github.com/quillpad/qu...

但是我在实际使用中,这种方案存在一个问题:会导致room数据关系链的断裂。

因为我的app的表结构是:一条笔记Note对应多个Tag标签,一个Tag也会对应多条笔记,是多对多的关系。

kotlin 复制代码
@Serializable
@Parcelize
@Entity(
    primaryKeys = ["note_id", "tag"],
    foreignKeys = [ForeignKey(
        entity = Note::class,
        parentColumns = arrayOf("note_id"),
        childColumns = arrayOf("note_id"),
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.CASCADE,
    ), ForeignKey(
        entity = Tag::class,
        parentColumns = arrayOf("tag"),
        childColumns = arrayOf("tag"),
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.CASCADE
    )]
)
data class NoteTagCrossRef(
    @ColumnInfo(name = "note_id") val noteId: Long, @ColumnInfo(index = true) val tag: String
) : Parcelable

上面方法将数据恢复到另外的手机后,会导致 Note 和 Tag 对应不上。

第二种方案

第二种方案我叫它整体搬迁方案,就是把需要的目录都整体打包成一个压缩包,还原时,再把以前app对应的目录删了,替换成备份的目录,然后重新启动app。

我的app需要的备份的是上面的三个目录。

将上面三个目录转变成list数据对象:

kotlin 复制代码
suspend fun export(context: Context, uri: Uri): String = suspendCoroutine { continuation ->
    context.contentResolver.openOutputStream(uri)?.use { stream ->
        val out = ZipOutputStream(stream)
        try {
            val files = listOf(
                ExportItem("/", File(context.dataDir.path + "/databases")),
                ExportItem("/", context.filesDir),
                ExportItem("/external/", context.getExternalFilesDir(null)!!)
            )
            for (i in files.indices) {
                val item = files[i]
                appendFile(out, item.dir, item.file)
            }
            context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
                if (cursor.moveToFirst()) {
                    val fileName = cursor.getStringValue(OpenableColumns.DISPLAY_NAME)
                    continuation.resume(fileName)
                }
            }
        } catch (e: Exception) {
            continuation.resumeWithException(e)
        } finally {
            IOUtils.closeQuietly(out)
        }
    }
}

然后将其打包成zip压缩包到手机sdk上

kotlin 复制代码
private fun appendFile(out: ZipOutputStream, dir: String, file: File) {
    if (file.isDirectory) {
        val files = file.listFiles() ?: return
        for (childFile in files) {
            appendFile(out, "$dir${file.name}/", childFile)
        }
    } else {
        val entry = ZipEntry("$dir${file.name}")
        entry.size = file.length()
        entry.time = file.lastModified()
        out.putNextEntry(entry)
        FileInputStream(file).use { input ->
            input.copyTo(out)
        }
        out.closeEntry()
    }
}

导出的备份文件解压如下:

我现在用的就是这种方案,这种方案相对于第一种方案的对比如下:

  • 代码量更少,实现简单
  • 不要要转换path,容错率高
  • 整体的替换,不能保存以前的数据

其实 Room 数据库的备份其实挺简单的,但是网上的资料东一块西一块,这里做个简单的总结。

Github

由于我写的碎碎记 App 已经被我上传到 Google Play,这里附上我开源练手的 Compose 项目地址:

github.com/ldlywt/Ligh...

不熟悉Compose代码也没关系,直接看备份的代码:BackUp.kt

后面会慢慢补上Room 数据库还原的代码~

相关推荐
web1350858863515 分钟前
前端node.js
前端·node.js·vim
m0_5127446416 分钟前
极客大挑战2024-web-wp(详细)
android·前端
潜意识起点40 分钟前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛44 分钟前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿1 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@3 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
F-2H7 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss7 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件