开发者社区> 长风呼啸> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

SQLite数据库修复方案(For Android App)

简介: 一、前言 SQLite性能好,对SQL支持全面,是久经考验的轻量的关系型数据库。移动开发者对SQLite应该都不陌生了,只是不同的 APP 对数据库的依赖程度不同(有的甚至不需要数据库-_-)。SQLite虽然是可靠性较高的数据库,但是在复杂的使用场景之下,也会不时地出点问题。
+关注继续查看

一、前言

SQLite性能好,对SQL支持全面,是久经考验的轻量的关系型数据库。
移动开发者对SQLite应该都不陌生了,只是不同的 APP 对数据库的依赖程度不同(有的甚至不需要数据库-_-)。
SQLite虽然是可靠性较高的数据库,但是在复杂的使用场景之下,也会不时地出点问题。
比如说有时候索引损坏,select count() from t_XXX 查询出的结果和select from t_XXX取出得记录数不一样;
有时候甚至存储的记录违反唯一约束,非空约束等等。
一旦出现这些问题,可能会引起数据不正确,或者功能异常。
为了尽量降低数据库不完整所引发的问题,我们需要有一套修复机制。

二、数据修复

一个简单的策略就是:检测-读取-写入-替换。
具体地说,就是先检测数据的完整性,当检测到数据库文件不完整时做修复。
SQLite提供了检测的API,但是没有提供直接修复的API,或许是因为错误的原因有很多,做纠错太困难了吧。
通常大家的做法就是转储数据到一个好的数据库文件中,再替换回去,就好比整理衣物,要先把衣服叠整齐,再放回衣柜。
有一些文章在写转储数据时, 会写dump sql, 然后执行sql,这也是转储的方式之一,但是有更高效的方式。

2.1 检测

SQLite提供了检测数据库完整性的API:

PRAGMA integrity_check

正常情况下执行此语句返回'ok', 而在当数据库不完整时(比如上面描述的一些情景),返回其他结果。

2.2 读取数据表

SQLite有一张内置表, sqlite_master, 此表中存储着数据库中所有表的相关信息,比如表的名称、索引、以及建表SQL等。
我们可以从中读取所有我们创建的表的名称:

    private static List<String> getTables(SQLiteDatabase desDb) {
        String sql = "SELECT name FROM sqlite_master " +
                "WHERE type='table' AND name!='android_metadata'";
        Cursor c = desDb.rawQuery(sql, null);
        try {
            List<String> tables = new ArrayList<>(c.getCount());
            while (c.moveToNext()) {
                tables.add(c.getString(0));
            }
            return tables;
        } finally {
            closeCursor(c);
        }
    }

2.3 读取数据

要读取数据,先要考虑读取出来之后,用什么方式存储。
先定一个数据结构:

public class TableData {
    public int row;
    public int column;
    public Object[] data;
}

然后,读取一张表的所有数据:

private static TableData getData(SQLiteDatabase srcDb, String sql) {
        Cursor c = srcDb.rawQuery(sql, null);
        try {
            int rawCount = c.getCount();
            if (rawCount <= 0) {
                return null;
            }
            int columnCount = c.getColumnCount();
            TableData tableData = new TableData();
            tableData.row = rawCount;
            tableData.column = columnCount;
            tableData.data = new Object[rawCount * columnCount];

            int row = 0;
            if (c instanceof AbstractWindowedCursor) {
                final AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) c;
                while (windowedCursor.moveToNext()) {
                    for (int i = 0; i < columnCount; i++) {
                        int index = row * columnCount + i;
                        if (windowedCursor.isBlob(i)) {
                            tableData.data[index] = windowedCursor.getBlob(i);
                        } else if (windowedCursor.isFloat(i)) {
                            tableData.data[index] = windowedCursor.getDouble(i);
                        } else if (windowedCursor.isLong(i)) {
                            tableData.data[index] = windowedCursor.getLong(i);
                        } else if (windowedCursor.isNull(i)) {
                            tableData.data[index] = null;
                        } else if (windowedCursor.isString(i)) {
                            tableData.data[index] = windowedCursor.getString(i);
                        } else {
                            tableData.data[index] = windowedCursor.getString(i);
                        }
                    }
                    row++;
                }
            } else {
                while (c.moveToNext()) {
                    for (int i = 0; i < columnCount; i++) {
                        int index = row * columnCount + i;
                        tableData.data[index] = c.getString(i);
                    }
                    row++;
                }
            }

            return tableData;
        } finally {
            closeCursor(c);
        }
    }

这里有一个疑问就是,为什么不读一行写一行?
也是可以的,但是那样的话会有两个坏处:
1、方法职能不单一,可读性低;
2、内存抖动。众所周知,连续读写的IO性能比随机读写要好。

但是读取全表再批量写入也有一个弊端:
如果一张表数据很大,可能会OOM。
当然,如果数据量比较大,我们可以采用分页的方式。

2.4 写入数据

  private static void insertToDb(SQLiteDatabase desDb, 
                                   String sql, 
                                   Object[] values, 
                                   int rows, 
                                   int columns) {
        if (values == null || columns <= 0 || rows <= 0 || values.length < (rows * columns)) {
            return;
        }
        SQLiteStatement statement = desDb.compileStatement(sql);
        try {
            for (int i = 0; i < rows; i++) {
                bindValues(statement, values, i, columns);
                try {
                    statement.executeInsert();
                } catch (SQLiteConstraintException e) {
                    LogUtil.e(TAG, e);
                }
                statement.clearBindings();
            }
        } finally {
            IOUtil.closeQuietly(statement);
        }
    }

   public static void bindValues(SQLiteStatement statement, 
                                  Object[] values, 
                                  int row, 
                                  int columns) {
        for (int j = 0; j < columns; j++) {
            Object value = values[row * columns + j];
            int index = j + 1;
            if (value == null) {
                statement.bindNull(index);
            } else if (value instanceof String) {
                statement.bindString(index, (String) value);
            } else if (value instanceof Number) {
                if (value instanceof Double 
                        || value instanceof Float 
                        || value instanceof BigDecimal) {
                    statement.bindDouble(index, ((Number) value).doubleValue());
                } else {
                    statement.bindLong(index, ((Number) value).longValue());
                }
            } else if (value instanceof byte[]) {
                statement.bindBlob(index, (byte[]) value);
            } else {
                statement.bindString(index, value.toString());
            }
        }
    }

其实很多其他的数据库引擎也提供了参数绑定的API。
这样的方式的好处就是,只用编译一次SQL。
而用SDK的insert方法,则每插入一条记录都需要编译一遍SQL。

需要注意的事,在转储数据时要捕获SQLiteConstraintException,因为在当数据文件不完整时,有的记录可能已经不满足约束(唯一约束,非空约束等)了。

2.5 复制数据

接下来,只需组装前面的方法,逐张表进行复制。

    private static void copyTable(SQLiteDatabase srcDb, SQLiteDatabase desDb,
                                  String table, StringBuilder builder) {
        TableData tableData = getData(srcDb, "SELECT * FROM " + table);
        if (tableData != null) {
            builder.setLength(0);
            builder.append("INSERT INTO ").append(table).append(" VALUES(");
            for (int i = 0; i < tableData.column; i++) {
                builder.append("?,");
            }
            builder.setCharAt(builder.length() - 1, ')');
            insertToDb(desDb, builder.toString(), tableData.data, tableData.row, tableData.column);
        }
    }

    private static void copyDataToNewDb(SQLiteDatabase srcDb, SQLiteDatabase desDb) {
        srcDb.beginTransaction();
        try {
            List<String> tables = getTables(desDb);
            StringBuilder builder = new StringBuilder(128);
            for (String table : tables) {
                desDb.execSQL("DELETE FROM " + table);
                copyTable(srcDb, desDb, table, builder);
            }
        } finally {
            srcDb.endTransaction();
        }
    }

复制完成后,把新数据库文件替换旧数据库文件即可。

三、预防措施

以上是数据库损坏后的对应策略,不一定有效,比如说数据库是彻底损坏(数据无法读取)时。
我们可以从另外两个方面做预防:

  • 1、防止数据库损坏
    比如检查磁盘剩余空间,当剩余空间小于一定大小时提醒用户清理空间;

还有就是注意切勿多进程访问数据库:集成推送,定位等服务,这些服务通常会有自己的进程,
这时候需要小心Application的onCreate方法,因为所有进程都会回调该方法。

  • 2、做备份
    定时做备份,比如每天或者每两天做一次备份,在数据库彻底损坏时至少还可以恢复绝大部分数据。

四、后记

我们的APP重度依赖数据库,数据量不算特别大,但是数据表多,操作路径多,数据库损坏什么的时有发生,对业务影响颇深。
用户数据有问题,有的会反馈,有的可能就卸载APP了。
最初没意识到SQLite完整性的问题,碰到一些奇怪的数据现象,钻进茫茫的业务代码中去查原因,有时候能找到一些可能的原因,但是常常是铩羽而归,最终也只是用一些临时方案使得用户可以恢复使用,治标而不治本。
后来渐渐意识到解决数据库损坏的问题,出了系一列措施之后,此类问题迎刃而解。

五、下载

已上传Demo到github。
地址:https://github.com/No89757/DBTest

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
数据火器库 - 八卦系列之瑞士军刀: 随APP携带的SQLite
来源:云数据库技术,数据库打工仔喃喃自语的八卦历史,1. 为导弹巡洋舰设计,用在手机上的数据库,2. Small and Simple, and Better,3. 如何看出是自己的娃:产品定位,特点和边界
0 0
[UWP小白日记-1]判断APP是否是第一次运行初始化SQLITE数据库
原文:[UWP小白日记-1]判断APP是否是第一次运行初始化SQLITE数据库   利用应用程序设置来实现此功能。   1.首先,获取APP设置的容器: ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings;   由于获取容器的时候并没有给key为FirestStart的赋值,所以下面就直接判断是不是为null,为null就是第一次运行APP, 然后在else里给key为FirestStart的赋值,那么else的代码就只能执行一次了。
969 0
相亲app开发,性能优化技术并不是突发奇想
相亲app开发,性能优化技术并不是突发奇想
0 0
基于kotlin开发的验证码发送注册的app
”麻雀虽小五脏俱全”就是它了,设计后端即springboot的开发,app处理网络请求的开发,appUI界面的设计(虽然只有一步,但也还是吧),数据库查询相关,app搭建相关架构的实现等等,值得学习一波。
0 0
语音聊天app开发,不同场景下的存储读写方法
语音聊天app开发,不同场景下的存储读写方法
0 0
短视频app开发,实现高可用并不是说说而已
短视频app开发,实现高可用并不是说说而已
0 0
《云原生时代下的App开发》电子版地址
2021年12月,阿里云携10+技术专家亮相年度顶级云原生开源技术峰会 ,并带来阿里云云原生专场,不仅汇聚行业发展方向的精彩主题演讲,在云基础设施、可观察性等云原生与开源技术等各大专题中,从阿里云真实业务场景中 走出来的云原生技术最佳实践也向全球开发者一一呈现。
0 0
交易所开发成品丨交易所系统开发(演示版)丨交易所APP源码设计
An exchange is an information platform for trading certain information and goods. A fixed place is called an exchange. The exchange, with the help of information platform, realizes the sharing of property rights information, long-distance trading, unified coordination, and balance of property right
0 0
数字藏品开发丨数字藏品APP系统开发(逻辑及方案)丨数字藏品源码功能及分析
 Digital collections are digital works,works of art and commodities that use blockchain technology to identify the ownership of rights and interests.Digital collections can mark their owners in the blockchain network and trace their subsequent circulation,including but not limited to digital picture
0 0
+关注
长风呼啸
从事移动开发,喜欢思考,喜欢折腾,想做一些特别的,有特色的事情。 &ldquo;就是让这个世界因为有我而有了一点点的不一样&rdquo;
文章
问答
文章排行榜
最热
最新
相关电子书
更多
移动App持续交付之路
立即下载
移动App研发加速—跨平台解决方案
立即下载
云原生时代下的App开发
立即下载