Android P - "SQLite: нет такой ошибки таблицы" после копирования базы данных из активов

У меня есть база данных, сохраненная в папке с ресурсами приложений, и я копирую базу данных, используя приведенный ниже код, когда приложение открывается.

inputStream = mContext.getAssets().open(Utils.getDatabaseName());

        if(inputStream != null) {

            int mFileLength = inputStream.available();

            String filePath = mContext.getDatabasePath(Utils.getDatabaseName()).getAbsolutePath();

            // Save the downloaded file
            output = new FileOutputStream(filePath);

            byte data[] = new byte[1024];
            long total = 0;
            int count;
            while ((count = inputStream.read(data)) != -1) {
                total += count;
                if(mFileLength != -1) {
                    // Publish the progress
                    publishProgress((int) (total * 100 / mFileLength));
                }
                output.write(data, 0, count);
            }
            return true;
        }

Вышеприведенный код работает без проблем, но когда вы пытаетесь запросить базу данных, вы получаете SQLite: нет такого исключения таблицы.

Эта проблема возникает только в Android P, все предыдущие версии Android работают правильно.

Это известная проблема с Android P или что-то изменилось?

Ответ 1

Эта проблема, как представляется, приводит к сбою гораздо чаще на Android P, чем на предыдущих версиях, но это не ошибка на самом Android P.

Проблема в том, что ваша строка, в которой вы назначаете значение своей String filePath открывает соединение с базой данных, которая остается открытой при копировании файла из активов.

Чтобы устранить проблему, замените строку

String filePath = mContext.getDatabasePath(Utils.getDatabaseName()).getAbsolutePath();

с кодом, чтобы получить значение пути к файлу, а затем закрыть базу данных:

MySQLiteOpenHelper helper = new MySQLiteOpenHelper();
SQLiteDatabase database = helper.getReadableDatabase();
String filePath = database.getPath();
database.close();

А также добавьте внутренний вспомогательный класс:

class MySQLiteOpenHelper extends SQLiteOpenHelper {

    MySQLiteOpenHelper(Context context, String databaseName) {
        super(context, databaseName, null, 2);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

Ответ 2

Имел аналогичную проблему, и решил это добавить это к моему SQLiteOpenHelper

    @Override
    public void onOpen(SQLiteDatabase db) {
        super.onOpen(db);
        db.disableWriteAheadLogging();
    }

По-видимому, Android P задает значение PRAGMA Log. По-прежнему нет идеи, если будут иметь побочные эффекты, но, похоже, работает!

Ответ 3

Мои проблемы с Android P были решены путем добавления this.close() после this.getReadableDatabase() в методе createDataBase(), как показано ниже.

private void createDataBase() throws IOException {
    this.getReadableDatabase();
    this.close(); 
    try {           
        copyDataBase();            
    } catch (IOException e) {           
        throw new RuntimeException(e);
    }
}

Ответ 4

Я столкнулся с подобной проблемой. Я копировал базу данных, но не из актива. Я обнаружил, что проблема не имеет ничего общего с моим кодом копирования файлов базы данных. Также это не связано с открытыми, не закрытыми, файлами или синхронизацией файлов. Мой код обычно перезаписывает существующую нераскрытую базу данных. То, что кажется новым/отличным от Android Pie и отличается от предыдущих выпусков Android, заключается в том, что, когда Android Pie создает базу данных SQLite, по умолчанию он устанавливает журнал_валюта в WAL (запись на запись). Я никогда не пользовался режимом WAL, и в документах SQLite указано, что journal_mode должен быть DELETE по умолчанию. Проблема заключается в том, что если я перезаписываю существующий файл базы данных, позвольте ему называть его my.db, журнал записи на запись, my.db-wal, все еще существует и эффективно "переопределяет" то, что было в недавно скопированном файле my.db. Когда я открыл свою базу данных, таблица sqlite_master обычно содержала только строку для android_metadata. Все таблицы, которые я ожидал, отсутствовали. Мое решение состоит в том, чтобы просто установить journal_mode обратно в DELETE после открытия базы данных, особенно при создании новой базы данных с Android Pie.

PRAGMA journal_mode = DELETE;

Возможно, WAL лучше, и, возможно, каким-то образом закрыть базу данных, чтобы журнал записи не мешал, но мне не нужен WAL и он не нужен для всех предыдущих версий Android.

Ответ 5

Решение без отключения WAL

В Android 9 представлен специальный режим SQLiteDatabase, называемый Compatibility WAL (loggin с опережением записи), который позволяет базе данных использовать "journal_mode = WAL", сохраняя при этом поведение поддержания не более одного соединения на базу данных.

Подробно здесь:
https://source.android.com/devices/tech/perf/compatibility-wal

Режим SQLite WAL подробно объясняется здесь:
https://www.sqlite.org/wal.html

Что касается официальных документов, режим WAL добавляет второй файл базы данных с именем databasename и "-wal". Поэтому, если ваша база данных называется "data.db", она называется "data -wal.db" в том же каталоге.

Решение теперь состоит в том, чтобы сохранять и восстанавливать ОБА файлы (data.db и data -wal.db) на Android 9.

После этого он работает как в предыдущих версиях.

Ответ 6

К сожалению, принятый ответ просто "срабатывает" в очень конкретных случаях, но он не дает последовательно работающего совета, чтобы избежать такой ошибки в Android 9.

Вот:

  1. В вашем приложении должен быть один экземпляр класса SQLiteOpenHelper для доступа к вашей базе данных.
  2. Если вам нужно переписать/скопировать базу данных, закройте базу данных (и закройте все соединения с этой базой данных), используя метод SQLiteOpenHelper.close() этого экземпляра, и больше не используйте этот экземпляр SQLiteOpenHelper.

После вызова close() не только все соединения с базой данных закрываются, но и дополнительные файлы журнала базы данных сбрасываются в основной файл .sqlite и удаляются. Таким образом, у вас есть только один файл database.sqlite, готовый к перезаписи или копированию.

  1. После копирования/перезаписи и т.д. Создайте новый синглтон SQLiteOpenHelper, метод getWritableDatabase() вернет новый экземпляр базы данных SQLite! И используйте его до следующего раза, когда вам понадобится скопировать/переписать вашу базу данных...

Этот ответ помог мне понять это: fooobar.com/questions/67446/...

У меня была эта проблема в Android 9 в моем приложении AndStatus https://github.com/andstatus/andstatus, которое имеет довольно большой набор автоматических тестов, которые последовательно воспроизводили "SQLiteException: нет такой таблицы" в эмуляторе Android 9 перед этим коммитом: https://github.com/andstatus/andstatus/commit/1e3ca0eee8c9fbb8f6326b72dc4c393143a70538 Так что если вам действительно любопытно, вы можете запустить все тесты до и после этого коммита, чтобы увидеть разницу.

Ответ 7

Во-первых, спасибо за сообщение этого вопроса. У меня было то же самое. Все работало хорошо, но затем, когда тестировалось приложение Android P Preview, я получал сбои. Вот ошибка, которую я нашел для этого кода:

private void copyDatabase(File dbFile, String db_name) throws IOException{
    InputStream is = null;
    OutputStream os = null;

    SQLiteDatabase db = context.openOrCreateDatabase(db_name, Context.MODE_PRIVATE, null);
    db.close();
    try {
        is = context.getAssets().open(db_name);
        os = new FileOutputStream(dbFile);

        byte[] buffer = new byte[1024];
        while (is.read(buffer) > 0) {
            os.write(buffer);
        }
    } catch (IOException e) {
        e.printStackTrace();
        throw(e);
    } finally {
        try {
            if (os != null) os.close();
            if (is != null) is.close();

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

Проблема, с которой я столкнулся, заключалась в том, что этот код работает очень хорошо, но в SDK 28+ openOrCreateDatabase больше не будет автоматически создавать таблицу android_metadata для вас. Поэтому, если вы выполните запрос "select * from TABLE", он не найдет ТАБЛИЦУ, потому что запрос начинает искать "первую" таблицу, которая должна быть таблицей метаданных. Я исправил это, вручную добавив таблицу android_metadata, и все было хорошо. Надеюсь, что кто-то найдет это полезным. Это потребовалось навсегда, чтобы понять, потому что конкретные запросы все еще работали нормально.

Ответ 8

Аналогичная проблема затронула только устройство Android P. У всех предыдущих версий проблем нет.

Отключено автоматическое восстановление на устройствах Android 9.

Мы сделали это для устранения неполадок. Не рекомендовал бы для производственных дел.

Автоматическое восстановление размещало копию файла базы данных в каталоге данных до вызова функции базы данных копирования в помощнике базы данных. Поэтому файл file.exists() возвратил true.

База данных, которая была скопирована с устройства разработки, отсутствовала в таблице. Поэтому "таблица не найдена" на самом деле была правильной.

Ответ 9

Кажется, что вы не закрываете выходной поток. Хотя он, вероятно, не объясняет, почему db на самом деле не создан (если Android P не добавил буфер с несколькими мегабайтами), рекомендуется использовать try-with-resource, например:

// garantees that the data are flushed and the resources freed
try (FileOutputStream output = new FileOutputStream(filePath)) {
    byte data[] = new byte[1024];
    long total = 0;
    int count;
    while ((count = inputStream.read(data)) != -1) {
        total += count;
        if (mFileLength != -1) {
            // Publish the progress
            publishProgress((int) (total * 100 / mFileLength));
        }
        output.write(data, 0, count);
    }

    // maybe a bit overkill
    output.getFD().sync();
}

Ответ 10

Самый простой ответ - использовать следующую строку для пути к файлу базы данных в Android PIE и выше:

DB_NAME="xyz.db";
DB_Path = "/data/data/" + BuildConfig.APPLICATION_ID + "/databases/"+DB_NAME;

Ответ 11

Вот идеальное решение этой проблемы:

Просто переопределите этот метод в вашем классе SQLiteOpenHelper:

@Override
public void onOpen(SQLiteDatabase db) {
    super.onOpen(db);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        db.disableWriteAheadLogging();
    }
}

Ответ 12

Я не могу комментировать принятый ответ, поэтому я должен открыть новый ответ.

mContext.getDatabasePath() НЕ открывает соединение с базой данных, ему даже не требуется существующее имя файла для успешного выполнения (см. sources/android-28/android/app/ContextImpl.java):

@Override
public File getDatabasePath(String name) {
    File dir;
    File f;

    if (name.charAt(0) == File.separatorChar) {
        // snip
    } else {
        dir = getDatabasesDir();
        f = makeFilename(dir, name);
    }

    return f;
}

private File makeFilename(File base, String name) {
    if (name.indexOf(File.separatorChar) < 0) {
        return new File(base, name);
    }
    throw new IllegalArgumentException(
            "File " + name + " contains a path separator");
}

Ответ 13

В версии P основным изменением является WAL (запись в журнал записи). Требуются следующие два шага.

  1. Отключите то же самое с помощью следующей строки в config.xml в папке значений в разделе ресурсов.

ложные

  1. Сделайте следующее изменение в классе DBAdapter в методе createDatabase. В противном случае происходит сбой телефонов с более ранними версиями Android.

    private void createDataBase() выбрасывает IOException { if (android.os.Build.VERSION.SDK_INT & lt; android.os.Build.VERSION_CODES.P)               this.getWritableDatabase();

    try {           
        copyDataBase();            
    } catch (IOException e) {           
        throw new RuntimeException(e);
    }
    

    }