AndroidがデータベースをSDカードに保存する実装


必要に応じて、外部ストレージまたはSDカードにデータベースを保存する場合があります(この場合、データの暗号化によってデータの解読を回避できます).例えば、1つのアプリケーションが複数のデータをサポートする場合、各データには対応するデータベースが必要であり、データライブラリ内の情報量が特に大きい場合、これは、RAMのサイズが限られているため、外部ストレージまたはSDカードにデータベースを保存する必要があることは明らかです.次に、いくつかのテストプログラムを書くときにデータベースをSDカードに保存すると、データベースの内容を表示しやすくなります.
  Android SQLiteOpenHelperでデータベースを作成する場合、デフォルトでは'/data/data/アプリケーション名/databases'ディレクトリにデータベースを保存します.SQLiteOpenHelperクラスを継承するコンストラクション関数にデータベース名を入力するだけですが、指定したパスの下にデータベースを保存すると、SQLiteOpenHelperクラスを継承するコンストラクション関数のcontextを書き換える必要があります.SQLiteOpenHelperを読んでいるからです.JAvaのソースコードでは、データベースの作成はContextのopenOrCreateDatabaseメソッドで行われています.指定したパスの下でデータベースを作成する必要がある場合は、クラス継承Contextを書き、openOrCreateDatabaseメソッドを複写し、openOrCreateDatabaseメソッドでデータベースに格納されているパスを指定する必要があります.次に、クラスSQLiteOpenHelperのgetWritableDatabaseメソッドとgetReadableDatabaseメソッドのソースコードを示します.SQLiteOpenHelperは、この2つの方法でデータベースを作成します.
/**
     * Create and/or open a database that will be used for reading and writing.
     * The first time this is called, the database will be opened and
     * {@link #onCreate}, {@link #onUpgrade} and/or {@link #onOpen} will be
     * called.
     *
     * <p>Once opened successfully, the database is cached, so you can
     * call this method every time you need to write to the database.
     * (Make sure to call {@link #close} when you no longer need the database.)
     * Errors such as bad permissions or a full disk may cause this method
     * to fail, but future attempts may succeed if the problem is fixed.</p>
     *
     * <p class="caution">Database upgrade may take a long time, you
     * should not call this method from the application main thread, including
     * from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
     *
     * @throws SQLiteException if the database cannot be opened for writing
     * @return a read/write database object valid until {@link #close} is called
     */
    public synchronized SQLiteDatabase getWritableDatabase() {
        if (mDatabase != null) {
            if (!mDatabase.isOpen()) {
                // darn! the user closed the database by calling mDatabase.close()
                mDatabase = null;
            } else if (!mDatabase.isReadOnly()) {
                return mDatabase;  // The database is already open for business
            }
        }

        if (mIsInitializing) {
            throw new IllegalStateException("getWritableDatabase called recursively");
        }

        // If we have a read-only database open, someone could be using it
        // (though they shouldn't), which would cause a lock to be held on
        // the file, and our attempts to open the database read-write would
        // fail waiting for the file lock.  To prevent that, we acquire the
        // lock on the read-only database, which shuts out other users.

        boolean success = false;
        SQLiteDatabase db = null;
        if (mDatabase != null) mDatabase.lock();
        try {
            mIsInitializing = true;
            if (mName == null) {
                db = SQLiteDatabase.create(null);
            } else {
                db = mContext.openOrCreateDatabase(mName, 0, mFactory, mErrorHandler);
            }

            int version = db.getVersion();
            if (version != mNewVersion) {
                db.beginTransaction();
                try {
                    if (version == 0) {
                        onCreate(db);
                    } else {
                        if (version > mNewVersion) {
                            onDowngrade(db, version, mNewVersion);
                        } else {
                            onUpgrade(db, version, mNewVersion);
                        }
                    }
                    db.setVersion(mNewVersion);
                    db.setTransactionSuccessful();
                } finally {
                    db.endTransaction();
                }
            }

            onOpen(db);
            success = true;
            return db;
        } finally {
            mIsInitializing = false;
            if (success) {
                if (mDatabase != null) {
                    try { mDatabase.close(); } catch (Exception e) { }
                    mDatabase.unlock();
                }
                mDatabase = db;
            } else {
                if (mDatabase != null) mDatabase.unlock();
                if (db != null) db.close();
            }
        }
    }

    /**
     * Create and/or open a database.  This will be the same object returned by
     * {@link #getWritableDatabase} unless some problem, such as a full disk,
     * requires the database to be opened read-only.  In that case, a read-only
     * database object will be returned.  If the problem is fixed, a future call
     * to {@link #getWritableDatabase} may succeed, in which case the read-only
     * database object will be closed and the read/write object will be returned
     * in the future.
     *
     * <p class="caution">Like {@link #getWritableDatabase}, this method may
     * take a long time to return, so you should not call it from the
     * application main thread, including from
     * {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
     *
     * @throws SQLiteException if the database cannot be opened
     * @return a database object valid until {@link #getWritableDatabase}
     *     or {@link #close} is called.
     */
    public synchronized SQLiteDatabase getReadableDatabase() {
        if (mDatabase != null) {
            if (!mDatabase.isOpen()) {
                // darn! the user closed the database by calling mDatabase.close()
                mDatabase = null;
            } else {
                return mDatabase;  // The database is already open for business
            }
        }

        if (mIsInitializing) {
            throw new IllegalStateException("getReadableDatabase called recursively");
        }

        try {
            return getWritableDatabase();
        } catch (SQLiteException e) {
            if (mName == null) throw e;  // Can't open a temp database read-only!
            Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);
        }

        SQLiteDatabase db = null;
        try {
            mIsInitializing = true;
            String path = mContext.getDatabasePath(mName).getPath();
            db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY,
                    mErrorHandler);
            if (db.getVersion() != mNewVersion) {
                throw new SQLiteException("Can't upgrade read-only database from version " +
                        db.getVersion() + " to " + mNewVersion + ": " + path);
            }

            onOpen(db);
            Log.w(TAG, "Opened " + mName + " in read-only mode");
            mDatabase = db;
            return mDatabase;
        } finally {
            mIsInitializing = false;
            if (db != null && db != mDatabase) db.close();
        }
    }

  上記の分析により、Contextを継承するカスタムContextクラスを書くことができますが、ContextにはopenOrCreateDatabaseメソッド以外の抽象関数があるため、Contextから継承された非抽象クラスContextWrapperを使用することをお勧めします.カスタムDatabaseContextクラスのソースコードは次のとおりです.
public class DatabaseContext extends ContextWrapper {
    public DatabaseContext(Context context){
        super( context );
    }

    /**
     *        ,     ,       
     * @param    name
     * @param    mode
     * @param    factory
     */
    @Override
    public File getDatabasePath(String name) {
        //      sd 
        boolean sdExist = android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState());
        if(!sdExist){//     ,
            return null;
        }else{//    
            //  sd   
            String dbDir= FileUtils.getFlashBPath();
            dbDir += "DB";//       
            String dbPath = dbDir+"/"+name;//     
            //        ,         
            File dirFile = new File(dbDir);
            if(!dirFile.exists()){
                dirFile.mkdirs();
            }

            //           
            boolean isFileCreateSuccess = false; 
            //        ,         
            File dbFile = new File(dbPath);
            if(!dbFile.exists()){
                try {                    
                    isFileCreateSuccess = dbFile.createNewFile();//    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }else{
                isFileCreateSuccess = true;
            }

            //         
            if(isFileCreateSuccess){
                return dbFile;
            }else{
                return null;
            }
        }
    }

    /**
     *       ,     SD       ,android 2.3          。
     * 
     * @param    name
     * @param    mode
     * @param    factory
     */
    @Override
    public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) {
        SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
        return result;
    }

    /**
     * Android 4.0           。
     * 
     * @see android.content.ContextWrapper#openOrCreateDatabase(java.lang.String, int, 
     *              android.database.sqlite.SQLiteDatabase.CursorFactory,
     *              android.database.DatabaseErrorHandler)
     * @param    name
     * @param    mode
     * @param    factory
     * @param     errorHandler
     */
    @Override
    public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) {
        SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);

        return result;
    }
}

 SQLiteOpenHelperのサブクラスを継承するコンストラクション関数では、contextの代わりにDatabaseContextのインスタンスを使用します.
DatabaseContext dbContext = new DatabaseContext(context);
super(dbContext, mDatabaseName, null, VERSION);

説明:本文はネット上の他の文章を大きく参考にして、ここで特にこれらの文章の作者に感謝します.
参考資料:
  • AndroidでSQLiteOpenHelperを使用してSDカードのデータベースを管理する