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卡上,使用手机文件管理器是查看不到的

image.png

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

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

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

image.png

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

Json 导出

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

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 路径即可。

image.png

这是转换后的 json 数据

image.png

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

github.com/quillpad/qu…

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

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

@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。

image.png

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

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

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上。

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

作者:方之长
链接:https://juejin.cn/post/7280113511103135799
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值