如何将 SQLiteOpenHelper 与 sd 卡上的数据库一起使用?

Posted

技术标签:

【中文标题】如何将 SQLiteOpenHelper 与 sd 卡上的数据库一起使用?【英文标题】:How using SQLiteOpenHelper with database on sd-card? 【发布时间】:2011-08-10 04:05:23 【问题描述】:

根据此处和网络扩展应用程序中的各种答案,它的继承方法 getDatabasePath() 将允许将数据库存储路径设置为从标准内部存储器位置到插入的更大尺寸的 SD 卡。

这对我不起作用。建议的构造仍在使用内部存储器上的数据库。事实上,SQLiteOpenHelper 从不调用 getDatabasePath() 方法。

我想启动并运行它。

这是我到目前为止所做的:

1.) 扩展应用:

public class MyApplication extends Application 

  @Override
  public File getDatabasePath(String name) 
    // Just a test
    File file = super.getDatabasePath(name);

    return file;
  

  @Override
  public void onCreate() 
    // Just a test
    super.onCreate();
  

2.) 将扩展应用程序添加到清单中:

<application
  ...
  android:name="MyApplication" 
  ... >

3.) 扩展和使用 SQLiteOpenHelper:

public class mysqliteOpenHelper extends SQLiteOpenHelper 

  public void onCreate(SQLiteDatabase sqliteDatabase) 
    ...
  

  @Override
  public void onUpgrade(SQLiteDatabase sqliteDatabase, int oldVersion, int newVersion) 
    ...
  

4.) 以通常的方式在我的活动中使用扩展的 SQLiteOpenHelper:

public class MyActivity extends Activity 

  private MySqliteOpenHelper mySqliteOpenHelper;
  private SQLiteDatabase     sqliteDatabase;

  @Override
  public void onCreate(Bundle bundle) 
    super.onCreate(bundle);
    ...
    mySqliteOpenHelper = new MySqliteOpenHelper(getApplicationContext());
    sqliteDatabase = mySqliteOpenHelper.getReadableDatabase();
    ...
  

  @Override
  protected void onDestroy() 
    if (mySqliteOpenHelper != null) 
      mySqliteOpenHelper.close();
      mySqliteOpenHelper = null;
    

    super.onDestroy();
  

我想指出,扩展的 Application 类通常可以正常工作。我可以看到这一点,因为调用了 MyApplication.onCreate()。但是没有调用 MyApplication.getDatabasePath()。

非常感谢任何帮助。

【问题讨论】:

在 sd 卡中保存一个普通的 sqlite 数据库文件是不安全的。以下是如何获得加密解决方案的链接:github.com/sqlcipher/android-database-sqlcipher/issues/67 【参考方案1】:

我发现我可以在 Android 2.2 中使用完整路径,但在 2.1 中,Context.openOrCreateDatabase() 方法引发了异常。为了解决这个问题,我将该方法包装为直接调用 SQLiteDatabase.openOrCreateDatabase() 。这是我的扩展 SQLOpenHelper 的构造函数

public class Database extends SQLiteOpenHelper 
  public Database(Context context) 
    super(new ContextWrapper(context) 
        @Override public SQLiteDatabase openOrCreateDatabase(String name, 
                int mode, SQLiteDatabase.CursorFactory factory) 

            // allow database directory to be specified
            File dir = new File(DIR);
            if(!dir.exists()) 
                dir.mkdirs();
            
            return SQLiteDatabase.openDatabase(DIR + "/" + NAME, null,
                SQLiteDatabase.CREATE_IF_NECESSARY);
        
    , NAME, null, VERSION);
    this.context = context;
  

【讨论】:

谢谢!这对我有用。 +1,因为到目前为止我必须向下滚动才能看到这个出色的答案。 (+1 应该向上移动)。 我使用了这个解决方案,在设备升级到 Android 4.0.3 之前它一直有效。然后它开始使用内部存储,而不是 SD 卡。在跟踪中,上面的 openOrCreateDatabase 方法没有被调用,并且在跟踪到 SQLiteDatabaseHelper 时它在错误的行上(在 2.3.3 和 4.0.3 中,它们都有不同的代码),因此很难看到发生了什么事。通过反复按 F5,我能够访问 ContextImpl.openOrCreateDatabase(),但源代码不可用。看起来像一个错误。底线是这种方法不再有效。我编写了自己的 SQLiteOpenHelper 来修复它。 后来:我为我的 SQLiteOpenHelper 添加了代码作为答案。【参考方案2】:

重写 SQLOpenHelper 以使用 SD 卡目录而不是上下文,然后扩展它似乎对我有用。

import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteException;
import android.util.Log;

/**
 * SDCardSQLiteOpenhelper is a class that is based on SQLiteOpenHelper except
 * that it does not use the context to get the database. It was written owing to
 * a bug in Android 4.0.3 so that using a ContextWrapper to override
 * openOrCreateDatabase, as was done with Android 2.3.3, no longer worked. <br>
 * <br>
 * The mContext field has been replaced by mDir. It does not use lock on the
 * database as that method is package private to
 * android.database.sqlite.SQLiteDatabase. Otherwise the implementation is
 * similar.<br>
 * <br>
 * 
 * @see android.database.sqlite.SQLiteOpenHelper
 */
public abstract class SDCardSQLiteOpenHelper 
    private static final String TAG = SDCardSQLiteOpenHelper.class
            .getSimpleName();

    // private final Context mContext;
    private final String mName;
    private final String mDir;
    private final CursorFactory mFactory;
    private final int mNewVersion;

    private SQLiteDatabase mDatabase = null;
    private boolean mIsInitializing = false;

    /**
     * Create a helper object to create, open, and/or manage a database. This
     * method always returns very quickly. The database is not actually created
     * or opened until one of @link #getWritableDatabase or
     * @link #getReadableDatabase is called.
     * 
     * @param dir
     *            the directory on the SD card. It must exist and the SD card
     *            must be available. The caller should check this.
     * @param name
     *            of the database file, or null for an in-memory database
     * @param factory
     *            to use for creating cursor objects, or null for the default
     * @param version
     *            number of the database (starting at 1); if the database is
     *            older, @link #onUpgrade will be used to upgrade the
     *            database; if the database is newer, @link #onDowngrade will
     *            be used to downgrade the database
     */
    public SDCardSQLiteOpenHelper(String dir, String name,
            CursorFactory factory, int version) 
        if (version < 1)
            throw new IllegalArgumentException("Version must be >= 1, was "
                    + version);
        // mContext = context;
        mDir = dir;
        mName = name;
        mFactory = factory;
        mNewVersion = version;
    

    /**
     * Return the name of the SQLite database being opened, as given to the
     * constructor.
     */
    public String getDatabaseName() 
        return mName;
    

    /**
     * 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;
        // NOT AVAILABLE
        // if (mDatabase != null) 
        // mDatabase.lock();
        // 
        try 
            mIsInitializing = true;
            if (mName == null) 
                db = SQLiteDatabase.create(null);
             else 
                String path = mDir + "/" + mName;
                // db = mContext.openOrCreateDatabase(mName, 0, mFactory,
                // mErrorHandler);
                db = SQLiteDatabase.openDatabase(path, null,
                        SQLiteDatabase.CREATE_IF_NECESSARY);
            

            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) 
                        // Do nothing
                    
                    // NOT AVAILABLE
                    // mDatabase.unlock();
                
                mDatabase = db;
             else 
                // NOT AVAILABLE
                // 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();
            String path = mDir + "/" + mName;

            db = SQLiteDatabase.openDatabase(path, mFactory,
                    SQLiteDatabase.OPEN_READONLY);
            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();
        
    

    /**
     * Close any open database object.
     */
    public synchronized void close() 
        if (mIsInitializing)
            throw new IllegalStateException("Closed during initialization");

        if (mDatabase != null && mDatabase.isOpen()) 
            mDatabase.close();
            mDatabase = null;
        
    

    /**
     * Called when the database is created for the first time. This is where the
     * creation of tables and the initial population of the tables should
     * happen.
     * 
     * @param db
     *            The database.
     */
    public abstract void onCreate(SQLiteDatabase db);

    /**
     * Called when the database needs to be upgraded. The implementation should
     * use this method to drop tables, add tables, or do anything else it needs
     * to upgrade to the new schema version.
     * 
     * <p>
     * The SQLite ALTER TABLE documentation can be found <a
     * href="http://sqlite.org/lang_altertable.html">here</a>. If you add new
     * columns you can use ALTER TABLE to insert them into a live table. If you
     * rename or remove columns you can use ALTER TABLE to rename the old table,
     * then create the new table and then populate the new table with the
     * contents of the old table.
     * 
     * @param db
     *            The database.
     * @param oldVersion
     *            The old database version.
     * @param newVersion
     *            The new database version.
     */
    public abstract void onUpgrade(SQLiteDatabase db, int oldVersion,
            int newVersion);

    /**
     * Called when the database needs to be downgraded. This is stricly similar
     * to onUpgrade() method, but is called whenever current version is newer
     * than requested one. However, this method is not abstract, so it is not
     * mandatory for a customer to implement it. If not overridden, default
     * implementation will reject downgrade and throws SQLiteException
     * 
     * @param db
     *            The database.
     * @param oldVersion
     *            The old database version.
     * @param newVersion
     *            The new database version.
     */
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) 
        throw new SQLiteException("Can't downgrade database from version "
                + oldVersion + " to " + newVersion);
    

    /**
     * Called when the database has been opened. The implementation should check
     * @link SQLiteDatabase#isReadOnly before updating the database.
     * 
     * @param db
     *            The database.
     */
    public void onOpen(SQLiteDatabase db) 
    

这是在 Roger Keays 上述方法在 Android 4.0.3 上停止工作时完成的。

【讨论】:

感谢您的贡献,但如果您想使用加密数据库 (github.com/sqlcipher/android-database-sqlcipher/issues/67),如果您保存在 sd 卡中更有意义,则无法使用您的解决方案。你找到更优雅的解决方案了吗? 谢谢。这有很大帮助。 :) @GeorgePligor 即使您将数据库保存在手机内存中,用户也可以使用有根设备打开它。我想你可以使用SecurePreferences 之类的东西进行加密。 显然,从 Android 2.2 开始,您可以在 SQLiteOpenHelper 中使用数据库的完整路径,这使得在 4.0.3(以及回到 2.2)上不需要这个 kludge。当我尝试在新应用程序中的 SD 卡上实现数据库时,我才发现这一点,发现我不需要做任何不寻常的事情。对不起。 @KennethEvans 你的意思是这适用于 2.2 及之后的所有版本吗?【参考方案3】:

这段代码修复了我的类似问题,我的应用程序类:

@Override
public File getDatabasePath(String name) 
    File result = new File(getExternalFilesDir(null), name);
    return result;


@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) 
    return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), factory);

希望对你有帮助。

【讨论】:

【参考方案4】:

好吧,我想你不能那样做。如果有人知道方法,请告诉我们方法。

所以当你打电话时

mySqliteOpenHelper.getReadableDatabase();

应该没问题,就好像我们看implementation我们看到的那样:

 String path = mContext.getDatabasePath(mName).getPath();

一切都好。但是,如果我们看一下几行:

return getWritableDatabase();

所以它实际上是在调用另一个方法,如果它失败了,它才会继续使用 getDatabasePath()。 如果我们查看 getWritableDatabase 的实现 - 我们可以清楚地看到它没有使用 getDatabasePath 而是:

db = mContext.openOrCreateDatabase(mName, 0, mFactory);

这让我们看看 openOrCreateDatabase 是如何实现的,我们将看看ContextImpl.java

 if (name.charAt(0) == File.separatorChar) 
            String dirPath = name.substring(0, name.lastIndexOf(File.separatorChar));
            dir = new File(dirPath);
            name = name.substring(name.lastIndexOf(File.separatorChar));
            f = new File(dir, name);
         else 
            dir = getDatabasesDir();
            f = makeFilename(dir, name);
        

所以我们可以看到,如果这个辅助方法 validateFilePath 获得完整路径(如 /some/truly/full/path)或尝试将 getDatabasesDir() 与文件名连接,则返回 File。 getDatabasesDir() 实现使用 getDataDirFile() ,它是公共的,理论上可能会被覆盖..但您必须检查。

目前我看到两种解决方案:

1) 如果您不需要写访问强制 sqlite db 进入只读模式,getWritableDatabase 将失败,getDatabasePath 将被调用 2) 将完整路径传入 SQLiteOpenHelper 构造函数,并确保 db 是可写的,例如:

public class MyDbOpenHelper extends SQLiteOpenHelper 

    public MyDbOpenHelper(final Context context) 
        super(context, Environment.getExternalStorageDirectory()
                + "/path/to/database/on/sdcard/database.sqlite", null, 1);
    

这对我来说确实没有意义,但是查看 android 源代码(至少 2.3.1)似乎是它的实现方式。

【讨论】:

谢谢。我只是在几个年龄的评论中写了这个 从这里开始的几页(不是年龄)。效果很好。【参考方案5】:

调用这个函数会调用SqliteOpen辅助类中的onCreate方法

    public dbOperation open() throws SQLException 
    
        db = DBHelper.getWritableDatabase();
        return this;
    

oncreate方法是这样的

       public void onCreate(SQLiteDatabase db) 
        
            try 
                db.execSQL(DATABASE_CREATE);

             catch (Exception e) 
                    e.printStackTrace();
            
        

DATABASE_CREATE 是包含创建数据库查询的字符串

【讨论】:

我确实有很多数据库在内部存储器上工作。关键是,根据上述建议,上述步骤应在外部存储器(SD 卡)上创建数据库文件 - 这不起作用。还是谢谢。【参考方案6】:

您的数据库保存在其内部存储器中,因此其他应用程序无法访问它并更改/损坏数据。

android 数据库的默认路径是 /data/data/APPLICATIONPACKAGENAME/databases/ 。以下是关于如何将数据库存储在文件中然后在运行时填充它的非常好的指南。

Article

【讨论】:

一定是我的英语不好,每个人都试图解释如何使用数据库或为什么我不应该在 SD 卡上存储数据库文件 ;-) 我只是想知道如何在 SD 上创建数据库-卡片。与此同时,我发现了问题并找到了解决方法。我必须通读原始 SQLiteOpenHelper 代码才能立即了解原因以及如何解决它。无论如何,谢谢。 当您找到解决方案时,您可以将其发布为答案。我会对你如何解决你的问题感兴趣。谢谢 getWritableDatabase() 不调用 getDatabasePath()。它只在 getReadableDatabase() 中调用。但是 getReadableDatabase() 本身调用 getWriteableDatabase() ,如果成功,则永远不会使用调用 getDatabasePath() 的部分。所以我目前正在做的是复制抽象类 SQLiteOpenHelper 并更改该行。是的,我知道后果,关于用户需要单击“确定”的安全风险,我一般都想继续使用 SQLiteOpenHelper。我不会将其发布为答案,因为这不是我向任何人建议的解决方案。

以上是关于如何将 SQLiteOpenHelper 与 sd 卡上的数据库一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

android 建数据库 SQLite 存储sd 卡或者内存

如何将一个长的单个 SQLiteOpenHelper 拆分为多个类,每个表一个类

如何操作android中的数据库

如何将TF卡转为SD卡在数码相机中使用使用?

SQLiteOpenHelper 单连接与多连接

如何模拟 SQLiteOpenHelper