Параллельный доступ к базе данных SQLite в Android — база данных уже закрыта

Прочитал много тем на эту тему, но никто не смог ответить на мой вопрос.

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

Я обращаюсь к своей базе данных с помощью такого кода:

 SQLiteDatabase db = DatabaseHelper.getInstance().getWritableDatabase();
 ...
 Do some update in the DB
 ...
 db.close();

Я не понимаю, почему я все еще получаю сообщение об ошибке «БД уже закрыто», разве метод getWritableDatabase() не должен блокировать базу данных до тех пор, пока не будет вызван метод close()? Другие вызовы getWritableDatabase() из других потоков должны ждать закрытия базы данных? Это верно или я что-то пропустил?


person Fr4nz    schedule 29.05.2012    source источник


Ответы (2)


Расширяя ответ elhadi, я сталкивался с похожими проблемами при открытии и закрытии соединений с базой данных для нескольких асинхронных задач. Из моего расследования в то время стало ясно, что нет необходимости постоянно открывать и закрывать соединения с БД. Подход, который я в конечном итоге принял, заключался в создании подкласса Application и выполнении одного открытия базы данных во время onCreate и одного закрытия базы данных onTerminate. Затем я настроил статический геттер для получения уже открытого объекта SQLiteDatabase. Не дружественный к DI (внедрение зависимостей), но Android пока не может этого сделать.

Что-то вроде этого;

    public class MainApplication extends Application {
          private static SQLiteDatabase database;

          /**
           * Called when the application is starting, before any other 
           * application objects have been created. Implementations 
           * should be as quick as possible...
           */
          @Override
          public void onCreate() {
          super.onCreate();
          try {
           database = SQLiteDatabase.openDatabase("/data/data/<yourdbpath>", null, SQLiteDatabase.OPEN_READWRITE);
          } catch (SQLiteException e) {
            // Our app fires an event spawning the db creation task...
           }
         }


          /**
           * Called when the application is stopping. There are no more 
           * application objects running and the process will exit.
           * <p>
           * Note: never depend on this method being called; in many 
           * cases an unneeded application process will simply be killed 
           * by the kernel without executing any application code...
           * <p>
           */
          @Override
          public void onTerminate() {
            super.onTerminate();
            if (database != null && database.isOpen()) {
              database.close();
            }
          }


          /**
           * @return an open database.
           */
          public static SQLiteDatabase getOpenDatabase() {
            return database;
          }
    }

Читая JavaDoc назад, я, конечно, где-то воспользовался этим, но это статическое открытие / закрытие одиночной базы данных решило эту проблему, с которой вы столкнулись. На SO есть еще один ответ, описывающий это решение.

Подробнее:

В ответ на комментарий Fr4nz о NPE ниже я предоставил более подробную информацию о нашей конкретной реализации.

Краткая версия

Приведенную ниже «полную картину» трудно понять без хорошего понимания BroadcastReceivers. В вашем случае (и в первую очередь) добавьте код создания БД, инициализируйте и откройте базу данных после того, как вы создали базу данных. Так что пишите;

      try {
       database = SQLiteDatabase.openDatabase("/data/data/<yourdbpath>", null, SQLiteDatabase.OPEN_READWRITE);
      } catch (SQLiteException e) {
        // Create your database here!
        database = SQLiteDatabase.openDatabase("/data/data/<your db path>", null, SQLiteDatabase.OPEN_READWRITE);
       }
     }

Длинная версия

Да, это немного больше, чем просто приведенный выше код. Обратите внимание на мой комментарий по поводу перехвата исключения в первом случае (т. е. при первом запуске вашего приложения). Здесь говорится: «Наше приложение запускает событие, порождающее задачу создания базы данных». Что на самом деле происходит в нашем приложении, так это то, что прослушиватель (Android BroadcastReceiver framework) регистрируется, и одна из первых вещей, которую делает основное действие приложения, — это проверка того, что статическая переменная database в MainApplication не равна нулю. Если он равен нулю, то создается асинхронная задача, которая создает базу данных, которая по завершении (т. е. запуск метода onPostExecute()) в конечном итоге запускает событие, которое, как мы знаем, будет получено прослушивателем, который мы зарегистрировали в try-catch. Получатель находится в классе MainApplication как внутренний класс и выглядит следующим образом;

    /**
    * Listener waiting for the application to finish
    * creating the database.
    * <p>
    * Once this has been completed the database is ready for I/O.
    * </p>
    *
    * @author David C Branton
    */
      public class OpenDatabaseReceiver extends BroadcastReceiver {
        public static final String BROADCAST_DATABASE_READY = "oceanlife.core.MainApplication$OpenDatabaseReceiver.BROADCAST_DATABASE_READY";

        /**
         * @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent)
         */
        @Override
        public void onReceive(final Context context, final Intent intent) {
          Log.i(CreatedDatabaseReceiver.class.getSimpleName(), String.format("Received filter event, '%s'", intent.getAction()));
          database = SQLiteDatabase.openDatabase("/data/data/<your db path>", null, SQLiteDatabase.OPEN_READWRITE);
          unregisterReceiver(openDatabaseReceiver);

          // Broadcast event indicating that the creation process has completed.
          final Intent databaseReady = new Intent();
          databaseReady.setAction(BROADCAST_DATABASE_READY);
          context.sendBroadcast(databaseReady);
        }
      }

Таким образом, краткое изложение процесса запуска для первой установки выглядит так;

  1. Class: MainApplication, role- check there is a database?
    • Yes? database variable is initialised
    • Нет? Получатель зарегистрирован (OpenDatabaseReceiver)
  2. Class: MainActivity: role- landing activity for the application and initially checks that the database variable is not null.
    • database is null? Does not add in the fragments that perform I/O and adds in dialog saying "creating the application database" or similar.
    • database не является нулевым? Продолжает основной поток выполнения приложения, добавляет в списки, поддерживаемые БД и т. д.
  3. Class: DatabaseCreationDialogFragment: role- spawns async task to create the database.
    • Registers new receiver listening for when the database has been created.
    • При сборе сообщения «Я создал базу данных» запускается другое событие (от получателя), сообщающее приложению, что нужно открыть базу данных.
  4. Class: MainApplication: role 2- listen for the "database created" message.
    • Receiver described above (OpenDatabaseReceiver) opens the database and broadcast (by another event!) that the database is ready to be used.
  5. Класс: MainActivity: роль 2. Получает сообщение «база данных готова», избавляется от диалога «мы создаем базу данных» и продолжает отображать данные/функции в приложении.

Мир восстановлен.

person BrantApps    schedule 29.05.2012
comment
Не забудьте обновить свой Manifest.xml путем к новому имени приложения, если вы попытаетесь это сделать. - person BrantApps; 29.05.2012
comment
Привет, у меня сейчас проблемы с путем к базе данных. По-видимому, он не может найти БД, и getOpenDatabase выдает исключение NullPointerException. Могу ли я использовать вместо openDatabase() что-то вроде DatabaseHelper.getInstance().getWritableDatabase() (только один раз)? - person Fr4nz; 30.05.2012
comment
(Значительно) больше подробностей добавил Fr4nz. Прочтите и набросайте кое-что. Если вы хотите узнать наши причины для «Длинной версии», нам лучше всего обсудить это в чате. Все дело в ролях и обязанностях фрагментов и информировании пользователя о том, что происходит. - person BrantApps; 30.05.2012
comment
Спасибо за подробный ответ, попробую - person Fr4nz; 30.05.2012
comment
Хорошее резюме! (даже если обрисовать в общих чертах несколько имхо. Глупый дизайн - диалоги не имеют права порождать потоки и приемники) - person Ярослав Рахматуллин; 23.02.2014
comment
Фрагмент запускает службу, на которой размещена AsyncTask. Слушатель вызывается, когда работа завершена. В настоящее время диалоговое окно блокируется (начальное создание базы данных). Слабая связь, высокая сплоченность. - person BrantApps; 23.02.2014

Если вы вызываете DatabaseHelper.getInstance().getWritableDatabase() в своем потоке, я советую вам управлять им перед запуском ваших потоков. вы открываете свою базу данных в основной программе, вы вызываете свои потоки. после завершения потоков вы закрываете свою базу данных в основной программе.

person elhadi dp ıpɐɥןǝ    schedule 29.05.2012
comment
Да, я вызываю DatabaseHelper.getInstance().getWritableDatabase() в каждом своем потоке. Спасибо за ваш ответ, я собираюсь попробовать это. Но правильно ли было мое размышление в теории? - person Fr4nz; 29.05.2012
comment
В вашем решении я должен вызывать getWritableDatabase() перед запуском потоков, чтобы инициировать базу данных? Я действительно не понимаю, как это должно помочь, потому что мне все еще нужно вызвать getWritableDatabase() в моем потоке, верно? - person Fr4nz; 29.05.2012