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 или что-то изменилось?


person Michael J    schedule 22.05.2018    source источник
comment
вы можете проверить, не является ли ваш inputStream нулевым? используя отладчик Android?   -  person Deepak kaku    schedule 23.05.2018
comment
Я могу подтвердить, что InputStream не равен нулю.   -  person Michael J    schedule 23.05.2018
comment
Непосредственно перед return true добавить Log.d("COPYINFO","Bytes Copied = " + String.valueOf(totalcount) + " to " + filepath);, какой результат будет в Журнал?   -  person MikeT    schedule 23.05.2018
comment
Вывод на моем работающем устройстве под управлением Android 8.1: (D / COPYINFO: Bytes Copied = 1687552 to /data/user/0/am.radiogr/databases/s.db) Вывод на Android P: (D / COPYINFO : Bytes Copied = 1687552 в /data/user/0/am.radiogr/databases/s.db) Они точно такие же.   -  person Michael J    schedule 23.05.2018
comment
Мой следующий шаг, поскольку очевидно, что БД копируется, должен был проверить таблицы в базе данных. Либо запрашиваю таблицу sqlite_master, либо лично я использую здесь метод logDatabaseInfo [Существуют ли какие-либо методы, помогающие решить общие проблемы SQLite? ] (stackoverflow.com/questions/46642269/).   -  person MikeT    schedule 23.05.2018
comment
Используя logDatabaseInfo, я получаю следующее: D/SQLITE_CSU: DatabaseList Row 1 Name=main File=/data/user/0/my.packagename/databases/databasename.db Database Version = 1 D/SQLITE_CSU: Table Name = android_metadata Created Using = CREATE TABLE android_metadata (locale TEXT) D/SQLITE_CSU: Table = android_metadata ColumnName = locale ColumnType = TEXT Default Value = null PRIMARY KEY SEQUENCE = 0 Я полагаю, это показывает пустую базу данных на Android P. При тестировании на Android 8.1 регистрируются все таблицы, которые я ожидал увидеть.   -  person Michael J    schedule 23.05.2018
comment
@MichaelJ да, это пустая / необработанная база данных. К сожалению, у меня нет устройства / эмулятора 8.1 для тестирования. Есть еще один вопрос, очень похожий на ваш, и мне интересно, есть ли проблема. Я немного осмотрелся, но не нашел никаких проблем с 8.1.   -  person MikeT    schedule 24.05.2018
comment
Вы копируете свою базу данных в папку / data / data / ‹packagename› / database?   -  person Ashish John    schedule 31.05.2018
comment
Отключение ведения журнала с упреждающей записью, как указал Рамон ниже, сработало для меня. stackoverflow.com/a/51953955/1172181   -  person Luis    schedule 05.10.2018


Ответы (15)


Эта проблема, кажется, приводит к сбою гораздо чаще на 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) {
    }
}
person rmtheis    schedule 31.05.2018
comment
Спасибо за постоянную помощь. Я заменил код на ваш, чтобы получить filePath, но результат сохраняется. Пустая база данных создается и не заполняется базой данных в папке с ресурсами. Я должен добавить, что тестирую с помощью эмулятора под управлением Android P, а не реального устройства, это не должно мешать работе кода, как задумано, не так ли? - person Michael J; 01.06.2018
comment
Я могу воспроизвести проблему на эмуляторе, это не проблема. Возможно, вам стоит убедиться, что мгновенный запуск отключен, на всякий случай. В любом случае проблема решается для меня, когда я проверяю отсутствие открытых подключений к базе данных перед копированием файла из ресурсов. Это ключ. - person rmtheis; 01.06.2018
comment
Извините, вы были правы. Я упустил из виду, что раньше называл этот (довольно бессмысленный) код: openOrCreateDatabase(Utils.getDatabaseName(), MODE_PRIVATE, null); Это поддерживало открытое соединение с базой данных и вызывало проблему. С тех пор я удалил этот код, и все работает нормально. Спасибо за помощь! - person Michael J; 02.06.2018
comment
Хороший! Рад, что ты понял. - person rmtheis; 02.06.2018
comment
Отключение ведения журнала с упреждающей записью, как указал Рамон ниже, сработало для меня. stackoverflow.com/a/51953955/1172181 - person Luis; 05.10.2018
comment
@rmtheis все еще есть проблема, как узнать, создана ли уже база данных? каждый раз, когда вы вызываете helper.getReadableDatabase (); чтобы получить путь, по которому вы создаете базу данных, поэтому проверка того, существует ли она, всегда верна, даже до ее создания. Итак ... как мы можем узнать, создан он или нет, чтобы заполнить его содержимым базы данных ресурсов только в первый раз? - person NullPointerException; 08.10.2018
comment
Этот ответ у меня не сработал, поэтому я, наконец, решил его, используя другой ответ: stackoverflow.com/questions/52706557/ - person NullPointerException; 08.10.2018
comment
Оно работает. Я могу скопировать файл db в базу данных после использования SQLiteOpenHelper, снова подключить старую базу данных и закрыть ее. - person JackWu; 18.07.2019

Была аналогичная проблема, и я решил ее добавить в мой SQLiteOpenHelper

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

Судя по всему, Android P настраивает PRAGMA Log по-другому. Все еще не знаю, будут ли побочные эффекты, но, похоже, работает!

person Ramon Canales    schedule 21.08.2018
comment
Хороший ответ, но для придирки: onConfigure - лучшее место для этого. В javadoc для onConfigure конкретно упоминается, что это место для таких вещей, как enableWriteAheadLogging. В моем тестировании оба места работают для решения проблемы на Android 9. - person TalkLittle; 11.11.2018
comment
Работай со мной! Если проблема только в 9 версии Android, в моем случае: if (Build.VERSION.SDK_INT ›= 28) {database.disableWriteAheadLogging ();} - person Bronz; 01.09.2019
comment
я тоже застрял с той же проблемой, что он отлично работал на устройствах ниже версии 9, и теперь с вашим решением он отлично работает и на Pie, спасибо - person Annie; 11.06.2020
comment
Меня устраивает. У меня была ошибка только на Android9. Благодарность - person Ehsan; 02.07.2020
comment
Для меня это работает, но если мы отключим WAL, он уменьшит количество операций записи на 10% до 15% согласно этому source.android.com/devices/tech/perf/compatibility-wal - person Sagar Vasoya; 08.08.2020

Мои проблемы с 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);
    }
}
person aaru    schedule 12.11.2018
comment
Я столкнулся с точной проблемой, решение тоже сработало, но мне интересно узнать о проблеме, почему и как эта проблема решена в Android P? - person buzzingsilently; 02.01.2019
comment
Спасибо! я потратил 3 дня на поиски решения, но почему? - person Ayoub Anbara; 03.11.2019

Я столкнулся с подобной проблемой. Я копировал базу данных, но не из актива. Я обнаружил, что проблема вообще не связана с кодом копирования файла базы данных. И это не имело отношения к файлам, оставленным открытыми, незакрытыми, сброшенными или синхронизируемыми. Мой код обычно перезаписывает существующую неоткрытую базу данных. Что кажется новым / отличным от Android Pie и отличается от предыдущих выпусков Android, так это то, что когда Android Pie создает базу данных SQLite, он по умолчанию устанавливает для journal_mode значение 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 = УДАЛИТЬ;

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

person KGBird    schedule 16.08.2018
comment
Это отличное объяснение и более подробное, чем другие. Спасибо - person CaptainCrunch; 09.12.2020

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

Вот:

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

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

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

Этот ответ помог мне понять это: https://stackoverflow.com/a/35648781/297710

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

person yvolk    schedule 16.01.2019

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

Android 9 представляет специальный режим SQLiteDatabase под названием Совместимость WAL (вход с упреждающей записью), который позволяет базе данных использовать «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.

В дальнейшем он работает как в более ранних версиях.

person chrisonline    schedule 05.01.2019
comment
как я могу сгенерировать файл data-wal.db? Может ли эмулятор сгенерировать его? Потому что проблема возникает на реальных устройствах, а не на эмуляторах. - person Muhammad Saqib; 17.01.2020

У меня было то же самое, что у меня было приложение в версии 4 для Android, и при обновлении моего мобильного телефона с Android 9 я 2 дня пытался найти ошибку, спасибо за комментарии, в моем случае мне просто пришлось добавить это. Закрыть ();

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

готово работает для всех версий !!

person Roque Ramos    schedule 08.11.2019

Во-первых, спасибо за вопрос. Со мной случилось то же самое. Все шло хорошо, но потом при тестировании с 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 автоматически. Поэтому, если вы выполните запрос «выберите * из ТАБЛИЦЫ», он не найдет эту ТАБЛИЦУ, потому что запрос начинает искать «первую» таблицу, которая должна быть таблицей метаданных. Я исправил это, вручную добавив таблицу android_metadata, и все было хорошо. Надеюсь, кто-то еще сочтет это полезным. Потребовалась целая вечность, чтобы понять, потому что конкретные запросы по-прежнему работали нормально.

person lil_matthew    schedule 13.08.2018
comment
как добавить таблицу android_metadata? Вы не сказали этого в своем ответе - person NullPointerException; 08.10.2018
comment
Я добавил его с помощью браузера БД для SQLite. Вы можете добавлять таблицы вручную с помощью этой программы. - person lil_matthew; 09.10.2018

Подобная проблема затронула только устройство Android P. Все предыдущие версии без проблем.

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

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

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

В базе данных, для которой была создана резервная копия с устройства разработки, отсутствует таблица. Следовательно, «таблица не найдена» на самом деле было правильным.

person Berry Wing    schedule 25.09.2018
comment
Эй, я сейчас столкнулся с той же проблемой. Я не могу понять, как он может восстановить копию файла, но без таблицы, как это вообще возможно? Есть какие-нибудь подсказки о том, как обойти это в продакшене? - person Shyri; 23.10.2018

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

DB_NAME="xyz.db";
DB_Path = "/data/data/" + BuildConfig.APPLICATION_ID + "/databases/"+DB_NAME;
person N_J    schedule 08.05.2019

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

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

@Override
public void onOpen(SQLiteDatabase db) {
    super.onOpen(db);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        db.disableWriteAheadLogging();
    }
}
person Muhammad Raza Saeed    schedule 24.07.2019

Похоже, вы не закрываете выходной поток. Хотя это, вероятно, не объясняет, почему 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();
}
person bwt    schedule 28.05.2018
comment
К сожалению, закрытие FileOutputStream не повлияло. Также переключение на метод «попробовать с ресурсами» привело к такой же пустой базе данных. - person Michael J; 31.05.2018

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

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");
}
person Smellyfish    schedule 01.08.2019

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

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

ложный

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

    private void createDataBase () выбрасывает IOException {

    if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) {
                    this.getWritableDatabase();
        try {           
            copyDataBase();            
        } catch (IOException e) {           
            throw new RuntimeException(e);
        }
    }
    

    }

person K R Jawaharlal    schedule 15.08.2019

Проблема, возникающая в Android Pie, решение:

 SQLiteDatabase db = this.getReadableDatabase();
        if (db != null && db.isOpen())
            db.close();
   copyDataBase();
person Akbar jan- JEE developer    schedule 05.12.2019