уроки - создание базы данных sqlite android




Каковы наилучшие практики для SQLite на Android? (7)

Параллельный доступ к базе данных

Такая же статья в моем блоге (мне больше нравится форматирование)

Я написал небольшую статью, в которой описывается, как обеспечить безопасный доступ к потоку базы данных андроидов.

Предполагая, что у вас есть собственный SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Теперь вы хотите записать данные в базу данных в отдельных потоках.

 // Thread 1
 Context context = getApplicationContext();
 DatabaseHelper helper = new DatabaseHelper(context);
 SQLiteDatabase database = helper.getWritableDatabase();
 database.insert(…);
 database.close();

 // Thread 2
 Context context = getApplicationContext();
 DatabaseHelper helper = new DatabaseHelper(context);
 SQLiteDatabase database = helper.getWritableDatabase();
 database.insert(…);
 database.close();

Вы получите следующее сообщение в своем логарифме, и одно из ваших изменений не будет записано.

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

Это происходит потому, что каждый раз, когда вы создаете новый объект SQLiteOpenHelper, вы фактически создаете новое соединение с базой данных. Если вы попытаетесь одновременно записать в базу данных из разных отдельных соединений, произойдет сбой. (из ответа выше)

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

Давайте сделаем singleton class Database Manager, который будет удерживать и возвращать один объект SQLiteOpenHelper .

public class DatabaseManager {

    private static DatabaseManager instance;
    private static SQLiteOpenHelper mDatabaseHelper;

    public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
        if (instance == null) {
            instance = new DatabaseManager();
            mDatabaseHelper = helper;
        }
    }

    public static synchronized DatabaseManager getInstance() {
        if (instance == null) {
            throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
                    " is not initialized, call initialize(..) method first.");
        }

        return instance;
    }

    public SQLiteDatabase getDatabase() {
        return new mDatabaseHelper.getWritableDatabase();
    }

}

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

 // In your application class
 DatabaseManager.initializeInstance(new MySQLiteOpenHelper());
 // Thread 1
 DatabaseManager manager = DatabaseManager.getInstance();
 SQLiteDatabase database = manager.getDatabase()
 database.insert(…);
 database.close();

 // Thread 2
 DatabaseManager manager = DatabaseManager.getInstance();
 SQLiteDatabase database = manager.getDatabase()
 database.insert(…);
 database.close();

Это принесет вам еще один крах.

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase

Поскольку мы используем только одно соединение с базой данных, метод getDatabase () возвращает тот же экземпляр объекта SQLiteDatabase для Thread1 и Thread2 . Что происходит, Thread1 может закрыть базу данных, а Thread2 все еще использует ее. Вот почему мы сбой IllegalStateException .

Нам нужно убедиться, что никто не использует базу данных и только затем ее закрывает. Некоторым людям в stackoveflow рекомендуется никогда не закрывать SQLiteDatabase . Это не только звучит глупо, но и чтит вас следующим сообщением logcat.

Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed

Рабочий образец

public class DatabaseManager {

    private int mOpenCounter;

    private static DatabaseManager instance;
    private static SQLiteOpenHelper mDatabaseHelper;
    private SQLiteDatabase mDatabase;

    public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
        if (instance == null) {
            instance = new DatabaseManager();
            mDatabaseHelper = helper;
        }
    }

    public static synchronized DatabaseManager getInstance() {
        if (instance == null) {
            throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
                    " is not initialized, call initializeInstance(..) method first.");
        }

        return instance;
    }

    public synchronized SQLiteDatabase openDatabase() {
        mOpenCounter++;
        if(mOpenCounter == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

    public synchronized void closeDatabase() {
        mOpenCounter--;
        if(mOpenCounter == 0) {
            // Closing database
            mDatabase.close();

        }
    }

}

Используйте его следующим образом.

SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way

Каждый раз, когда вам нужна база данных, вы должны вызвать метод openDatabase () класса DatabaseManager . Внутри этого метода у нас есть счетчик, который показывает, сколько раз база данных открыта. Если он равен единице, это означает, что нам нужно создать новое соединение с базой данных, если нет, соединение с базой данных уже создано.

То же самое происходит в методе closeDatabase () . Каждый раз, когда мы вызываем этот метод, счетчик уменьшается, всякий раз, когда он обращается к нулю, мы закрываем соединение с базой данных.

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

Что было бы лучше всего использовать при выполнении запросов в базе данных SQLite в приложении для Android?

Безопасно ли запускать вставки, удалять и выбирать запросы из doInBackground из AsyncTask? Или я должен использовать поток пользовательского интерфейса? Я полагаю, что запросы к базе данных могут быть «тяжелыми» и не должны использовать поток пользовательского интерфейса, поскольку он может заблокировать приложение, в результате чего приложение не отвечает (ANR).

Если у меня есть несколько AsyncTasks, должны ли они совместно использовать соединение или должны ли они открывать соединение каждый?

Есть ли какая-либо передовая практика для этих сценариев?


База данных очень гибкая с многопоточными. Мои приложения попадают в свои БД из разных потоков одновременно, и все отлично. В некоторых случаях у меня есть несколько процессов, одновременно ударяющих по БД, и это отлично работает.

Ваши задачи async - используйте одно и то же соединение, если это возможно, но если вам нужно, его OK для доступа к БД из разных задач.


Вы можете попытаться применить новый подход к архитектуре, объявленный в Google I / O 2017.

Он также включает новую библиотеку ORM под названием Room

Он содержит три основных компонента: @Entity, @Dao и @Database

User.java

@Entity
public class User {
  @PrimaryKey
  private int uid;

  @ColumnInfo(name = "first_name")
  private String firstName;

  @ColumnInfo(name = "last_name")
  private String lastName;

  // Getters and setters are ignored for brevity,
  // but they're required for Room to work.
}

UserDao.java

@Dao
public interface UserDao {
  @Query("SELECT * FROM user")
  List<User> getAll();

  @Query("SELECT * FROM user WHERE uid IN (:userIds)")
  List<User> loadAllByIds(int[] userIds);

  @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
       + "last_name LIKE :last LIMIT 1")
  User findByName(String first, String last);

  @Insert
  void insertAll(User... users);

  @Delete
  void delete(User user);
}

AppDatabase.java

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}

Имея некоторые проблемы, я думаю, что понял, почему я ошибся.

Я написал класс оболочки базы данных, который включал функцию close() которая вызывала помощника как зеркало open() которое вызывало getWriteableDatabase, а затем переместилось в ContentProvider . Модель ContentProvider не использует SQLiteDatabase.close() которая, по моему мнению, является большой подсказкой, поскольку код действительно использует getWriteableDatabase В некоторых случаях я все еще выполнял прямой доступ (запросы проверки экрана в основном, поэтому я перенесился в модель getWriteableDatabase / rawQuery ,

Я использую синглтон, и в закрытой документации есть слегка зловещий комментарий

Закрыть любой открытый объект базы данных

(мой смелый).

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

Поэтому я думаю, что close() заставляет базу данных закрываться независимо от каких-либо других потоков, содержащих ссылки, поэтому close() сам по себе не просто getWriteableDatabase сопоставление getWriteableDatabase а принудительно закрывает любые открытые запросы. В большинстве случаев это не проблема, поскольку код является однопоточным, но в многопоточных случаях всегда есть возможность открытия и закрытия синхронизации.

Прочитав комментарии в другом месте, в которых объясняется, что экземпляр экземпляра SqLiteDatabaseHelper подсчитывается, тогда единственный раз, когда вы хотите закрыть, вы хотите, чтобы ситуация, в которой вы хотите сделать резервную копию, и вы хотите принудительно закрыть все соединения и заставить SqLite напишите все кэшированные вещи, которые могут быть оскорблены - другими словами, прекратите всю деятельность базы данных приложений, закройте только в том случае, если Помощник потерял трек, выполните любую операцию на уровне файла (резервное копирование / восстановление), затем начните все заново.

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

Поэтому я считаю, что подход:

Используйте getWriteableDatabase, чтобы открыть оболочку singleton. (Я использовал производный класс приложения, чтобы обеспечить контекст приложения из статики, чтобы разрешить необходимость контекста).

Никогда не звоните напрямую.

Никогда не храните результирующую базу данных в любом объекте, который не имеет видимой области видимости, и полагайтесь на подсчет ссылок, чтобы вызвать неявное закрытие ().

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


Ответ Дмитрия отлично подходит для моего дела. Я думаю, что лучше объявить функцию синхронизированной. по крайней мере, для моего случая, он будет ссылаться на исключение нулевого указателя иначе: например, getWritableDatabase еще не возвращается в одном потоке, а openDatabse - в другой поток.

public synchronized SQLiteDatabase openDatabase() {
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

Я знаю, что ответ задерживается, но лучший способ выполнить sqlite-запросы в android - через пользовательский контент-провайдер. Таким образом, пользовательский интерфейс развязывается с классом базы данных (класс, который расширяет класс SQLiteOpenHelper). Также запросы выполняются в фоновом потоке (Cursor Loader).


  • Используйте Thread или AsyncTask для длительных операций (50 мс +). Проверьте свое приложение, чтобы узнать, где это. Большинство операций (возможно) не требуют потока, потому что большинство операций (возможно) содержат только несколько строк. Используйте поток для массовых операций.
  • Разделите один экземпляр SQLiteDatabase для каждого БД на диске между потоками и SQLiteDatabase систему подсчета, чтобы отслеживать открытые соединения.

Есть ли какая-либо передовая практика для этих сценариев?

Поделите статическое поле между всеми вашими классами. Раньше я держал синглтон вокруг этого и других вещей, которые нужно разделить. Также необходимо использовать схему счета (обычно с использованием AtomicInteger), чтобы вы никогда не закрывали базу данных раньше или не открывали ее.

Мое решение:

Для самой последней версии см. https://github.com/JakarCo/databasemanager но я постараюсь также обновить код. Если вы хотите понять мое решение, посмотрите на код и прочитайте мои заметки. Мои заметки обычно очень полезны.

  1. скопируйте / вставьте код в новый файл с именем DatabaseManager . (или загрузить его из github)
  2. расширить DatabaseManager и реализовать onCreate и onUpgrade как обычно. Вы можете создать несколько подклассов одного класса DatabaseManager , чтобы иметь разные базы данных на диске.
  3. getDb() вашего подкласса и вызовите getDb() для использования класса SQLiteDatabase .
  4. Вызов close() для каждого созданного вами подкласса

Код для копирования / вставки :

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;

import java.util.concurrent.ConcurrentHashMap;

/** Extend this class and use it as an SQLiteOpenHelper class
 *
 * DO NOT distribute, sell, or present this code as your own. 
 * for any distributing/selling, or whatever, see the info at the link below
 *
 * Distribution, attribution, legal stuff,
 * See https://github.com/JakarCo/databasemanager
 * 
 * If you ever need help with this code, contact me at [email protected] (or [email protected] )
 * 
 * Do not sell this. but use it as much as you want. There are no implied or express warranties with this code. 
 *
 * This is a simple database manager class which makes threading/synchronization super easy.
 *
 * Extend this class and use it like an SQLiteOpenHelper, but use it as follows:
 *  Instantiate this class once in each thread that uses the database. 
 *  Make sure to call {@link #close()} on every opened instance of this class
 *  If it is closed, then call {@link #open()} before using again.
 * 
 * Call {@link #getDb()} to get an instance of the underlying SQLiteDatabse class (which is synchronized)
 *
 * I also implement this system (well, it's very similar) in my <a href="http://androidslitelibrary.com">Android SQLite Libray</a> at http://androidslitelibrary.com
 * 
 *
 */
abstract public class DatabaseManager {

    /**See SQLiteOpenHelper documentation
    */
    abstract public void onCreate(SQLiteDatabase db);
    /**See SQLiteOpenHelper documentation
     */
    abstract public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
    /**Optional.
     * *
     */
    public void onOpen(SQLiteDatabase db){}
    /**Optional.
     * 
     */
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
    /**Optional
     * 
     */
    public void onConfigure(SQLiteDatabase db){}



    /** The SQLiteOpenHelper class is not actually used by your application.
     *
     */
    static private class DBSQLiteOpenHelper extends SQLiteOpenHelper {

        DatabaseManager databaseManager;
        private AtomicInteger counter = new AtomicInteger(0);

        public DBSQLiteOpenHelper(Context context, String name, int version, DatabaseManager databaseManager) {
            super(context, name, null, version);
            this.databaseManager = databaseManager;
        }

        public void addConnection(){
            counter.incrementAndGet();
        }
        public void removeConnection(){
            counter.decrementAndGet();
        }
        public int getCounter() {
            return counter.get();
        }
        @Override
        public void onCreate(SQLiteDatabase db) {
            databaseManager.onCreate(db);
        }

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

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

        @Override
        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onDowngrade(db, oldVersion, newVersion);
        }

        @Override
        public void onConfigure(SQLiteDatabase db) {
            databaseManager.onConfigure(db);
        }
    }

    private static final ConcurrentHashMap<String,DBSQLiteOpenHelper> dbMap = new ConcurrentHashMap<String, DBSQLiteOpenHelper>();

    private static final Object lockObject = new Object();


    private DBSQLiteOpenHelper sqLiteOpenHelper;
    private SQLiteDatabase db;
    private Context context;

    /** Instantiate a new DB Helper. 
     * <br> SQLiteOpenHelpers are statically cached so they (and their internally cached SQLiteDatabases) will be reused for concurrency
     *
     * @param context Any {@link android.content.Context} belonging to your package.
     * @param name The database name. This may be anything you like. Adding a file extension is not required and any file extension you would like to use is fine.
     * @param version the database version.
     */
    public DatabaseManager(Context context, String name, int version) {
        String dbPath = context.getApplicationContext().getDatabasePath(name).getAbsolutePath();
        synchronized (lockObject) {
            sqLiteOpenHelper = dbMap.get(dbPath);
            if (sqLiteOpenHelper==null) {
                sqLiteOpenHelper = new DBSQLiteOpenHelper(context, name, version, this);
                dbMap.put(dbPath,sqLiteOpenHelper);
            }
            //SQLiteOpenHelper class caches the SQLiteDatabase, so this will be the same SQLiteDatabase object every time
            db = sqLiteOpenHelper.getWritableDatabase();
        }
        this.context = context.getApplicationContext();
    }
    /**Get the writable SQLiteDatabase
     */
    public SQLiteDatabase getDb(){
        return db;
    }

    /** Check if the underlying SQLiteDatabase is open
     *
     * @return whether the DB is open or not
     */
    public boolean isOpen(){
        return (db!=null&&db.isOpen());
    }


    /** Lowers the DB counter by 1 for any {@link DatabaseManager}s referencing the same DB on disk
     *  <br />If the new counter is 0, then the database will be closed.
     *  <br /><br />This needs to be called before application exit.
     * <br />If the counter is 0, then the underlying SQLiteDatabase is <b>null</b> until another DatabaseManager is instantiated or you call {@link #open()}
     *
     * @return true if the underlying {@link android.database.sqlite.SQLiteDatabase} is closed (counter is 0), and false otherwise (counter > 0)
     */
    public boolean close(){
        sqLiteOpenHelper.removeConnection();
        if (sqLiteOpenHelper.getCounter()==0){
            synchronized (lockObject){
                if (db.inTransaction())db.endTransaction();
                if (db.isOpen())db.close();
                db = null;
            }
            return true;
        }
        return false;
    }
    /** Increments the internal db counter by one and opens the db if needed
    *
    */
    public void open(){
        sqLiteOpenHelper.addConnection();
        if (db==null||!db.isOpen()){
                synchronized (lockObject){
                    db = sqLiteOpenHelper.getWritableDatabase();
                }
        } 
    }
}




sqlite3