SQLiteReadOnlyDatabaseException: 尝试写入只读数据库 (代码 1032)

16
在一些罕见的情况下,我看到了“尝试写只读数据库”的消息,但我无法确定问题出在哪里。我会从我的logcat堆栈跟踪开始...正如您可以从时间戳中看到的那样,我仅在尝试写入之前1毫秒检查db.isReadOnly()。(isOpen=true,readOnly=false)
01-29 13:47:49.115: D/AWT(11055): #479.Got writable database (230537815): isOpen: (true) isReadOnly: (false) inTransaction: (false)
01-29 13:47:49.116: D/AWT(11055): #479.in transaction: Got writable database (230537815): isOpen: (true) isReadOnly: (false) inTransaction: (true)
01-29 13:47:49.116: E/SQLiteLog(11055): (1032) statement aborts at 15: [INSERT INTO Events(col1,col2,col3,col4) VALUES (?,?,?,?)] 
01-29 13:47:49.117: E/SQLiteDatabase(11055): Error inserting data="scrubbed"
01-29 13:47:49.117: E/SQLiteDatabase(11055): android.database.sqlite.SQLiteReadOnlyDatabaseException: attempt to write a readonly database (code 1032)
01-29 13:47:49.117: E/SQLiteDatabase(11055):    at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
01-29 13:47:49.117: E/SQLiteDatabase(11055):    at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:780)
01-29 13:47:49.117: E/SQLiteDatabase(11055):    at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
01-29 13:47:49.117: E/SQLiteDatabase(11055):    at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:86)
01-29 13:47:49.117: E/SQLiteDatabase(11055):    at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1471)
01-29 13:47:49.117: E/SQLiteDatabase(11055):    at android.database.sqlite.SQLiteDatabase.insert(SQLiteDatabase.java:1341)
01-29 13:47:49.117: E/SQLiteDatabase(11055):    at com.company.DbHelper.insertBatch(EventsDbHelper.java:174)
01-29 13:47:49.117: D/AWT(11055): #479.finalizing transaction: Got writable database (230537815): isOpen: (true) isReadOnly: (false) inTransaction: (true)
01-29 13:47:49.118: W/SQLiteLog(12120): (28) file unlinked while open: /data/user/0/com.company.app/databases/MyDatabase.db

根据我的消息来源:

public void insertBatch(LinkedList<WriteQueue.DatabaseRecord> writeQueue) throws Exception {
    Log.d("AWT", "EventsDbHelper->insertBatch()");

    if (writeQueue == null) {
        return;
    }

    Iterator<DatabaseRecord> it = writeQueue.iterator();

    SQLiteDatabase db = this.getWritableDatabase();

    Log.d("AWT", String.format("Got writable database (%s): isOpen: (%s) isReadOnly: (%s) inTransaction: (%s)",
            db.hashCode(), db.isOpen(), db.isReadOnly(), db.inTransaction()));

    try {
        db.beginTransaction();

        while (it.hasNext()) {
            DatabaseRecord record = it.next();

            ContentValues initialValues = new ContentValues();
            initialValues.put(col1, val1);
            initialValues.put(col2, val2);
            initialValues.put(col3, val3);
            initialValues.put(col4, val4);

            Log.d("AWT", String.format("in transaction: Got writable database (%s): isOpen: (%s) isReadOnly: (%s) inTransaction: (%s)",
                    db.hashCode(), db.isOpen(), db.isReadOnly(), db.inTransaction()));

            db.insert(DBTBL, null, initialValues);
        }
        Log.d("AWT", String.format("finalizing transaction: Got writable database (%s): isOpen: (%s) isReadOnly: (%s) inTransaction: (%s)",
                db.hashCode(), db.isOpen(), db.isReadOnly(), db.inTransaction()));
        db.setTransactionSuccessful();

    } catch (Exception e) {
        Log.e(TAG, "Error inserting batch record into database.", e);
    } finally {
        try {
            db.endTransaction();
            db.close();
        } catch (Exception e) {
            Log.e(TAG, Global.DB_ERROR, e);
        }
    }
}

所以我认为可能是以下两种情况之一正在发生:

  1. 在检查和尝试批量插入之间的1ms内,DB确实被关闭/设置为“只读”。
  2. isReadOnly欺骗了我,并没有准确报告数据库的状态。
  3. 数据库在我的插入过程中被删除!请查看上面日志的最后一行。我打开了SQLite的严格日志记录,并注意到了上述情况。我怀疑第三方库可能会删除我的所有数据库。

到这一步为止我已经没有想法了,但我愿意尝试任何建议。


在批量插入完成后,关闭操作在dbHelper类的其他位置完成。我也将所有的关闭调用都删除了,但这并没有对这种行为产生任何影响。 - AWT
也添加了这个(请参见更新后的代码和日志),但没有任何变化。 - AWT
我会尝试创建一个完全干净的项目,只提供足够的支持来插入数据库。这将有助于隔离问题。我还会尝试设置一个标志,以便调用仅允许一次调用。您可能会在第一个事件完成之前启动第二个事件。 - Keith John Hutchison
我也遇到了类似的异常情况,而且陷入了困境。你找到了任何解决方案或者是找到了原因吗? - Srushti
4个回答

2

我也遇到了几乎完全相同的问题,我在该问题上找到了一个开放的缺陷,这很有意义...

https://code.google.com/p/android/issues/detail?id=174566

我的解决方法 - 虽然不是最好的解决方案 - 是永远不要升级数据库版本并自己跟踪这个版本,从而永远不会调用onUpgrade(),并在更新应用程序时手动进行升级。

或者,如果您有一个小的只读数据库,则可以在DBHelper类的每个onCreate()中触发从资产中复制数据库,但如果文件系统已满,则可能会出现不必要的问题,因此只有在寻找更好的解决方案时才使用此方法。

@Override
public void onCreate(SQLiteDatabase db) {
    // Workaround for Issue 174566
    myContext.deleteDatabase(DB_NAME);
    try {
        copyDataBase();
    }
    catch(IOException e) {
        System.out.println("IOException " + e.getLocalizedMessage());
    }
}

我的应用现在通过我的解决方法升级了,根据最初提出此缺陷的时间长短来判断,它可能永远不会被修复...

很抱歉这不是问题的完整解决方案,但至少是一个前进的方式。


这并没有提供问题的答案。如果您要对作者进行批评或请求澄清,请在其文章下留言-您始终可以在自己的文章上发表评论,一旦您获得足够的声望,您将能够评论任何文章。如果您有一个相关但不同的问题,请提出一个新问题,并引用这个问题(如果它有助于提供背景信息)。 - Bhargav Rao
我更新了我的答案,以便为原始问题提供一些解答。我同意这本来可以作为评论,但现在至少提供了解决此问题的方法。 - Jens Andree
嗨Jens,我很高兴这对你起作用了,但我的问题并不仅发生在onUpgrade()期间。它发生在一批写入的中间,根本没有调用onUpgrade()。 根本原因是该文件被...某个东西删除了。还不确定是什么导致了这个问题。我的解决方法是在每次批量插入之前调用getWriteableDatabase()。这不是最理想的解决方案 - 只是一个hacky的解决方法,直到我弄清楚是什么导致了这个问题。 - AWT

1
所以,乍一看,这个问题的根本原因似乎是一个第三方库。除非我错了,Mobeix的Tagit会在应用程序启动时删除数据库。我添加了一些详细的SQLite日志记录,包括以下策略:
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
    .detectLeakedSqlLiteObjects()
    .detectLeakedClosableObjects()
    .penaltyLog()
    .penaltyDeath()
    .build());

我注意到日志中显示,在我创建并打开数据库之后,它被取消链接。更详细的日志显示,这发生在初始化Mobeix库时。问题出在以下代码行:

01-29 13:47:49.118: W/SQLiteLog(12120): (28) file unlinked while open: /data/user/0/com.company.app/databases/MyDatabase.db

所以我的数据库文件已经解除链接了。奇怪。下一次调用getWritableDatabase()会重新创建它,然后一直正常,直到应用程序被杀死并重新启动,在这种情况下,它将被删除并重新创建。

如果我能够确切地找出是什么导致了取消链接,我会更新这个信息。


我也有同样的问题,你找到任何解决方案了吗? - Pooya
不,我无法防止某个东西删除我的数据库,而且这只发生在我的应用程序第一次启动时。因此,我不得不添加一堆检查来确保它存在、打开和可写。 - AWT
我从未找到删除我的数据库的答案。我所能做的最好的事情就是确保每次在使用它之前都存在,并根据情况作出反应。 - AWT

1
我遇到了类似的问题。然而,作为恢复过程的一部分,我有意删除了当前数据库。
我认为发生的情况是SQLite将数据库标记为只读,以防止"文件在打开时被取消链接"的情况发生。
在恢复后,任何更新都会失败,并显示"尝试写入只读数据库(代码1032)"。
我的解决方案是重新实例化DBHelper。我通过添加一个"reopen"方法来实现这个目的,并在其中调用该方法。
例如:
    public static void reopen(Context context) {
        instance = new DBHelper(context);
    }

我将其翻译为中文:"

然后我使用以下代码进行调用/调用:

"
                if(copytaken && origdeleted && restoredone) {
                    DBHelper.reopen(context);
                    DBHelper.getHelper(context).expand(null,true);
                }

调用expand方法是我对onUpgrade/versions的等效/变通方式。它根据伪模式与实际数据库进行比较,添加表格和列。

完整的DBHelper是:

/**
 * DBHelper
 */
@SuppressWarnings("WeakerAccess")
class DBHelper extends SQLiteOpenHelper {

    private static final String LOGTAG = "SW-DBHelper";
    private static final String DBNAME = DBConstants.DATABASE_NAME;
    private static final String dbcreated =
            "001I Database " + DBNAME + " created.";
    private static final String dbunusable =
            "002E Database " + DBNAME +
            " has been set as unusable (according to schema).";
    private   static final String dbexpanded =
            "003I Database " + DBNAME + " expanded.";
    private static final String dbexpandskipped =
            "004I Database " + DBNAME + " expand skipped - nothing to alter.";
    private static final String dbbuildskipped =
            "005I Database" + DBNAME + " build skipped - no tables to add";
    public static final String THISCLASS = DBHelper.class.getSimpleName();

    /**
     * Consrtuctor
     *
     * @param context activity context
     * @param name    database name
     * @param factory cursorfactory
     * @param version database version
     */
    DBHelper(Context context, @SuppressWarnings("SameParameterValue") String name, @SuppressWarnings("SameParameterValue") SQLiteDatabase.CursorFactory factory, @SuppressWarnings("SameParameterValue") int version) {
        super(context, name, factory, version);
    }

    /**
     * Instantiates a new Db helper.
     *
     * @param context the context
     */
    DBHelper(Context context) {
        super(context, DBConstants.DATABASE_NAME, null, 1);
    }

    private static DBHelper instance;

    /**
     * Gets helper.
     *
     * @param context the context
     * @return the helper
     */
    static synchronized DBHelper getHelper(Context context) {
        if(instance == null) {
            instance = new DBHelper(context);
        }
        return instance;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        expand(db, false);
    }


    @Override
    public void onUpgrade(SQLiteDatabase db, int oldversion, int newversion) {

    }

    /**
     * expand create database tables
     *
     * @param db             SQLIte Database, if null then instance is used
     * @param buildandexpand to attempt both create and expand
     */
    void expand(SQLiteDatabase db, boolean buildandexpand) {

        String mode = "Create Mode.";
        if (buildandexpand) {
            mode = "Expand Mode.";
        }
        String msg = mode;
        String methodname = new Object(){}.getClass().getEnclosingMethod().getName();
        LogMsg.LogMsg(LogMsg.LOGTYPE_INFORMATIONAL,LOGTAG,msg,THISCLASS,methodname);
        // if no database has been passed then get the database
        if(db == null) {
            db = instance.getWritableDatabase();
        }
        // Build Tables to reflect schema (SHOPWISE) only if schema is usable
        if(DBConstants.SHOPWISE.isDBDatabaseUsable()) {
            // Check to see if any tables need to be added
            ArrayList<String> buildsql = DBConstants.SHOPWISE.generateDBBuildSQL(db);
            if (!buildsql.isEmpty()) {
                DBConstants.SHOPWISE.actionDBBuildSQL(db);
                msg = dbcreated + buildsql.size() + " tables added.";
                LogMsg.LogMsg(LogMsg.LOGTYPE_INFORMATIONAL,LOGTAG,msg,THISCLASS,methodname);
            } else {
                msg = dbbuildskipped;
                LogMsg.LogMsg(LogMsg.LOGTYPE_INFORMATIONAL,LOGTAG,msg,THISCLASS,methodname);
            }
            if(buildandexpand) {
                ArrayList<String> altersql = DBConstants.SHOPWISE.generateDBAlterSQL(db);
                if(!altersql.isEmpty()) {
                    msg = dbexpanded + altersql.size() + " columns added.";
                    LogMsg.LogMsg(LogMsg.LOGTYPE_INFORMATIONAL,LOGTAG,msg,THISCLASS,methodname);
                    DBConstants.SHOPWISE.actionDBAlterSQL(db);
                }  else {
                    msg = dbexpandskipped;
                    LogMsg.LogMsg(LogMsg.LOGTYPE_INFORMATIONAL,LOGTAG,msg,THISCLASS,methodname);
                }
            }
        } else {
            msg = dbunusable + "\n" +
                    DBConstants.SHOPWISE.getAllDBDatabaseProblemMsgs();
            LogMsg.LogMsg(LogMsg.LOGTYPE_ERROR,LOGTAG,msg,THISCLASS,methodname);
        }
    }
    public static void reopen(Context context) {
        instance = new DBHelper(context);
    }
}

嗨,Mike,感谢提供详细信息。在我的情况下,整个数据库文件都被删除了。我追踪到这一步,但没有进一步探究。我怀疑某个第三方库在初始化过程中删除了我的数据库(所有数据库!)。我也使用了一个databasehelper,并调用getWritableDatabase()确实重新创建了我的数据库。我只是困惑为什么我的数据库首先会被删除。 - AWT

0

我遇到了类似的问题,非常烦人的是它随时都会发生,很难复制出导致它发生的确切条件。我只在MainActivity类的ondestroy方法中关闭DDBB。我所做的是在每次使用db时添加try/catch,并添加以下catch,在这种情况下它位于while循环的中间,在其他函数中我再次调用该函数:

catch (SQLException e) {
    e.printStackTrace();
    Log.d(TAG, "MainService: error in AccSaveToDB with "+mainDB.getPath()+" in iteration "+j+". Closing and re-opening DB");
    DBHelper.close();
    mainDB.close();
    j--;
}

每个访问数据库的函数开头都要这样写:
if (mainDB==null || !mainDB.isOpen()) {
    DBHelper = DefSQLiteHelper.getInstance(getApplicationContext(), "Data.db", null, 1);
    mainDB = DBHelper.getWritableDatabase();
}

到目前为止,我仍然遇到了一些错误,但是我还没有找出原因,不过至少我的应用程序没有崩溃,并且可以恢复它该做的事情。我无法确定文件是否已被删除,但这个解决方案对我有效。


请保持您的回答简洁。 - Eugen Pechanec

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接