What are the best practices for SQLite on Android?

前端 未结 10 2009
自闭症患者
自闭症患者 2020-11-21 23:45

What would be considered the best practices when executing queries on an SQLite database within an Android app?

Is it safe to run inserts, deletes and select queries

相关标签:
10条回答
  • 2020-11-22 00:16

    Concurrent Database Access

    Same article on my blog(I like formatting more)

    I wrote small article which describe how to make access to your android database thread safe.


    Assuming you have your own SQLiteOpenHelper.

    public class DatabaseHelper extends SQLiteOpenHelper { ... }
    

    Now you want to write data to database in separate threads.

     // 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();
    

    You will get following message in your logcat and one of your changes will not be written.

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

    This is happening because every time you create new SQLiteOpenHelper object you are actually making new database connection. If you try to write to the database from actual distinct connections at the same time, one will fail. (from answer above)

    To use database with multiple threads we need to make sure we are using one database connection.

    Let’s make singleton class Database Manager which will hold and return single SQLiteOpenHelper object.

    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();
        }
    
    }
    

    Updated code which write data to database in separate threads will look like this.

     // 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();
    

    This will bring you another crash.

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

    Since we are using only one database connection, method getDatabase() return same instance of SQLiteDatabase object for Thread1 and Thread2. What is happening, Thread1 may close database, while Thread2 is still using it. That’s why we have IllegalStateException crash.

    We need to make sure no-one is using database and only then close it. Some folks on stackoveflow recommended to never close your SQLiteDatabase. This will result in following logcat message.

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

    Working sample

    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();
    
            }
        }
    
    }
    

    Use it as follows.

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

    Every time you need database you should call openDatabase() method of DatabaseManager class. Inside this method, we have a counter, which indicate how many times database is opened. If it equals to one, it means we need to create new database connection, if not, database connection is already created.

    The same happens in closeDatabase() method. Every time we call this method, counter is decreased, whenever it goes to zero, we are closing database connection.


    Now you should be able to use your database and be sure it's thread safe.

    0 讨论(0)
  • 2020-11-22 00:23

    after struggling with this for a couple of hours, I've found that you can only use one db helper object per db execution. For example,

    for(int x = 0; x < someMaxValue; x++)
    {
        db = new DBAdapter(this);
        try
        {
    
            db.addRow
            (
                    NamesStringArray[i].toString(), 
                    StartTimeStringArray[i].toString(),
                    EndTimeStringArray[i].toString()
            );
    
        }
        catch (Exception e)
        {
            Log.e("Add Error", e.toString());
            e.printStackTrace();
        }
        db.close();
    }
    

    as apposed to:

    db = new DBAdapter(this);
    for(int x = 0; x < someMaxValue; x++)
    {
    
        try
        {
            // ask the database manager to add a row given the two strings
            db.addRow
            (
                    NamesStringArray[i].toString(), 
                    StartTimeStringArray[i].toString(),
                    EndTimeStringArray[i].toString()
            );
    
        }
        catch (Exception e)
        {
            Log.e("Add Error", e.toString());
            e.printStackTrace();
        }
    
    }
    db.close();
    

    creating a new DBAdapter each time the loop iterates was the only way I could get my strings into a database through my helper class.

    0 讨论(0)
  • 2020-11-22 00:24

    The Database is very flexible with multi-threading. My apps hit their DBs from many different threads simultaneously and it does just fine. In some cases I have multiple processes hitting the DB simultaneously and that works fine too.

    Your async tasks - use the same connection when you can, but if you have to, its OK to access the DB from different tasks.

    0 讨论(0)
  • 2020-11-22 00:24

    Having had some issues, I think I have understood why I have been going wrong.

    I had written a database wrapper class which included a close() which called the helper close as a mirror of open() which called getWriteableDatabase and then have migrated to a ContentProvider. The model for ContentProvider does not use SQLiteDatabase.close() which I think is a big clue as the code does use getWriteableDatabase In some instances I was still doing direct access (screen validation queries in the main so I migrated to a getWriteableDatabase/rawQuery model.

    I use a singleton and there is the slightly ominous comment in the close documentation

    Close any open database object

    (my bolding).

    So I have had intermittent crashes where I use background threads to access the database and they run at the same time as foreground.

    So I think close() forces the database to close regardless of any other threads holding references - so close() itself is not simply undoing the matching getWriteableDatabase but force closing any open requests. Most of the time this is not a problem as the code is single threading, but in multi-threaded cases there is always the chance of opening and closing out of sync.

    Having read comments elsewhere that explains that the SqLiteDatabaseHelper code instance counts, then the only time you want a close is where you want the situation where you want to do a backup copy, and you want to force all connections to be closed and force SqLite to write away any cached stuff that might be loitering about - in other words stop all application database activity, close just in case the Helper has lost track, do any file level activity (backup/restore) then start all over again.

    Although it sounds like a good idea to try and close in a controlled fashion, the reality is that Android reserves the right to trash your VM so any closing is reducing the risk of cached updates not being written, but it cannot be guaranteed if the device is stressed, and if you have correctly freed your cursors and references to databases (which should not be static members) then the helper will have closed the database anyway.

    So my take is that the approach is:

    Use getWriteableDatabase to open from a singleton wrapper. (I used a derived application class to provide the application context from a static to resolve the need for a context).

    Never directly call close.

    Never store the resultant database in any object that does not have an obvious scope and rely on reference counting to trigger an implicit close().

    If doing file level handling, bring all database activity to a halt and then call close just in case there is a runaway thread on the assumption that you write proper transactions so the runaway thread will fail and the closed database will at least have proper transactions rather than potentially a file level copy of a partial transaction.

    0 讨论(0)
提交回复
热议问题