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 数据库还原的代码~

相关推荐
qq_390161779 分钟前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test39 分钟前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫1 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.1 小时前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家2 小时前
Vue 计算属性和监听器
前端·javascript·vue.js